Search code examples
c#wpfdata-bindingdatatabledatagrid

DataGrid is partially updated when bound to DataTable in code-behind


I have a WPF window that has a DataGrid as one of its children. The number of columns in the DataGrid is only determined at runtime. Here is the code for the DataGrid in my window.xaml file:

 <DataGrid Name="pathLossGrid" 
                 AutoGenerateColumns="True"
                 ItemsSource="{Binding }"
                 VerticalScrollBarVisibility="Auto"
                 HorizontalScrollBarVisibility="Auto"
                 HorizontalAlignment="Center"
                 GridLinesVisibility="All">
                <DataGrid.Columns>
                    <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <local:PathLossCell/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
 </DataGrid>

In the Windows.xaml.cs, I've declared a dependency property for a DataTable property:

public static DependencyProperty PathLossTableProperty = DependencyProperty.Register(nameof(PathLossTable), typeof(DataTable), typeof(PathLossSelector));

public DataTable PathLossTable
{
     get => (DataTable)GetValue(PathLossTableProperty);
     set => SetValue(PathLossTableProperty, value);
}

I should note at this point that each cell in the DataGrid is of a custom user control type, which I have called a PathLossCell in my example code and that user control is a simple grid with two columns:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="0.8*"/>
        <ColumnDefinition Width="0.2*"/>
    </Grid.ColumnDefinitions>

    <TextBox Grid.Column="0" Text="{Binding S2pId }"
               MinWidth="50"/>
    <Button Grid.Column="1"
            Margin="3 0 0 0"
            Command="{Binding SelectS2pFileCommand}">
        <Viewbox Stretch="Uniform">
            <ContentControl Content="{DynamicResource icon_plus}"/>
        </Viewbox>
    </Button>            
</Grid>

the constructor of my Windows.xaml.cs file calls the following method:

public void InitPathLossTable()
{
    if (PathLossTable == null && SelectedPortList != null)
    {
        var tmpTable = new DataTable("pathLossTable");
        foreach (var port in SelectedPortList)
        {
            var portLoss = 0;
            DataColumn newColumn = new DataColumn($"{port} (Max Loss= {portLoss} dB)");
            newColumn.DataType = typeof(PathLossCell);
            tmpTable.Columns.Add(newColumn);
        }

        //add default row
        DataRow newRow = tmpTable.NewRow();
        for (int col = 0; col < tmpTable.Columns.Count; col++)
        {
            newRow[col] = new PathLossCell() { S2pFile = "no data", RowId = 0, ColumnId = col, PortName = tmpTable.Columns[col].ColumnName };
        }

        tmpTable.Rows.Add(newRow);

        PathLossTable = tmpTable;
        pathLossGrid.DataContext = PathLossTable.DefaultView;
    }
}

Based on my research on this site and other sources, this is the way to bind a DataTable to a DataGrid and what I expected to see in my window is a grid with 2 or 3 columns (depending on the number of items in the SelectedPortList and one row of default PathLossCell items. What I see when I run this code is a table that has the right number of columns (and the correct column headers), but only the cell in position [0,0] has the PathLossCell control in it and all the other cells display the fully qualified type string for PathLossCell and somehow an extra row: enter image description here

I've tried setting the binding directly in xaml to: ItemsSource="{Binding PathLossTable.DefaultView }" with no discernable difference in the result and not also not including the DataGridTemplateColumn definition, in which case I don't see even the one instance of the PathLossCell user control in the first column. Any ideas would be appreciated.

Edit after applying fix suggested by @BionicCode:

Replaced the DataGrid.Columns template with DataGrid.CellStyle as suggested and here is the result:

enter image description here


Solution

  • When you define a column then you are doing just that, defining a column explicitly. This is not to be confused with a DataTemplate e.g. for all cells.

    If you set DataGrid.AutogenerateColumns to true then the DataGrid will generate the column definitions for you by adding a DataGridTextColumn to the DataGrid.Columns collection for each column. As the name DataGridTextColumn suggests, it only displays text (the object.ToString value of the cell's item) as the ControlTemplate of the DataGridCell will simply contain a TextBlock. If you need a more sophisticated column layout you can define a DataGridTemplateColumn which supports a custom column layout by providing a cell template - only for the current column. That's why the name DataGridTemplateColumn contains the word "Template". It's not a template for all columns but for the particular column definition.

    To create a default template for all cells of the DataGrid you simply provide a Style for the DataGridCell and assign it to the DataGrid.CellStyle property.

    Regarding your DataGrid.ItemsSource bindig, you don't have to explicitly bind to the DataTable.DataView property. The binding will automatically get the data view.

    <DataGrid ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=PathLossTable}">
      <DataGrid.CellStyle>
        <Style TargetType="DataGridCell">
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate TargetType="DataGridCell">
    
                <!-- The DataContext is the current cell value (not the row item) -->
                <local:PathLossCell/>
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </DataGrid.CellStyle>
    </DataGrid>