首页 > 解决方案 > 数据绑定不适用于集合内的自定义控件

问题描述

WPF 数据绑定不适用于在 xaml 集合标记内定义的自定义控件。我只想在自定义控件中定义一组自定义小部件,并将一些小部件属性与视图模型属性绑定。像这样。

<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 以稍后获取 propertychanged 回调(现在未在示例中使用)

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!");
        }
    }
}

最后是我的简单视图模型:

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 的属性。我知道在这种情况下绑定可能会失败,因为 observablecollection 是在 mycustomcontrol 的构造函数内部创建的,但我不知道如何修复它以使绑定在 mycustomwidget 中工作。

标签: wpfxamldata-bindingobservablecollectiondependency-properties

解决方案


要使该绑定起作用,您local:MyCustomWidget需要具有与主窗口相同的 DataContext。WPF 元素继承其逻辑父级的 DataContext。MyCustomWidget 没有,因为它不在逻辑树中。它只是坐在那里。您不会将其添加到其父级的任何类型的普通子集合中,只是添加到框架不知道的随机 ObservableCollection 中。

下面的代码可能是一个粗略的 hack。我还没有调查过WPF的这个角落。我以最大的诚意敦促您找出正确的方法。但是通过添加到您的代码中,我在绑定初始化时点击了 MyCustomWidget 中的 propertychanged 事件。

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,这将是一大堆乏味的样板)。

我的自定义控件.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!");
    }
}

主窗口.xaml

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

推荐阅读