Cosmicjive
Cosmicjive

Reputation: 185

WPF TemplateBinding to an ObservableCollection

I'm creating a content control that contains another usercontrol. We'll call them InnerControl and OuterControl. The InnerControl has a dependency property of type ObservableCollection called "Items." I'm trying to bind that to an identical Dependency Property in the OuterControl. Here is a stub of the InnerControl code:

public class InnerControl : UserControl {

    public InnerControl() {
        InnerItems = new ObservableCollection<string>();
    }

    public ObservableCollection<string> InnerItems
    {
        get { return (ObservableCollection<string>)GetValue(InnerItemsProperty); }
        set { SetValue(InnerItemsProperty, value); }
    }
    public static DependencyProperty InnerItemsProperty =
        DependencyProperty.Register("InnerItems",
            typeof(ObservableCollection<string>),
            typeof(InnerControl),
            new PropertyMetadata());
}

The outer control contains an identical Items property:

public class OuterControl : ContentControl {

    public OuterControl() {
        OuterItems = new ObservableCollection<string>();
    }

    static OuterControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(OuterControl),
                    new FrameworkPropertyMetadata(typeof(OuterControl)));
    }

    public ObservableCollection<string> OuterItems
    {
        get { return (ObservableCollection<string>)GetValue(OuterItemsProperty); }
        set { SetValue(OuterItemsProperty, value); }
    }
    public static DependencyProperty OuterItemsProperty =
        DependencyProperty.Register("OuterItems",
            typeof(ObservableCollection<string>),
            typeof(OuterControl),
            new PropertyMetadata());
}

Then I'm defining the OuterControl's appearance in the generic.xaml file:

<Style TargetType="{x:Type userControls:OuterControl}">
    <Setter Property="Padding" Value="10" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type userControls:OuterControl}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <local:InnerControl Grid.Row="0" Grid.Column="0"
                            InnerItems="{TemplateBinding OuterItems}"/>
                    <ContentPresenter Grid.Row="1" Grid.Column="0" 
                            Content="{TemplateBinding Content}"
                            Margin="{TemplateBinding Padding}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The really important part of the above code I want to call your attention to is:

<local:InnerControl Grid.Row="0" Grid.Column="0" 
InnerItems="{TemplateBinding OuterItems}"/>

What I expect to happen is that when items are added to the OuterItems collection of the OuterControl, those same items will be added to the InnerControl.InnerItems collection. However, that doesn't happen, and I can't figure out why.

I've also tried a relative binding so that I could experiment with using TwoWay mode and so on. Something like this:

InnerItems="{Binding OuterItems, Mode=TwoWay, 
RelativeSource={RelativeSource TemplatedParent}}"

But so far that hasn't worked either.

UPDATE

Everything that I thought solved this problem so far has only exposed new problems, so I've removed my previous updates. What I'm stuck with at this point is:

If I initialize InnerItems in the constructor, then the TemplateBinding doesn't seem to work (the items never get updated)

If I don't initialize InnerItems at all, the TemplateBinding works. However, if InnerControl is just used by itself in the Designer, it breaks, because InnerItems is null when the designer tries to add items to it.

Upvotes: 1

Views: 691

Answers (2)

Clemens
Clemens

Reputation: 128013

When you have a collection type dependency property, you must not use an instance of the collection class as default value of the property. Doing so will make all instances of the control that owns the property use the same collection instance.

So your property metadata

new PropertyMetadata(new ObservableCollection<string>())

should be replaced by

new PropertyMetadata(null)

or you do not specify any metadata at all

public static DependencyProperty InnerItemsProperty =
    DependencyProperty.Register(
        "InnerItems", typeof(ObservableCollection<string>), typeof(InnerControl));

Now you would somehow have to initialize the property value. As usual, you'll do it in the control's constructor, like

public InnerControl()
{
    InnerItems = new ObservableCollection<string>();
}

When you now bind the property of the control like

<local:InnerControl InnerItems="{Binding ...}" />

the value set in the constructor is replaced by the value produced by the Binding.

However, this does not happen when you create the Binding in a Style Setter, because values from Style Setters have lower precedence than so-called local values (see Dependency Property Value Precedence).

A workaround is to set the default value by the DependencyObject.SetCurrentValue() method, which does not set a local value:

public InnerControl()
{
    SetCurrentValue(InnerItemsProperty, new ObservableCollection<string>()); 
}

Upvotes: 2

Dave Williams
Dave Williams

Reputation: 2246

I find it quite likely that @Clemens comment has the right answer. Anyhow, I tested your solution using the code below and it worked fine for me.

Check how you are binding and adding items. You did not post that code in your question.

OuterControl

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace TemplateBindingTest.Controls
{
    public class OuterControl : UserControl
    {
        static OuterControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(OuterControl), new FrameworkPropertyMetadata(typeof(OuterControl)));
        }

        public ObservableCollection<string> OuterItems
        {
            get { return (ObservableCollection<string>)GetValue(OuterItemsProperty); }
            set { SetValue(OuterItemsProperty, value); }
        }
        public static DependencyProperty OuterItemsProperty =
            DependencyProperty.Register("OuterItems",
                typeof(ObservableCollection<string>),
                typeof(OuterControl),
                new PropertyMetadata(new ObservableCollection<string>()));
    }
}

InnerControl

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace TemplateBindingTest.Controls
{
    public class InnerControl : UserControl
    {
        static InnerControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(InnerControl), new FrameworkPropertyMetadata(typeof(InnerControl)));
        }

        public ObservableCollection<string> InnerItems
        {
            get { return (ObservableCollection<string>)GetValue(InnerItemsProperty); }
            set { SetValue(InnerItemsProperty, value); }
        }

        public static DependencyProperty InnerItemsProperty =
            DependencyProperty.Register("InnerItems",
                typeof(ObservableCollection<string>),
                typeof(InnerControl),
                new PropertyMetadata(new ObservableCollection<string>()));
    }
}

Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="clr-namespace:TemplateBindingTest.Controls">

    <Style TargetType="{x:Type controls:OuterControl}">
        <Setter Property="Padding" Value="10" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:OuterControl}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <ContentPresenter Grid.Row="0" Grid.Column="0" 
                            Content="{TemplateBinding Content}"
                            Margin="{TemplateBinding Padding}"/>
                        <ItemsControl Grid.Row="1" ItemsSource="{TemplateBinding OuterItems}" />
                        <Border Grid.Row="2" BorderThickness="1" BorderBrush="Red">
                            <controls:InnerControl  Grid.Column="0"
                            InnerItems="{TemplateBinding OuterItems}">Inner Control</controls:InnerControl>
                        </Border>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style TargetType="{x:Type controls:InnerControl}">
        <Setter Property="Padding" Value="10" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:InnerControl}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <ContentPresenter Grid.Row="0" Grid.Column="0" 
                            Content="{TemplateBinding Content}"
                            Margin="{TemplateBinding Padding}"/>
                        <ItemsControl Grid.Row="1" ItemsSource="{TemplateBinding InnerItems}" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

MainWindow.xaml

<Window x:Class="TemplateBindingTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:controls="clr-namespace:TemplateBindingTest.Controls"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <controls:OuterControl OuterItems="{Binding OuterItems}">Outer Control</controls:OuterControl>
        <Button Grid.Row="1" Content="Add" Click="Button_Click" HorizontalAlignment="Left" />
    </Grid>
</Window>

MainWindow.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;

namespace TemplateBindingTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private ObservableCollection<string> _OuterItems;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;

            _OuterItems = new ObservableCollection<string>(new List<string>()
            {
                "Test 1",
                "Test 2",
                "Test 3",
            });
        }

        public ObservableCollection<string> OuterItems
        {
            get
            {
                return _OuterItems;
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            _OuterItems.Add(System.IO.Path.GetRandomFileName());
        }
    }
}

Upvotes: 1

Related Questions