Привязка данных не работает для пользовательских элементов управления внутри коллекции - PullRequest
0 голосов
/ 02 мая 2019

Привязка данных WPF не работает для пользовательских элементов управления, которые определены внутри тега коллекции xaml.Я просто хочу определить коллекцию пользовательских виджетов внутри пользовательского элемента управления и связать некоторые свойства виджетов со свойствами viewmodel.Вот так.

<Window x:Class="WpfApp1.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:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <local:MyCustomControl>
            <local:MyCustomControl.Widgets>
                <local:MyCustomWidget ImportantToggle="{Binding SomeToggle}"/>
            </local:MyCustomControl.Widgets>
        </local:MyCustomControl>
    </Grid>
</Window>

Это мой пользовательский элемент управления.Я использую obseravblecollection для виджетов и вызываю SetValue в конструкторе, чтобы позже получить обратный вызов с измененным свойством (сейчас не используется в примере)

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;

namespace WpfApp1
{
    public class MyCustomControl : FrameworkElement
    {
        public ObservableCollection<MyCustomWidget> Widgets
        {
            get { return (ObservableCollection<MyCustomWidget>)this.GetValue(WidgetsProperty); }
            set { this.SetValue(WidgetsProperty, value); }
        }
        public static DependencyProperty WidgetsProperty = DependencyProperty.Register("Widgets", typeof(ObservableCollection<MyCustomWidget>), typeof(MyCustomControl), new PropertyMetadata(null, (e, args) => ((MyCustomControl)e).WidgetsChanged(args)));

        public void WidgetsChanged(DependencyPropertyChangedEventArgs e)
        {
            Debug.WriteLine("widgets collection object changed inside my custom control!");
        }

        public MyCustomControl()
        {
            this.SetValue(WidgetsProperty, new ObservableCollection<MyCustomWidget>());
        }
    }
}

, и это мой пользовательский виджет:

namespace WpfApp1
{
    public class MyCustomWidget : FrameworkContentElement
    {
        public bool ImportantToggle
        {
            get { return (bool)this.GetValue(ImportantToggleProperty); }
            set { this.SetValue(ImportantToggleProperty, value); }
        }

        public static DependencyProperty ImportantToggleProperty = DependencyProperty.Register("ImportantToggle", typeof(bool), typeof(MyCustomWidget), new PropertyMetadata(false, (e, args) => ((MyCustomWidget)e).ImportantToggleChanged(args)));

        public void ImportantToggleChanged(DependencyPropertyChangedEventArgs e)
        {
            Debug.WriteLine("my toggle changed inside my custom widget!");
        }
    }
}

И, наконец, мой упрощенный ViewModel:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfApp1
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private bool _someToggle;
        public bool SomeToggle
        {
            get { return this._someToggle; }
            set
            {
                this._someToggle = value;
                this.NotifyPropertyChanged();
            }
        }

        public MainViewModel()
        {
            this.SomeToggle = !this.SomeToggle;
        }
    }
}

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

Замечание: я не могу связываться со свойствами MyCustomWidget,Я понимаю, что в этом сценарии может произойти сбой привязки, поскольку наблюдаемая коллекция создается внутри конструктора mycustomcontrol, но я не знаю, как это исправить, чтобы привязка работала внутри mycustomwidget.

1 Ответ

2 голосов
/ 02 мая 2019

Чтобы эта привязка работала, ваш local:MyCustomWidget должен иметь тот же DataContext, что и главное окно. Элементы WPF наследуют DataContext своего логического родителя. MyCustomWidget нет, потому что он не находится в логическом дереве. Это просто сидит там. Вы не добавляете его в какую-либо нормальную дочернюю коллекцию его родителя, просто в случайную коллекцию ObservableCollection, о которой фреймворк не знает.

Код ниже, вероятно, грубый взлом. Я не исследовал этот угол WPF. Я призываю вас со всей искренностью найти правильный способ сделать это. Но с этим дополнением к вашему коду я столкнулся с событием измененного свойства в MyCustomWidget при инициализации привязки.

public MyCustomControl()
{
    this.SetValue(WidgetsProperty, new ObservableCollection<MyCustomWidget>());
    Widgets.CollectionChanged += Widgets_CollectionChanged;
}

private void Widgets_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.NewItems is System.Collections.IEnumerable)
    {
        foreach (MyCustomWidget widget in e.NewItems)
        {
            AddLogicalChild(widget);
        }
    }
}

Кстати, вы можете избавить себя от необходимости переключения переключателя в конструкторе MainViewModel. Это происходит задолго до того, как привязка существует. Вместо этого я добавил флажок:

<StackPanel>
    <CheckBox IsChecked="{Binding SomeToggle}">Test Toggle</CheckBox>
    <local:MyCustomControl>
        <local:MyCustomControl.Widgets>
            <local:MyCustomWidget 
                ImportantToggle="{Binding SomeToggle}"
                />
        </local:MyCustomControl.Widgets>
    </local:MyCustomControl>
</StackPanel>
<Ч />

Обновление:

Это полностью исключает вашу коллекцию Widgets, и переплет работает без каких-либо усилий с нашей стороны. Дочерние виджеты будут в MyCustomControl.Children. Важно отметить, что мы больше не ограничиваем дочерний тип MyCustomWidget. Это существенное изменение дизайна, которое может не соответствовать вашим требованиям. Вы могли бы внимательно изучить класс Panel и написать класс, который работает таким же образом, но принимает только один тип потомка (это будет означать написание аналога UIElementCollection, который будет в основном большой кучей утомительного шаблона) ,

MyCustomControl.cs

[ContentProperty("Children")]
public class MyCustomControl : Panel
{
}

MyCustomWidget.cs

public class MyCustomWidget : Control
{
    public bool ImportantToggle
    {
        get { return (bool)this.GetValue(ImportantToggleProperty); }
        set { this.SetValue(ImportantToggleProperty, value); }
    }

    public static DependencyProperty ImportantToggleProperty = 
        DependencyProperty.Register("ImportantToggle", typeof(bool), typeof(MyCustomWidget), 
            new PropertyMetadata(false, (e, args) => ((MyCustomWidget)e).ImportantToggleChanged(args)));

    public void ImportantToggleChanged(DependencyPropertyChangedEventArgs e)
    {
        Debug.WriteLine("my toggle changed inside my custom widget!");
    }
}

MainWindow.xaml

<local:MyCustomControl>
    <local:MyCustomWidget 
        ImportantToggle="{Binding SomeToggle}"
        />
</local:MyCustomControl>
...