Contentpresenter with type based datatemplate selection and binding
Asked Answered
I

3

5

I have an ItemsControl that binds to a list of items. These items have a name and value property. The value property is of type Object to allow different datatypes to be used. To display the value property correctly I use a ContentPresenter with a datatemplate for every datatype I might use.

  <ItemsControl ItemsSource="{Binding Items}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto" />
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>

                <TextBlock Text="{Binding Path=Name}"/>

                <GridSplitter Width="1" 
                              Grid.RowSpan="4" Grid.Column="1" 
                              HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>

                <ContentPresenter Grid.Column="2" Content="{Binding Value}">
                    <ContentPresenter.Resources>
                        <DataTemplate DataType="{x:Type System:String}">
                            <TextBox Text="{Binding Path=Content, RelativeSource={RelativeSource AncestorType={x:Type ContentPresenter}}}" 
                                     BorderThickness="0"/>
                        </DataTemplate>
                        <DataTemplate DataType="{x:Type System:Int32}">
                            <TextBox Text="{Binding Path=Content, RelativeSource={RelativeSource AncestorType={x:Type ContentPresenter}}}" 
                                     TextAlignment="Right"
                                     BorderThickness="0"/>
                        </DataTemplate>
                        <DataTemplate DataType="{x:Type System:Double}">
                            <TextBox Text="{Binding Path=Content, RelativeSource={RelativeSource AncestorType={x:Type ContentPresenter}}}" 
                                     TextAlignment="Right"
                                     BorderThickness="0"/>
                        </DataTemplate>
                        <DataTemplate DataType="{x:Type System:Boolean}">
                            <CheckBox IsChecked="{Binding Path=Content, RelativeSource={RelativeSource AncestorType={x:Type ContentPresenter}}}"
                                          HorizontalAlignment="Center"/>
                        </DataTemplate>
                    </ContentPresenter.Resources>
                </ContentPresenter>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

The ContentPresenter uses the correct datatype and works great. My problem is that editing these values does not have any effect on the bound items. I suspect it is because I bind to the content property of the ContentPresenter rather than directly to the Value. I've tried using the ContentPresenter like this:

<ContentPresenter Grid.Column="2" Content="{Binding}">
    <ContentPresenter.Resources>
        <DataTemplate DataType="{x:Type System:String}">
            <TextBox Text="{Binding Value}" 
                 BorderThickness="0"/>
        </DataTemplate>

But this way the correct DataTemplate isn't selected and it just displays the Object instead of a String for example. I also tried to leave out the path in the binding of the DataTemplate like this:

 <DataTemplate DataType="{x:Type System:String}">
    <TextBox Text="{Binding}" BorderThickness="0"/>
 </DataTemplate>

With this I get an exception telling me to use the Path or XPath attribute.

So my question is: how do I correctly bind to the Value so it displays with the right DataTemplate and that any editing of the values is applied to the bound item.

Btw for some reason the formatted code blocks in my question indent much more after the first line. I tried fixing it but I don't understand what's happening.

Insurrectionary answered 3/2, 2014 at 15:42 Comment(2)
In future, please format your code so that it can be seen, rather than the majority of it being off screen. Hint: if you select the code when you're editing, you can click on the 'Code Sample' button to move it all forwards or backwards.Ehr
You said To display the value property correctly I use a ContentPresenter with a datatemplate for every datatype I might use... that is not how to display it correctly... generally a ContentPresenter should not be used outside of a ControlTemplate. From the linked page: You typically use the ContentPresenter in the ControlTemplate of a ContentControl to specify where the content is to be added.Ehr
I
5

Already being uncomfortable with my solution I also ran into the problem of not being able to add a List DataType to the DataTemplates. I ended up using a DataTemplateSelector which resulted in much nicer code. Here it is:

The ContentControl. A container for the data which the DataTemplate is applied over:

<ContentControl Grid.Column="2" Content="{Binding}"
                ContentTemplateSelector="{StaticResource propertyItemTemplateSelector}">
</ContentControl>

A few DataTemplates and a declaration for the DataTemplateSelector:

<Style.Resources>
    <local:PropertyTemplateSelector x:Key="propertyItemTemplateSelector"/>
    <DataTemplate x:Key="dtStringValue">
        <TextBox Text="{Binding Path=Value}"
                 BorderThickness="0"
                 IsReadOnly="{Binding Path=IsReadOnly}">
        </TextBox>
    </DataTemplate>

    <DataTemplate x:Key="dtIntegerValue">
        <TextBox Text="{Binding Path=Value}"
                 TextAlignment="Right"
                 BorderThickness="0"
                 IsReadOnly="{Binding Path=IsReadOnly}"/>
    </DataTemplate>
...

The code for the DataTemplateSelector:

 public class PropertyTemplateSelector : DataTemplateSelector
 {
    public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
    {
        DataTemplate template = null;

        IPropertyItem propertyItem = item as IPropertyItem;

        if (propertyItem != null)
        {
            FrameworkElement element = container as FrameworkElement;
            if (element != null)
            {
                var value = propertyItem.Value;

                if (value is String)
                {
                    template = element.FindResource("dtStringValue") as DataTemplate;
                }
                else if (value is Int32)
                {
                    template = element.FindResource("dtIntegerValue") as DataTemplate;
                }
                 ....
Insurrectionary answered 6/2, 2014 at 11:5 Comment(0)
E
7

I think you'll benefit from reading about DataTemplates. First, I'd advise you to have a good, long read of the Data Templating Overview page on MSDN. As I mentioned in my comments, you should not use a ContentPresenter in your DataTemplates. From the linked page:

You typically use the ContentPresenter in the ControlTemplate of a ContentControl to specify where the content is to be added.

The thing that you seem to be missing is how and what to data bind to from inside a DataTemplate. The DataContext of the DataTemplate will automatically be set to an instance of the type specified in the DataType property. Therefore, the properties that the DataTemplate has access to will also depend on the type specified in the DataType property. For example, you cannot do this, because a string does not have a Value property.

<DataTemplate DataType="{x:Type System:String}">
    <TextBox Text="{Binding Value}" BorderThickness="0" />
</DataTemplate>

Instead, for a string, you'd need to data bind to the whole DataContext value like this:

<DataTemplate DataType="{x:Type System:String}">
    <TextBox Text="{Binding}" BorderThickness="0" />
</DataTemplate>

Alternatively, if you had a class names SomeClass and that class has a Value property, then you could do this:

<DataTemplate DataType="{x:Type YourDataTypesPrefix:SomeClass}">
    <TextBox Text="{Binding Value}" BorderThickness="0"/>
</DataTemplate>

Now, because these DataTemplates have been defined without setting their x:Key values, the Framework will automatically render the content of each DataTemplate whenever it sees an object of the relevant type (and no other, explicit templates set). So try this out and if you still have a problem, let me know.

Ehr answered 3/2, 2014 at 16:22 Comment(4)
Thank you for your answer. I read the Data Templating Overview and learned a lot from it. I definitely see some ways to improve my design. But I still do not know how to solve my problem. The reason I used a ContentPresenter was because I needed something to present my content and structure my data presentation depending on the type of the content. I do not know another control that merely acts as a placeholder. I did try to bind to the DataContext without giving a path exactly like you said, but I get a XamlParseException with the message "Two-way binding requires Path or XPath."Insurrectionary
You need a ContentControl, not a ContentPresenter.Ehr
I understand now that the ContentControl is the correct control to use in this case. But both the ContentPresenter and ContentControl have the same result. I did find that using a dot as path does prevent the exception from being thrown, but the binding still does not work. This answer seems to explain why "Are “{Binding Path=.}” and “{Binding}” really equal". It seems my approach is all wrong. I have no idea how I should implement this behaviour but I know this way is not correct.Insurrectionary
You can just use one DataTemplate and one ContentControl per data type/control... no problem.Ehr
I
5

Already being uncomfortable with my solution I also ran into the problem of not being able to add a List DataType to the DataTemplates. I ended up using a DataTemplateSelector which resulted in much nicer code. Here it is:

The ContentControl. A container for the data which the DataTemplate is applied over:

<ContentControl Grid.Column="2" Content="{Binding}"
                ContentTemplateSelector="{StaticResource propertyItemTemplateSelector}">
</ContentControl>

A few DataTemplates and a declaration for the DataTemplateSelector:

<Style.Resources>
    <local:PropertyTemplateSelector x:Key="propertyItemTemplateSelector"/>
    <DataTemplate x:Key="dtStringValue">
        <TextBox Text="{Binding Path=Value}"
                 BorderThickness="0"
                 IsReadOnly="{Binding Path=IsReadOnly}">
        </TextBox>
    </DataTemplate>

    <DataTemplate x:Key="dtIntegerValue">
        <TextBox Text="{Binding Path=Value}"
                 TextAlignment="Right"
                 BorderThickness="0"
                 IsReadOnly="{Binding Path=IsReadOnly}"/>
    </DataTemplate>
...

The code for the DataTemplateSelector:

 public class PropertyTemplateSelector : DataTemplateSelector
 {
    public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
    {
        DataTemplate template = null;

        IPropertyItem propertyItem = item as IPropertyItem;

        if (propertyItem != null)
        {
            FrameworkElement element = container as FrameworkElement;
            if (element != null)
            {
                var value = propertyItem.Value;

                if (value is String)
                {
                    template = element.FindResource("dtStringValue") as DataTemplate;
                }
                else if (value is Int32)
                {
                    template = element.FindResource("dtIntegerValue") as DataTemplate;
                }
                 ....
Insurrectionary answered 6/2, 2014 at 11:5 Comment(0)
I
3

I found sort of a workaround for this problem. The reason the binding did not work is because I bound to the content of the ContentControl. Two-way binding to a binding source does not work as stated here. This is the reason I got the exception. I still use the ContentControl with the DataTemplates to differentiate between the data types. But instead of binding to the content of the ContentControl I bind to the value the ContentControl binds to. Notice the Path in the binding.

<ContentControl Content="{Binding Value}" Grid.Column="2">
    <ContentControl.Resources>
        <DataTemplate DataType="{x:Type System:String}">
            <TextBox Text="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.Value}" BorderThickness="0" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type System:Int32}">
            <TextBox Text="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.Value}"
                     TextAlignment="Right"
                     BorderThickness="0"/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type System:Double}">
            <TextBox Text="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.Value}"
                     TextAlignment="Right"
                     BorderThickness="0"/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type System:Boolean}">
            <CheckBox IsChecked="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.Value}"
                          HorizontalAlignment="Center"/>
        </DataTemplate>
    </ContentControl.Resources>
</ContentControl>

It is a solution to the problem. I just feel a bit uncomfortable for using the ContentControl just to diferentiate between the DataTypes and having an illogical binding.

Also, thank you for helping me unravel this problem Sheridan.

Insurrectionary answered 4/2, 2014 at 14:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.