In the built-in WPF DataGrid, can I set the dataso

2019-05-26 05:52发布

问题:

In this hypothetical example, imagine I have an object FooSet that has five properties Foo1, Foo2, Foo3 Foo4 and Foo5 all of which are type Foo which itself has several properties. Finally, I have a DataTemplate called FooTemplate that knows how to display objects of type Foo in a graphical way.

Now when using the built-in DataGrid, ItemsSource is a collection of FooSet objects. What I want to do is set up five templated columns that all use the FooTemplate data template. However, the DataGrid's template column type doesn't let me set the data source for that column (e.g. Foo1, Foo2, etc.) so I end up duplicating the template, once for each column, just changing Foo1.SomeProp to Foo2.SomeProp in the template's bindings, which is ridiculous of course. But I for the life of me can't find how to say 'Column B uses Foo2 as it's data source.'

Here's some Pseudo-XAML to show what I want...

<Resources>
    <DataTemplate TargetType="Foo">
        <StackPanel>
            <local:FooPropAControl Value="{Binding FooPropA}" />
            <local:FooPropBControl Value="{Binding FooPropB}" />
            <local:FooPropCControl Value="{Binding FooPropC}" />
        </StackPanel>
    </DataTemplate>
</Resources>

<DataGrid ItemsSource="{Binding MyItems}" AutoGenerateColumns="false">
    <DataGrid.Columns>
        <DataGridTemplateColumn DataSource="{Binding Foo1}" />
        <DataGridTemplateColumn DataSource="{Binding Foo2}" />
        <DataGridTemplateColumn DataSource="{Binding Foo3}" />
        <DataGridTemplateColumn DataSource="{Binding Foo4}" />
        <DataGridTemplateColumn DataSource="{Binding Foo5}" />
    </DataGrid.Columns>
</DataGrid>

Even if I have to explicitly specify the template in the column, that's still fine. It's setting the data source for that column to a property of FooSet so I can just use one DataTemplate. All the other columns let you set some binding that does that. I even tried subclassing DataGridTemplateColumn to add DataSource but didn't get too far (my guess is because there isn't a column per se but rather that dictates how cells in rows are generated, but that's just a guess.)

Now I know the 3rd-party Xceed grid lets you specify exactly that but I'm hoping for a native solution.

So, howzyadoodat? Or can you?

M

回答1:

Good question, i would approach it using a ContentControl, the code will still be a bit inflated but it's better than duplicating the whole template, e.g.:

<DataGrid ItemsSource="{Binding EmpSets}">
    <DataGrid.Resources>
        <DataTemplate DataType="{x:Type obj:Employee}">
            <TextBlock>
                <Run Text="{Binding Name}"/>
                <Run Name="RunChan" Text=" - "/>
                <Run Text="{Binding Occupation}"/>
            </TextBlock>
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="Emp1">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <ContentControl Content="{Binding Emp1}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
        <DataGridTemplateColumn Header="Emp2">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <ContentControl Content="{Binding Emp2}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
        <!-- ... -->
    </DataGrid.Columns>
</DataGrid>

Here i use one implicit DataTemplate in the resources but you could also apply it explicitly as the ContentTemplate of each ContentControl by defining & referencing a key but you know that anyway.


Barebone subclassing approach:

public class DataGridTemplateMemberColumn : DataGridTemplateColumn
{
    public static readonly DependencyProperty MemberPathProperty =
            DependencyProperty.Register("MemberPath", typeof(string), typeof(DataGridTemplateMemberColumn), new UIPropertyMetadata(null));
    public string MemberPath
    {
        get { return (string)GetValue(MemberPathProperty); }
        set { SetValue(MemberPathProperty, value); }
    }

    protected override System.Windows.FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
        return GenerateContent(CellEditingTemplate, dataItem);
    }

    protected override System.Windows.FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        return GenerateContent(CellTemplate, dataItem);
    }

    private FrameworkElement GenerateContent(DataTemplate template, object dataItem)
    {
        var contentControl = new ContentControl();
        contentControl.ContentTemplate = template;
        if (MemberPath != null)
        {
            Binding binding = new Binding(MemberPath);
            binding.Source = dataItem;
            contentControl.SetBinding(ContentControl.ContentProperty, binding);
        }
        else
        {
            contentControl.Content = dataItem;
        }
        return contentControl;
    }
}
<DataGrid.Columns>
    <cex:DataGridTemplateMemberColumn MemberPath="Emp1" />
    <cex:DataGridTemplateMemberColumn MemberPath="Emp2" />
    <cex:DataGridTemplateMemberColumn MemberPath="Emp3" />
    <cex:DataGridTemplateMemberColumn MemberPath="Emp4" />
    <cex:DataGridTemplateMemberColumn MemberPath="Emp5" />
</DataGrid.Columns>


回答2:

You could use a ContentControl within each column to provide the required binding:

<DataGrid ItemsSource="{Binding MyItems}" AutoGenerateColumns="false">
    <DataGrid.Columns>
        <DataGridTemplateColumn>
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <ContentControl Content="{Binding Foo1}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>                    
        </DataGridTemplateColumn>
        ...
    </DataGrid.Columns>
</DataGrid>

A ContentControl is a lookless control that renders its Content, (which defaults to its DataContext) using a template. Therefore your implicit DataTemplate should be used.



回答3:

This is a cleaned-up version of something extremely similar to what @H.B. suggested. However, SO etiquette says to vote for others when you can so even though this is the one I'm using, I still voted him as accepted.

public class DataGridTemplateMemberColumn : DataGridTemplateColumn
{

    public static readonly DependencyProperty MemberPathProperty = DependencyProperty.Register(
        "MemberPath",
        typeof(PropertyPath),
        typeof(DataGridTemplateMemberColumn),
        new UIPropertyMetadata(null)
    );

    public PropertyPath MemberPath
    {
        get { return (PropertyPath)GetValue(MemberPathProperty); }
        set { SetValue(MemberPathProperty, value); }
    }

    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    protected override FrameworkElement GenerateEditingElement(DataGridCell cell, object dataItem)
    {
        return LoadTemplateContent(CellEditingTemplate ?? CellTemplate, CellEditingTemplateSelector ?? CellTemplateSelector);
    }

    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
    {
        return LoadTemplateContent(CellTemplate, CellTemplateSelector);
    }

    private FrameworkElement LoadTemplateContent(DataTemplate template, DataTemplateSelector selector)
    {
        ContentPresenter target = new ContentPresenter();

        target.ContentTemplate         = template;
        target.ContentTemplateSelector = selector;

        BindingOperations.SetBinding(
            target,
            ContentPresenter.ContentProperty,
            new Binding(){Path = MemberPath}
        );

        return target;

    }

}

...and here's how you use it...

<DataGrid AutoGenerateColumns="False">
    <DataGrid.Columns>
        <foo:DataGridTemplateMemberColumn Header="Input"  MemberPath="Input"  />
        <foo:DataGridTemplateMemberColumn Header="Output" MemberPath="Output" />
    </DataGrid.Columns>
</DataGrid>