locked
Binding to data source with dynamically created columns in DataGrid RRS feed

  • Question

  • Hi

    Like many others, I have a need for some dynamically created columns in a DataGrid within a WPF 4.5 project.  As well as some static columns, new columns can be created or removed via the interface depending on user requirements. The project takes a View-Model-first approach and uses the Caliburn Micro framework (v1.4.1).

    To tackle the problem of dynamic columns, I’m using an Observable Collection of DataGrid columns, per http://stackoverflow.com/questions/3065758/wpf-mvvm-datagrid-dynamic-columns.  I then create (or remove) the columns in the View Model.

    In order to overcome the problem of how to bind the cells within the new columns to the backing data context, I’ve been experimenting with inheriting from the DataGridTemplateColumn thus:

    Public Class DataGridBoundTemplateColumn
        Inherits DataGridTemplateColumn
    
        Public Property BindingPath() As String
    
        Protected Overrides Function GenerateElement(ByVal cell As DataGridCell, ByVal dataItem As Object) As FrameworkElement
            Dim element = MyBase.GenerateElement(cell, dataItem)
            element.SetBinding(ContentPresenter.ContentProperty, New Binding(Me.BindingPath))
            Return element
        End Function
    
        Protected Overrides Function GenerateEditingElement(ByVal cell As DataGridCell, ByVal dataItem As Object) As FrameworkElement
            Dim element = MyBase.GenerateEditingElement(cell, dataItem)
            element.SetBinding(ContentPresenter.ContentProperty, New Binding(Me.BindingPath))
            Return element
        End Function
    
    End Class
    

    This is a VB translation of the work presented at http://stackoverflow.com/questions/14000873/define-datatemplate-in-xaml-instantiate-and-modify-in-code. 

    When it comes time to add a column, I do the following:

    Dim myCol As New DataGridBoundTemplateColumn
    Dim viewTemplate = CType(Application.Current.TryFindResource("EnvTemplate"), DataTemplate)
    Dim editTemplate = CType(Application.Current.TryFindResource("EnvEditingTemplate"), DataTemplate)
    
    EnvVarsDT.Columns.Add(EnvName)
    
    With myCol
      .Header = EnvName
      .Width = 100
      .CellTemplate = viewTemplate
      .CellEditingTemplate = editTemplate
      .BindingPath = EnvName
    End With
    
    DataGridColumns.Add(myCol)
    

    The XAML code for the DataGrid is simply:

    <DataGrid Name="EnvVarsDT" Grid.Row="1" CanUserAddRows="False" Margin="5" AutoGenerateColumns="False"
    jas:DataGridExtension.Columns="{Binding Path=DataContext.DataGridColumns, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
    

    … where jas: is the namespace for my project.  EnvVarsDT is a DataTable within the View Model and the DataGrid is named the same, following Caliburn conventions.  So far so good; the columns get added, with the correct headers.

    Now to the problem.  Using a templated approach (either the custom DataGridBoundTemplateColumn or a plain vanilla DataGridTemplateColumn), I am unable to get the bindings to work.  No binding errors show up in the Immediate window (or Output for that matter).  Meanwhile Snoop shows the DataContext property on each cell in the column to be empty.  User edits get lost as soon as they tab off the cell, and the values are not available via the DataTable.

    I can however get the bindings working if I use a regular DataGridTextColumn:

    Dim myCol As New DataGridTextColumn
    With myCol
       .Binding = New Binding(EnvName)
       .Width = 100
       .CellStyle = CType(Application.Current.TryFindResource("CellStyle"), Style)
       .Header = EnvName
    End With
    

    Now each cell in the dynamically added columns is properly bound and user edits are retained.

    But I need the construction of the cells to vary according to another data value in the row, so I don’t think I can use a DataGridTextColumn.  The editing template referenced in the template column approach specifies a button in one case and a textbox otherwise (and these show up properly, albeit with no binding value, when using a DataGridTemplateColumn).  I have experimented just with plain textboxes so as to take a complicated data template out of the equation, but to no avail.  Using a DataGridTemplateColumn, the editing controls are revealed and one can enter text, but the data is lost on tabbing away.  Using the DataGridBoundTemplateColumn, the controls are not revealed when attempting to edit – it is as if the cells were Read Only.

    The DataTemplates I’m using are, for simple test purposes, as follows:

    <DataTemplate x:Key="EnvTemplate">
        <TextBlock Text="{Binding Path=.}"/>
    </DataTemplate>
    
    <DataTemplate x:Key="EnvEditingTemplate">
        <TextBox Text="{Binding Path=.}"/>
    </DataTemplate>
    

    I’ve tried playing with setting the DataContext in these templates, eg DataContext="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type data:DataRowView}}}", but without success.

    I feel close to a working solution, with the binding on a dynamically added DataGridTemplate column being the outstanding matter.  Any guidance as to things to try will be much appreciated.

    With thanks and regards

    Sebastian Crewe







    Friday, December 6, 2013 4:06 PM

Answers

  • I think I'm there.  It took a lot of searching and experimentation, but I'm now getting what I want, thanks to your helpful steer.  For others following, here's what I did.  Your more informed comments on this approach will be very welcome.

    Most of what I read said that the FrameworkElementFactory was not the way to go.  Nonetheless, I used it for the non-editing template - a simple TextBlock - see the VB code section lower down.

    For the editing part, I transferred my DataContext into an xml file, with Build Action of Resource.  It looks like this:

    <DataTemplate
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        xmlns:local="clr-namespace:DeploymentPackager"
        xmlns:data="clr-namespace:System.Data;assembly=System.Data">
      
        <ContentControl>
            <ContentControl.Style>
                <Style TargetType="ContentControl">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="SQL Server">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <Button Name="btnSelectSQLServer" Content="Click to select">
                                            <!--<Button.DataContext>
                                                <local:PackagerView/>
                                            </Button.DataContext>-->
                                            <i:Interaction.Triggers>
                                                <i:EventTrigger EventName="Click">
                                                    <cal:ActionMessage MethodName="SelectSQLServer">
                                                        <cal:Parameter Value="{Binding ElementName=EnvVarsDT, Path=SelectedItem.VarName}"/>
                                                        <cal:Parameter Value="$eventargs"/>
                                                    </cal:ActionMessage>
                                                </i:EventTrigger>
                                            </i:Interaction.Triggers>
                                        </Button>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="Oracle Server">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="Instance Name">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="File Path">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="File Name">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="Integer">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="String">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    
    </DataTemplate>

    As an aside, if there's a way to have a default ContentTemplate for VarTypes other than SQL Server, that would be helpful to know and would save some lines of code.

    Anyway, in my Add event code, I added the new column to the underlying DataTable (EnvVarsDT in my case), then extracted the XML for the DataTemplate from the resource.  I looked for all instances of TextBox and set the Text property to:

    "{Binding DataContext." & EnvName & ", RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}}}"

    Exactly how to 'phrase' this took a lot of attempts, trying separately to set the DataContext and so on.  But the above works.  The code for the Add section looks like this:

    If Action = "Add" Then
    
                EnvVarsDT.Columns.Add(EnvName)
    
                Dim myCol As New DataGridTemplateColumn
                myCol.Header = EnvName
                myCol.Width = 100
    
                Dim myBinding As New Binding(EnvName)
                myBinding.Mode = BindingMode.TwoWay
    
                'Create the non-editing template
                Dim viewFactory As New FrameworkElementFactory(GetType(TextBlock))
                viewFactory.SetBinding(TextBlock.TextProperty, myBinding)
                Dim viewTemplate = New DataTemplate
                viewTemplate.VisualTree = viewFactory
    
                '------------- XML approach for the editing template
                Dim uri As New Uri("/Resources/EnvSettingDataTemplate.xml", UriKind.Relative)
                Dim info As Windows.Resources.StreamResourceInfo = Application.GetResourceStream(uri)
                Dim doc As New XmlDocument
                doc.Load(info.Stream)
    
                Dim nodes As XmlNodeList = doc.GetElementsByTagName("TextBox")
    
                For Each n As XmlNode In nodes
                    Dim myAttribute As XmlAttribute = doc.CreateAttribute("Text")
                    myAttribute.Value = "{Binding DataContext." & EnvName & ", RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}}}"
                    n.Attributes.Append(myAttribute)
                Next
    
                Dim editTemplate As DataTemplate = CType(XamlReader.Parse(doc.OuterXml), DataTemplate)
    
                myCol.CellTemplate = viewTemplate
                myCol.CellEditingTemplate = editTemplate
    
                DataGridColumns.Add(myCol)
    
            ElseIf Action = "Remove" Then
    ...

    Now the TextBoxes accept input that shows in the TextBlock after the edit is finished, and I can retrieve the entered values from the underlying DataTable.  Whoopee.

    I've still got to get the button for the SQL Server VarType to save its resultant value (from a pop-up window) in the correct cell, but I think that will be relatively straightforward.

    Thank you again for your interest and for making it clear where I had to go to resolve my problem.

    Sebastian


    • Proposed as answer by Terrence-Jones Wednesday, December 11, 2013 5:02 AM
    • Marked as answer by Sebastian Crewe Wednesday, December 11, 2013 10:24 AM
    Tuesday, December 10, 2013 5:57 PM

All replies

  • Shouldn't each specific template know to which source property to display? How do you know how these templates will look like when you create the columns, i.e. which kind of elements they contain? The DataGridTemplateColumns's CellTemplate or CellEditingTemplate should specify which property or column of the DataContext, i.e. the row in the DataTable, to bind to. Then it is simply a matter of choosing the right template for the right column, right?

    Here's an example:

    public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                //set the ItemsSource of the DataGrid:
                DataTable dt = new DataTable();
                dt.Columns.Add(new DataColumn("myCol"));
                dt.Rows.Add("1");
                dt.Rows.Add("2");
                EnvVarsDT.ItemsSource = dt.AsDataView();
    
                //create the dynamic column:
                //get a specific template that knows which property or properties to display:
                DataTemplate templateforMyCol = this.Resources["templateforMyCol"] as DataTemplate;
    
                DataGridTemplateColumn myCol = new DataGridTemplateColumn();
                myCol.CellTemplate = templateforMyCol;
                ...
                myCol.Header = "header....";
                myCol.Width = 100;
                this.DataGridColumns = new ObservableCollection<DataGridColumn>();
                this.DataGridColumns.Add(myCol);
               
            }
    
            public ObservableCollection<DataGridColumn> DataGridColumns
            {
                get;
                set;
            }
        }
    

        <Window.Resources>
            <DataTemplate x:Key="EnvTemplate">
                <TextBlock Text="{Binding Path=.}"/>
            </DataTemplate>
    
            <DataTemplate x:Key="EnvEditingTemplate">
                <TextBox Text="{Binding Path=.}"/>
            </DataTemplate>
            
     <!-- Specific template that knows how to display the values of the source properties on the screen -->
            <DataTemplate x:Key="templateforMyCol">
                <Button Content="{Binding myCol}"/>
            </DataTemplate>
        </Window.Resources>
        <Grid>
    
            <DataGrid Name="EnvVarsDT" Grid.Row="1" CanUserAddRows="False" Margin="5" 
                      AutoGenerateColumns="False"
                        jas:DataGridExtensions.Columns="{Binding Path=DataGridColumns, 
                RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, 
                UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
    
            </DataGrid>
        </Grid>
    

    Friday, December 6, 2013 4:57 PM
  • Many thanks for your quick response. Unfortunately, I don't think I can do as you suggest since the binding (myCol in your example) is not known at design time.  The user could choose one or more of 7 pre-set environments, or they can create a new one, per the screenshot below:

    Having made their choice(s), they move to the Settings screen to enter values appropriate to each environment.  The following screenshot shows two dynamically added columns (Dev and Prod).  These have been appended to the standard columns in the underlying DataTable (Name through Mandatory).

    I've used a straight DataGridTemplate column in my View Model file, so the templates work - a button where the Type is SQL Server (in order to select one from a pop-up window), and text boxes for other Types.

    When the user selects an environment, its name becomes the column header and a column in the backing DataTable.  It is the latter that I would like to bind the cell to.  It is only the dynamic columns that have a data template associated; the other fields are read-only.

    Perhaps, based on what you say, I need to construct the template in code as well, at which point I do know the column name.  Does that seem to be the most likely route for a solution?

    Given that making a dynamic DataGridTextColumn works fine, I wonder how the binding for it works differently than that for a DataGridBoundTemplateColumn per the second link in my original post.

    Many thanks for your interest

    Sebastian

    Friday, December 6, 2013 6:09 PM
  • Perhaps, based on what you say, I need to construct the template in code as well, at which point I do know the column name.  Does that seem to be the most likely route for a solution?

    Yes, it seems likely to me. How else should the elements in the data template know what source property to bind to?

    Given that making a dynamic DataGridTextColumn works fine, I wonder how the binding for it works differently than that for a DataGridBoundTemplateColumn per the second link in my original post.

    A DataGridTemplateColumn's templates are supposed to be defined by the developer - a DataGridTemplateColumn is used when you somehow want to customize the appearance of a column - while a DataGridTextColumn has a pre-defined CellTemplate in the form of a TextBlock and a CellEditingTemplate in form of a TextBox. In other words, a DataGridTemplateColumn could be a lot more complex with a lot of elements and it has no single Binding property like the DataGridTextColumn. If you simply need to display a scalar value in the columns, you should use a DataGridTextColumn. If you decide to use a DataGridTemplateColumn, you should define each individual template for it including the bindings.

    Friday, December 6, 2013 9:57 PM
  • I think I'm there.  It took a lot of searching and experimentation, but I'm now getting what I want, thanks to your helpful steer.  For others following, here's what I did.  Your more informed comments on this approach will be very welcome.

    Most of what I read said that the FrameworkElementFactory was not the way to go.  Nonetheless, I used it for the non-editing template - a simple TextBlock - see the VB code section lower down.

    For the editing part, I transferred my DataContext into an xml file, with Build Action of Resource.  It looks like this:

    <DataTemplate
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        xmlns:local="clr-namespace:DeploymentPackager"
        xmlns:data="clr-namespace:System.Data;assembly=System.Data">
      
        <ContentControl>
            <ContentControl.Style>
                <Style TargetType="ContentControl">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="SQL Server">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <Button Name="btnSelectSQLServer" Content="Click to select">
                                            <!--<Button.DataContext>
                                                <local:PackagerView/>
                                            </Button.DataContext>-->
                                            <i:Interaction.Triggers>
                                                <i:EventTrigger EventName="Click">
                                                    <cal:ActionMessage MethodName="SelectSQLServer">
                                                        <cal:Parameter Value="{Binding ElementName=EnvVarsDT, Path=SelectedItem.VarName}"/>
                                                        <cal:Parameter Value="$eventargs"/>
                                                    </cal:ActionMessage>
                                                </i:EventTrigger>
                                            </i:Interaction.Triggers>
                                        </Button>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="Oracle Server">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="Instance Name">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="File Path">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="File Name">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="Integer">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding Path=VarType}" Value="String">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                      <TextBox />
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    
    </DataTemplate>

    As an aside, if there's a way to have a default ContentTemplate for VarTypes other than SQL Server, that would be helpful to know and would save some lines of code.

    Anyway, in my Add event code, I added the new column to the underlying DataTable (EnvVarsDT in my case), then extracted the XML for the DataTemplate from the resource.  I looked for all instances of TextBox and set the Text property to:

    "{Binding DataContext." & EnvName & ", RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}}}"

    Exactly how to 'phrase' this took a lot of attempts, trying separately to set the DataContext and so on.  But the above works.  The code for the Add section looks like this:

    If Action = "Add" Then
    
                EnvVarsDT.Columns.Add(EnvName)
    
                Dim myCol As New DataGridTemplateColumn
                myCol.Header = EnvName
                myCol.Width = 100
    
                Dim myBinding As New Binding(EnvName)
                myBinding.Mode = BindingMode.TwoWay
    
                'Create the non-editing template
                Dim viewFactory As New FrameworkElementFactory(GetType(TextBlock))
                viewFactory.SetBinding(TextBlock.TextProperty, myBinding)
                Dim viewTemplate = New DataTemplate
                viewTemplate.VisualTree = viewFactory
    
                '------------- XML approach for the editing template
                Dim uri As New Uri("/Resources/EnvSettingDataTemplate.xml", UriKind.Relative)
                Dim info As Windows.Resources.StreamResourceInfo = Application.GetResourceStream(uri)
                Dim doc As New XmlDocument
                doc.Load(info.Stream)
    
                Dim nodes As XmlNodeList = doc.GetElementsByTagName("TextBox")
    
                For Each n As XmlNode In nodes
                    Dim myAttribute As XmlAttribute = doc.CreateAttribute("Text")
                    myAttribute.Value = "{Binding DataContext." & EnvName & ", RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}}}"
                    n.Attributes.Append(myAttribute)
                Next
    
                Dim editTemplate As DataTemplate = CType(XamlReader.Parse(doc.OuterXml), DataTemplate)
    
                myCol.CellTemplate = viewTemplate
                myCol.CellEditingTemplate = editTemplate
    
                DataGridColumns.Add(myCol)
    
            ElseIf Action = "Remove" Then
    ...

    Now the TextBoxes accept input that shows in the TextBlock after the edit is finished, and I can retrieve the entered values from the underlying DataTable.  Whoopee.

    I've still got to get the button for the SQL Server VarType to save its resultant value (from a pop-up window) in the correct cell, but I think that will be relatively straightforward.

    Thank you again for your interest and for making it clear where I had to go to resolve my problem.

    Sebastian


    • Proposed as answer by Terrence-Jones Wednesday, December 11, 2013 5:02 AM
    • Marked as answer by Sebastian Crewe Wednesday, December 11, 2013 10:24 AM
    Tuesday, December 10, 2013 5:57 PM
  • Thanks for sharing your solution here !
    Wednesday, December 11, 2013 5:01 AM