WPF Telerik.Windows.Controls.Data.PropertyGrid 自定义属性编辑器

发布于:2025-09-13 ⋅ 阅读:(23) ⋅ 点赞:(0)

1.AI帮忙定义新用户控件

2.在属性上添加TelerikEditorAttribute特性

 private ObservableCollection<string> _axisOrder;
 [Display(Description = "点位", GroupName = "通用", Name = "轴&顺序", Order = 1)]
 [DataMember]
 [TelerikEditorAttribute(typeof(DoubleListBoxEditor), "SelectedItems")]
 public ObservableCollection<string> AxisOrder
 {
     get => _axisOrder;
     set => this.RaiseAndSetIfChanged(ref _axisOrder, value);
 }
[TelerikEditorAttribute(typeof(DoubleListBoxEditor), "SelectedItems")]

SelectedItems属性可以根据实体情况进行更换;

3.效果

4.扩展

这个编辑控件包含了两个ListBox,我想根据实体的其他属性变更然后变更其中一个ListBox的数据源:

思路就是在DoubleListBoxEditor类里面监听这个属性变化,要挂载事件;

要想找到这个属性的事件就得找到对应的实体;

(要有控件的Parent属性, 视觉树这些概念)

使用自定义Editor控件的Parent属性,可以得到对应的在PropertyGrid中对应的条目包装:

在这个包装中找到DataContext属性得到对应的实体属性包装;

然后再找到Instance属性得到对应实体,再得到对应想拿到的属性;

挂载事件的时机要把控好,不能写在构造函数里,此时界面对象还未赋值;

代码:

  public override void OnApplyTemplate()
  {
      base.OnApplyTemplate();
      if (!(Parent is PropertyGridField field))
      {
          return;
      }
      var def = (PropertyDefinition)field.DataContext;
      CustomPropertyDescriptor pd = null;
      if (def.Instance is MovePositionActionNode node)
      {
          node.PropertyChanged -= PositionPropertyChanged;
          node.PropertyChanged += PositionPropertyChanged;
          
      }
      
  }

更新:

测试中发现会多次调用PositionPropertyChanged方法,和我预想的结果不同,因为我的写法是:

node.PropertyChanged -= PositionPropertyChanged;
 node.PropertyChanged += PositionPropertyChanged;

本来想的是始终只会挂载一个方法上去,但是测试的时候发现每调用一次OnApplyTemplate(),就会多挂载一个方法上去。

原因:

在 C# 中,委托实际上是对象(继承自 System.MulticastDelegate)。每次使用方法名创建委托时,都会在堆上创建一个新的委托对象实例。

写一段代码模拟上面出现的bug:

 public class EventSource
        {
            public event EventHandler Event;
            public void RaiseEvent()
            {
                Event.Invoke(null,null);
            }
        }

        public class Example
        {
            public void MyMethod(object sender, EventArgs e) { Console.WriteLine("Hello, World!"); }

          
        }

然后这样调用:

 EventSource obj = new EventSource();

            for (int i = 0; i < 3; i++)
            {
                Example ex = new Example();
                obj.Event -= ex.MyMethod;
                obj.Event += ex.MyMethod;
            }
            obj.RaiseEvent();

结果会调用三次MyMethod,因为我的代码中,执行OnApplyTemplate时都是新创建对象的时候,所以效果类似这段测试代码;

查看IL:

node.PropertyChanged -= PositionPropertyChanged;
 node.PropertyChanged += PositionPropertyChanged;

  IL_0045: ldloc.2      // node
  IL_0046: ldarg.0      // this
  IL_0047: ldftn        instance void Lithography.Model.ActionAbout.UI.DoubleListBoxEditor::PositionPropertyChanged(object, class [System]System.ComponentModel.PropertyChangedEventArgs)
  IL_004d: newobj       instance void [System]System.ComponentModel.PropertyChangedEventHandler::.ctor(object, native int)
  IL_0052: callvirt     instance void [ReactiveUI]ReactiveUI.ReactiveObject::remove_PropertyChanged(class [System]System.ComponentModel.PropertyChangedEventHandler)
  IL_0057: nop

  // [82 17 - 82 65]
  IL_0058: ldloc.2      // node
  IL_0059: ldarg.0      // this
  IL_005a: ldftn        instance void Lithography.Model.ActionAbout.UI.DoubleListBoxEditor::PositionPropertyChanged(object, class [System]System.ComponentModel.PropertyChangedEventArgs)
  IL_0060: newobj       instance void [System]System.ComponentModel.PropertyChangedEventHandler::.ctor(object, native int)
  IL_0065: callvirt     instance void [ReactiveUI]ReactiveUI.ReactiveObject::add_PropertyChanged(class [System]System.ComponentModel.PropertyChangedEventHandler)
  IL_006a: nop

挂载对象前创建了一个PropertyChangedEventHandler对象,对应着上方:委托实际上是对象(继承自 System.MulticastDelegate)。每次使用方法名创建委托时,都会在堆上创建一个新的委托对象实例。

也就是说+=操作符相当于执行了Add_TestEvent(new Action(memory.Run)),就是这个new Action包含了对memory指向的内存的引用。而这个引用在CLR看来是可达的,可以通过引发事件来调用该内存,所以这种情况会有内存泄漏的风险。

引用:https://blog.csdn.net/nodeathphoenix/article/details/84549399?fromshare=blogdetail&sharetype=blogdetail&sharerId=84549399&sharerefer=PC&sharesource=qq_59062726&sharefrom=from_link

防止内存泄漏解决办法:

1.在界面对象销毁前,主动取消订阅

新添加一个字段保存事件发布者的对象:

private MovePositionActionNode _currentNode;

然后在界面Loaded事件里订阅事件,Unloaded事件里取消订阅:

 private void OnLoaded(object sender, RoutedEventArgs e)
 {
     // 通过视觉树找到PropertyGridItem
     var propertyGridItem = VisualTreeExtensions.FindParent<PropertyGridField>(this);
     var def = (PropertyDefinition)propertyGridItem.DataContext;
     if (def.Instance is MovePositionActionNode node)
     {
         _currentNode = node;
         _currentNode.PropertyChanged += OnNodePropertyChanged;
         UpdateAvailableItemsBasedOnPositionType(node.EquipmentPositionType);
     }
 }

 private void OnUnloaded(object sender, RoutedEventArgs e)
 {
     if (_currentNode != null)
     {
         _currentNode.PropertyChanged -= OnNodePropertyChanged;
         _currentNode = null;
     }
 }

辅助方法:

// 辅助方法:在视觉树中查找父元素
public static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
    var parent = VisualTreeHelper.GetParent(child);
    while (parent != null && !(parent is T))
    {
        parent = VisualTreeHelper.GetParent(parent);
    }
    return parent as T;
}

2.使用弱引用事件:WeakEventManager
 // 保存委托引用
 private PropertyChangedEventHandler _positionPropertyChangedHandler;
 //发布者对象引用
 private MovePositionActionNode _currentNode;
 public override void OnApplyTemplate()
 {
     base.OnApplyTemplate();
     if (!(Parent is PropertyGridField field))
     {
         return;
     }
     var def = (PropertyDefinition)field.DataContext;

     if (def.Instance is MovePositionActionNode node)
     {
         // 移除旧节点的订阅
         if (_currentNode != null)
         {
             WeakEventManager<MovePositionActionNode, PropertyChangedEventArgs>.RemoveHandler(
                 _currentNode,
                 nameof(INotifyPropertyChanged.PropertyChanged),
                 OnNodePropertyChanged);
         }

         // 订阅新节点
         _currentNode = node;
         WeakEventManager<MovePositionActionNode, PropertyChangedEventArgs>.AddHandler(
             node,
             nameof(INotifyPropertyChanged.PropertyChanged),
             OnNodePropertyChanged);

         UpdateAvailableItemsBasedOnPositionType(node.EquipmentPositionType);
     }

 }

引用1:https://www.cnblogs.com/monkeyZhong/p/4596914.html

引用2:https://blog.csdn.net/weixin_30621959/article/details/97911511?fromshare=blogdetail&sharetype=blogdetail&sharerId=97911511&sharerefer=PC&sharesource=qq_59062726&sharefrom=from_link

.Net中的事件有时会引起内存泄露问题。例如,A注册了B的某个事件,此时B就会暗中保留A的一个强引用,导致A无法被内存回收,直到B被回收或A反注册了B的事件。例如,我有一个对象注册了主窗口的Loaded事件,只要我不反注册该事件,那么主窗口会一直引用该对象,直到主窗口被关闭,该对象才会被回收。所以,每当我们注册某个对象的事件时,都有可能在不经意间埋下内存泄露的隐患。

解决这个问题的根本方法是,在必要的时候进行事件的反注册。但是,在某些情况下,我们可能很难判定这个“必要的时候”。另外,当我们作为类库的提供者时,我们也很难保证类库的使用者都记得要反注册事件。因此,另一个解决方案就是使用弱事件。

弱事件的实现原理很简单,就是对事件进行一层封装。不让事件发布者直接引用监听者,而是让他们保留一个监听者的弱引用。当事件触发时,发布者会先检查监听者是否还存在于内存中,如果存在才通知它。如此一来,监听者的生命周期就不会依赖于发布者了。

-------------------------------------------------------------------------------------------------

此番论述,未尽其详。乞盼来日,续有心得,再行补益。


网站公告

今日签到

点亮在社区的每一天
去签到