1

I'm trying to change the template of a cell inside a DataGrid depending on a List<List<int>> which holds the type of the cell (I.e 1=bool, 2=int, 3=string, 4=custom, etc...). The custom types (types because they can be more than 1) have to be represented by a ComboBox. For numbers and strings, I need a normal TextBox and for boolean I need a CheckBox. The DataGrid is binded to a DataTable which I can resize and edit at runtime. Here it is some code:

<Grid>
    <DataGrid ItemsSource="{Binding Path=DataTable}" Name="Grid" AutoGenerateColumns="True" 
      CanUserResizeRows="True" CanUserDeleteRows="False"
      CanUserAddRows="False" AreRowDetailsFrozen="False"
      SelectionUnit="Cell" LoadingRow="Grid_LoadingRow">
        <DataGrid.Style>
            <Style TargetType="DataGrid">
                <Setter Property="AlternatingRowBackground" Value="LightYellow"/>
            </Style>
        </DataGrid.Style>
    </DataGrid>
</Grid>
public partial class TableEditorWindow : Window
    {
        public string[] DebugNames = { "Bob", "Dan", "Pierre", "Mark", "Gary" };

        // Stores the values of the Table
        public ds_grid Table { get; set; }

        // Stores the types of each cell in the Table
        public ds_grid ValueTypesTable { get; set; }

        // Used as wrapper between the Table variable and the DataGrid
        public DataTable DataTable { get; set; }


        public TableEditorWindow()
        {
            InitializeComponent();

            Table = new ds_grid(5, 5);

            // Fills the Table with 1s
            for (int i = 0; i < 5; ++i)
            {
                for (int j = 0; j < Table.Width; ++j)
                {
                    Table.Set(i, j, 1d);
                }
            }

            DataTable = new DataTable();

            // Add the columns
            for (int i = 0; i < 5; ++i)
            {
                DataTable.Columns.Add(DebugNames[i]);
            }

            // Add the rows
            for (int i = 0; i < Table.Height; ++i)
            {
                DataRow _row = DataTable.NewRow();

                for (int j = 0; j < Table.Width; ++j)
                {
                    _row[j] = Table.Get(j, i);
                }

                DataTable.Rows.Add(_row);
            }

            Grid.DataContext = this;
            Grid.RowHeaderWidth = 50;
            Grid.ColumnWidth = 100;
        }

        // Gives to each row the correct name
        private void Grid_LoadingRow(object sender, DataGridRowEventArgs e)
        {
            int _id = e.Row.GetIndex();

            e.Row.Header = DebugNames[_id];
        }
    }

ds_grid is basically a List<List<object>> with some utility methods around it.

I saw that there are some solutions, such as using DataTrigger, but I think that in that case I'd need to write in in the DataGrid in the XAML file, but I can't because AutoGenerateColumns is True. There is also the possibility to change the Type of each column of the DataTable but I don't want that every cell of that column is of that type, I want that only a single cell becomes of that type, at runtime.

Maybe there are better solutions, such as not using a DataGrid, or not using a DataTable, or there is a way to set AutoGenerateColumns to False and manually generating every column when needed, by code. Any suggestion is really appreciated.

Thanks in advance.

Penca53
  • 80
  • 1
  • 8
  • there is an event to intercept and change generated column: https://stackoverflow.com/a/40359000/1506454 – ASh Jun 12 '20 at 21:41
  • Yeah but I don't want the entire column to be shown in that way. I only want 1 cell of that column to be different from the others. – Penca53 Jun 13 '20 at 08:20

2 Answers2

2

This is different enough from my original answer that I'm submitting it separately.

I also want to point out that is a very unconventional use of a DataGrid. Common data structure is that each column has a single type and I've hardly ever needed otherwise. My previous answer works if you stick with that convention. That being said, what you're asking for can be done.

If you really want to disregard common data structure and customize things on the cell level, you'll need a custom DataGridColumn:

public class DataTableBoundColumn : DataGridBoundColumn
{
    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        if (dataItem == CollectionView.NewItemPlaceholder) { return null; }

        DataRowView dataRow = (dataItem as DataRowView);
        if (dataRow == null) { throw new ArgumentException(); }

        object cellData = dataRow[cell.Column.DisplayIndex];

        var contentHost = new ContentControl() { Content = cellData };

        //Do some tests on cellData to determine the type and pick a DataTemplate
        //Alternatively, you could build the actual content here in code-behind, but a DataTemplate would probably be cleaner
        contentHost.ContentTemplate = (DataTemplate)SomeResourceDictionary["SomeResourceKey"];

        return contentHost;
    }

    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
        return GenerateElement(cell, dataItem);
    }
}

The above is based off the example from this article. Using this column type, your AutoGeneratingColumn handler would be as follows:

private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
    DataTableBoundColumn col = new DataTableBoundColumn();
    e.Column = col;
    e.Column.Header = "Whatever you want";
}
Keith Stein
  • 6,235
  • 4
  • 17
  • 36
  • Thank you so much! After a lot of research about the topic (the link you sent me was awesome), I've managed to do it! Because I wanted to get rid of the DataTable and of the Binding (because it was too slow on big grids (1000x1000)), I've also not used the DataTemplate in the XAML file. Instead, I've created each element by code, depending on the condition. In this way, I've boosted a lot my performance and I got rid of the DataTable. The Binding now is manual: each time you edit one cell, I raise an event which updates the content inside the data structure that I use, so that it is updated. – Penca53 Jun 22 '20 at 13:26
  • this may appear "off topic", but it cost me quite some: when adding columns to the `DataTable` it was necessary in my case to use the overload `Add(string columnName, **Type type**)`, because otherwise the result of `dataRow[cell.Column.DisplayIndex]` fetched in `GenerateElement` was not what I expected (a string, in my case). – mike Sep 20 '21 at 20:25
  • also, see [here](https://social.msdn.microsoft.com/Forums/sqlserver/en-US/7b21f84a-f977-4074-8aa4-3f446e0443dd/datagrid-shows-duplicate-rows-after-scrolling?forum=silverlightbugs): if `DataGrid.EnableRowVirtualization="True"` (default value), `GenerateElement` may not be called for rows coming into sight after scrolling. same goes for `EnableColumnVirtualization`, i guess. – mike Sep 20 '21 at 21:02
0

If you want to customize the auto-generated columns you will have to use the DataGrid.AutoGeneratingColumn event. I understand that you don't want the entity of a column to be the same, but you'll still need to use this event- you just need to use it a bit differently.

You were on the right track with thinking of Styles and DataTriggers to dynamically change the cell template. This would usually be done by declaring the columns in XAML, but you could get around that by using DataGrid.AutoGeneratingColumn and declaring your column as a resource.

Take something like this:

<DataGrid>
    <DataGrid.Resources>
        <DataGridTemplateColumn x:Key="TemplateColumn" x:Shared="False">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <ContentControl Content="{Binding}">
                        <ContentControl.Style>
                            <Style TargetType="ContentControl">
                                <Style.Triggers>
                                    <!--Use DataTriggers to set the content to whatever you need-->
                                    <DataTrigger>
                                        <!--...-->
                                    </DataTrigger>

                                    <DataTrigger>
                                        <!--...-->
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </ContentControl.Style>
                    </ContentControl>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Resources>
</DataGrid>

The above is a DataTemplate that uses a ContentControl with DataTriggers to dynamically set the ContentTemplate. I defined it as resource of the DataGrid with an x:Key. x:Shared="False" means that WPF will create a new instance of this column whenever the resource is requested, instead of creating one and giving out references to that single instance so users can "share" it. This will let you add multiple instances of the column to the DataGrid.

Your AutoGeneratingColumn would be something like this:

private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
    //Add an if statement if you only want to replace some of the columns with the dynamic template one

    var DG = (DataGrid)sender;
    DataGridTemplateColumn col = (DataGridTemplateColumn)DG.Resources["TemplateColumn"];
    e.Column = col;
    e.Column.Header = "Whatever you want";
}

This replaces your column with an insance of the TemplateColumn resource.

Keith Stein
  • 6,235
  • 4
  • 17
  • 36
  • First of all, thanks for your clear answer! But I still have some problems understanding it: 1) What should I write in the comment that you've written in the `DataGrid_AutoGeneratingColumn`? Let's make an example: The column height is 4. All the cells of that columns are the default ones, except for the second and the fourth. How would I do that? – Penca53 Jun 14 '20 at 09:12
  • 2) What should I put as condition in the DataTrigger? Let's keep the example above... in that case there are 2 types of cells, the default one and the modified one. The only way to know the type of the cell is by accessing to the element of `ValueTypesTable` at the exact position. How do I know the index of that specific column in XML? Or maybe I should use as condition another data? If you could make an example it would be awesome! Thank you so much again! – Penca53 Jun 14 '20 at 09:14
  • @Penca53 `DataGrid` works on the premise that each column represents a single, particular property of the items in the list. All the cells in a column are usually the same. You won't be able to have some default and some custom in a single column- they will have to be all custom, you just have to make the custom cell look like the default one (when you want it to) using `DataTrigger`s. – Keith Stein Jun 14 '20 at 16:11
  • @Penca53 The `DataContext` inside that cell will be the row item, which for you I think will be a [`DataRow`](https://learn.microsoft.com/en-us/dotnet/api/system.data.datarow). In `AutoGeneratingColumn`, you can get the `DisplayIndex` of the column, you should be able to use that to set up the bindings of that column to point to the right column in the `DataTable`. – Keith Stein Jun 14 '20 at 16:16
  • if the condition is a variable called X, which can be 1-2-3, what should I write in the DataTrigger? And how do I bind that variable to that specific data trigger? That's the question. – Penca53 Jun 14 '20 at 18:49
  • So? I just need to understand this part and it's done. – Penca53 Jun 15 '20 at 18:17