Search code examples
wpfxamlmvvmdatagriddatatable

Using MVVM, how can I dynamically generate WPF DataGrid columns from a DataTable and also show button columns?


I am trying to dynamically generate a DataGrid from a DataTable while at the same time display two button columns for "Edit" and "Delete" functionality. I can manually create a button column in XAML, and I can dynamically generate columns from a DataTable, but I can't seem to do both. If I were just coding this in the code-behind I think it would be a much simpler problem to solve because I would have direct access to the control. However, I am building this View using MVVM and I can't think of a way to manipulate the View dynamically to this level of detail.

Here's a little bit of my code (please note that there may be some Copy/Paste FAILS that are obvious to someone with more WPF/MVVM experience than I have):

XAML:

        <DataGrid   x:Name="grdMapValues"
                    AutoGenerateColumns="True"
                    ItemsSource="{Binding GridSourceDataTable}">
            <DataGrid.Columns>
                <DataGridTemplateColumn IsReadOnly="True">
                    <DataGridTemplateColumn.HeaderTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <Button HorizontalAlignment="Left"
                                        Command="{Binding Path=DataContext.Commands.AddMapValue,
                                        RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}"                                                    
                                        Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}">
                                    <Image Width="14" Height="14" Source="/Controls;component/Resources/Images/new.gif" ToolTip="New" />
                                </Button>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.HeaderTemplate>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <Button Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"                                 
                                        Command="{Binding Path=DataContext.Commands.EditMapValue,
                                        RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}">
                                    <Image Width="14" Height="14" Source="/Controls;component/Resources/Images/edit.gif" />
                                </Button>
                                <Button Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}"                                                    
                                        Command="{Binding Path=DataContext.Commands.DeleteMapValue,
                                        RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}}">
                                    <Image Width="14" Height="14" Source="/Controls;component/Resources/Images/delete.gif" />
                                </Button>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>

ViewModel:

private DataTable _gridSource;
public DataTable GridSourceDataTable
{
    get
    {
        return _gridSource;
    }
    set
    {
        _gridSource = value;
        OnPropertyChanged("GridSourceDataTable");
    }
}

private void GenerateDataTable()
{        
    switch (caseInt)
    {
        case 1:
            if (_isDate)
            {
                _gridSource.Columns.Add("Date", typeof(DateTime));
            }
            else
            {
                _gridSource.Columns.Add("Number", typeof(string));
            }
            _gridSource.Columns.Add("Value", typeof(decimal));
            _gridSource.Columns.Add("SourceString", typeof(string));
            GridIsVisible = true;
            break;
        //Other similar case blocks removed
        default:
            GridIsVisible = false;
            break;
    }
    OnPropertyChanged("GridSourceDataTable");
}

So I am guessing that the solution, if there is one, will involve either some hybrid of XAML and the DataTable property OR it will involve a more sophisticated object or property than my simple DataTable.

I've been working on this one for several hours, so any help that can be offered will be greatly appreciated.


Solution

  • You can use attached behaviors to achieve what you are trying. You will be manipulating and adding additional columns to the grid from an attached behavior - that means you are still following MVVM pattern.

    This is the attached behavior I tried:

    public class DataGridColumnBehavior : Behavior<DataGrid>
    {
        public AdditionalColumnsList AdditionalColumns { get; set; }
    
        protected override void OnAttached()
        {
            base.OnAttached();
            AddAdditionalColumns();
        }
    
        void AddAdditionalColumns()
        {
            if(AdditionalColumns == null || AdditionalColumns.Count == 0) return;
    
            foreach (var additionalColumn in AdditionalColumns)
            {
                AssociatedObject.Columns.Add(new DataGridTemplateColumn
                {
                    HeaderTemplate = additionalColumn.HeaderTemplate,
                    CellTemplate = additionalColumn.CellTemplate
                });
            }
        }
    }
    

    I have used couple of simple classes to represent additional column and additional column list.

    public class AdditionalColumn
    {
        public DataTemplate HeaderTemplate { get; set; }
    
        public DataTemplate CellTemplate { get; set; }
    }
    
    public class AdditionalColumnsList : List<AdditionalColumn>
    {
        
    }
    

    And, this is how you attach a behavior to DataGrid:

    <DataGrid Margin="5" ItemsSource="{Binding GridSource}">
        <i:Interaction.Behaviors>
            <local:DataGridColumnBehavior>
                <local:DataGridColumnBehavior.AdditionalColumns>
                    <local:AdditionalColumnsList>
                        <local:AdditionalColumn>
                            <local:AdditionalColumn.HeaderTemplate>
                                <DataTemplate>
                                    <TextBlock Text="Button Header" />
                                </DataTemplate>
                            </local:AdditionalColumn.HeaderTemplate>
                            <local:AdditionalColumn.CellTemplate>
                                <DataTemplate>
                                    <Button Content="Button" Command="{Binding ButtonCommand}" />
                                </DataTemplate>
                            </local:AdditionalColumn.CellTemplate>
                        </local:AdditionalColumn>
                    </local:AdditionalColumnsList>
                </local:DataGridColumnBehavior.AdditionalColumns>
            </local:DataGridColumnBehavior>
        </i:Interaction.Behaviors>
    </DataGrid>
    

    And the resulting grid:

    enter image description here

    Read this CodeProject article on attached behaviors.

    Please note, I have used Blend Behavior here. This blog post discusses differences between attached behaviors and blend behaviors.