How do I use x:Bind function binding with a null parameter? - c#

I am trying to format several properties of an object and bind the result to a TextBlock using x:Bind function binding. The binding looks like this:
<TextBlock Text="{x:Bind local:MainViewModel.FormatWidget(ViewModel.SelectedItem), Mode=OneWay}" />
As long as the object is not null, this works perfectly. However, when the object is null, my function is not called. Or to be more precise, if the object is null initially, the function is called, but if the object changes to null later, the function is not called.
Why is the function not being called when the parameter is null and how can I use it for this case?
Here's a repro. When you run it, notice that initially the function binds correctly to the null SelectedItem and displays "No widget selected." But when you select an item and then unselect it (CTRL + click to unselect), it does not call the function and displays the FallbackValue. (If the FallbackValue is not set, it does not update the binding at all.)
MainPage.xaml
<Page
x:Class="NullFunctionBindingParameter.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:NullFunctionBindingParameter">
<Page.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="20" />
</Style>
</Page.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListView
Grid.Column="0"
ItemsSource="{x:Bind ViewModel.Widgets, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedItem, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Widget">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock Grid.Column="1" Text="{x:Bind local:MainViewModel.FormatWidget(ViewModel.SelectedItem), Mode=OneWay, FallbackValue=MyFallbackValue}" />
</Grid>
</Page>
MainPage.xaml.cs
using Windows.UI.Xaml.Controls;
namespace NullFunctionBindingParameter
{
public sealed partial class MainPage : Page
{
public MainPage()
{
InitializeComponent();
}
public MainViewModel ViewModel { get; } = new MainViewModel();
}
}
MainViewModel.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace NullFunctionBindingParameter
{
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Widget _selectedItem;
public Widget SelectedItem
{
get => _selectedItem;
set
{
if (_selectedItem != value)
{
_selectedItem = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
}
}
}
public ObservableCollection<Widget> Widgets { get; } = new ObservableCollection<Widget>()
{
new Widget
{
Id = Guid.NewGuid(),
Name = "Regular Widget",
Model = "WX2020-01",
Description = "Your typical everyday widget."
},
new Widget
{
Id = Guid.NewGuid(),
Name = "Super Widget",
Model = "WX2020-02",
Description = "An extra special upgraded widget."
},
new Widget
{
Id = Guid.NewGuid(),
Name = "Broken Widget",
Model = "WX2020-03",
Description = "A widget that has been used and abused."
},
new Widget
{
Id = Guid.NewGuid(),
Name = "Fake Widget",
Model = "WX2020-04",
Description = "It's not really a widget at all!"
},
new Widget
{
Id = Guid.NewGuid(),
Name = "Surprise Widget",
Model = "WX2020-05",
Description = "What kind of widget will it be?"
},
new Widget
{
Id = Guid.NewGuid(),
Name = "Invisible Widget",
Model = "WX2020-06",
Description = "Our most inexpensive widget."
},
new Widget
{
Id = Guid.NewGuid(),
Name = "Backwards Widget",
Model = "WX2020-07",
Description = "Really more of a tegdiw, come to think of it."
}
};
public static string FormatWidget(Widget widget)
{
if (widget == null)
return "No widget selected";
else
return $"{widget.Name} [{widget.Model}] {widget.Description}";
}
public string GetFormattedWidget()
{
return FormatWidget(SelectedItem);
}
}
}
Widget.cs
using System;
using System.ComponentModel;
namespace NullFunctionBindingParameter
{
public class Widget : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Guid _id;
private string _name;
private string _model;
private string _description;
public Guid Id
{
get => _id;
set
{
if (_id != value)
{
_id = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Id)));
}
}
}
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
public string Model
{
get => _model;
set
{
if (_model != value)
{
_model = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Model)));
}
}
}
public string Description
{
get => _description;
set
{
if (_description != value)
{
_description = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description)));
}
}
}
}
}

In this case, I recommend that you use Converter instead of using static methods directly in the binding statement.
WidgetConverter
public class WidgetConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var widget = value as Widget;
return MainViewModel.FormatWidget(widget);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
Usage
<Page.Resources>
...
<local:WidgetConverter x:Key="WidgetConverter"/>
</Page.Resources>
...
<TextBlock
Grid.Row="2"
Grid.Column="2"
Text="{x:Bind ViewModel.SelectedItem, Mode=OneWay,Converter={StaticResource WidgetConverter}}"/>
Best regards.

Related

UWP Binding to ConverterParameter not working

I am binding a VM property to my ConverterParemeter, but it always appear to be null in Converter, is there any alternative to pass property to the converter.
As I can't share the original code, below is the replica of the issue that I am facing. In my DummyConverter parameter is always null even though FilterType value is set.
Xaml:
<Grid>
<ComboBox x:Name="myComboBox" ItemsSource="{Binding ComboBoxList, Converter={StaticResource DummyConverter}, ConverterParameter={Binding FilterType}}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}" HorizontalAlignment="Center" VerticalAlignment="Center" Height="50" Width="150">
</ComboBox>
</Grid>
VM:
public class MainViewModel : INotifyPropertyChanged
{
private string header;
public string Header
{
get { return header; }
set
{
header = value;
RaisePropertyChange(nameof(Header));
}
}
private Person selectedPerson;
public Person SelectedPerson
{
get { return selectedPerson; }
set { selectedPerson = value; RaisePropertyChange(nameof(SelectedPerson)); }
}
private ObservableCollection<Person> comboBoxList;
public ObservableCollection<Person> ComboBoxList
{
get { return comboBoxList; }
set { comboBoxList = value; }
}
public FilterType FilterType { get; set; }
public DelegateCommand DropDownClosedCommand { get; set; }
public MainViewModel()
{
Header = "My Header";
FilterType = FilterType.None;
ComboBoxList = new ObservableCollection<Person> {
new Person() { Name = "Person 1", IsChecked = false },
new Person() { Name = "Person 2", IsChecked = false },
new Person() { Name = "Person 3", IsChecked = false },
new Person() { Name = "Person 4", IsChecked = false }
};
DropDownClosedCommand = new DelegateCommand(OnDropDownClosed);
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChange(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void OnDropDownClosed(object e)
{
//CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
//() =>
//{
SelectedPerson = ComboBoxList.FirstOrDefault(x => x.IsChecked);
//});
}
}
Converter:
public class DummyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return value;
}
}
In WPF, I can do with MultiBinding but in UWP MultiBinding is not available.
EDIT:
From this blog post I found out the reason of getting null values is "The reason why is that the ConverterParameter IS NOT a dependency property but a “simple” object."
So below is the modified code:
Xaml:
<Page.Resources>
<converters:DummyConverter x:Name="DummyConverter" FilterType="{Binding FilterType}"/>
</Page.Resources>
<Grid>
<ComboBox x:Name="myComboBox" ItemsSource="{Binding ComboBoxList, Converter={StaticResource DummyConverter}}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}" HorizontalAlignment="Center" VerticalAlignment="Center" Height="50" Width="150">
<interactivity:Interaction.Behaviors>
<core:DataTriggerBehavior Binding="{Binding IsDropDownOpen, ElementName=myComboBox}" ComparisonCondition="NotEqual" Value="True">
<core:InvokeCommandAction Command="{Binding DropDownClosedCommand}"/>
</core:DataTriggerBehavior>
</interactivity:Interaction.Behaviors>
</ComboBox>
</Grid>
Converter:
public class DummyConverter : DependencyObject, IValueConverter
{
public FilterType FilterType
{
get { return (FilterType)GetValue(FilterTypeProperty); }
set { SetValue(FilterTypeProperty, value); }
}
public static readonly DependencyProperty FilterTypeProperty =
DependencyProperty.Register("FilterType",
typeof(FilterType),
typeof(DummyConverter),
new PropertyMetadata(null));
public object Convert(object value, Type targetType, object parameter, string language)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return value;
}
}
MainViewModel.cs
private void OnDropDownClosed(object e)
{
//CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
//() =>
//{
SelectedPerson = ComboBoxList.FirstOrDefault(x => x.IsChecked);
FilterType = FilterType.Descending;
this.RaisePropertyChange(nameof(ComboBoxList));
//});
}
I am changing the value of FilterType in OnDropDownClosed but it's not affecting in the converter.
I figured out the issue why FilterType was not changing, it's because PropertyChangedEvent was not firing. I updated the code as below and it's working now as expected.
private void OnDropDownClosed(object e)
{
SelectedPerson = ComboBoxList.FirstOrDefault(x => x.IsChecked);
FilterType = FilterType.Descending;
this.RaisePropertyChange(nameof(FilterType));
this.RaisePropertyChange(nameof(ComboBoxList));
}

Bind custom header controls in DataGrid

I have a custom column header where each column's header has TextBox which contains name of the column and ComboBox, which contains information about the type of the column, e.g. "Date", "Number", etc.
I'm trying to bind ComboBox and keep its value somewhere, so that when user selects new value from ComboBox I can recreate table with the column's type changed. Basically all I need is to store somehow each ComboBox's value in a list somehow. I want to do the same with TextBox which should contain name of the column.
This is what I have so far.
<DataGrid x:Name="SampleGrid" Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2" ItemsSource="{Binding SampledData}">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding ., Mode=OneWay}"/>
<ComboBox>
// How can I set it from ViewModel?
<ComboBoxItem Content="Date"></ComboBoxItem>
<ComboBoxItem Content="Number"></ComboBoxItem>
</ComboBox>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
</DataGrid>
ViewModel:
private DataTable _sampledData = new DataTable();
public DataTable SampledData
{
get => _sampledData;
set { _sampledData = value; NotifyOfPropertyChange(() => SampledData); }
}
Solutions in code behind are welcome too as long as I can pass the mappings to ViewModel later.
EDIT:
I've been trying to make this work with a List of ViewModels, but no luck:
public class ShellViewModel : Screen
{
public List<MyRowViewModel> Rows { get; set; }
public ShellViewModel()
{
Rows = new List<MyRowViewModel>
{
new MyRowViewModel { Column1 = "Test 1", Column2= 1 },
new MyRowViewModel { Column1 = "Test 2", Column2= 2 }
};
}
}
View
<DataGrid ItemsSource="{Binding Rows}">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding ., Mode=OneWay}"/>
<ComboBox ItemsSource="{Binding ??????}" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
</DataGrid>
Row
public class MyRowViewModel : PropertyChangedBase
{
public string Column1 { get; set; }
public int Column2 { get; set; }
}
EDIT2:
To clarify, I need a solution that will handle dynamic number of columns, so some files may store 3 columns and some might store 40 columns. I use this for parsing csv files to later display the data. In order to do that I have to know what types of values the file contains. Because some types may be ambiguous, I let the user decide which types they want. This is identical to Excel's "Load From File" wizard.
The wizard loads a small chunk of data (100 records) and allows user to decide what type the columns are. It automatically parses the columns to:
Let user see how the data will look like
Validate if the column can actually be parsed (e.g. 68.35 cannot
be parsed as DateTime)
Another thing is naming each column. Someone might load csv with each column named C1, C2... but they want to assign meaningful names such as Temperature, Average. This of course has to be parsed later too, because two columns cannot have the same name, but I can take care of this once I have a bindable DataGrid.
Let's break your problem into parts and solve each part separately.
First, the DataGrid itemsource, to make things easier, let's say that our DataGrid has only two columns, column 1 and column 2. A basic model for the DataGrid Items should looks like this:
public class DataGridModel
{
public string FirstProperty { get; set; }
public string SecondProperty { get; set; }
}
Now, assuming that you have a MainWindow (with a ViewModel or the DataContext set to code behind) with a DataGrid in it , let's define DataGridCollection as its ItemSource:
private ObservableCollection<DataGridModel> _dataGridCollection=new ObservableCollection<DataGridModel>()
{
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"}
};
public ObservableCollection<DataGridModel> DataGridCollection
{
get { return _dataGridCollection; }
set
{
if (Equals(value, _dataGridCollection)) return;
_dataGridCollection = value;
OnPropertyChanged();
}
}
Second, now the interesting part, the columns structure. Let's define a model for your DataGrid's columns, the model will hold all the required properties to set your DataGrid columns, including:
-DataTypesCollection: a collection that holds the combobox itemsource.
-HeaderPropertyCollection: a collection of Tuples, each Tuple represent a column name and a data type, the data type is basically the selected item of column's combobox.
public class DataGridColumnsModel:INotifyPropertyChanged
{
private ObservableCollection<string> _dataTypesCollection = new ObservableCollection<string>()
{
"Date","String","Number"
};
public ObservableCollection<string> DataTypesCollection
{
get { return _dataTypesCollection; }
set
{
if (Equals(value, _dataTypesCollection)) return;
_dataTypesCollection = value;
OnPropertyChanged();
}
}
private ObservableCollection<Tuple<string, string>> _headerPropertiesCollection=new ObservableCollection<Tuple<string, string>>()
{
new Tuple<string, string>("Column 1", "Date"),
new Tuple<string, string>("Column 2", "String")
}; //The Dictionary has a PropertyName (Item1), and a PropertyDataType (Item2)
public ObservableCollection<Tuple<string,string>> HeaderPropertyCollection
{
get { return _headerPropertiesCollection; }
set
{
if (Equals(value, _headerPropertiesCollection)) return;
_headerPropertiesCollection = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Now in you MainWindow's viewmodel (or codebehind) define an instance of the DataGridColumnsModel that we will be using to hold our DataGrid structure:
private DataGridColumnsModel _dataGridColumnsModel=new DataGridColumnsModel();
public DataGridColumnsModel DataGridColumnsModel
{
get { return _dataGridColumnsModel; }
set
{
if (Equals(value, _dataGridColumnsModel)) return;
_dataGridColumnsModel = value;
OnPropertyChanged();
}
}
Third, getting the column's TextBox's value. For that w'll be using a MultiBinding and a MultiValueConverter, the first property that w'll be passing to the MultiBinding is the collection of tuples that we define (columns' names and datatypes): HeaderPropertyCollection, the second one is the current column index that w'll retrieve from DisplayIndex using an ancestor binding to the DataGridColumnHeader:
<TextBox >
<TextBox.Text>
<MultiBinding Converter="{StaticResource GetPropertConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="DataGridColumnsModel.HeaderPropertyCollection"/>
<Binding Path="DisplayIndex" Mode="OneWay" RelativeSource="{RelativeSource RelativeSource={x:Type DataGridColumnHeader}}"/>
</MultiBinding>
</TextBox.Text>
The converter will simply retrieve the right item using the index from collection of tuples:
public class GetPropertConverter:IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
var theCollection = values[0] as ObservableCollection<Tuple<string, string>>;
return (theCollection?[(int)values[1]])?.Item1; //Item1 is the column name, Item2 is the column's ocmbobox's selectedItem
}
catch (Exception)
{
//use a better implementation!
return null;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Fourth, The last part is to update the DataGrid's ItemSource when the Combobox's selection changed, for that you could use the Interaction tools defined in System.Windows.Interactivity namespace (which is part of Expression.Blend.Sdk, use NuGet to install it: Install-Package Expression.Blend.Sdk):
<ComboBox ItemsSource="{Binding DataGridColumnsModel.DataTypesCollection,RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding UpdateItemSourceCommand,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
Each time the selectionChanged event occurred, update your DataGrid's ItemSource in the UpdateItemSourceCommand that should be added to your mainWindow's ViewModel:
private RelayCommand _updateItemSourceCommand;
public RelayCommand UpdateItemSourceCommand
{
get
{
return _updateItemSourceCommand
?? (_updateItemSourceCommand = new RelayCommand(
() =>
{
//Update your DataGridCollection, you could also pass a parameter and use it.
//Update your DataGridCollection based on DataGridColumnsModel.HeaderPropertyCollection
}));
}
}
Ps: the RelayCommand class i am using is part of GalaSoft.MvvmLight.Command namespace, you could add it via NuGet, or define your own command.
Finally here the full xaml code:
Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<local:GetPropertConverter x:Key="GetPropertConverter"/>
</Window.Resources>
<Grid>
<DataGrid x:Name="SampleGrid" ItemsSource="{Binding DataGridCollection}" AutoGenerateColumns="False">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel>
<TextBox >
<TextBox.Text>
<MultiBinding Converter="{StaticResource GetPropertConverter}">
<Binding RelativeSource="{RelativeSource AncestorType={x:Type Window}}" Path="DataGridColumnsModel.HeaderPropertyCollection"/>
<Binding Path="DisplayIndex" Mode="OneWay" RelativeSource="{RelativeSource AncestorType={x:Type DataGridColumnHeader}}"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
<ComboBox ItemsSource="{Binding DataGridColumnsModel.DataTypesCollection,RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding UpdateItemSourceCommand,RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="First Column" Binding="{Binding FirstProperty}" />
<DataGridTextColumn Header="Second Column" Binding="{Binding SecondProperty}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
And view models / codebehind:
public class GetPropertConverter:IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
try
{
var theCollection = values[0] as ObservableCollection<Tuple<string, string>>;
return (theCollection?[(int)values[1]])?.Item1; //Item1 is the column name, Item2 is the column's ocmbobox's selectedItem
}
catch (Exception)
{
//use a better implementation!
return null;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class DataGridColumnsModel:INotifyPropertyChanged
{
private ObservableCollection<string> _dataTypesCollection = new ObservableCollection<string>()
{
"Date","String","Number"
};
public ObservableCollection<string> DataTypesCollection
{
get { return _dataTypesCollection; }
set
{
if (Equals(value, _dataTypesCollection)) return;
_dataTypesCollection = value;
OnPropertyChanged();
}
}
private ObservableCollection<Tuple<string, string>> _headerPropertiesCollection=new ObservableCollection<Tuple<string, string>>()
{
new Tuple<string, string>("Column 1", "Date"),
new Tuple<string, string>("Column 2", "String")
}; //The Dictionary has a PropertyName (Item1), and a PropertyDataType (Item2)
public ObservableCollection<Tuple<string,string>> HeaderPropertyCollection
{
get { return _headerPropertiesCollection; }
set
{
if (Equals(value, _headerPropertiesCollection)) return;
_headerPropertiesCollection = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class DataGridModel
{
public string FirstProperty { get; set; }
public string SecondProperty { get; set; }
}
public partial class MainWindow : Window,INotifyPropertyChanged
{
private RelayCommand _updateItemSourceCommand;
public RelayCommand UpdateItemSourceCommand
{
get
{
return _updateItemSourceCommand
?? (_updateItemSourceCommand = new RelayCommand(
() =>
{
//Update your DataGridCollection, you could also pass a parameter and use it.
MessageBox.Show("Update has ocured");
}));
}
}
private ObservableCollection<DataGridModel> _dataGridCollection=new ObservableCollection<DataGridModel>()
{
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"},
new DataGridModel(){FirstProperty = "first item",SecondProperty = "second item"}
};
public ObservableCollection<DataGridModel> DataGridCollection
{
get { return _dataGridCollection; }
set
{
if (Equals(value, _dataGridCollection)) return;
_dataGridCollection = value;
OnPropertyChanged();
}
}
private DataGridColumnsModel _dataGridColumnsModel=new DataGridColumnsModel();
public DataGridColumnsModel DataGridColumnsModel
{
get { return _dataGridColumnsModel; }
set
{
if (Equals(value, _dataGridColumnsModel)) return;
_dataGridColumnsModel = value;
OnPropertyChanged();
}
}
public MainWindow()
{
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Result:
Update
You will achieve the same result by setting AutoGenerateColumns="True" and creating you columns dynamically.
This is not exactly a complete answer but more a hint towards what I think your looking to do, if so you can query me for additional information.
I think what you want to do is define let say a DataGridColumDef type such as this:
public class DataGridColumnDef : NotifyPropertyChangeModel
{
public string Name
{
get => _Name;
set => SetValue(ref _Name, value);
}
private string _Name;
public Type DataType
{
get => _DataType;
set => SetValue(ref _DataType, value);
}
private Type _DataType;
public DataGridColumnDef(string name, Type type)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
DataType = type ?? throw new ArgumentNullException(nameof(type));
}
}
Then I imagine your view model acting as the data context for the DataGrid could look something like this:
public class MainViewModel : NotifyPropertyChangeModel
{
public ObservableList<DataGridColumnDef> ColumnDefinitions
{
get => _ColumnDefinitions;
set => SetValue(ref _ColumnDefinitions, value);
}
private ObservableList<DataGridColumnDef> _ColumnDefinitions;
public ObservableList<DataGridRowDef> RowDefinitions
{
get => _RowDefinitions;
set => SetValue(ref _RowDefinitions, value);
}
private ObservableList<DataGridRowDef> _RowDefinitions;
public MainViewModel()
{
// Define initial columns
ColumnDefinitions = new ObservableList<DataGridColumnDef>()
{
new DataGridColumnDef("Column 1", typeof(string)),
new DataGridColumnDef("Column 2", typeof(int)),
};
// Create row models from initial column definitions
RowDefinitions = new ObservableList<DataGridRowDef>();
for(int i = 0; i < 100; ++i)
{
RowDefinitions.Add(new DataGridRowDef(ColumnDefinitions));
// OR
//RowDefinitions.Add(new DataGridRowDef(ColumnDefinitions, new object[] { "default", 10 }));
}
}
}
This way on the main view model you could subscribe to collection/property changed events in the ColumnDefinitions property and then re-create the rows collection.
Now the trick that I am not 100% sure would work, but not sure why it wouldn't, is making your DataGridRowDef type inherit from DynamicObject so you can spoof members and their values, something like this:
public class DataGridRowDef : DynamicObject
{
private readonly object[] _columnData;
private readonly IList<DataGridColumnDef> _columns;
public static object GetDefault(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
return null;
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return _columns.Select(c => c.Name).Union(base.GetDynamicMemberNames());
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var columnNames = _columns.Select(c => c.Name).ToList();
if(columnNames.Contains(binder.Name))
{
var columnIndex = columnNames.IndexOf(binder.Name);
result = _columnData[columnIndex];
return true;
}
return base.TryGetMember(binder, out result);
}
public DataGridRowDef(IEnumerable<DataGridColumnDef> columns, object[] columnData = null)
{
_columns = columns.ToList() ?? throw new ArgumentNullException(nameof(columns));
if (columnData == null)
{
_columnData = new object[_columns.Count()];
for (int i = 0; i < _columns.Count(); ++i)
{
_columnData[i] = GetDefault(_columns[i].DataType);
}
}
else
{
_columnData = columnData;
}
}
}
Anyway if this kind of solution seems approachable to you I can try and work through it a bit more possibly.
Try this.
Window1.xaml
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:this="clr-namespace:WpfApplication1"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<this:RowDataConverter x:Key="RowDataConverter1" />
</Window.Resources>
<Grid>
<DataGrid ItemsSource="{Binding Rows, Mode=OneWay}">
<DataGrid.Columns>
<DataGridTextColumn>
<DataGridTextColumn.Binding>
<MultiBinding Converter="{StaticResource RowDataConverter1}">
<Binding Path="Column1" Mode="OneWay" />
<Binding Path="Column1OptionString" Mode="OneWay" RelativeSource="{RelativeSource AncestorType=Window, Mode=FindAncestor}" />
</MultiBinding>
</DataGridTextColumn.Binding>
<DataGridTextColumn.HeaderTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="Column Header 1" />
<ComboBox ItemsSource="{Binding ColumnOptions, Mode=OneWay, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
SelectedValue="{Binding Column1OptionString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
SelectedValuePath="Option">
<ComboBox.ItemTemplate>
<DataTemplate DataType="this:ColumnOption">
<TextBlock Text="{Binding Option, Mode=OneTime}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</DataTemplate>
</DataGridTextColumn.HeaderTemplate>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Window1.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication1
{
public partial class Window1 : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public List<RowData> Rows { get; set; }
public List<ColumnOption> ColumnOptions { get; set; }
private string _column1OptionString;
public string Column1OptionString
{
get
{
return _column1OptionString;
}
set
{
_column1OptionString = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Column1OptionString"));
}
}
public Window1()
{
InitializeComponent();
ColumnOptions = new List<ColumnOption>() {
new ColumnOption(){ Option = "String", StringFormat = "" },
new ColumnOption(){ Option = "Int32", StringFormat = "" }
};
Rows = new List<RowData>() {
new RowData(){ Column1 = "01234" }
};
_column1OptionString = "String";
this.DataContext = this;
}
}
public class ColumnOption
{
public string Option { get; set; }
public string StringFormat { get; set; }
}
public class RowData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private object _column1;
public object Column1
{
get
{
return _column1;
}
set
{
_column1 = value;
if (PropertyChanged!= null)
PropertyChanged(this, new PropertyChangedEventArgs("Column1"));
}
}
}
public class RowDataConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values[1] == null)
return values[0].ToString();
switch (values[1].ToString())
{
case "String":
return values[0].ToString();
case "Int32":
Int32 valueInt;
Int32.TryParse(values[0].ToString(), out valueInt);
return valueInt.ToString();
default:
return values[0].ToString();
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
UPDATE
based on #FCin comment
"This is nice, but I use this to load csv files and the number of columns changes depending of csv file. Here I have to hardcore each column, but some files may have 1 column and some might have 30 columns".
Assume your csv file using format:
line1: Headers,
line2: Data Type,
line3-end: Records.
Example data1.csv:
ColumnHeader1,ColumnHeader2
Int32,String
1,"A"
2,"B"
3,"C"
I try to parse csv file using TextFieldParser, then generate the DataGrid's columns programmatically.
Window2.xaml
<Window x:Class="WpfApplication1.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<Label Content="File:" />
<ComboBox x:Name="FileOption"
SelectionChanged="FileOption_SelectionChanged">
<ComboBox.Items>
<Run Text="Data1.csv" />
<Run Text="Data2.csv" />
</ComboBox.Items>
</ComboBox>
</StackPanel>
<DataGrid x:Name="DataGrid1" Grid.Row="1"
AutoGenerateColumns="False"
ItemsSource="{Binding ListOfRecords, Mode=OneWay}">
</DataGrid>
</Grid>
</Window>
Window2.xaml.cs
using Microsoft.VisualBasic.FileIO;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
namespace WpfApplication1
{
public partial class Window2 : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
List<myDynamicObject> _listOfRecords;
public List<myDynamicObject> ListOfRecords
{
get
{
return _listOfRecords;
}
}
public Window2()
{
InitializeComponent();
DataContext = this;
}
public void LoadData(string fileName)
{
_listOfRecords = new List<myDynamicObject>();
myDynamicObject record;
TextFieldParser textFieldParser = new TextFieldParser(fileName);
textFieldParser.TextFieldType = FieldType.Delimited;
textFieldParser.SetDelimiters(",");
string[] headers = null;
string[] dataTypes = null;
string[] fields;
int i = 0;
while(!textFieldParser.EndOfData)
{
fields = textFieldParser.ReadFields();
if (i == 0)
{
headers = fields;
}
else if (i == 1)
{
dataTypes = fields;
}
else
{
record = new myDynamicObject();
for (int j = 0; j < fields.Length; j++)
{
switch(dataTypes[j].ToLower())
{
case "string":
record.SetMember(headers[j], fields[j]);
break;
case "int32":
Int32 data;
if (Int32.TryParse(fields[j], out data))
{
record.SetMember(headers[j], data);
}
break;
default:
record.SetMember(headers[j], fields[j]);
break;
}
}
_listOfRecords.Add(record);
}
i += 1;
}
PropertyChanged(this, new PropertyChangedEventArgs("ListOfRecords"));
DataGrid1.Columns.Clear();
for (int j = 0; j < headers.Length; j++)
{
DataGrid1.Columns.Add(new DataGridTextColumn()
{
Header = headers[j],
Binding = new Binding()
{
Path = new PropertyPath(headers[j]),
Mode = BindingMode.OneWay
}
});
}
}
private void FileOption_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
LoadData((FileOption.SelectedItem as Run).Text);
}
}
public class myDynamicObject : DynamicObject
{
Dictionary<string, object> dictionary = new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
string name = binder.Name;
return dictionary.TryGetValue(name, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
dictionary[binder.Name] = value;
return true;
}
public void SetMember(string propertyName, object value)
{
dictionary[propertyName] = value;
}
}
}

Multiple Language Binding ItemsSource

I want to change my single language project into multiple language, so I use ResourceDictionary to do it like this:
XAML
<Button Content="{DynamicResource LanguageSetting}" Click="btn_LanguageSetting_Click"/>
Code Behind
public static string windowCurrentLanguageFile = "Language/en.xaml";
private void btn_LanguageSetting_Click(object sender, RoutedEventArgs e)
{
windowCurrentLanguageFile = windowCurrentLanguageFile == "Language/en.xaml"
? "Language/ch.xaml"
: "Language/en.xaml";
var rd = new ResourceDictionary() { Source = new Uri(windowCurrentLanguageFile, UriKind.RelativeOrAbsolute) };
if (this.Resources.MergedDictionaries.Count == 0)
this.Resources.MergedDictionaries.Add(rd);
else
this.Resources.MergedDictionaries[0] = rd;
}
This works fine for me. But I have an ItemsControl
<ItemsControl ItemsSource="{Binding ItemOperate}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewmodel:SelectableViewModel}">
<Border x:Name="Border" Padding="0,8,0,8" BorderThickness="0 0 0 1" BorderBrush="{DynamicResource MaterialDesignDivider}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="Checkerz" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ToggleButton VerticalAlignment="Center" IsChecked="{Binding IsSelected}"
Style="{StaticResource MaterialDesignActionLightToggleButton}"
Content="{Binding Code}" />
<StackPanel Margin="8 0 0 0" Grid.Column="7">
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="Border" Property="Background" Value="{DynamicResource MaterialDesignSelection}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Which Binding to the ViewModel like this:
public class SelectableViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected == value) return;
_isSelected = value;
OnPropertyChanged();
}
}
private char _code;
public char Code
{
get { return _code; }
set
{
if (_code == value) return;
_code = value;
OnPropertyChanged();
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
OnPropertyChanged();
}
}
private string _description;
public string Description
{
get { return _description; }
set
{
if (_description == value) return;
_description = value;
OnPropertyChanged();
}
}
}
And
public MainViewModel()
{
_itemOperate = CreateData();
}
private static ObservableCollection<SelectableViewModel> CreateData()
{
return new ObservableCollection<SelectableViewModel>
{
new SelectableViewModel
{
Code = 'E',
Name = "Erase",
Description = "Erase The MCU Chip By Page"
},
new SelectableViewModel
{
Code = 'D',
Name = "Detect",
Description = "Detect The MCU Flash",
},
new SelectableViewModel
{
Code = 'P',
Name = "Programming",
Description = "Programming The MCU Chip By Hex File",
},
new SelectableViewModel
{
Code = 'V',
Name = "Verify",
Description = "Verify The Downing Code",
},
new SelectableViewModel
{
Code ='L',
Name = "Lock",
Description = "Lock The Code To Protect The MCU",
}
};
}
So how should I change this into multiple language?
First of all, I would recommend you to change your localization engine.
There are a lot of different ways.
There is the simplest variant:
https://www.codeproject.com/Articles/299436/WPF-Localization-for-Dummies
Also, this tool will help you to manage your resources files:
https://marketplace.visualstudio.com/items?itemName=TomEnglert.ResXManager
And the answer to your question:
If you want to localize your model, you should create it using resources dictionary but not hard coded strings.
It's quite easy if you implement your localization engine like in mentioned
article.
{
return new ObservableCollection<SelectableViewModel>
{
new SelectableViewModel
{
Code = 'E',
Name = YourResourcesProject.Resources.Erase,
Description = YourResourcesProject.Resources.EraseTheMCUChipByPage
},
new SelectableViewModel
{
Code = 'D',
Name = YourResourcesProject.Resources.Detect,
Description = YourResourcesProject.Resources.DetectTheMCUFlash
},
new SelectableViewModel
{
Code = 'P',
Name = YourResourcesProject.Resources.Programming,
Description = YourResourcesProject.Resources.ProgrammingTheMCUChipByHexFile
},
new SelectableViewModel
{
Code = 'V',
Name = YourResourcesProject.Resources.Verify,
Description = YourResourcesProject.Resources.VerifyTheDowningCode
},
new SelectableViewModel
{
Code ='L',
Name = YourResourcesProject.Resources.Lock,
Description = YourResourcesProject.Resources.LockTheCodeToProtectTheMCU
}
};
}
Don't change anything...make your collection of SelectableViewModel into a XML and change the CreateData to load it into the localized version - you can either have a file for each language or mix all together
20 lines of code....as you want - cannot more explicit than that :-(
public class Selectable(View)Model
{
[XmlAttribute]
public string Code { get; set; }
[XmlAttribute]
public string Name { get; set; }
[XmlAttribute]
public string Description { get; set; }
}
///you can deserialize your view model directly
private ObservableCollection<SelectableViewModel> CreateData()
{
return new ObservableCollection<SelectableViewModel>( Deserialize( file_name_code_lang.xml, SelectableViewModel) );
}
// or going through a model class
private ObservableCollection<SelectableViewModel> CreateData()
{
return new ObservableCollection<SelectableViewModel>( Deserialize( file_name_code_lang.xml, SelectableModel ).Foreach(p=> new SelectableViewModel(p) );
}
static public object Deserialize(string filePath, Type objType)
{
object objToDeserialize = null;
XmlTextReader xmlReader = null;
XmlSerializer xmls = null;
try
{
xmlReader = new XmlTextReader(filePath);
xmls = new XmlSerializer(objType);
objToDeserialize = xmls.Deserialize(xmlReader);
}
catch (Exception err)
{
BusinessLogger.Manage(err);
return null;
}
finally
{
xmls = null;
xmlReader.Close();
}
return objToDeserialize;
}

How to avoid cumbersome code while binding form controls to classes hierarchy?

I have a form, which is binded to the model. There is one standard, basic model and few children models (with additional fields).
Above the model's controls there is a radio buttons group and upon selecting one of them the forementioned additional fields appear (in this case the sentence field).
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void sentencedPersonRadioButton_Checked(object sender, RoutedEventArgs e)
{
sentenceTextBox.Visibility = Visibility.Visible;
DataContext = new SentencedPerson();
}
private void personRadioButton_Checked(object sender, RoutedEventArgs e)
{
sentenceTextBox.Visibility = Visibility.Hidden;
DataContext = new Person();
}
}
Lets say there is a Person and SentencedPerson:
public class Person: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private String name;
public String Name
{
get
{
return name;
}
set
{
if (value == name)
{
return;
}
name = value;
OnPropertyChanged("Name");
}
}
}
public class SentencedPerson : Person
{
private String sentence;
public String Sentence
{
get
{
return sentence;
}
set
{
if (value == sentence)
{
return;
}
sentence = value;
OnPropertyChanged("Sentence");
}
}
}
What is the proper way to design such a connections? Adding new 'checked' event handlers feels so cumbersome... I heard about MVVM pattern in which there would be some kind of PersonContext with Person and SentencedPerson props inside. But it does not change the need of 'checked' events.
Also know there is a problem because the values of common fields are after setting the new DataContext.
This is a quite broad question but I will give you some pointers.
MVVM is the recommended design pattern to use when building XAML based applications.
You could create a view model class with a "CurrentSelectedContent" property of type object or Person and an enum property that you bind the RadioButton to.
Please refer to the following link for more information and an example of how to bind a RadioButton to an enum source property using MVVM:
How to bind RadioButtons to an enum?
Once you have done this you could set the value of the "CurrentSelectedContent" property based on the radio button selection in the setter of the enum source property in the view model:
private MyLovelyEnum _enum;
public MyLovelyEnum VeryLovelyEnum
{
get
{
return _enum;
}
set
{
_enum = value;
switch (value)
{
case MyLovelyEnum.Person:
CurrentSelectedContent = new Person();
break;
//...
}
OnPropertyChanged("VeryLovelyEnum");
}
}
Make sure that the "CurrentSelectedContent" property raises the PropertyChanged event and that the view model class implements the INotifyPropertyChanged interface.
In the view you could then use a ContentControl and bind its Content property to the "CurrentSelectedContent" property:
<ContentControl Content="{Binding Content}">
<ContentControl.ContentTemplate>
<DataTemplate>
<TextBox Text="{Binding Name}" />
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
Also make sure that you set the DataContext of the view to an instance of your view model:
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
This is the rough idea on how to do this using the MVVM pattern. Instead of handling events in the code-behind of the view you bind to source properties and instead of setting the DataContext property of specific UI elements explicitly you bind the Content property of a ContentControl to an object that you create in the view model class.
Hope that helps.
You just need One model:
public class Person : INotifyPropertyChanged
{
string _name;
public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } }
bool _isSentenced;
public bool IsSentenced { get { return _isSentenced; } set { _isSentenced = value; RaisePropertyChanged("IsSentenced"); } }
string _sentence;
public string Sentence { get { return _sentence; } set { _sentence = value; RaisePropertyChanged("Sentence"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
Use IsSentenced and bind the RadioButton to it. Also, check the Visibility of the TextBox displaying the Sentence string to the IsChecked property of the RadioButton, using a Visibility to Bool converter. Here is a simple example:
<Window.Resources>
<local:VisibilityToBoolConverter x:Key="VisibilityToBoolConverter"/>
</Window.Resources>
<ListBox DataContext="{Binding}" ItemsSource="{Binding Persons}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Name}" />
<RadioButton Content="Is sentenced to death" IsChecked="{Binding IsSentenced}" />
<DockPanel Visibility="{Binding IsSentenced , Converter={StaticResource VisibilityToBoolConverter}}">
<Label Content="Sentence: "/>
<TextBlock Text="{Binding Sentence}" />
</DockPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
in which
public class VisibilityToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((bool)value == true)
return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((Visibility)value == Visibility.Visible)
return true;
return false;
}
}
and the ViewModel is:
public class PersonViewModel : INotifyPropertyChanged
{
public PersonViewModel()
{
Person m1 = new Person() { Name = "person 1", IsSentenced = false, Sentence = "S S S" };
Person m2 = new Person() { Name = "person 2", IsSentenced = false, Sentence = "B B B" };
Person m3 = new Person() { Name = "person 3", IsSentenced = true, Sentence = "F F F" };
_persons = new ObservableCollection<Person>() { m1, m2, m3 };
}
ObservableCollection<Person> _persons;
public ObservableCollection<Person> Persons { get { return _persons; } set { _persons = value; RaisePropertyChanged("Persons"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
your main window should set the DataContext:
public MainWindow()
{
PersonViewModel mv = new PersonViewModel();
this.DataContext = mv;
InitializeComponent();
}
Edit
If there are many states for a person, ComboBox is a more natural choice. You should have an Enum that describes the states:
public enum MyTypes
{
None,
IsA,
IsB,
IsC
}
and the Person should have a peroperty that shows the state:
public class Person : INotifyPropertyChanged
{
MyTypes _thetype;
public MyTypes TheType { get { return _thetype; } set { _thetype = value; RaisePropertyChanged("TheType"); } }
string _name;
public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
Since you need to bind the ItemsSource of your ComboBox to a list of states, one possibility is to adjust the ViewModel to have such a list:
public class PersonViewModel : INotifyPropertyChanged
{
public PersonViewModel()
{
Person m0 = new Person() { Name = "person 1", TheType = MyTypes.None };
Person m1 = new Person() { Name = "person 1", TheType = MyTypes.IsA };
Person m2 = new Person() { Name = "person 2", TheType = MyTypes.IsB };
Person m3 = new Person() { Name = "person 3", TheType = MyTypes.IsC };
_persons = new ObservableCollection<Person>() { m0, m1, m2, m3 };
_types = Enum.GetNames(typeof(MyTypes)).ToList();
}
List<string> _types;
public List<string> Types { get { return _types; } }
ObservableCollection<Person> _persons;
public ObservableCollection<Person> Persons { get { return _persons; } set { _persons = value; RaisePropertyChanged("Persons"); } }
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
and at last, the View:
<Window.Resources>
<local:EnumToSentenceConverterx:Key="EnumToSentenceConverter"/>
<local:NoneToCollapsedConverter x:Key="NoneToCollapsedConverter"/>
<local:EnumToStringConverter x:Key="EnumToStringConverter"/>
</Window.Resources>
<ListBox Name="lb" DataContext="{Binding}" ItemsSource="{Binding Persons}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Name}" />
<ComboBox Name="cb" ItemsSource="{Binding ElementName=lb, Path=DataContext.Types}" SelectedValue="{Binding TheType, Mode=TwoWay, Converter={StaticResource EnumToStringConverter}}" />
<DockPanel Visibility="{Binding ElementName=cb, Path=SelectedValue, Converter={StaticResource NoneToCollapsedConverter}}">
<Label Content="Sentence: " DockPanel.Dock="Left"/>
<TextBlock Text="{Binding TheType, Converter={StaticResource EnumToStringConverter}}" DockPanel.Dock="Right" VerticalAlignment="Center" />
</DockPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Note that you need three converters. One sets the Visibility of The Sentence part to Collapsed, it the type is None:
public class NoneToCollapsedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.ToString() == "None")
return Visibility.Collapsed;
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The two others are self descriptive:
public class EnumToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Enum.Parse(typeof(MyTypes), value.ToString());
}
}
and
public class EnumToSentenceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
switch ((MyTypes)value)
{
case MyTypes.IsA:
break;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Hope it helps.

Combox SelectedItem does not apply when restoring from serialized ViewModel

I'm facing a strange problem when using C# WPF and MVVM Pattern while restoring a ViewModel (serialized using Json.Net).
The idea of the software is - when closing the window - to persist the current Viewmodel state in a json file.
At the next startup the app just serarches for the json.
If there a file, then deserialize it and restore the ViewModel (set public properties).
If there is no file, then the viewmodel is created and default values are set.
Now my problem is, that when restoring it with the json file, a combobox containing a list of a custom type, the combobox has values but no SelectedItem. When creating the viewmodel instance and initiailizing the public properties with default values (doing this via the code behind) then everything is fine.
Here is some code that represents the "error":
View
<Window x:Class="CrazyWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CrazyWpf"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
Closing="Window_Closing"
Loaded="Window_Loaded">
<StackPanel
x:Name="rootElement"
Orientation="Vertical"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="10">
<StackPanel.DataContext>
<local:DemoViewModel />
</StackPanel.DataContext>
<StackPanel
Orientation="Horizontal">
<Label
x:Name="lblID"
Width="30"
Content="ID:"/>
<TextBox
x:Name="tbID"
Width="50"
Margin="30,0,0,0"
Text="{Binding ID, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel
Orientation="Horizontal">
<Label
x:Name="lblName"
Width="45"
Content="Name:"/>
<TextBox
x:Name="tbName"
Width="200"
Margin="15,0,0,0"
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<StackPanel
Orientation="Horizontal">
<Label
x:Name="lblStai"
Width="60"
Content="Status:"/>
<ComboBox
x:Name="cbStati"
Width="200"
ItemsSource="{Binding StatusTypeList}"
SelectedItem="{Binding StatusType, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Name"/>
</StackPanel>
</StackPanel>
</Window>
Code Behind
using System;
using System.Windows;
using System.IO;
using Newtonsoft.Json;
namespace CrazyWpf
{
public partial class MainWindow : Window
{
private DemoViewModel dvm;
public MainWindow()
{
InitializeComponent();
this.dvm = (DemoViewModel)this.rootElement.DataContext;
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
string filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "settings.json");
if (File.Exists(filePath))
File.Delete(filePath);
File.WriteAllText(filePath, JsonConvert.SerializeObject(this.dvm, Formatting.Indented));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
string filePath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "settings.json");
if (!File.Exists(filePath))
{ this.SetDefaultSettings(); return; }
DemoViewModel d = JsonConvert.DeserializeObject<DemoViewModel>(File.ReadAllText(filePath));
this.dvm.ID = d.ID;
this.dvm.Name = d.Name;
this.dvm.StatusType = d.StatusType;
}
}
}
BaseViewModel:
using System.ComponentModel;
namespace CrazyWpf
{
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ViewModel
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace CrazyWpf
{
class DemoViewModel : BaseViewModel
{
[JsonIgnore]
private int id;
[JsonProperty(Order = 1)]
public int ID
{
get { return this.id; }
set
{
if (this.id != value)
{
this.id = value;
this.OnPropertyChanged("ID");
}
}
}
[JsonIgnore]
private string name;
[JsonProperty(Order = 2)]
public string Name
{
get { return this.name; }
set
{
if (this.name != value && value != null)
{
this.name = value;
this.OnPropertyChanged("Name");
}
}
}
[JsonIgnore]
private StatusTyp statusType;
[JsonProperty(Order = 3)]
public StatusTyp StatusType
{
get { return this.statusType; }
set
{
if (this.statusType != value && value != null)
{
this.statusType = value;
this.OnPropertyChanged("StatusType");
}
}
}
[JsonIgnore]
private List<StatusTyp> statusTypeList;
[JsonProperty(Order = 4)]
public List<StatusTyp> StatusTypeList
{
get { return this.statusTypeList; }
set
{
if (this.statusTypeList != value && value != null)
{
this.statusTypeList = value;
this.OnPropertyChanged("StatusTypeList");
}
}
}
public DemoViewModel()
{
this.StatusTypeList = new Func<List<StatusTyp>>(() =>
{
var list = Enum.GetValues(typeof(Status))
.Cast<Status>()
.ToDictionary(k => (int)k, v => v.ToString())
.Select(e => new StatusTyp()
{
Value = e.Key,
Name = e.Value,
Status =
Enum.GetValues(typeof(Status))
.Cast<Status>().
Where(x =>
{
return (int)x == e.Key;
}).FirstOrDefault()
})
.ToList();
return list;
})();
}
}
public class StatusTyp
{
public int Value { get; set; }
public string Name { get; set; }
public Status Status { get; set; }
}
public enum Status
{
NotDetermined = 0,
Determined = 1,
Undeterminded = 2,
Unknown = 3
}
}
If you have an ItemsSource and a SelectedItem, the instance in SelectedItem MUST BE in the collection bound to ItemsSource. If it is not, then your bindings will not work as expected.
The control uses reference equality to determine which item in ItemsSource is the one in SelectedItem and update the UI. This normally isn't a problem as the control populates SelectedItem for you, but if you are updating from the ViewModel side, you have to make sure your references are managed correctly.
This can be an issue when serializing/deserializing your view model. Most common serializers don't track references, and so cannot restore these on deserialization. The same object may be referenced multiple places in the original object graph, but after deserialization you now have multiple instances of the original spread throughout the rehydrated graph. This won't work with your requirements.
What you have to do is, after deserializing, find the matching instance in your collection and substitute it for the instance in SelectedItem. Or, use a serializer that tracks instances.. The XAML serializer already does this, and is a surprisingly good xml serializer for .net object graphs.

Categories

Resources