WPF toolkit data column visibility binding - c#

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

Related

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;
}
}
}

IValueConverter not updating Datagrid When Combobox Selection is Changed

Im trying to learn and make the application using the MVVM principle. What Im working on is this,
When trying to implement 2 IValueConverters, only one of them is triggered i.e only one is loaded and works, the OnStockIconConverter, which is loaded when the Datagrid is initialised.
The other one, CurrencyConverter, which depends on a ComboBox toggle doesnt react on the DataGrid when I change the selected value.
I have been trying to solve this for hours, and most of the content out there is rather obscure for an MVVM beginner.
All other questions on here explain the opposite of what I want to do.
C#
namespace Coding_Dojo_3
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
///
///
public partial class MainWindow : Window
{
public ObservableCollection<StockEntryModel> StockEntriesCollection { get; }
public MainWindow()
{
InitializeComponent();
var sampleManager = new SampleManager();
StockEntriesCollection = new ObservableCollection<StockEntryModel>();
foreach (var stockEntry in sampleManager.CurrentStock.OnStock)
{
StockEntriesCollection.Add((new StockEntryModel{SoftwarePackage = stockEntry.SoftwarePackage, Amount = stockEntry.Amount}));
}
DataContext = this;
}
private void ComboBoxCurrency_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var text = ((sender as ComboBox).SelectedItem as ComboBoxItem).Content as string;
SelectedCurrency.Name = text;
Console.WriteLine("Currency changed to {0}", text);
}
}
public class StockEntryModel : INotifyPropertyChanged
{
private object _softwarePackage;
private int _amount;
public event PropertyChangedEventHandler PropertyChanged;
public object SoftwarePackage
{
get => _softwarePackage;
set
{
_softwarePackage = value;
OnPropertyChanged(nameof(SoftwarePackage));
}
}
public int Amount
{
get { return _amount; }
set
{
_amount = value;
OnPropertyChanged(nameof(Amount));
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class StockEntryViewModel
{
public StockEntryModel NewStockEntryModel { get; set; }
public StockEntryViewModel()
{
NewStockEntryModel = new StockEntryModel();
}
}
public static class SelectedCurrency
{
public static String Name;
}
public class CurrencyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var currency = SelectedCurrency.Name;
switch (currency)
{
case "EUR":
return CodingDojo4DataLib.Converter.CurrencyConverter.ConvertFromEuroTo(Currencies.EUR,
(double)value);
case "USD":
return CodingDojo4DataLib.Converter.CurrencyConverter.ConvertFromEuroTo(Currencies.EUR,
(double)value);
case "GBP":
return CodingDojo4DataLib.Converter.CurrencyConverter.ConvertFromEuroTo(Currencies.EUR,
(double)value);
default:
return CodingDojo4DataLib.Converter.CurrencyConverter.ConvertFromEuroTo(Currencies.EUR,
(double)value);
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class OnStockIconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int)
{
if ((int)value < 10)
return "red";
if ((int)value > 10 && (int)value < 20)
return "orange";
if ((int)value > 20)
return "green";
}
return "red";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
XAML
<Window x:Class="Coding_Dojo_3.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:Coding_Dojo_3"
mc:Ignorable="d"
Title="Coding Dojo 3" Height="400" Width="1024">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0.125*" />
<RowDefinition Height="1*" />
<RowDefinition Height="0.125*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<Label VerticalAlignment="Top">Currency :</Label>
<ComboBox AllowDrop="True" Name="ComboBoxCurrency" VerticalAlignment="Top" SelectionChanged="ComboBoxCurrency_SelectionChanged">
<ComboBoxItem IsSelected="True">EUR</ComboBoxItem>
<ComboBoxItem>USD</ComboBoxItem>
</ComboBox>
</StackPanel>
<DataGrid Grid.Row="1"
Name="DataGridStock"
AutoGenerateColumns="False"
ColumnWidth="*"
x:FieldModifier="public"
IsReadOnly="False"
CanUserAddRows="True"
CanUserDeleteRows="True"
ItemsSource="{Binding StockEntriesCollection}">
<DataGrid.Resources>
<local:CurrencyConverter x:Key="CurrencyConverter"/>
<local:OnStockIconConverter x:Key="OnStockIconConverter"/>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding SoftwarePackage.Name}"></DataGridTextColumn>
<DataGridTextColumn Header="Group" Binding="{Binding SoftwarePackage.Category.Name}"></DataGridTextColumn>
<DataGridTextColumn Header="Sales Price" Binding="{Binding SoftwarePackage.SalesPrice, NotifyOnTargetUpdated=True, Converter={StaticResource CurrencyConverter}}"></DataGridTextColumn>
<DataGridTextColumn Header="Purchase Price" Binding="{Binding SoftwarePackage.PurchasePrice, NotifyOnTargetUpdated=True, Converter={StaticResource CurrencyConverter}}"></DataGridTextColumn>
<DataGridTextColumn Header="Amount" Binding="{Binding Amount}"></DataGridTextColumn>
<DataGridTextColumn Header="On Stock" Binding="{Binding Amount, Converter={StaticResource OnStockIconConverter}}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<Button Margin="3">Add</Button>
<Button Margin="3">Edit</Button>
<Button Margin="3">Delete</Button>
</StackPanel>
</Grid>
</Window>
You should separate your viewmodel and view for you can work with real MVVM. Now your view is at the same time a viewmodel.
To reach the goal add to your entry model a method e.g. NotifyAllChanged to recalculate all properties and call it in your combobox event handler.
public class StockEntryModel : INotifyPropertyChanged
{
...
public void NotifyAllChanged()
{
OnPropertyChanged(string.Empty);
}
}
private void ComboBoxCurrency_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var text = ((sender as ComboBox).SelectedItem as ComboBoxItem).Content as string;
SelectedCurrency.Name = text;
Console.WriteLine("Currency changed to {0}", text);
StockEntriesCollection?.ForEach(entry=>entry.NotifyAllChanged()); // <==
}

MVVM Setting GridViewColumn width dynamically

In my mvvm application I have a listview.
I like to show/hide some columns of the
listview, depending on the state of the checkbox "Show all columns" (Here: Col1 should be showed/hidden).
That's my very simplified code. What is wrong?!
Obviously this does't work!
<Window x:Class="WpfApplication1.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:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="185">
<Window.Resources>
<local:ConverterHideListViewColumn x:Key="ConverterHideListViewColumn" />
</Window.Resources>
<Grid>
<ListView Height="100" Width="100">
<ListView.View>
<GridView>
<GridViewColumn Header="Col0" Width="40"/>
<GridViewColumn Header="Col1" Width="{Binding ShowAllColumns, Converter={StaticResource ConverterHideListViewColumn}}"/>
</GridView>
</ListView.View>
</ListView>
<CheckBox
Content="Show all columns"
IsChecked="{Binding ShowAllColumns, Mode=TwoWay}"
Margin="40,140,0,0">
</CheckBox>
</Grid>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
VM _vm;
public MainWindow ()
{
InitializeComponent ();
_vm = new VM ();
this.DataContext = _vm;
}
}
/// <summary>
/// Dummy Viewmodel
/// </summary>
public class VM : INotifyPropertyChanged
{
private bool _bShowAllColumns;
public event PropertyChangedEventHandler PropertyChanged;
public VM ()
{
ShowAllColumns = true;
}
public bool ShowAllColumns
{
get { return _bShowAllColumns; }
set { _bShowAllColumns = value; }
}
private void OnPropertyChanged (string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler (this, new PropertyChangedEventArgs (propertyName));
}
}
/// <summary>
/// Converter for setting the ListView-Column width, depending on value VM.ShowAllColumns
/// </summary>
class ConverterHideListViewColumn : IValueConverter
{
public object Convert (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool bShowAllColumns = false;
double dWidth = 0;
if (value is bool)
{
bShowAllColumns = (bool) value;
dWidth = bShowAllColumns? 40 : 0;
}
return dWidth;
}
public object ConvertBack (object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException ();
}
}
}
A GridViewColumn is not added to the visual tree and doesn't inherit any DataContext so you cannot bind its Width property to a source property of a view model without using a BindingProxy class as suggested here: https://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/
This is all you need...
public bool ShowAllColumns
{
get { return _bShowAllColumns; }
set
{
if (_bShowAllColumns != value)
{
_bShowAllColumns = value;
OnPropertyChanged("ShowAllColumns");
}
}
}

Flexible way to show a lot of columns in datagrid

I have a lot of columns in WPF DataGrid containg orders, over 80. They can be visible or hidden depending on view options menu. For now I am doing options menu separately, orders view model separately, columns visibility and headers processing in OnAutoGeneratingColumn event. So I have 3 different classes (ViewOptions, OrdersViewModel, ViewOptionsViewModel) and a lot of logic in event handler. Also It will be necessary to modify code in 4 places on adding/removing columns.
Is there a better way to bind menu headers to columns headers, as well as binding columns visibility (DataGrid) to checkboxes in menu (ViewOptionsViewModel)?
use binding ,When I checked the "Display Property 1 ,the Column visible
Xaml:
<Window x:Class="WpfApplication6.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:test="clr-namespace:WpfApplication6"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<test:Bool2Visibility x:Key="bool2Visibility"/>
<test:BindingProxy x:Key="bpProperty1"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<CheckBox Content="Display Property1" IsChecked="{Binding Source={StaticResource bpProperty1},Path=Data,Mode=OneWayToSource}"/>
</StackPanel>
<DataGrid Grid.Row="1" ItemsSource="{Binding}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="property1" Binding="{Binding Property1}" Visibility="{Binding Source={StaticResource bpProperty1},Path=Data,Converter={StaticResource bool2Visibility}}"/>
<DataGridTextColumn Header="property2" Binding="{Binding Property2}"/>
<DataGridTextColumn Header="property3" Binding="{Binding Property3}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
c# code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
List<TestData> list = new List<TestData>();
for (int i = 0; i < 10; i++)
{
TestData item = new TestData();
item.Property1 = "property1" + i.ToString();
item.Property2 = "property2" + i.ToString();
item.Property3 = "property3" + i.ToString();
list.Add(item);
}
this.DataContext = list;
}
}
public class TestData
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
}
public class Bool2Visibility : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool flag = false;
if (value != null)
{
flag = System.Convert.ToBoolean(value);
}
return flag ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}

WPF DataGrid Dynamic Styling

I have a DataGrid control in a WPF application. I want to be able to at runtime, select a value from a combo box, click a button and highlight the background color of all rows that meet this condition within the DataGrid. This was pretty easy to do in Windows Forms in the code behind, but I can not figure it out in WPF.
Any help gratefully received.
Thank you
Hello I created a sample which solves this by using triggers and a valueconverter.
Basically, I have a trigger on the gridcell, which is bound to the selecteditem of the combo-box. When you change selection, the trigger fires, and the cell uses the valueconverter to see if the selecteditem is the same as the value of the grid cell.
MainWindow.xaml
<Window x:Class="ComboBoxFilter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ComboBoxFilter="clr-namespace:ComboBoxFilter" Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ComboBoxFilter:NameValueConverter x:Key="NameValueConverter" />
</Window.Resources>
<Grid>
<StackPanel>
<ComboBox ItemsSource="{Binding Names}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}" x:Name="TheComboBox" />
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding Names}" x:Name="DataGrid" >
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<DataTrigger Value="True" >
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource NameValueConverter}">
<Binding Path="SelectedItem.Name" ElementName="TheComboBox" Mode="TwoWay" />
<Binding Path="Name" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
</DataGrid>
</StackPanel>
</Grid>
MainWindow code behind file
namespace ComboBoxFilter
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
}
NameValueConverter
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows.Data;
namespace ComboBoxFilter
{
public class NameValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var string1 = values[0];
var string2 = values[1];
return string1 == string2;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
ViewModel
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace ComboBoxFilter
{
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
Names = new ObservableCollection<Person>
{
new Person {Name = "Andy"},
new Person {Name = "hkon"},
new Person {Name = "dandy"},
new Person {Name = "Andy"}
};
}
private Person _selectedPerson;
public Person SelectedPerson
{
get { return _selectedPerson; }
set { _selectedPerson = value; NotifyPropertyChanged("SelectedPerson"); }
}
public ObservableCollection<Person> Names { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class Person
{
public string Name { get; set; }
public override string ToString()
{
return Name;
}
}
}

Categories

Resources