-->

Combining DataTemplates at runtime

2019-01-29 13:32发布

问题:

I have a ListBox that presents a databound list of objects via its ItemSource. Because each object has special display needs I’m defining an ItemTemplateSelector that returns the appropriate DataTemplate depending on the object. That all works without a hitch.

The DataTemplates for each object follow a common formula, but contains custom elements in the middle. For example:

    <DataTemplate x:Key="collectibleTemplate">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Border BorderBrush="LightGray" BorderThickness="1">
                <Expander IsExpanded="True" Header="{Binding ComponentName}" Background="WhiteSmoke">
                    <StackPanel>
                        <TextBlock Margin="5,5,5,0" Text="{Binding EditDescription}" TextWrapping="Wrap" />

                        <!-- This is the only custom part of each template -->
                        <StackPanel Margin="0,10,5,0" Orientation="Horizontal">
                            <Label Content="Type:" />
                            <ComboBox Height="22" HorizontalAlignment="Left" SelectedItem="{Binding Path=CollectibleType, Mode=TwoWay}"
                                            ItemsSource="{Binding Source={StaticResource collectibleTypeFromEnum}}" />
                        </StackPanel>
                        <!-- End custom part -->

                        <StackPanel Margin="0,0,0,5">
                            <Label Content="Available Actions:" >
                                <Label.Style>
                                    <Style TargetType="Label">
                                        <Setter Property="Visibility" Value="Visible" />
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding EditActions.Count}" Value="0">
                                                <Setter Property="Visibility" Value="Collapsed" />
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </Label.Style>
                            </Label>
                            <ItemsControl ItemsSource="{Binding EditActions}">
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate>
                                        <Button Command="{Binding}" Content="{Binding Title}" ToolTip="{Binding ToolTip}" Margin="5,0,5,0"/>
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                        </StackPanel>
                    </StackPanel>
                </Expander>
            </Border>
        </Grid>
    </DataTemplate>

As you can see there’s lots of shared XAML, wrapping a small custom section in the middle.

Additional data templates will be written by other engineers (they’ll want to create one for each new object type that they add), so I’m interested in making the creation of a new DataTemplate as fool-proof and painless as possible. No copying of the entire DataTemplate with the custom “stuff” added in the middle, of course – but I’m also not partial to extracting parts of the template as reusable parts and referencing them in because it still leads to lots of duplicate code in each new DataTemplate, and that means possible errors and hard maintainability. I.e., this right here is a more maintainable approach but still feels suboptimal:

<DataTemplate x:Key="collectibleTemplate">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Border BorderBrush="LightGray" BorderThickness="1">
                <Expander IsExpanded="True" Header="{Binding ComponentName}" Background="WhiteSmoke">
                    <StackPanel>
                        <TextBlock Margin="5,5,5,0" Text="{Binding EditDescription}" TextWrapping="Wrap" />

                        <!-- This is the only custom part of each template -->
                        [...]
                        <!-- End custom part -->

                        <ContentPresenter Content="{StaticResource AvailableActions}" />

                    </StackPanel>
                </Expander>
            </Border>
        </Grid>
    </DataTemplate>

    <StackPanel Margin="0,0,0,5" x:Key="AvailableActions" x:Shared="false">
        <Label Content="Available Actions:" >
            <Label.Style>
        <!-- 
        [Bottom half of shared XAML from the first example, offloaded here]
        -->
    </StackPanel>

So: what is my best strategy to solve this? AFAIK I’m stuck with using DataTemplates because that’s the only element that a ListBox ItemTemplateSelector accepts. Is there a way to create a compound DataTemplate in the DataTemplateSelector? I'd provide the stock DataTemplate that is shared by all objects, and the DataTemplateSelector references in the bit of custom XAML needed for each object type. Other engineers would hook into that generalized code behavior.

Not sure, fumbling a bit in the dark here as whether there is a pattern that allows me to solve this elegantly.

And, just for reference: my current DataTemplateSelector is super straightforward. This is where I would expect to construct the final DataTemplate, rather than simply returning one that's hardcoded in XAML.

public class NodeComponentDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        FrameworkElement element = container as FrameworkElement;

        if (element != null && item != null)
        {
            if (item is CollectibleComponent)
                return element.FindResource("collectibleTemplate") as DataTemplate;

            // [...]
        }
    }
}

回答1:

You could create the DataTemplate dynamically using the XamlReader.Parse or XamlReader.Load method, e.g.:

string template = "<DataTemplate xmlns =\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:x =\"http://schemas.microsoft.com/winfx/2006/xaml\"><StackPanel>[PLACEHOLDER]</StackPanel></DataTemplate>".Replace("[PLACEHOLDER]", "...custom code...");
return System.Windows.Markup.XamlReader.Parse(template) as DataTemplate;

The custom parts could be defined as UserControls.

I am afraid there is no way to base a DataTemplate on another one in pure XAML though.



回答2:

You could create a new CustomControl that fits your needs. It will apply the style by itself and you can give additional DepdendencyProperties to make it more convinient. In the end you can still put it in a DataTemplate to use it with your DataTemplateSelector.