I've merged two programs, and now there are two DataContexts in my code. One is DataContext = this; and the other is DataContext = _uiData;. I'd like to keep the former, because there are many bindings relying on this DataContext (not shown in my code, though). For the latter, I changed it to textBox.Text = _uiData.ToString();, but it doesn't show anything. How would you take care of this kind of situation?
Here is my code:
MainWindow.xaml.cs
using Both_ListBox_n_TextBox_Binding.ViewModel;
using System.Collections.ObjectModel;
using System.Windows;
namespace Both_ListBox_n_TextBox_Binding
{
public partial class MainWindow : Window
{
public ObservableCollection<GraphViewModel> RightListBoxItems { get; }
= new ObservableCollection<GraphViewModel>();
private UISimpleData _uiData = new UISimpleData();
public MainWindow()
{
InitializeComponent();
RightListBoxItems.Add(new GraphViewModel("T1"));
RightListBoxItems.Add(new GraphViewModel("T2"));
RightListBoxItems.Add(new GraphViewModel("T3"));
DataContext = _uiData; // How can I show this?
//textBox.Text = _uiData.ToString(); // This doesn't show anything
DataContext = this; // I'd like to KEEP this
//RightListBox.ItemsSource = RightListBoxItems; // Works, but not for this time
}
}
}
MainWindow.xaml
<Window x:Class="Both_ListBox_n_TextBox_Binding.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:Both_ListBox_n_TextBox_Binding"
mc:Ignorable="d"
Title="MainWindow" Height="253.5" Width="297.5">
<Grid>
<ListBox x:Name="RightListBox"
ItemsSource="{Binding RightListBoxItems}" DisplayMemberPath="Text"
SelectionMode="Extended" Margin="20,20,20,100"/>
<TextBox Margin="100,0,0,40" HorizontalAlignment="Left" VerticalAlignment="Bottom" Height="22" Width="100"
Text="{Binding DoubleField, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding (Validation.Errors)[0].ErrorContent,
RelativeSource={RelativeSource Self}}"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</Grid>
</Window>
ViewModel/UISimpleData.cs
using System;
using System.ComponentModel;
namespace Both_ListBox_n_TextBox_Binding.ViewModel
{
public class UISimpleData : INotifyPropertyChanged, IDataErrorInfo
{
private double _doubleField = 5.5;
public double DoubleField
{
get
{
return _doubleField;
}
set
{
if (_doubleField == value)
return;
_doubleField = value;
RaisePropertyChanged("DoubleField");
}
}
public string this[string propertyName]
{
get
{
string validationResult = null;
switch (propertyName)
{
case "DoubleField":
{
if (DoubleField < 0.0 || DoubleField > 10.0)
validationResult = "DoubleField is out of range";
break;
}
default:
throw new ApplicationException("Unknown Property being validated on UIData");
}
return validationResult;
}
}
public string Error { get { return "Not Implemented"; } }
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
}
}
ViewModel/GraphViewModel.cs
namespace Both_ListBox_n_TextBox_Binding.ViewModel
{
public class GraphViewModel
{
public string Text { get; }
public GraphViewModel(string text) => Text = text;
}
}
Thank you in advance.
All code is C# & XAML using .NET Framework 4.6.1.
Minimal, Reproducable Example
App.xaml
<Application x:Class="ComboBoxSwapTest.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources />
</Application>
App.xaml.cs
using System.Windows;
namespace ComboBoxSwapTest
{
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
var vm = new ViewModel();
var window = new MainWindow() { DataContext = vm };
window.Show();
base.OnStartup(e);
}
}
}
Option.cs
namespace ComboBoxSwapTest
{
public class Option
{
public int Id { get; set; }
public string Number { get; set; }
public string Name { get; set; }
public string NumberName { get { return string.Format("{0} - {1}", Number, Name); } }
public string NameNumber { get { return string.Format("{1} - {0}", Number, Name); } }
}
}
ViewModel.cs
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace ComboBoxSwapTest
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool _nameFirst = true;
public bool NameFirst
{
get { return _nameFirst; }
set
{
if (_nameFirst != value)
{
_nameFirst = value;
RaisePropertyChanged(nameof(NameFirst));
}
}
}
public List<Option> Options { get; }
public List<Option> NameNumberOptions
{
get { return Options.OrderBy(s => s.Name).ThenBy(s => s.Number).ToList(); }
}
public List<Option> NumberNameOptions
{
get { return Options.OrderBy(s => s.Number).ThenBy(s => s.Name).ToList(); }
}
private Option _selectedOption;
public Option SelectedOption
{
get { return _selectedOption; }
set
{
if (_selectedOption != value)
{
_selectedOption = value;
RaisePropertyChanged(nameof(SelectedOption));
}
}
}
public ViewModel()
{
Options = new List<Option>();
Options.Add(new Option() { Id = 1, Name = "Foo", Number = "111" });
Options.Add(new Option() { Id = 2, Name = "Bar", Number = "222" });
Options.Add(new Option() { Id = 3, Name = "Baz", Number = "333" });
}
}
}
InvertBoolConverter.cs
using System;
using System.Globalization;
using System.Windows.Data;
namespace ComboBoxSwapTest
{
public class InvertBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return !(bool)value;
}
}
}
MainWindow.xaml
<Window x:Class="ComboBoxSwapTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ComboBoxSwapTest"
Title="Main">
<Window.Resources>
<local:InvertBoolConverter x:Key="InvertBoolConverter" />
</Window.Resources>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<RadioButton GroupName="Toggle"
Content="Name First"
IsChecked="{Binding Path=NameFirst}" />
<RadioButton GroupName="Toggle"
Content="Number First"
IsChecked="{Binding Path=NameFirst, Converter={StaticResource InvertBoolConverter}}" />
</StackPanel>
<ComboBox IsEditable="False" IsReadOnly="False"
SelectedItem="{Binding Path=SelectedOption}">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource {x:Type ComboBox}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=NameFirst}" Value="True">
<Setter Property="DisplayMemberPath" Value="NameNumber" />
<Setter Property="ItemsSource" Value="{Binding Path=NameNumberOptions}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=NameFirst}" Value="False">
<Setter Property="DisplayMemberPath" Value="NumberName" />
<Setter Property="ItemsSource" Value="{Binding Path=NumberNameOptions}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</StackPanel>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace ComboBoxSwapTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
Steps to Reproduce:
1) Start Application.
2) Select an item from the drop down.
3) Change the radio button to Number First.
At this point, the item is still selected, but it is in a different format, and is using a list instance with a different sort.
4) Change the radio button back to Name First.
Now the item is no longer selected, while we are back to a list instance with the original sort.
That brings us to the title question: Why is SelectedItem cleared when changing ItemsSource using a DataTrigger when the item exists in both sources?
Note that I'm not looking for a philosophical why, as in why is the framework designed this way. I'm looking for the place in the framework design that causes this behavior.
I've tried to debug using decompiled sources, but a lot of variable values are optimized away so it's pretty hard to follow.
Here's some interesting things to try starting from the base example in each scenario:
Change the order of the data triggers. Now, the SelectedItem is cleared when switching to Number First, but it is preserved when switching to Name First.
Adding this line to the ComboBox's Style alongside the DataTriggers causes the SelectedItem to be preserved in both directions:
<Setter Property="ItemsSource" Value="{Binding Path=Options}" />
Expose a list like this:
public List<Option> SortedOptions
{
get { return NameFirst ? NameNumberOptions : NumberNameOptions; }
}
Make NameFirst indicate this property has changed as well, then bind to it instead of using DataTriggers. Again, the SelectedItem is preserved in both directions.
Change to using CollectionViewSources that are bound to the basic Options list, set the ComboBox's IsSynchronizedWithCurrentItem to False, and then swap between those sources with the DataTriggers. You get the same behavior where the SelectedItem clears.
Note that my actual solution uses CollectionViewSources to sort the lists and includes the "baseline" ItemsSource to workaround the lost selection. However, I'm still curious about why this behavior exists.
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;
}
}
}
I have the following code in my test application.
XAML file:
<Window x:Class="TestWpfApplication.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:TestWpfApplication"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<CheckBox Grid.Row="0" IsChecked="{Binding IsItemEnabled}" Margin="11,30,-10,129">IsItemEnabled</CheckBox>
<ComboBox Grid.Row="0" Margin="15,54,352,225" ItemsSource="{Binding Source={StaticResource dataFromEnum}}">
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="IsEnabled">
<Setter.Value>
<MultiBinding Converter="{StaticResource EnabledConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="Content" />
<Binding RelativeSource="{RelativeSource PreviousData}" Path="IsItemEnabled" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
</Grid>
</Window>
ViewModel
using System.ComponentModel;
using System.Runtime.CompilerServices;
using TestWpfApplication.Annotations;
namespace TestWpfApplication
{
public class MainWindowViewModel : INotifyPropertyChanged
{
private bool _isItemEnabled;
public event PropertyChangedEventHandler PropertyChanged;
public bool IsItemEnabled
{
get { return _isItemEnabled; }
set
{
_isItemEnabled = value;
OnPropertyChanged(nameof(IsItemEnabled));
}
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Converter
public class EnabledConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0].ToString() != "Time" || (bool)parameter != false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
How to do the same functionality when the combo box will bind with enum like this one? It`s possible at all?
public enum OpTypes
{
Time,
Cost,
Fuel
}
In the my converter i'm getting {DependencyProperty.UnsetValue} instead true or false and in the second parameter value i'm getting the values of enum. Another issue is that converter fires only first time when i open the combobox.
Or how to do it correct? Any suggestions please. Thanks in advance.
Now it works) Thanks for all
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using TestWpfApplication.Properties;
namespace TestWpfApplication
{
public class MainWindowViewModel : INotifyPropertyChanged
{
private bool _isItemEnabled;
private IList<OptType> _optimizationTypes;
private OptimizeTypes _selectedIndex;
public event PropertyChangedEventHandler PropertyChanged;
public OptimizeTypes SelectedIndex
{
get { return _selectedIndex;}
set
{
_selectedIndex = value;
OnPropertyChanged(nameof(SelectedIndex));
}
}
public IList<OptType> OptimizationTypes
{
get { return _optimizationTypes; }
set
{
_optimizationTypes = value;
OnPropertyChanged(nameof(OptimizationTypes));
}
}
public bool IsItemEnabled
{
get { return _isItemEnabled; }
set
{
_isItemEnabled = value;
InitOptimizationTypes(value);
OnPropertyChanged(nameof(IsItemEnabled));
}
}
public MainWindowViewModel()
{
InitOptimizationTypes(false);
}
private void InitOptimizationTypes(bool isEnabled)
{
OptimizationTypes = new List<OptType>
{
new OptType
{
TypeName = "Time",
IsItemEnabled = isEnabled,
},
new OptType
{
TypeName = "Cost",
IsItemEnabled = true,
},
new OptType
{
TypeName = "Fuel",
IsItemEnabled = true,
}
};
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
using System.Collections;
namespace TestWpfApplication
{
public class OptType
{
public string TypeName { get; set; }
public bool IsItemEnabled { get; set; }
}
}
<Window x:Class="TestWpfApplication.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:TestWpfApplication"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<CheckBox Grid.Row="0" IsChecked="{Binding IsItemEnabled}" Margin="11,30,15,129">IsItemEnabled</CheckBox>
<ComboBox Grid.Row="0" Margin="15,54,352,231" IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding OptimizationTypes}" DisplayMemberPath="TypeName"
SelectedIndex="{Binding SelectedIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="IsEnabled" Value="{Binding IsItemEnabled}"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
</Grid>
</Window
>
I want to bind datagrid view column visibility with a property of class.
I am passing a collection as ItemSource to grid.
I am not able to do this. Any idea why?
This one is a bit tricky. The problem comes from the fact that DataGrid.Columns is just a property and not part of the visual tree. This means that normal binding tools like ElementName or RelativeSource will not work. If, however, you override the Metadata for the DataGrid.DataContext property, you can get it to work properly. Example code below:
<Window x:Class="DataGridColumnVisibilitySample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
xmlns:l="clr-namespace:DataGridColumnVisibilitySample"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<l:VisibilityConverter x:Key="VisibilityC" />
</Window.Resources>
<DockPanel LastChildFill="True">
<CheckBox DockPanel.Dock="Top" Margin="8" Content="Show Column B" IsChecked="{Binding ShowColumnB}" />
<tk:DataGrid ItemsSource="{Binding Items}" AutoGenerateColumns="False" CanUserAddRows="False">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn Header="Column A" Binding="{Binding ColumnA}" />
<tk:DataGridTextColumn Header="Column B" Binding="{Binding ColumnB}"
Visibility="{Binding (FrameworkElement.DataContext).ShowColumnB,
RelativeSource={x:Static RelativeSource.Self},
Converter={StaticResource VisibilityC}}" />
<tk:DataGridTextColumn Header="Column C" Binding="{Binding ColumnC}" />
</tk:DataGrid.Columns>
</tk:DataGrid>
</DockPanel>
</Window>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using Microsoft.Windows.Controls;
namespace DataGridColumnVisibilitySample
{
public partial class Window1 : INotifyPropertyChanged
{
public Window1()
{
InitializeComponent();
new DataGridContextHelper(); // Initialize Helper
Items = Enumerable.Range(1, 3).Select(i => new MyItem {ColumnA = "A" + i, ColumnB = "B" + i, ColumnC = "C" + i}).ToList();
DataContext = this;
}
public List<MyItem> Items { get; private set; }
private bool mShowColumnB;
public bool ShowColumnB
{
get { return mShowColumnB; }
set
{
if (mShowColumnB == value) return;
mShowColumnB = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("ShowColumnB"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class DataGridContextHelper
{
static DataGridContextHelper()
{
FrameworkElement.DataContextProperty.OverrideMetadata(typeof(DataGrid),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, OnDataContextChanged));
}
public static void OnDataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grid = d as DataGrid;
if (grid == null) return;
foreach (var col in grid.Columns)
col.SetValue(FrameworkElement.DataContextProperty, e.NewValue);
}
}
[ValueConversion(typeof(bool), typeof(Visibility))]
public class VisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool && (bool)value)
return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class MyItem
{
public string ColumnA { get; set; }
public string ColumnB { get; set; }
public string ColumnC { get; set; }
}
}
I sourced this post by Jaime Rodriguez in creating my solution.
That works as long your datagrid is in a window, control, etc, if it's in a controltemplate this still won't work