2

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

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286

3 Answers3

2

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>
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • Aaah! @H.B. my ol' buddy! LOL!! I even included code just for you this time! ;) As for your answer, doesn't that put a ContentControl within another ContentControl, as that's what the DataGridTemplateColumn already generates internally to hold the template? Still, this is better than nothing. I'm still leaning towards subclassing DataGridTemplateColumn to add a DataSource there. – Mark A. Donohoe May 02 '11 at 06:55
  • @MarquelIV: Actually the proper class to subclass is simply DataGridColumn because you do not want all those templating properties etc, i've been trying to do that just now and **i am sure it's the best solution**, just not quite there yet. Not sure what the visual tree of the DataGridCells looks like, personally i only consider ContentPresenter for use in ControlTemplates. – H.B. May 02 '11 at 07:05
  • @H.W. Actually you WANT all those other template-related things. You just want an additional way to set the source. Actually, take a look here... stackoverflow.com/questions/1104164/…. Specifically look at the 'SetBinding' line in the LoadTemplateContent code. We just need to add a 'BindingPath' DP (we could even have separate display/editing paths) to a subclass of DataGridTemplateColumn, then override that method (which we can't because it's private and not overridable) and init the binding with the path. Time to hit up Reflector Pro! :) – Mark A. Donohoe May 02 '11 at 07:27
  • @marqueiv: Not quite sure how to do it with direct bindings yet but i have a working memberpath-using class, see answer. – H.B. May 02 '11 at 07:32
  • @H.B. (re my prior comment...) AAAH! But you CAN override GenerateElement and GenerateEditingElement, then just make them call your own LoadTemplateContent function! Done and done! Testing now... – Mark A. Donohoe May 02 '11 at 07:40
  • @marqueiv: It's the obvious thing to do really. – H.B. May 02 '11 at 07:42
  • @H.B. Mine is very similar to yours except mine uses an actual PropertyPath object instead of a string. Also, I always use bindings for the content and never set the content directly as you did. I just create a pathless binding. I also have the support for TemplateSelectors which yours didn't. Finally, I left in the optimizations that Reflector revealed MS used. Still, I'm marking yours as accepted since it's better to do that than mark my own. :) – Mark A. Donohoe May 02 '11 at 08:16
1

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.

ColinE
  • 68,894
  • 15
  • 164
  • 232
  • Yeah @ColinE, @H.B. just said the same thing. Just out of curiosity to you both however, why not use a ContentPresenter? I mean doesn't the ContentControl just use that internally, so why not cut out the middleman since ContentPresenter was meant to be used in a template for exactly that purpose? – Mark A. Donohoe May 02 '11 at 06:59
1

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>
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • I think if you are going to make the property a `PropertyPath` you should first create the binding and then set the `Binding.Path` property, otherwise you might as well use a `string` which is parsed by the constructor of the binding. Besically you convert it back and forth once for no reason. – H.B. May 02 '11 at 08:57
  • @H.B. Good point. Updated! But this is definitely better than using the string outright as behind the scenes it's setting an object. This way, no converters are needed either explicitly in XAML or at all in C#. – Mark A. Donohoe May 02 '11 at 12:22