【问题标题】:How to re-render a custom control when its sub-items and their properties change?当子项及其属性发生变化时如何重新渲染自定义控件?
【发布时间】:2014-09-29 06:52:50
【问题描述】:

最初的问题是不知道为什么PropertyChangedCallback 没有触发,它是由一些代码拼写错误引起的(对于 SO 来说过于本地化)。但是,我已经修改了问题以解决如何在自定义控件具有添加/删除的子项或子项具有更改的属性时重新呈现自定义控件。 请visit my answer深入了解如何触发重新渲染。


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

我正在创作的自定义控件具有以下结构:

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

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

当集合项具有更改的属性时,我的控件也应该重新呈现。因此,MyItem 也实现了INotifyPropertyChanged,当MyItem 对象被添加/删除到集合中时,我订阅了PropertyChangedEventHandler。

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

MyItem 类定义了更多的DependencyProperies,每个都有相同的PropertyChangedCallback 函数。 但令我惊讶的是,当我在 XAML 中修改 MyItem 属性之一时,并未触发此回调函数。

我希望了解的是为什么会发生这种情况以及为什么 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));

这是我对设置/取消设置 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;
}

在ObservableCollection&lt;MyItem&gt; 中添加或删除项目时调用以下函数

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

现在,当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();
}

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

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

以下函数是那些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));;
}

嗯...我有一组类似的事件/处理程序,用于MyItem.SubItems DependencyProperty,但目前没有任何用处。

如果您能够就PropertyChangedCallback 的工作原理添加任何见解,我将不胜感激。感谢您阅读这篇相当长的帖子。

【问题讨论】:

  • MyItem 中有依赖属性的原因是什么?难道不是标准的 CLR 属性在其设置器中引发 PropertyChanged 事件吗?
  • 好吧。主要原因是为了让 MyItem 可以接受绑定系统。另一方面,MySubItem 只是一小段数据,因此它只能具有 CLR 属性。 MyControl 是 Graph,MyItem 是 GraphLine,MySubItem 是 DataPoint。 GraphLine 有几个属性可以控制它和它的数据点如何以图形方式显示。也许这并不一定需要数据绑定,但 ListBoxItem 与 GraphLine 的目的相似。
  • @Clemens,显然,我使用的代码没有问题。我的项目有很多错别字,因此问题过于本地化。我已经发布了关于如何执行 OP 中描述的目标的深入教程。不过感谢您的建议;)

标签: c# wpf xaml wpf-controls dependency-properties


【解决方案1】:

我很抱歉,但最初的问题过于本地化(小错别字和复制粘贴错误)。然而,为了使这个页面有用,我准备了一个完整的解释,说明如何创建一个自定义控件,其中包含子项或子子项。此页面还说明了如何配置每个项目,以便属性更改和集合更改导致原始控件重新呈现。

首先,自定义控件必须包含用于项目集合的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...
}

此时,在设置和取消设置ObservabledCollection&lt;MyItem&gt; 时会触发重新渲染。 此集合在控件实例化时自动设置,这会导致第一次重新渲染。

接下来,应监控集合以检测何时添加和删除项目。为此,我们必须使用提供给DependencyProperty 的PropertyChangedCallback 函数。 此函数只是订阅/取消订阅 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;
}

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

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

正如您在上面的函数中看到的那样,MyItem 中定义的 PropertyChanged 事件会根据需要订阅/取消订阅,以便在 MyItem 类中的属性被通知时通知自定义控件改变了。这允许控件在其子项的属性发生更改时重新呈现。

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

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

此时自定义控件会因为以下情况而重新渲染:

  • ObservableCollection&lt;MyItem&gt; 已设置/取消设置。
  • ObservableCollection&lt;MyItem&gt; 添加/删除了一个项目,或者如果集合已重置。
  • ObservableCollection&lt;MyItem&gt; 有一个项目,该项目的属性已更改。

完成此说明的最后一步是在MyItem 类中实现INotifyPropertyChanged 接口。 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);
    }
}

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

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

【讨论】:

    猜你喜欢
    • 2020-04-29
    • 2020-02-27
    • 1970-01-01
    • 1970-01-01
    • 2017-05-06
    • 2016-11-06
    • 2022-06-30
    • 2017-10-18
    • 2018-12-06
    相关资源
    最近更新 更多