Add data-binding for DataGridTemplateColumn created in code
Asked Answered
B

3

5

The question:

Is there a way to define a DataTemplate in XAML and instantiate it in code (rather than retrieve singleton by FindResource) and modify its VisualTree before sending to where a DataTemplate is required such as DataGridTemplateColumn.CellTemplate?

Background:

I am displaying a 2-dimensional array data[][] in a DataGrid by adding DataGridTemplateColumn columns on my own and there is a DataTemplate defined in XAML that knows how to present each element in the array. However the default DataContext for each cell is the row, i.e. data[x]. So I need to "parameterize" the DataTemplate for each column by setting the root visual element's DataContext to binding "[y]" where y is the column index. Currently the DataTemplate is defined as in DataGrid.Resources and retrieved by FindResource() which is returning the same instance every time. Besides calling LoadContent() gives me the UIElement tree rather than loading the VisualTree on the DataTemplate itself. I am looking for a way to instantiate the DataTemplate in code, do the desired modification and set to DataGridTemplateColumn.CellTemplate.

Beef answered 22/12, 2012 at 7:17 Comment(0)
B
9

Inspired by Sisyphe's answer, I found this more portable solution:

public class DataGridBoundTemplateColumn : DataGridTemplateColumn
{
    public string BindingPath { get; set; }

    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
        var element = base.GenerateEditingElement(cell, dataItem);
        element.SetBinding(ContentPresenter.ContentProperty, new Binding(this.BindingPath));
        return element;
    }

    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        var element = base.GenerateElement(cell, dataItem);
        element.SetBinding(ContentPresenter.ContentProperty, new Binding(this.BindingPath));
        return element;
    }
}

Usage:

var cellTemplate = (DataTemplate)this.dataGrid.FindResource("cellTemplate");
foreach (var c in data.Columns)
{
    var col = new DataGridBoundTemplateColumn
    {
        Header = c.HeaderText,
        CellTemplate = cellTemplate,
        BindingPath = string.Format("[{0}]", c.Index)
    };

    this.dataGrid.Columns.Add(col);
}

Hope this helps someone who has the same requirement as the one in my question.

Beef answered 28/12, 2012 at 21:44 Comment(1)
That's a very interresting answer. I didn't know the virtual methods GenerateEditingElement and GenerateElement. I'm probably going to use this in my project ! Thanks a lot for this and for the bounty !Balbur
F
1
(templateKey as DataTemplate).LoadContent()

Description: When you call LoadContent, the UIElement objects in the DataTemplate are created, and you can add them to the visual tree of another UIElement.

Franz answered 22/12, 2012 at 7:27 Comment(7)
Thanks for reply. If I understand correctly, LoadContent() doesn't return a new DataTemplate, but I need a DataTemplate instance to feed DataGridTemplateColumn.CellTemplate.Beef
@Beef LoadContent is a method of DataTemplate class and create new instance of UIElement in DataTemplate.Franz
I see. I need a way to create new instance of the DataTemplate, not only the UIElement within it.Beef
Ok, but you could not customize a DataTemplate unless you define each datatemplate in resource and use DataTemplateSelector.Franz
(x as Y).Z() is a bad choice. Why not (Y)x.Z()?Buyer
@KierenJohnstone You would need ((Y)x).Z() due to operator precedence. Otherwise, I don't see why it is a bad choice.Rennet
@Rennet - Ah true. Because NullReferenceException is less explanatory than InvalidCastException I think - and InvalidCastException directly explains the problem, if it happensBuyer
B
1

You should see DataTemplate in WPF as a Factory. Thus I think that you don't really need a new instance of the DataTemplate, you just want it to be applied differently based on your context.

If I understand correctly your issue, the problem is that the DataContext of your DataGrid Cells is not correct : it's the Row ViewModel whereas you want it to be the Cell ViewModel (which makes perfect sense). This is however the basic behavior of the DataGrid and is probably tied to the fact that Cells in each rows are hold by a DataGridCellsPresenter (which is basically an ItemsControl) whose ItemsSource dependency property has not been set (thus explaining the bad DataContext).

I've run into this problem and found two way to fix this (but I only managed to make one work).

First one is to subclass DataGridCellsPresenter and override OnItemChanged method to set the ItemsSource manually.

protected override void OnItemChanged(object oldItem, object newItem)
{
    var rowViewModel = newItem as ViewModel;
    if (rowViewModel != null)
    {
        ItemsSource = rowViewModel.Items;
    }
    else
    {
        ItemsSource = null;
    }
}

where rowViewModel.Items should point to something like data[x] in your case. However I ran into some troubles using this fix and couldnt make it work correctly.

Second solution is to subclass DataGridCell and update the dataContext on change of the ColumnProperty. You also have to subclass DataGridCellsPresenter to make it create the right cell controls

public class MyDataGridCell : DataGridCell
{
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        if (e.Property == ColumnProperty)
        {
            var viewModel = DataContext as YourViewModelType;
            if (viewModel != null)
            {
                var column = (e.NewValue as DataGridTemplateColumn);
                if (column != null)
                {
                    var cellViewModel = viewModel[column.DisplayIndex];
                    DataContext = cellViewModel;
                }
            }
        }
        base.OnPropertyChanged(e);
    }
}

public class MyDataGridCellsPresenterControl : DataGridCellsPresenter
{
    protected override System.Windows.DependencyObject GetContainerForItemOverride()
    {
        return new MyDataGridCell();
    }
}

Finally you will also have to override the DataGridRow default ControlTemplate to make it use your custom DataGridCellsPresenter in place of the original DataGridCellsPresenter.

<ControlTemplate x:Key="DataGridRowControlTemplate" TargetType="{x:Type DataGridRow}">
    <Border x:Name="DGR_Border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
        <SelectiveScrollingGrid>
            <SelectiveScrollingGrid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </SelectiveScrollingGrid.ColumnDefinitions>
            <SelectiveScrollingGrid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </SelectiveScrollingGrid.RowDefinitions>
            <local:MyDataGridCellsPresenter Grid.Column="1" ItemsPanel="{TemplateBinding ItemsPanel}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
            <DataGridDetailsPresenter Grid.Column="1" Grid.Row="1" Visibility="{TemplateBinding DetailsVisibility}">
                <SelectiveScrollingGrid.SelectiveScrollingOrientation>
                    <Binding Path="AreRowDetailsFrozen" RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type DataGrid}}">
                        <Binding.ConverterParameter>
                            <SelectiveScrollingOrientation>Vertical</SelectiveScrollingOrientation>
                        </Binding.ConverterParameter>
                    </Binding>
                </SelectiveScrollingGrid.SelectiveScrollingOrientation>
            </DataGridDetailsPresenter>
            <DataGridRowHeader Grid.RowSpan="2" SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical">
                <DataGridRowHeader.Visibility>
                    <Binding Path="HeadersVisibility" RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type DataGrid}}">
                        <Binding.ConverterParameter>
                            <DataGridHeadersVisibility>Row</DataGridHeadersVisibility>
                        </Binding.ConverterParameter>
                    </Binding>
                </DataGridRowHeader.Visibility>
            </DataGridRowHeader>
        </SelectiveScrollingGrid>
    </Border>
</ControlTemplate>
Balbur answered 27/12, 2012 at 10:11 Comment(2)
You understood my issue precisely. The solution is not along the line of my question but looks promising to solve the problem. Thank you very much!Beef
You're welcome. Don't hesitate to pop on the WPF chat room if you need more help on this issue ;)Balbur

© 2022 - 2024 — McMap. All rights reserved.