Click here to Skip to main content
15,884,866 members
Articles / Desktop Programming / WPF
Tip/Trick

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

Rate me:
Please Sign up or sign in to vote.
4.96/5 (14 votes)
7 Nov 2013CPOL3 min read 79.9K   2K   16   18
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.
C#
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 RowBinding and ColumnBinding, we can add MappedValue to the MappedValueCollection.

C#
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.

C#
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.

C#
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 CostingCollection which will be the ItemsSource of the DataGrid, the Suppliers which will be the additional columns to the grid and the SupplierCostValues which is the mapped value for the cells under the attached columns.

C#
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.

XML
<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.

Image 1

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
I'm a Senior Software Developer in New Jersey

Comments and Discussions

 
QuestionSource Sample Update Pin
Member 108350565-Sep-20 9:33
Member 108350565-Sep-20 9:33 
GeneralMy vote of 5 Pin
musthe26-Apr-20 4:40
musthe26-Apr-20 4:40 
Questioncell binding Pin
arunchinni27-Apr-15 6:54
arunchinni27-Apr-15 6:54 
AnswerRe: cell binding Pin
Lance Contreras11-Jun-15 7:15
Lance Contreras11-Jun-15 7:15 
BugColumn duplicating Pin
grzechooo11-Mar-15 8:34
grzechooo11-Mar-15 8:34 
GeneralRe: Column duplicating Pin
Lance Contreras11-Jun-15 10:56
Lance Contreras11-Jun-15 10:56 
GeneralRe: Column duplicating Pin
DavePajdhsgh14-Jan-20 10:35
DavePajdhsgh14-Jan-20 10:35 
GeneralMy vote of 1 Pin
PandIyan T20-Feb-15 22:54
PandIyan T20-Feb-15 22:54 
QuestionNo values show up in cells. Pin
southsouth15-Feb-15 11:46
southsouth15-Feb-15 11:46 
BugCell values moving around Pin
skyapie18-Mar-14 21:18
skyapie18-Mar-14 21:18 
GeneralRe: Cell values moving around Pin
Lance Contreras2-Jul-14 6:54
Lance Contreras2-Jul-14 6:54 
AnswerRe: Cell values moving around Pin
grzechooo11-Mar-15 7:43
grzechooo11-Mar-15 7:43 
GeneralRe: Cell values moving around Pin
Lance Contreras11-Jun-15 10:58
Lance Contreras11-Jun-15 10:58 
SuggestionShouldn't have use a global static DataGrid variable , since it will be in conflict if I attached it to another instance of a grid. Pin
Lance Contreras6-Nov-13 13:23
Lance Contreras6-Nov-13 13:23 
QuestionMiising reference "Galasoft"... Pin
Sperneder Patrick1-Nov-13 21:48
professionalSperneder Patrick1-Nov-13 21:48 
humm....
What Kind of 3rd Party lib is missing?
AnswerRe: Miising reference "Galasoft"... Pin
Lance Contreras1-Nov-13 23:23
Lance Contreras1-Nov-13 23:23 
QuestionSource code Pin
Smitha Nishant31-Oct-13 3:49
protectorSmitha Nishant31-Oct-13 3:49 
AnswerRe: Source code Pin
Lance Contreras31-Oct-13 15:25
Lance Contreras31-Oct-13 15:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.