Question

What I currently have

I currently have an ItemsControl that I use to display a list of controls. Due to the fact each "item" contains multiple controls I have it setup by specifying a DataTemplate. Something like this (I have removed some element attributes to make the code easier to follow):

<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyItems}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="10"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Path Grid.Column="0"/>
                <StackPanel Grid.Column="1" Orientation="Horizontal">
                    <c:MyControl />
                    <c:MyButton />
                </StackPanel>
            </Grid>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

What I am trying to do

The above gives me exactly what I want in terms of functionality, but I have it in a few places and I want to minimize the duplicate code. In regards to the above xaml, the only things that need to be different when the DataTemplate is reused are the controls for "MyButton" and "MyControl". With that in mind my ideal way to define the XAML above would be something like this:

<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyItems}">
    <c:MyControl />
    <c:MyButton />
</ItemsControl>

I am happy for some variation of course, but hopefully it is clear the duplication I am trying to eliminate.

What I have tried

So far I have tried creating a template in my resources file, but that isn't working so well and I am not even sure I am going down the right track. This is what I have:

<DataTemplate x:Key="MyTemplate">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Path Grid.Column="0" Fill="..." Data="..." />
        <StackPanel Grid.Column="1" Orientation="Horizontal">
            <ItemsPresenter />
        </StackPanel>
    </Grid>
</DataTemplate>

Which I try to apply to my XAML like so:

<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyItems}" ItemTemplate="{StaticResource MyTemplate}">
    <c:MyControl />
    <c:MyButton />
</ItemsControl>

It all builds fine, but during run-time I get an error: "Items collection must be empty before using ItemsSource." which is obviously a side effect of my incorrect approach.

What am I doing wrong? How can I setup my template to work the way I want it to?

Was it helpful?

Solution

You may create a derived ItemsControl class that uses ContentControl (instead of ContentPresenter) for the item container type:

public class MyItemsControl : ItemsControl
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ContentControl();
    }
}

Now you may separate your current DataTemplate into an "outer" reusable part that resides in the ContentControl's Template and an "inner" part defined by the remaining DataTemplate:

<!-- somewhere in Resources -->
<Style x:Key="ReusableItemContainerStyle" TargetType="ContentControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ContentControl">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="10"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <Path Grid.Column="0" ... />
                    <ContentPresenter Grid.Column="1"
                        Content="{TemplateBinding Content}"
                        ContentTemplate="{TemplateBinding ContentTemplate}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<local:MyItemsControl
    ItemsSource="{Binding MyItems}"
    ItemContainerStyle="{StaticResource ReusableItemContainerStyle}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <c:MyControl />
                <c:MyButton />
            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</local:MyItemsControl>

Update: You may also set the reusable ItemContainerStyle in a default style for your derived ItemsControl in Generic.xaml like this:

<Style TargetType="local:MyItemsControl"
       BasedOn="{StaticResource {x:Type ItemsControl}}">
    <Setter Property="ItemContainerStyle">
        <Setter.Value>
            <Style TargetType="ContentControl">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="ContentControl">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="10"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <Path Grid.Column="0" ... />
                                <ContentPresenter Grid.Column="1"
                                    Content="{TemplateBinding Content}"
                                    ContentTemplate="{TemplateBinding ContentTemplate}"/>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Setter.Value>
    </Setter>
</Style>

Then you would also have to set the default style key for your ItemsControl:

public class MyItemsControl : ItemsControl
{
    static MyItemsControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(MyItemsControl),
            new FrameworkPropertyMetadata(typeof(MyItemsControl)));
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ContentControl();
    }
}

OTHER TIPS

Declare your template as a new control as:

<UserControl x:Class="UI.Views.NewControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300" Name="myNewControl">
    <Grid>
        <ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyItems}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="10"/>
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <Path Grid.Column="0"/>
                        <ContentControl Grid.Row="1" Content="{Binding MyCustomControl, ElementName=myNewControl}"/>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</UserControl>

MyCustomControl should be a dependency property of your control.

How you would use this :

<MyNewControl>
   <MyNewControl.MyCustomControl>
        <StackPanel>
             <MyControl/>
             <MyButton/>
        </StackPanel>
   </MyNewControl.MyCustomControl>
</MyNewControl>

Hope this helps

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top