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看来是可达的,可以通过引发事件来调用该内存,所以这种情况会有内存泄漏的风险。
防止内存泄漏解决办法:
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
.Net中的事件有时会引起内存泄露问题。例如,A注册了B的某个事件,此时B就会暗中保留A的一个强引用,导致A无法被内存回收,直到B被回收或A反注册了B的事件。例如,我有一个对象注册了主窗口的Loaded事件,只要我不反注册该事件,那么主窗口会一直引用该对象,直到主窗口被关闭,该对象才会被回收。所以,每当我们注册某个对象的事件时,都有可能在不经意间埋下内存泄露的隐患。
解决这个问题的根本方法是,在必要的时候进行事件的反注册。但是,在某些情况下,我们可能很难判定这个“必要的时候”。另外,当我们作为类库的提供者时,我们也很难保证类库的使用者都记得要反注册事件。因此,另一个解决方案就是使用弱事件。
弱事件的实现原理很简单,就是对事件进行一层封装。不让事件发布者直接引用监听者,而是让他们保留一个监听者的弱引用。当事件触发时,发布者会先检查监听者是否还存在于内存中,如果存在才通知它。如此一来,监听者的生命周期就不会依赖于发布者了。
-------------------------------------------------------------------------------------------------
此番论述,未尽其详。乞盼来日,续有心得,再行补益。