How to Bind a Wpf Datagrid to a Variable Number of Columns

How do I bind a WPF DataGrid to a variable number of columns?

Here's a workaround for Binding Columns in the DataGrid. Since the Columns property is ReadOnly, like everyone noticed, I made an Attached Property called BindableColumns which updates the Columns in the DataGrid everytime the collection changes through the CollectionChanged event.

If we have this Collection of DataGridColumn's

public ObservableCollection<DataGridColumn> ColumnCollection
{
get;
private set;
}

Then we can bind BindableColumns to the ColumnCollection like this

<DataGrid Name="dataGrid"
local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
AutoGenerateColumns="False"
...>

The Attached Property BindableColumns

public class DataGridColumnsBehavior
{
public static readonly DependencyProperty BindableColumnsProperty =
DependencyProperty.RegisterAttached("BindableColumns",
typeof(ObservableCollection<DataGridColumn>),
typeof(DataGridColumnsBehavior),
new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = source as DataGrid;
ObservableCollection<DataGridColumn> columns = e.NewValue as ObservableCollection<DataGridColumn>;
dataGrid.Columns.Clear();
if (columns == null)
{
return;
}
foreach (DataGridColumn column in columns)
{
dataGrid.Columns.Add(column);
}
columns.CollectionChanged += (sender, e2) =>
{
NotifyCollectionChangedEventArgs ne = e2 as NotifyCollectionChangedEventArgs;
if (ne.Action == NotifyCollectionChangedAction.Reset)
{
dataGrid.Columns.Clear();
foreach (DataGridColumn column in ne.NewItems)
{
dataGrid.Columns.Add(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Add)
{
foreach (DataGridColumn column in ne.NewItems)
{
dataGrid.Columns.Add(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Move)
{
dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
}
else if (ne.Action == NotifyCollectionChangedAction.Remove)
{
foreach (DataGridColumn column in ne.OldItems)
{
dataGrid.Columns.Remove(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Replace)
{
dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
}
};
}
public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
{
element.SetValue(BindableColumnsProperty, value);
}
public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
{
return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
}
}

WPF DataGrid column with variable number of buttons/shapes

Some parts of the code are in C#, I hope it's not a problem. Wpf code:

<Window.Resources>
<ResourceDictionary>
<local:PositiveIntegersConverter x:Key="PositiveIntegersConverter" />
<local:EmployeePlatformOptionConverter x:Key="EmployeePlatformOptionConverter"/>
</ResourceDictionary>
</Window.Resources>

<DataGrid ItemsSource="{Binding employees}" AutoGenerateColumns="False" x:Name="DataGrid1">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=id}" Header="Id" />
<DataGridTemplateColumn Header="No" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding numberofplatforms, Converter={StaticResource PositiveIntegersConverter}}" SelectedItem="{Binding selectedplatform}"
x:Name="ListBox1">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}" Command="{Binding Source={x:Reference DataGrid1}, Path=DataContext.SelectOptionCommand}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource EmployeePlatformOptionConverter}">
<Binding ElementName="ListBox1" Path="DataContext"/>
<Binding Path="."/>
</MultiBinding>
</Button.CommandParameter>
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="Gray" />
<Setter Property="Foreground" Value="White" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBoxItem}, Path=IsSelected}"
Value="True">
<Setter Property="Background" Value="Blue" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>

I used two converters here:

PositiveIntegersConverter which returns positive integers for a given numberofplatforms - these are available platform options for an employee

EmployeePlatformOptionConverter which converts two parameters that we want to pass to SelectOptionCommand: employee and selected platform option into one object of EmployeePlatformOption type.

public class PositiveIntegersConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var toInteger = System.Convert.ToInt32(value);
return toInteger > 0 ? Enumerable.Range(1, toInteger) : null;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

public class EmployeePlatformOptionConverter
: IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return new EmployeePlatformOption
{
Employee = values[0] as Employee,
Platform = (int)values[1]
};
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

Using encapsulating object:

public class EmployeePlatformOption
{
public Employee Employee { get; set; }
public int Platform { get; set; }
}

you pass selection to SelectOptionCommand in your ViewModel class:

public ICommand SelectOptionCommand { get; set; }
private void SelectOptionExecute(EmployeePlatformOption employeePlatformOption)
{
if (employeePlatformOption != null && employeePlatformOption.Employee != null &&
employeePlatformOption.Platform > 0)
{
employeePlatformOption.Employee.selectedplatform = employeePlatformOption.Platform;
}
}

Employee should implement INotifyPropertyChange interface so selectedplatform update is visible on the screen.

How to build A WPF Datagrid with an unknown number of columns

There is DataTable class in .Net. Its primary purpose is to communicate with relational database but it can be used nicely to store, display and edit tabular data (e.g. read and display .csv/Excel files -> DataTable + DataGrid in wpf, DataTable + DataGridView in WinForms).

DataTable columns (DataColumn) can be added/removed at runtime and DataGrid auto-generates columns (DataGridColumn) for them (enabled by default), using Name property for headers. Also DataTable supports sorting and filtering out-of-box.

note: DataGrid doesn't clear Columns when new ItemsSource is assigned. So it is possible to have some predefined columns and use autogenerate as well.

note: DataGrid creates DataGridTextColumns by default. If more complex template is required, the process can be intercepted via AutoGeneratingColumn event (see example)

here is an example:

code

public class MyViewModel
{
public DataTable Test { get; set; }
}

public MyWindow()
{
InitializeComponent();
var vm = new MyViewModel
{
Test = new DataTable
{
Columns = {"A", "B", "C"}
}
};
this.DataContext = vm;

}

xaml

<DataGrid AutoGenerateColumns="True"
ItemsSource="{Binding Path=Test.DefaultView}">
<DataGrid.Columns>
<DataGridTextColumn Header="#"/>
</DataGrid.Columns>
</DataGrid>

the result (I entered some values)

datagrid

DataGrid with variable number of columns. Keeping the horizontal scrollbar

You may need to remove Width="*" in DataGridTemplateColumn because it modifies the value which the Scrollbar depends.

Doing this will display the scrollbar at the bottom of the DataGrid.
If you will not add other control to the layout, you may not need the StackPanel and the ScrollViewer anymore.

WPF: How to make DataGrid binding with dynamic columns editable?

Do you bind to an indexer?. can you show us how your DataList Property looks like?

i did the same a while ago with an indexed property.

 public SomeObjectWithIndexer DataList
{get; set;}


public class SomeObjectWithIndexer
{
public string this
{
get { ... }
set { ... }//<-- you need this one for TwoWay
}
}

EDIT: the reason that you cant edit your Property, is that you try to edit a "double field".
one workaround would be to wrap your double into a class with INotifyPropertyChanged.

public class DataListItem
{
public double MyValue { get; set;}//with OnPropertyChanged() and stuff
}

then you can use a

ObservableCollection<DataListItem>

and you can edit your value. the question wether the index are always the same stay still around :)

Binding binding = new Binding(string.Format("DataList[{0}].MyValue", n++));

EDIT2: working example: just to show twoway is working

public class DataItem
{
public string Name { get; set; }
public ObservableCollection<DataListItem> DataList { get; set; }

public DataItem()
{
this.DataList = new ObservableCollection<DataListItem>();
}
}

Wrapper for double:

public class DataListItem
{
private double myValue;
public double MyValue
{
get { return myValue; }
set { myValue = value; }//<-- set breakpoint here to see that edit is working
}
}

usercontrol with a datagrid

<UserControl x:Class="WpfStackoverflow.IndexCollectionDataGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<DataGrid ItemsSource="{Binding MyList}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" />
<DataGridTextColumn Header="Index1" Binding="{Binding Path=DataList[0].MyValue, Mode=TwoWay}" />
<DataGridTextColumn Header="Index2" Binding="{Binding Path=DataList[1].MyValue, Mode=TwoWay}" />
</DataGrid.Columns>
</DataGrid>
</UserControl>

.cs

public partial class IndexCollectionDataGrid : UserControl
{
public IndexCollectionDataGrid()
{
InitializeComponent();
this.MyList = new ObservableCollection<DataItem>();

var m1 = new DataItem() {Name = "test1"};
m1.DataList.Add(new DataListItem() { MyValue = 10 });
m1.DataList.Add(new DataListItem() { MyValue = 20 });

var m2 = new DataItem() { Name = "test2" };
m2.DataList.Add(new DataListItem() { MyValue = 100 });
m2.DataList.Add(new DataListItem() { MyValue = 200 });

this.MyList.Add(m1);
this.MyList.Add(m2);

this.DataContext = this;
}

public ObservableCollection<DataItem> MyList { get; set; }
}

i hope you get in the right direction with this example.



Related Topics



Leave a reply



Submit