WPF TemplateBinding для ObservableCollection

Я создаю элемент управления контентом, который содержит другой usercontrol. Мы будем называть их InnerControl и OuterControl. InnerControl имеет свойство зависимостей типа ObservableCollection, которое называется "Items". Я пытаюсь связать это с идентичным свойством зависимости в OuterControl. Вот заглушка кода InnerControl:

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());
}

Внешний элемент управления содержит идентичное свойство Items:

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());
}

Затем я определяю внешний вид OuterControl в файле generic.xaml:

<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>

Действительно важная часть приведенного выше кода, на которую я хочу обратить ваше внимание:

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

Я ожидаю, что при добавлении элементов в коллекцию OuterItems OuterControl эти же элементы будут добавлены в коллекцию InnerControl.InnerItems. Однако этого не происходит, и я не могу понять, почему.

Я также попробовал относительное связывание, чтобы я мог поэкспериментировать с использованием режима TwoWay и так далее. Что-то вроде этого:

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

Но пока это тоже не сработало.

ОБНОВИТЬ

Все, что я думал, решило эту проблему до сих пор, только выявило новые проблемы, поэтому я удалил свои предыдущие обновления. На этом я застрял:

Если я инициализирую InnerItems в конструкторе, то TemplateBinding, кажется, не работает (элементы никогда не обновляются)

Если я вообще не инициализирую InnerItems, то TemplateBinding работает. Однако, если InnerControl просто используется сам по себе в Designer, он ломается, потому что InnerItems имеет значение null, когда дизайнер пытается добавить в него элементы.

ОБНОВЛЕНИЕ: решено (?)

По предложению Клемена мне нужно было инициализировать InnerItems в конструкторе, используя этот метод:

SetCurrentValue(InnerItemsProperty, new ObservableCollection<string>());

Finger пересекся, это, кажется, работает и позволяет мне использовать его в обоих направлениях (TemplateBinding работает, а элемент управления работает автономно).

2 ответа

Решение

Если у вас есть свойство зависимости типа коллекции, вы не должны использовать экземпляр класса коллекции в качестве значения свойства по умолчанию. В результате все экземпляры элемента управления, которому принадлежит свойство, будут использовать один и тот же экземпляр коллекции.

Так что ваши метаданные собственности

new PropertyMetadata(new ObservableCollection<string>())

должен быть заменен

new PropertyMetadata(null)

или вы не указываете какие-либо метаданные вообще

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

Теперь вам нужно как-то инициализировать значение свойства. Как обычно, вы будете делать это в конструкторе элемента управления, как

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

Когда вы теперь связываете свойство элемента управления, как

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

значение, установленное в конструкторе, заменяется значением, созданным привязкой.

Однако этого не происходит при создании привязки в установщике стилей, поскольку значения из установщиков стилей имеют меньший приоритет, чем так называемые локальные значения (см. Приоритет значений свойства зависимости).

Обходной путь должен установить значение по умолчанию DependencyObject.SetCurrentValue() метод, который не устанавливает локальное значение:

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

Я считаю вполне вероятным, что комментарий @Clemens имеет правильный ответ. Во всяком случае, я проверил ваше решение, используя приведенный ниже код, и он работал нормально для меня.

Проверьте, как вы связываете и добавляете предметы. Вы не опубликовали этот код в своем вопросе.

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());
        }
    }
}
Другие вопросы по тегам