当子控件及其属性更改时,如何重新呈现自定义控件?

Nic*_*ler 2 c# wpf xaml dependency-properties wpf-controls

最初的问题是不知道为什么PropertyChangedCallback不触发,它是由某些代码输入错误(对于SO而言太本地化)引起的。但是,我已经修改了问题,以解决在添加/删除了子项目或子项目的属性已更改时如何重新呈现自定义控件的问题。 访问我的答案以深入了解如何触发重新渲染。


标题可能不是对问题的最好描述,但是无论如何,问题本身都不是很清楚。我认为发布此问题可能会有一定用处,因为PropertyChangedCallback触发得非常难以预测。

我要编写的自定义控件具有以下结构:

  • MyControl : FrameworkElement
    • MyControl.Items
      • MyItem : FrameworkContentElement
        • MyItem.SubItems
          • MySubItem : FrameworkContentElement
          • MySubItem
          • ....
      • MyItem
      • MyItem
      • ....

因此,基本上,我的控件有一个DependencyProperty存储的ObservableCollection<MyItem>。它DependencyProperty具有一个PropertyChangedCallback用于检测集合设置/取消设置的时间。此回调函数用于预订CollectionChanged事件,以便在MyItem从集合中添加/删除a时可以引起控件的重新呈现。

当集合项的属性发生更改时,我的控件也应重新呈现。因此,MyItem也实现INotifyPropertyChanged,并且我PropertyChangedEventHandler在将MyItem对象添加/删除到集合时进行订阅。

到目前为止,一切都很好...

MyItem类定义了几个DependencyProperies,其中的每一个具有相同的PropertyChangedCallback功能。 但令我惊讶的是,当我修改MyItemXAML 中的属性之一时,不会触发此回调函数。

我希望学习的是为什么发生这种情况以及为什么PropertyChangedCallback不触发这种情况。另外,我想知道什么情况会导致回调触发。

我的目标是拥有一个在以下情况下重新渲染的控件:
a)更改其属性
b)添加/删除
其子级c)更改其子级的属性
d)添加/删除其子级的孩子e)更改
其子级的孩子的属性改变了。


代码样例

这是我注册MyControl.Items财产的方式。这DependencyProperty可以成功触发属性更改事件。

public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(
    "Items", 
    typeof(ObservableCollection<MyItem>), 
    typeof(MyControl), 
    new FrameworkPropertyMetadata(
        null, //Default to null.  Instance-scope value is set in constructor.
        FrameworkPropertyMetadataOptions.AffectsRender, 
        OnItemsPropertyChanged));
Run Code Online (Sandbox Code Playgroud)

这是我对MyItems集合的设置/取消设置的回应:

private static void OnItemsPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{   //This callback is called when ObservableCollection<MyItem> is set/unset.
    MyControl ctrl = (MyControl)obj;

    INotifyCollectionChanged oldList = args.OldValue as INotifyCollectionChanged;
    INotifyCollectionChanged newList = args.NewValue as INotifyCollectionChanged;

    //If the old list implements the INotifyCollectionChanged interface, then unsubscribe to CollectionChanged events.
    if (oldList != null)
        oldList.CollectionChanged -= ctrl .OnItemsCollectionChanged;
    //If the new list implements the INotifyCollectionChanged interface, then subscribe to CollectionChanged events.
    if (newList != null)
        newList.CollectionChanged += ctrl .OnItemsCollectionChanged;
}
Run Code Online (Sandbox Code Playgroud)

当添加或删除项目时调用以下函数 ObservableCollection<MyItem>

private void OnItemsCollectionChanged(object source, NotifyCollectionChangedEventArgs args)
{   //Invaliate the visual, causing it to re-layout and re-render.
    InvalidateVisual();

    //The contents of the Items collection was modified.
    //Subscribe/Unsubcribe to the PropertyChanged event as necessary.
    switch (args.Action)
    {
        case NotifyCollectionChangedAction.Add:
            foreach (MyItem mi in args.NewItems)
                mi.PropertyChanged += OnMyItemPropertyChanged;
            break;
        case NotifyCollectionChangedAction.Remove:
            foreach (MyItem mi in args.OldItems)
                mi.PropertyChanged -= OnMyItemPropertyChanged;
            break;
        case NotifyCollectionChangedAction.Replace:
            foreach (MyItem  mi in args.NewItems)
                mi.PropertyChanged += OnMyItemPropertyChanged;
            foreach (MyItem mi in args.OldItems)
                mi.PropertyChanged -= OnMyItemPropertyChanged;
                break;
        case NotifyCollectionChangedAction.Reset:
            foreach (MyItem mi in (source as IEnumerable<MyItem >))
                mi.PropertyChanged += OnMyItemPropertyChanged;
            break;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,我需要能够对MyItem具有更改的属性的情况做出反应。因此,我建立了当一个回调函数MyItem有一个PropertyChanged事件:

private void OnMyItemPropertyChanged(object source, PropertyChangedEventArgs args)
{   //One of the MyItems had a property that was changed, 
    //invalidate the visual and re-render.
    InvalidateVisual();
}
Run Code Online (Sandbox Code Playgroud)

永远不会调用前一个函数,因为MyItem's DependencyProperties从不触发属性更改事件。下面说明了如何设置DependencyProperties在XAML中尝试修改的:

public static readonly DependencyProperty MyIntProperty = DependencyProperty.Register(
    "MyInt", 
    typeof(int), 
    typeof(MyItem), 
    new PropertyMetadata(0, DependencyPropertyChanged));

public static readonly DependencyProperty MyDoubleProperty = DependencyProperty.Register(
    "MyDouble", 
    typeof(double), 
    typeof(MyItem), 
    new PropertyMetadata(0d, DependencyPropertyChanged));

public static readonly DependencyProperty MyStringProperty = DependencyProperty.Register(
    "MyString", 
    typeof(string), 
    typeof(MyItem), 
    new PropertyMetadata("", DependencyPropertyChanged));
Run Code Online (Sandbox Code Playgroud)

以下函数是这些的回调DependencyProperties。如果被解雇,它将引发INotifyPropertyChanged.PropertyChangedEventHandler

private static void DependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    MyItem item= (MyItem )obj;
    item.RaisePropertyChanged(args.Property.Name);
}

protected void RaisePropertyChanged(string name)
{   //Notify listeners (such as the parent control) when a property changes.
    if(PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(name));;
}
Run Code Online (Sandbox Code Playgroud)

好吧...我有一组类似的事件/处理程序,用于MyItem.SubItems DependencyProperty,但是在这一点上没有任何用处。

如果您能够对PropertyChangedCallback工作原理有所了解,我将不胜感激。感谢您阅读这篇冗长的文章。

Nic*_*ler 5

我很抱歉,但是原来的问题太局限了(小的错别字和复制粘贴错误)。但是,为了使此页面有用,我准备了有关如何创建内部包含子项或子子项的自定义控件的完整说明。此页面还说明了如何配置每个项目,以便属性更改和集合更改导致在原始控件上重新呈现。

首先,自定义控件必须包含DependencyProperty用于项目集合的(和CLR支持的属性)。此集合应实现INotifyCollectionChanged,这是ObservableCollection一个不错的选择。应该将此集合参数化以保存控件的子项。使用原始帖子中的名称,这需要类似于以下内容的代码:

[ContentProperty("Items")] //This allows the "Items" property to be implicitly used in XAML.
public class MyControl : Control
{
    public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register(
        "Items", 
        typeof(ObservableCollection<MyItem>), 
        typeof(MyControl),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnItemsChangedProperty));

    //CLR-property.
    [Category("MyControl")]
    public ObservableCollection<MyItem> Items
    {
        get { return (ObservableCollection<MyItem>)GetValue(ItemsProperty); }
        set { SetValue(ItemsProperty, value); }
    }

    public MyControl() : base()
    {   //Set a new collection per control, but don't destroy binding.
        SetCurrentValue(ItemsProperty, new ObservableCollection<MyItem>());
    }

    protected override void OnRender(DrawingContext dc)
    {
        //Draw stuff here.
    }

    //More methods defined later...
}
Run Code Online (Sandbox Code Playgroud)

此时,ObservabledCollection<MyItem>设置和取消设置时会触发重新渲染。实例化控件时,将自动设置此集合,这将导致第一次重新呈现。

接下来,应该监视集合以检测何时添加和删除项目。为此,我们必须使用PropertyChangedCallback提供给的功能DependencyProperty。此函数仅CollectionChanged根据项目集合是已设置还是未设置来订阅/取消订阅事件:

private static void OnItemsChangedProperty(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    MyControl ctrl = (MyControl)obj;

    INotifyCollectionChanged oldList = args.OldValue as INotifyCollectionChanged;
    INotifyCollectionChanged newList = args.NewValue as INotifyCollectionChanged;

    //If the old list implements the INotifyCollectionChanged interface, then unsubscribe to CollectionChanged events.
    if (oldList != null)
        oldList.CollectionChanged -= ctrl.OnItemsCollectionChanged;
    //If the new list implements the INotifyCollectionChanged interface, then subscribe to CollectionChanged events.
    if (newList != null)
        newList.CollectionChanged += ctrl.OnItemsCollectionChanged;
}
Run Code Online (Sandbox Code Playgroud)

下面是处理添加/删除项目时的回调函数。在这里也触发了重新渲染:

private void OnItemsCollectionChanged(object source, NotifyCollectionChangedEventArgs args)
{
    InvalidateVisual(); //Re-render MyControl

    switch (args.Action)
    {
        case NotifyCollectionChangedAction.Add:
            foreach (MyItem item in args.NewItems)
                item.PropertyChanged += OnItemPropertyChanged;
            break;
        case NotifyCollectionChangedAction.Remove:
            foreach (MyItem item in args.OldItems)
                item.PropertyChanged -= OnItemPropertyChanged;
            break;
        case NotifyCollectionChangedAction.Replace:
            foreach (MyItem item in args.NewItems)
                item.PropertyChanged += OnItemPropertyChanged;
            foreach (MyItem item in args.OldItems)
                item.PropertyChanged -= OnItemPropertyChanged;
                break;
        case NotifyCollectionChangedAction.Reset:
            foreach (MyItem item in (source as IEnumerable<MyItem>))
                item.PropertyChanged += OnItemPropertyChanged;
            break;
    }
}
Run Code Online (Sandbox Code Playgroud)

如您在上面的函数中看到的那样PropertyChanged,定义在中的事件MyItem将根据需要进行预订/取消预订,以便在MyItem更改类内的属性时可以通知自定义控件。这允许控件在子项的属性更改时重新呈现。

这是子项属性更改时的处理程序:

private void OnItemPropertyChanged(object source, PropertyChangedEventArgs args)
{
    InvalidateVisual(); //Just re-render.
}
Run Code Online (Sandbox Code Playgroud)

此时,由于以下情况,自定义控件将重新呈现:

  • ObservableCollection<MyItem>设置警戒/解除警戒。
  • ObservableCollection<MyItem>具有添加/移除项目,或者如果集合复位。
  • ObservableCollection<MyItem>有有被更改的属性的项目。

完成此说明的最后一步是INotifyPropertyChangedMyItem类中实现接口。更改PropertyChanged任何事件时,只需调用该事件DependencyProperties。请参见下面的代码:

public class MyItem : FrameworkContentElement, INotifyPropertyChanged
{
    //INotifyPropertyChanged members:
    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }

    //DependencyProperties
    public static readonly DependencyProperty MyIntProperty = DependencyProperty.Register(
        "MyInt", 
        typeof(int), 
        typeof(MyItem), 
        new PropertyMetadata(0, DependencyPropertyChanged));
    public static readonly DependencyProperty MyStringProperty = DependencyProperty.Register(
        "MyString", 
        typeof(string), 
        typeof(MyItem), 
        new PropertyMetadata("", DependencyPropertyChanged));

    //Callback that invokes the INotifyPropertyChanged.PropertyChangedEventHandler
    private static void DependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        MyItem item = (MyItem)obj;
        item.RaisePropertyChanged(args.Property.Name);
    }
}
Run Code Online (Sandbox Code Playgroud)

如果需要,可以重复此模式,以允许子子项引起自定义控件的重新呈现。只需DependencyProperty在第一个子项目中建立一个包含another的子项,ObservableCollection就像MyControl该类一样。但是,不是在子子项有PropertyChanged事件时直接导致重新渲染,而是调用RaisePropertyChanged方法将通知传递回最父级控件。

我希望这可以帮助任何控件作者管理控件的重新呈现!:)