Home > Software design >  How to display DataGrid inside DataGrid RowDetailsTemplate?
How to display DataGrid inside DataGrid RowDetailsTemplate?

Time:09-17

I want to add a single row DataGrid inside another DataGrids row details template.

This is my XAML:

<DataGrid  x:Name="dataGrid" Margin="10,100,10,96" PreviewKeyDown="dataGrid_PreviewKeyDown" AutoGenerateColumns="False" ItemsSource="{Binding }" AlternatingRowBackground="AliceBlue" CellEditEnding="onCellEditEnding"  SelectedItem="{Binding SelectedItem}"  >

    <DataGrid.Columns>
        <DataGridTextColumn Width="*" Header="No" Binding= "{Binding Path=Id, Mode=TwoWay}" IsReadOnly="True" />
        <DataGridTextColumn Width="*" Header="Barcode" Binding= "{Binding Path=Barcode, Mode=TwoWay}"  />

        <DataGridTextColumn  Width="*" Header="Item" Binding= "{Binding Path=Item, Mode=TwoWay}" IsReadOnly="True"/>
        <DataGridTextColumn  Width="*" Header="Qty" Binding= "{Binding Path=Qty, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
        <DataGridTextColumn  Width="*" Header="Mrp" Binding= "{Binding Path=Mrp, Mode=TwoWay}" IsReadOnly="True"/>
        <DataGridTextColumn  Width="*" Header="Discount" Binding= "{Binding Path=Discount, Mode=TwoWay}" IsReadOnly="True"/>
        <DataGridTextColumn  Width="*" Header="Cgst" Binding= "{Binding Path=Cgst, Mode=TwoWay}" IsReadOnly="True"/>
        <DataGridTextColumn  Width="*" Header="Sgst" Binding= "{Binding Path=Sgst, Mode=TwoWay}" IsReadOnly="True"/>
        <DataGridTextColumn  Width="*" Header="Amount" Binding= "{Binding Path=Amount, Mode=OneWay ,UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True"/>
        <DataGridTextColumn  Width="*" Header="Salesman" Binding= "{Binding Path=Salesman, Mode=TwoWay}" IsReadOnly="True"/>
        <DataGridTemplateColumn Header="Delete">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Button Content="X" Click="Button_Click"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
    <DataGrid.RowDetailsTemplate >
        <DataTemplate>
            <DataGrid x:Name="RowDetailsDataGrid" AutoGenerateColumns="False">
                <DataGrid.Columns>
                    <DataGridTextColumn Width="*" Binding="{Binding Details}"  />
                    <DataGridTextColumn Width="*" Binding="{Binding CgstDetails}"  />
                    <DataGridTextColumn Width="*" Binding="{Binding SgstDetails}"  />
                    <DataGridTextColumn Width="*" Binding="{Binding DiscountDetails}"/>
                </DataGrid.Columns>
                
            </DataGrid> 
            
        </DataTemplate>
    </DataGrid.RowDetailsTemplate>
</DataGrid>

This is (roughly) my code-behind:

ObservableCollection<TableData> tableData = new ObservableCollection<TableData>();

public MainWindow()
{
    InitializeComponent();

    dataGrid.DataContext = tableData;
    
}

public class TableData : INotifyPropertyChanged
{
    public int Id { get; set; }
    public int No { get; set; }

  
    public string Details { get; set; }
    public string CgstDetails { get; set; }

    public string SgstDetails { get; set; }
    public string DiscountDetails { get; set; }
    public string Barcode { get; set; }
    public string Item { get; set; }
    private double qty;
    public double Qty {
        get { return qty; }
        set
        {
            qty = value;
            OnPropertyChanged("Qty");
            
        }
    }
    public double Mrp { get; set; }
    public double Discount { get; set; }
    public double Cgst { get; set; }
    public double Sgst { get; set; }
    private double amount; // field

    public double Amount
    {
        get { return amount; }
        set { 
            amount = (Qty * Mrp)   ((Cgst / 100) * (Mrp * Qty))   ((Sgst / 100) * (Mrp * Qty));
            OnPropertyChanged("Amount");
        }
    }

    public int Salesman { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(String propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        
        
    }

    public bool RowAdded{ get; set; }
}

private void onCellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
    var rowFetched = new TableData()
    {
        Id = currentRowIndex   1,
        No = currentRowIndex   1,
        Barcode = item.Barcode,
        Qty = item.Qty,
        Mrp = item.Mrp,
        Item = item.Item,
        Discount = item.Discount,
        Cgst = item.Cgst,
        Sgst = item.Cgst,
        Amount = item.Amount,
        Salesman = item.Salesman,
        Details = item.Details,
        DiscountDetails = item.DiscountDetails,
        CgstDetails = item.CgstDetails,
        SgstDetails = item.SgstDetails,
        RowAdded = true
    };
    
    tableData.Insert(currentRowIndex, rowFetched);
}

This is not working. Instead of a DataGrid I have just put a TextBlock inside WrapPanel (see the code below) and the data-binding is working perfectly and the data is shown in the row details, but I want it to be a row (DataGrid) instead of a WrapPanel.

<DataGrid.RowDetailsTemplate >
    <DataTemplate>
        <WrapPanel Background="LightGray">
            <Border BorderBrush="Black" BorderThickness="1" Width="355" Padding="40 0 0 0" >
                <TextBlock FontSize="16" Foreground="MidnightBlue" Text="{Binding Details}" VerticalAlignment="Center" />
            </Border>
            <Border BorderBrush="Black" BorderThickness="1" Width="71">
                <TextBlock FontSize="16" Foreground="MidnightBlue" Text="{Binding CgstDetails}" VerticalAlignment="Center" />
            </Border>
            <Border BorderBrush="Black" BorderThickness="1"  Width="71">
                <TextBlock FontSize="16" Foreground="MidnightBlue" Text="{Binding SgstDetails}" VerticalAlignment="Center" />
            </Border>
            <Border BorderBrush="Black" BorderThickness="1"  Width="71">
                <TextBlock FontSize="16" Foreground="MidnightBlue" Text="{Binding DiscountDetails}" VerticalAlignment="Center" />
            </Border>
            <Border BorderBrush="Black" BorderThickness="1"  Width="188">
                <TextBlock FontSize="16" Foreground="MidnightBlue" VerticalAlignment="Center" />
            </Border>
        </WrapPanel> 
    </DataTemplate>
</DataGrid.RowDetailsTemplate>

If anyone could help?

CodePudding user response:

I do not think that it is a good idea to put a nested DataGrid into the row details template just to display a single row, but if that is your only option, you can do it as follows.

A DataGrid expects an ItemsSource of a collection type, more specifically an IEnumerable. What you tried to do is bind properties on the DataContext itself directly, which does not work. There are at least two options that you have to make it work, both of which essentially provide a collection of a single item as ItemsSource to the nested DataGrid.

  • Create a value converter that wraps your TableData item in a collection (here an array).

    public class ItemToCollectionConverter : IValueConverter
    {
       public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
       {
          return value is null ? null : new[] { value };
       }
    
       public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
       {
          throw new InvalidOperationException();
       }
    }
    

    Create an instance of the converter in XAML (e.g. in your DataGrid.Resources) and simply bind the data context using the converter to the ItemsSource of the nested DataGrid. This way, your TableData item gets wrapped in a collection and is treated as a single item collection, so the column bindings will work correctly.

    <DataGrid  x:Name="dataGrid" Margin="10,100,10,96" PreviewKeyDown="dataGrid_PreviewKeyDown" AutoGenerateColumns="False" ItemsSource="{Binding}" AlternatingRowBackground="AliceBlue" CellEditEnding="onCellEditEnding"  SelectedItem="{Binding SelectedItem}"  >
        <DataGrid.Resources>
           <local:ItemToCollectionConverter x:Key="ItemToCollectionConverter"/>
        </DataGrid.Resources>
        <DataGrid.Columns>
            <DataGridTextColumn Width="*" Header="No" Binding= "{Binding Path=Id, Mode=TwoWay}" IsReadOnly="True" />
            <DataGridTextColumn Width="*" Header="Barcode" Binding= "{Binding Path=Barcode, Mode=TwoWay}"  />
            <DataGridTextColumn  Width="*" Header="Item" Binding= "{Binding Path=Item, Mode=TwoWay}" IsReadOnly="True"/>
            <DataGridTextColumn  Width="*" Header="Qty" Binding= "{Binding Path=Qty, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
            <DataGridTextColumn  Width="*" Header="Mrp" Binding= "{Binding Path=Mrp, Mode=TwoWay}" IsReadOnly="True"/>
            <DataGridTextColumn  Width="*" Header="Discount" Binding= "{Binding Path=Discount, Mode=TwoWay}" IsReadOnly="True"/>
            <DataGridTextColumn  Width="*" Header="Cgst" Binding= "{Binding Path=Cgst, Mode=TwoWay}" IsReadOnly="True"/>
            <DataGridTextColumn  Width="*" Header="Sgst" Binding= "{Binding Path=Sgst, Mode=TwoWay}" IsReadOnly="True"/>
            <DataGridTextColumn  Width="*" Header="Amount" Binding= "{Binding Path=Amount, Mode=OneWay ,UpdateSourceTrigger=PropertyChanged}" IsReadOnly="True"/>
            <DataGridTextColumn  Width="*" Header="Salesman" Binding= "{Binding Path=Salesman, Mode=TwoWay}" IsReadOnly="True"/>
            <DataGridTemplateColumn Header="Delete">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Button Content="X" Click="Button_Click"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
        <DataGrid.RowDetailsTemplate>
            <DataTemplate>
                <DataGrid x:Name="RowDetailsDataGrid" AutoGenerateColumns="False" ItemsSource="{Binding Converter={StaticResource ItemToCollectionConverter}}">
                    <DataGrid.Columns>
                        <DataGridTextColumn Width="*" Binding="{Binding Details}"  />
                        <DataGridTextColumn Width="*" Binding="{Binding CgstDetails}"  />
                        <DataGridTextColumn Width="*" Binding="{Binding SgstDetails}"  />
                        <DataGridTextColumn Width="*" Binding="{Binding DiscountDetails}"/>
                    </DataGrid.Columns>
                </DataGrid>
            </DataTemplate>
        </DataGrid.RowDetailsTemplate>
    </DataGrid>
    
  • Expose a collection property from your TableData type that contains a single item of a sub type or helper type that only contains the details properties.

    public class TableDetailsData
    {
       public string Details { get; set; }
       public string CgstDetails { get; set; }
       public string SgstDetails { get; set; }
       public string DiscountDetails { get; set; }
    }
    
    public class TableData : INotifyPropertyChanged
    {
       private TableDetailsData _detailsData;
       public TableDetailsData DetailsData
       {
          get => _detailsData;
          set
          {
             _detailsData = value;
             DetailsDataCollection = new List<TableDetailsData> { _detailsData };
             OnPropertyChanged(nameof(DetailsDataCollection));
          }
       }
    
       public IEnumerable<TableDetailsData> DetailsDataCollection { get; private set; }
    
       // ...other code.
    }
    

    The DetailsDataCollection is always updated when the DetailsData changes. Whether you create the collection like this and expose the DetailsData as object or as properties is up to you, there are many possibilities and this is only to get an idea what to do. Now you can bind this collection as ItemsSource in the nested DataGrid.

    <DataGrid x:Name="RowDetailsDataGrid" AutoGenerateColumns="False" ItemsSource="{Binding DetailsDataCollection}" CanUserAddRows="False">
    

Personally, I prefer the value converter solution as you do not have to expose a redundant collection property and pollute your code, as it is not a pretty clean solution. I would recommend you to create a proper details type and think about a better way to represent the data than a single row DataGrid, which seems out of place and does not provide a good user experience in this scenario.

  • Related