Penca53
Penca53

Reputation: 80

How can I change cell template in a dynamic DataGrid (binded to a DataTable)?

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.

Upvotes: 1

Views: 1665

Answers (2)

Keith Stein
Keith Stein

Reputation: 6766

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";
}

Upvotes: 2

Keith Stein
Keith Stein

Reputation: 6766

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.

Upvotes: 0

Related Questions