How to Add Columns to a DataGrid through Binding and Map Its Cell Values

Lance Contreras, 7 Nov 2013 CPOL

   4.94 (9 votes)

1

2

3

4

5
4.94/5 - 9 votes
μ 4.94, σa 1.04 [?]
 
Rate:
Add a reason or comment to your vote: x
Votes of 3 or less require a comment
 
How to add columns to a DataGrid through binding and map its cell values

Introduction

There are some ways on how you can dynamically bind to the columns of a DataGrid. In this article, I'll discuss a solution on how bind a collection to the DataGrid's column and display a mapped value to the cell using the row and column's binding.

Background

In my current project, I was tasked to create a datagrid that can have additional columns through binding. Thanks to Paul Stovell, I got an idea on how to implement a CustomBoundColumn. But I still have another problem, how can I get the cell value based on the Column and Row binding? Another problem is where should I bind the values of the new cells?

Normally, we can create a cell template and the binding of the row will apply to that template. But since we have a dynamic column, we should match the column's binding to the row binding in order for the data in the cell to make sense.

The main idea here is to get the Column's binding and the Row's binding then with that information, we can generate a sensible data for the cell. Then, we need a way to manage those new cell values though data binding.

Using the Code

I created a behavior for my DataGrid with the following attached properties:

  • AttachedColumnsProperty - This should be bound to an ObservableCollection of ViewModels for the additional columns.
  • MappedValuesProperty - This should be bound to an ObservableCollection of MappedValues. I created a MappedValue class that contains the binding source of the column header, the binding source of the view and the value that will be placed in the cell.
  • HeaderTemplateProperty - The column header template.
  • AttachedCellTemplateProperty - The cell template of the cell under the attached columns. This should be the template of those newly generated cells because of the attached columns.
    public class AttachedColumnBehavior
{
public static readonly DependencyProperty AttachedColumnsProperty =
DependencyProperty.RegisterAttached("AttachedColumns",
typeof(IEnumerable),
typeof(AttachedColumnBehavior),
new UIPropertyMetadata(null, OnAttachedColumnsPropertyChanged)); public static readonly DependencyProperty MappedValuesProperty =
DependencyProperty.RegisterAttached("MappedValues",
typeof(MappedValueCollection),
typeof(AttachedColumnBehavior),
new UIPropertyMetadata(null, OnMappedValuesPropertyChanged)); public static readonly DependencyProperty HeaderTemplateProperty =
DependencyProperty.RegisterAttached("HeaderTemplate",
typeof(DataTemplate),
typeof(AttachedColumnBehavior),
new UIPropertyMetadata(null, OnHeaderTemplatePropertyChanged)); public static readonly DependencyProperty AttachedCellTemplateProperty =
DependencyProperty.RegisterAttached("AttachedCellTemplate",
typeof(DataTemplate),
typeof(AttachedColumnBehavior),
new UIPropertyMetadata(null, OnCellTemplatePropertyChanged)); public static readonly DependencyProperty AttachedCellEditingTemplateProperty =
DependencyProperty.RegisterAttached("AttachedCellEditingTemplate",
typeof(DataTemplate),
typeof(DataGrid),
new UIPropertyMetadata(null, OnCellEditingTemplatePropertyChanged)); private static void OnAttachedColumnsPropertyChanged
(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var dataGrid = dependencyObject as DataGrid;
if (dataGrid == null) return;
var columns = e.NewValue as INotifyCollectionChanged;
if (columns != null)
{
columns.CollectionChanged += (sender, args) =>
{
if (args.Action == NotifyCollectionChangedAction.Remove)
RemoveColumns(dataGrid, args.OldItems);
else if(args.Action == NotifyCollectionChangedAction.Add)
AddColumns(dataGrid, args.NewItems);
};
dataGrid.Loaded += (sender, args) => AddColumns(dataGrid, GetAttachedColumns(dataGrid));
var items = dataGrid.ItemsSource as INotifyCollectionChanged;
if (items != null)
items.CollectionChanged += (sender, args) =>
{
if (args.Action == NotifyCollectionChangedAction.Remove)
RemoveMappingByRow(dataGrid, args.NewItems);
};
}
} private static void AddColumns(DataGrid dataGrid, IEnumerable columns)
{
foreach (var column in columns)
{
CustomBoundColumn customBoundColumn = new CustomBoundColumn()
{
Header = column,
HeaderTemplate = GetHeaderTemplate(dataGrid),
CellTemplate = GetAttachedCellTemplate(dataGrid),
CellEditingTemplate = GetAttachedCellEditingTemplate(dataGrid),
MappedValueCollection = GetMappedValues(dataGrid)
}; dataGrid.Columns.Add(customBoundColumn);
}
} private static void RemoveColumns(DataGrid dataGrid, IEnumerable columns)
{
foreach (var column in columns)
{
DataGridColumn col = dataGrid.Columns.Where(x => x.Header == column).Single();
GetMappedValues(dataGrid).RemoveByColumn(column);
dataGrid.Columns.Remove(col);
}
} private static void RemoveMappingByRow(DataGrid dataGrid, IEnumerable rows)
{
foreach (var row in rows)
{
GetMappedValues(dataGrid).RemoveByRow(row);
}
} #region OnChange handlers
private static void OnCellTemplatePropertyChanged
(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{ }
private static void OnHeaderTemplatePropertyChanged
(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{ } private static void OnCellEditingTemplatePropertyChanged
(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{ }
private static void OnMappedValuesPropertyChanged
(DependencyObject d, DependencyPropertyChangedEventArgs e)
{ }
#endregion public static IEnumerable GetAttachedColumns(DependencyObject dataGrid)
{
return (IEnumerable)dataGrid.GetValue(AttachedColumnsProperty);
} public static void SetAttachedColumns(DependencyObject dataGrid, IEnumerable value)
{
dataGrid.SetValue(AttachedColumnsProperty, value);
} public static MappedValueCollection GetMappedValues(DependencyObject dataGrid)
{
return (MappedValueCollection)dataGrid.GetValue(MappedValuesProperty);
} public static void SetMappedValues(DependencyObject dataGrid, MappedValueCollection value)
{
dataGrid.SetValue(MappedValuesProperty, value);
} public static DataTemplate GetHeaderTemplate(DependencyObject dataGrid)
{
return (DataTemplate)dataGrid.GetValue(HeaderTemplateProperty);
} public static void SetHeaderTemplate(DependencyObject dataGrid, DataTemplate value)
{
dataGrid.SetValue(HeaderTemplateProperty, value);
} public static DataTemplate GetAttachedCellTemplate(DependencyObject dataGrid)
{
return (DataTemplate)dataGrid.GetValue(AttachedCellTemplateProperty);
} public static void SetAttachedCellTemplate(DependencyObject dataGrid, DataTemplate value)
{
dataGrid.SetValue(AttachedCellTemplateProperty, value);
} public static DataTemplate GetAttachedCellEditingTemplate(DependencyObject dataGrid)
{
return (DataTemplate)dataGrid.GetValue(AttachedCellEditingTemplateProperty);
} public static void SetAttachedCellEditingTemplate(DependencyObject dataGrid, DataTemplate value)
{
dataGrid.SetValue(AttachedCellEditingTemplateProperty, value);
}
}

Now here's the CustomBoundColumn, this is an extension of the DataGridTemplateColumn. Here, we will combine the binding source of the column and the row and this will be the binding of our cell template. With RowBindingand ColumnBinding, we can add MappedValue to the MappedValueCollection.

public class CustomBoundColumn : DataGridTemplateColumn//DataGridBoundColumn
{
public DataTemplate CellTemplate { get; set; }
public DataTemplate CellEditingTemplate { get; set; }
public MappedValueCollection MappedValueCollection { get; set; } protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
var content = new ContentControl();
MappedValue context = MappedValueCollection.ReturnIfExistAddIfNot(cell.Column.Header, dataItem);
var binding = new Binding() { Source = context };
content.ContentTemplate = cell.IsEditing ? CellEditingTemplate : CellTemplate;
content.SetBinding(ContentControl.ContentProperty, binding);
return content;
} protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
{
return GenerateElement(cell, dataItem);
}
}

The MappedValueCollection is just an ObservableCollection of MappedValues. I created it to easily manipulate the mapped values.

public class MappedValueCollection : ObservableCollection<MappedValue>
{
public MappedValueCollection()
{
} public bool Exist(object ColumnBinding, object RowBinding)
{
return this.Count(x => x.RowBinding == RowBinding &&
x.ColumnBinding == ColumnBinding) > 0;
} public MappedValue ReturnIfExistAddIfNot(object ColumnBinding, object RowBinding)
{
MappedValue value = null; if (Exist(ColumnBinding, RowBinding))
{
return this.Where(x => x.RowBinding == RowBinding &&
x.ColumnBinding == ColumnBinding).Single();
}
else
{
value = new MappedValue();
value.ColumnBinding = ColumnBinding;
value.RowBinding = RowBinding;
this.Add(value);
}
return value;
} public void RemoveByColumn(object ColumnBinding)
{
foreach (var item in this.Where(x => x.ColumnBinding == ColumnBinding).ToList())
this.Remove(item);
} public void RemoveByRow(object RowBinding)
{
foreach (var item in this.Where(x => x.RowBinding == RowBinding).ToList())
this.Remove(item);
}
}

The MappedValue is where we bind the value of the new cells generated by newly attached columns.

public class MappedValue : ViewModelBase, IMappedValue
{
object value;
public object ColumnBinding { get; set; }
public object RowBinding { get; set; }
public object Value
{
get
{
return value;
}
set
{
if (this.value != value)
{
this.value = value;
base.RaisePropertyChanged(() => Value);
}
}
}
}

In the ViewModel example, here we have three Collections, the CostingCollectionwhich will be the ItemsSourceof the DataGrid, the Suppliers which will be the additional columns to the grid and the SupplierCostValueswhich is the mapped value for the cells under the attached columns.

public class MainWindowViewModel : ViewModelBase
{
ObservableCollection<CostViewModel> costingCollection;
ObservableCollection<SupplierViewModel> suppliers;
MappedValueCollection supplierCostValues; public MappedValueCollection SupplierCostValues
{
get { return supplierCostValues; }
set
{
if (supplierCostValues != value)
{
supplierCostValues = value;
base.RaisePropertyChanged(() => this.SupplierCostValues);
}
}
}
public ObservableCollection<SupplierViewModel> Suppliers
{
get { return suppliers; }
set
{
if (suppliers != value)
{
suppliers = value;
base.RaisePropertyChanged(() => this.Suppliers);
}
}
}
public ObservableCollection<CostViewModel> CostingCollection
{
get { return costingCollection; }
set
{
if (costingCollection != value)
{
costingCollection = value;
base.RaisePropertyChanged(() => this.CostingCollection);
}
}
} public MainWindowViewModel()
{
SupplierCostValues = new MappedValueCollection();
this.Suppliers = new ObservableCollection<SupplierViewModel>();
this.CostingCollection = new ObservableCollection<CostViewModel>(); this.Suppliers.Add(new SupplierViewModel(new Supplier() { SupplierId = 1,
Currency = "PHP",
Location = "Philippines", SupplierName = "Bench" }));
this.Suppliers.Add(new SupplierViewModel(new Supplier() { SupplierId = 2,
Currency = "JPY",
Location = "Japan", SupplierName = "Uniqlo" }));
this.Suppliers.Add(new SupplierViewModel(new Supplier() { SupplierId = 3,
Currency = "USD",
Location = "United States", SupplierName = "Aeropostale" }));
this.Suppliers.Add(new SupplierViewModel(new Supplier() { SupplierId = 4,
Currency = "HKD",
Location = "Hong Kong", SupplierName = "Giordano" })); CostingCollection.Add(new CostViewModel(new Cost()
{ CostId = 1, Name = "Service Cost" }));
CostingCollection.Add(new CostViewModel(new Cost()
{ CostId = 2, Name = "Items Cost" }));
CostingCollection.Add(new CostViewModel(new Cost()
{ CostId = 3, Name = "Shipping Cost" }));
} public ICommand RemoveCommand
{
get
{
return new RelayCommand(() =>
{
this.Suppliers.RemoveAt(this.Suppliers.Count - 1);
});
}
}
public ICommand AddCommand
{
get
{
return new RelayCommand(() =>
{
this.Suppliers.Add(new SupplierViewModel(new Supplier() { SupplierId = 1,
Currency = "PHP",
Location = "Philippines", SupplierName = "Bench" }));
});
}
}
}

The HeaderTemplate's binding would be a SupplierViewModel, the Row's binding will be CostViewModel and the CellTemplate's binding is a MappedValue.

<Window x:Class="BindableColumn.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:BindableColumn"
xmlns:vm="clr-namespace:BindableColumn.ViewModel"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0">
<StackPanel.Resources>
<DataTemplate x:Key="headerTemplate">
<StackPanel>
<TextBlock Text="{Binding SupplierName}" />
<TextBlock Text="{Binding Currency}" />
<TextBlock Text="{Binding Location}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="cellTemplate">
<DataTemplate.Resources>
<loc:RowAndColumnMultiValueConverter x:Key="Converter"/>
</DataTemplate.Resources>
<StackPanel>
<TextBlock Text="{Binding Value}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="cellEditingTemplate">
<DataTemplate.Resources>
<loc:RowAndColumnMultiValueConverter x:Key="Converter" />
</DataTemplate.Resources>
<StackPanel>
<TextBox Text="{Binding Value}"/>
</StackPanel>
</DataTemplate>
</StackPanel.Resources>
<DataGrid ItemsSource="{Binding CostingCollection}" x:Name="myGrid"
loc:AttachedColumnBehavior.HeaderTemplate="{StaticResource headerTemplate}"
loc:AttachedColumnBehavior.AttachedCellTemplate="{StaticResource cellTemplate}"
loc:AttachedColumnBehavior.AttachedColumns="{Binding Suppliers}"
loc:AttachedColumnBehavior.AttachedCellEditingTemplate=
"{StaticResource cellEditingTemplate}"
loc:AttachedColumnBehavior.MappedValues="{Binding SupplierCostValues}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="CostId" Binding="{Binding CostId}"/>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" />
</DataGrid.Columns>
</DataGrid>
<Button Content="Add Column" Command="{Binding AddCommand}"/>
<Button Content="Remove Lastcolumn" Command="{Binding RemoveCommand}" />
</StackPanel>
</Grid>
</Window>

Conclusion

Using the AttachedColumns property, we can bind an observable collection, then collection changes (add/remove) will reflect to the UI. We can now add/remove a column without having to mess up with the code behind. Aside from binding to the AttachedColumns, we can always add columns via the XAML, that makes it more flexible to use.

Allowing dynamic columns gives more complexity since there will be some new cells because of the attached columns. Using the MappedValueCollection, we can play around with the values of the newly generated cells with reference to the Row and Column of the cell. Again, no code behind involved, it can be done in the ViewModel.

Practically, this is my way of implementing a dynamic DataGrid columns using the MVVM pattern. I hope this solution helps in your development.

Further Improvement

In this solution, I only used one type of view model for all the attached columns. I think we can use different type of viewmodels for each attached column by utilizing the HeaderTemplateSelector.

References

http://www.codeproject.com/Tips/676530/How-to-Add-Columns-to-a-DataGri

How to Add Columns to a DataGrid through Binding and Map Its Cell Values的更多相关文章

  1. Spark 2.x不支持ALTER TABLE ADD COLUMNS,没关系,我们改进下

    SparkSQL从2.0开始已经不再支持ALTER TABLE table_name ADD COLUMNS (col_name data_type [COMMENT col_comment], .. ...

  2. 自己动手为Spark 2.x添加ALTER TABLE ADD COLUMNS语法支持

    SparkSQL从2.0开始已经不再支持ALTER TABLE table_name ADD COLUMNS (col_name data_type [COMMENT col_comment], .. ...

  3. Add Columns to the Web Sessions List

    To add custom columns to the Web Sessions List, add rules using FiddlerScript. The BindUIColumn Attr ...

  4. R12: How to add Microsoft Excel as Type to the Create Template List of Values in BI Publisher (Doc ID 1343225.1)

    Modified: 27-Oct-2013 Type: HOWTO In this Document Goal Solution References APPLIES TO: BI Publisher ...

  5. WPF 4 DataGrid 控件(基本功能篇)

    原文:WPF 4 DataGrid 控件(基本功能篇)      提到DataGrid 不管是网页还是应用程序开发都会频繁使用.通过它我们可以灵活的在行与列间显示各种数据.本篇将详细介绍WPF 4 中 ...

  6. [No0000123]WPF DataGrid Columns Visibility的绑定

    场景:根据配置文件显示DataGrid中的某些列. 问题:Columns集合只是DataGrid的一个属性,这个集合在逻辑树或视觉树中是看不到的,也不会继承DataContext属性. 方法一:对Da ...

  7. MVVM框架下,WPF实现Datagrid里的全选和选择

    最近的一个项目是用MVVM实现,在实现功能的时候,就会有一些东西,和以前有很大的区别,项目中就用到了常用的序号,就是在Datagrid里的一个字段,用checkbox来实现. 既然是MVVM,就要用到 ...

  8. 如何用easyui+JAVA 实现动态拼凑datagrid表格

    先给大家看一看效果,最近一段时间都在研究这个东西. 如果我把日期间隔选宽呢?比如5月日到5月5日?下面给大家看看效果,不用担心哦 看到了吧,哈哈,这个日期都是动态生成的,下面就来跟大家分享一下这个的实 ...

  9. Jquery easy ui datagrid動態加載列問題

    1.如下图效果是当选择不同的日期范围时datagrid则会加载出对应的列数

随机推荐

  1. MySQL(一) -- MySQL学习路线、数据库的基础、关系型数据库、关键字说明、SQL、MySQL数据库、MySQL服务器对象、SQL的基本操作、库操作、表操作、数据操作、中文数据问题、 校对集问题、web乱码问题

    1 MySQL学习路线 基础阶段:MySQL数据库的基本操作(增删改查),以及一些高级操作(视图.触发器.函数.存储过程等). 优化阶段:如何提高数据库的效率,如索引,分表等. 部署阶段:如何搭建真实 ...

  2. stable_sort()与sort

    stable_sort与sort()都是c++库函数,调用<algorithm>库,但区别是sort是不稳定的排序,而stable_sort是稳定的,有时候stable_sort比sort ...

  3. 团队作业10——事后分析(Beta版本)

    团队作业10--事后分析(Beta版本) 目录 一.设想与目标 二.计划 三.资源 四.变更管理 五.设计与实现 六.测试与发布 七.总结 八.图片和贡献分分配 一.设想和目标 1.我们的软件要解决什 ...

  4. 团队作业4——第一次项目冲刺 fOURth DaY

    项目冲刺--Quadra Kill 兄弟们,再坚持一下,再坚持一下,再给我一个头我就五杀了. 今天可谓是项目的一个转折点,因为跳转和数据库已经基本写好啦,鼓掌~[啪啪啪啪啪啪] 让我们来看看今天大家做 ...

  5. 第二次项目冲刺(Beta阶段)5.24

    1.提供当天站立式会议照片一张 会议内容: ①检查前一天的任务情况. ②制定新一轮的任务计划. 2.每个人的工作 (1)工作安排 队员 今日进展 明日安排 王婧 #63Web输出以文件名为标题 #63 ...

  6. 201521123018 《Java程序设计》第1周学习总结

    1. 本章学习总结 *Java程序的特点 *可以跨平台运行 *语言简单 *利用控制台运行java程序 *cmd调出控制台->用javac指令编译源代码->用java指令运行 2. 书面作业 ...

  7. 201521123075 《Java程序设计》第11周学习总结

    1. 本周学习总结 2. 书面作业 本次PTA作业题集多线程 1.互斥访问与同步访问 完成题集4-4(互斥访问)与4-5(同步访问) 1.1 除了使用synchronized修饰方法实现互斥同步访问, ...

  8. java.lang.IllegalArgumentException: object is not an instance of declaring class

    今天在使用反射的时候,出现了java.lang.IllegalArgumentException: object is not an instance of declaring class错误-具体是 ...

  9. mongoose api 图表整理

    一.背景 今天看 mongoose 的基础 API,参考了下面的链接做了图表以供查阅. 参考资料: http://www.cnblogs.com/xiaohuochai/p/7215067.html ...

  10. Java-Filter过滤器用于过滤整个项目的编码

    整个分为实现类以及在web.xml文件中对编写的filter类进行注册 代码如下 package cn.itcast.itcaststore.web.filter; import java.io.IO ...