如何修复 AttachedBehavior 上的 DependencyPropertyDescriptor AddValueChanged 内存泄漏?

Posted

技术标签:

【中文标题】如何修复 AttachedBehavior 上的 DependencyPropertyDescriptor AddValueChanged 内存泄漏?【英文标题】:How can I fix the DependencyPropertyDescriptor AddValueChanged Memory Leak on AttachedBehavior? 【发布时间】:2014-07-04 03:16:31 【问题描述】:

我知道我需要打电话给RemoveValueChanged,但我一直找不到可靠的地方打电话。我知道可能没有。

我似乎需要找到一种不同的方法来监控更改,然后使用AddValueChanged 添加处理程序。我正在寻找实现这一目标的最佳方法的建议。我已经看到在PropertyMetadata 中使用PropertyChangedCallback 的建议,但是当我的TextBoxAdorner 不是静态的时,我不确定如何执行此操作。此外,IsFocused 属性不是在我的班级中创建的 DependencyProperty

public sealed class WatermarkTextBoxBehavior

    private readonly TextBox m_TextBox;
    private TextBlockAdorner m_TextBlockAdorner;

    private WatermarkTextBoxBehavior(TextBox textBox)
    
        if (textBox == null)
            throw new ArgumentNullException("textBox");

        m_TextBox = textBox;
    

    #region Behavior Internals

    private static WatermarkTextBoxBehavior GetWatermarkTextBoxBehavior(DependencyObject obj)
    
        return (WatermarkTextBoxBehavior)obj.GetValue(WatermarkTextBoxBehaviorProperty);
    

    private static void SetWatermarkTextBoxBehavior(DependencyObject obj, WatermarkTextBoxBehavior value)
    
        obj.SetValue(WatermarkTextBoxBehaviorProperty, value);
    

    private static readonly DependencyProperty WatermarkTextBoxBehaviorProperty =
        DependencyProperty.RegisterAttached("WatermarkTextBoxBehavior",
            typeof(WatermarkTextBoxBehavior), typeof(WatermarkTextBoxBehavior), new UIPropertyMetadata(null));

    public static bool GetEnableWatermark(TextBox obj)
    
        return (bool)obj.GetValue(EnableWatermarkProperty);
    

    public static void SetEnableWatermark(TextBox obj, bool value)
    
        obj.SetValue(EnableWatermarkProperty, value);
    

    public static readonly DependencyProperty EnableWatermarkProperty =
        DependencyProperty.RegisterAttached("EnableWatermark", typeof(bool),
            typeof(WatermarkTextBoxBehavior), new UIPropertyMetadata(false, OnEnableWatermarkChanged));

    private static void OnEnableWatermarkChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        if (e.OldValue != null)
        
            var enabled = (bool)e.OldValue;

            if (enabled)
            
                var textBox = (TextBox)d;
                var behavior = GetWatermarkTextBoxBehavior(textBox);
                behavior.Detach();

                SetWatermarkTextBoxBehavior(textBox, null);
            
        

        if (e.NewValue != null)
        
            var enabled = (bool)e.NewValue;

            if (enabled)
            
                var textBox = (TextBox)d;
                var behavior = new WatermarkTextBoxBehavior(textBox);
                behavior.Attach();

                SetWatermarkTextBoxBehavior(textBox, behavior);
            
        
    

    private void Attach()
    
        m_TextBox.Loaded += TextBoxLoaded;
        m_TextBox.TextChanged += TextBoxTextChanged;
        m_TextBox.DragEnter += TextBoxDragEnter;
        m_TextBox.DragLeave += TextBoxDragLeave;
        m_TextBox.IsVisibleChanged += TextBoxIsVisibleChanged;
    

    private void Detach()
    
        m_TextBox.Loaded -= TextBoxLoaded;
        m_TextBox.TextChanged -= TextBoxTextChanged;
        m_TextBox.DragEnter -= TextBoxDragEnter;
        m_TextBox.DragLeave -= TextBoxDragLeave;
        m_TextBox.IsVisibleChanged -= TextBoxIsVisibleChanged;
    

    private void TextBoxDragLeave(object sender, DragEventArgs e)
    
        UpdateAdorner();
    

    private void TextBoxDragEnter(object sender, DragEventArgs e)
    
        m_TextBox.TryRemoveAdorners<TextBlockAdorner>();
    

    private void TextBoxIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    
        UpdateAdorner();
    

    private void TextBoxTextChanged(object sender, TextChangedEventArgs e)
    
        var hasText = !string.IsNullOrEmpty(m_TextBox.Text);
        SetHasText(m_TextBox, hasText);
    

    private void TextBoxLoaded(object sender, RoutedEventArgs e)
    
        Init();
    

    #endregion

    #region Attached Properties

    public static string GetLabel(TextBox obj)
    
        return (string)obj.GetValue(LabelProperty);
    

    public static void SetLabel(TextBox obj, string value)
    
        obj.SetValue(LabelProperty, value);
    

    public static readonly DependencyProperty LabelProperty =
        DependencyProperty.RegisterAttached("Label", typeof(string), typeof(WatermarkTextBoxBehavior));

    public static Style GetLabelStyle(TextBox obj)
    
        return (Style)obj.GetValue(LabelStyleProperty);
    

    public static void SetLabelStyle(TextBox obj, Style value)
    
        obj.SetValue(LabelStyleProperty, value);
    

    public static readonly DependencyProperty LabelStyleProperty =
        DependencyProperty.RegisterAttached("LabelStyle", typeof(Style),
            typeof(WatermarkTextBoxBehavior));

    public static bool GetHasText(TextBox obj)
    
        return (bool)obj.GetValue(HasTextProperty);
    

    private static void SetHasText(TextBox obj, bool value)
    
        obj.SetValue(HasTextPropertyKey, value);
    

    private static readonly DependencyPropertyKey HasTextPropertyKey =
        DependencyProperty.RegisterAttachedReadOnly("HasText", typeof(bool),
            typeof(WatermarkTextBoxBehavior), new UIPropertyMetadata(false));

    public static readonly DependencyProperty HasTextProperty =
        HasTextPropertyKey.DependencyProperty;

    #endregion

    private void Init()
    
        m_TextBlockAdorner = new TextBlockAdorner(m_TextBox, GetLabel(m_TextBox), GetLabelStyle(m_TextBox));
        UpdateAdorner();

        DependencyPropertyDescriptor focusProp = DependencyPropertyDescriptor.FromProperty(UIElement.IsFocusedProperty, typeof(FrameworkElement));
        if (focusProp != null)
        
            focusProp.AddValueChanged(m_TextBox, (sender, args) => UpdateAdorner());
        

        DependencyPropertyDescriptor containsTextProp = DependencyPropertyDescriptor.FromProperty(HasTextProperty, typeof(TextBox));
        if (containsTextProp != null)
        
            containsTextProp.AddValueChanged(m_TextBox, (sender, args) => UpdateAdorner());
        
    

    private void UpdateAdorner()
    
        if (GetHasText(m_TextBox) ||
            m_TextBox.IsFocused ||
            !m_TextBox.IsVisible)
        
            // Hide the Watermark Label if the adorner layer is visible
            m_TextBox.ToolTip = GetLabel(m_TextBox);
            m_TextBox.TryRemoveAdorners<TextBlockAdorner>();
        
        else
        
            // Show the Watermark Label if the adorner layer is visible
            m_TextBox.ToolTip = null;
            m_TextBox.TryAddAdorner<TextBlockAdorner>(m_TextBlockAdorner);
        
    

【问题讨论】:

【参考方案1】:

FrameworkElementsFrameworkContentElements 的更轻量级的解决方案是订阅Unloaded 事件并删除处理程序。这需要一个非匿名委托(在这种情况下为UpdateAdorner):

focusProp.AddValueChanged(m_TextBox, UpdateAdorner);
m_TextBox.Unloaded += (sender, args) => focusProp.RemoveValueChanged(sender, UpdateAdorner);

【讨论】:

这不是正确的方法。如果您将“TextBox”放在弹出窗口或类似内容中,您会看到“TextBox”的 Unloaded 事件将在弹出窗口关闭时触发。 如果您在Loaded 事件中添加处理程序,这是正确的方法。 LoadedUnloaded 应始终成对。【参考方案2】:

AddValueChanged 的依赖属性描述符会导致内存泄漏,如您所知。因此,如here 所述,您可以创建自定义类PropertyChangeNotifier 来监听任何依赖属性的变化。

完整的实现可以在这里找到 - PropertyDescriptor AddValueChanged Alternative


引用链接:

这个类利用了绑定使用弱的事实 管理关联的引用,因此该类不会根 属性改变的对象正在观察。它还使用了一个 WeakReference 维护对其属性的对象的引用 正在观看而不生根该对象。这样,您可以维护 这些对象的集合,以便您可以解开该属性 稍后更改,而不必担心该集合使对象生根 你在看谁的价值观。

另外为了回答的完整性,我在这里发布完整的代码以避免将来出现任何腐烂问题。

public sealed class PropertyChangeNotifier : DependencyObject, IDisposable

    #region Member Variables

    private readonly WeakReference _propertySource;

    #endregion // Member Variables

    #region Constructor
    public PropertyChangeNotifier(DependencyObject propertySource, string path)
        : this(propertySource, new PropertyPath(path))
    
    
    public PropertyChangeNotifier(DependencyObject propertySource, DependencyProperty property)
        : this(propertySource, new PropertyPath(property))
    
    
    public PropertyChangeNotifier(DependencyObject propertySource, PropertyPath property)
    
        if (null == propertySource)
            throw new ArgumentNullException("propertySource");
        if (null == property)
            throw new ArgumentNullException("property");
        _propertySource = new WeakReference(propertySource);
        Binding binding = new Binding
        
            Path = property, 
            Mode = BindingMode.OneWay, 
            Source = propertySource
        ;
        BindingOperations.SetBinding(this, ValueProperty, binding);
    
    #endregion // Constructor

    #region PropertySource
    public DependencyObject PropertySource
    
        get
        
            try
            
                // note, it is possible that accessing the target property
                // will result in an exception so i’ve wrapped this check
                // in a try catch
                return _propertySource.IsAlive
                ? _propertySource.Target as DependencyObject
                : null;
            
            catch
            
                return null;
            
        
    
    #endregion // PropertySource

    #region Value
    /// <summary>
    /// Identifies the <see cref="Value"/> dependency property
    /// </summary>
    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value",
    typeof(object), typeof(PropertyChangeNotifier), new FrameworkPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        PropertyChangeNotifier notifier = (PropertyChangeNotifier)d;
        if (null != notifier.ValueChanged)
            notifier.ValueChanged(notifier, EventArgs.Empty);
    

    /// <summary>
    /// Returns/sets the value of the property
    /// </summary>
    /// <seealso cref="ValueProperty"/>
    [Description("Returns/sets the value of the property")]
    [Category("Behavior")]
    [Bindable(true)]
    public object Value
    
        get
        
            return GetValue(ValueProperty);
        
        set
        
            SetValue(ValueProperty, value);
        
    
    #endregion //Value

    #region Events
    public event EventHandler ValueChanged;
    #endregion // Events

    #region IDisposable Members

    public void Dispose()
    
        BindingOperations.ClearBinding(this, ValueProperty);
    

    #endregion

【讨论】:

感谢您的帮助。我不知道我是怎么错过这个资源的! 这个解决方案似乎对我不起作用,我没有从 PropertyChangedNotifier 获得 OnPropertyChanged 事件 重要的是要注意,这个通知类在超出范围时会停止工作,因此您必须将实例保留在私有字段或其他东西中。

以上是关于如何修复 AttachedBehavior 上的 DependencyPropertyDescriptor AddValueChanged 内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

如何修复div上的负位置值?

如何修复 python tkinter 上的按钮位置?

如何修复 phpMyAdmin 上的 HTTP 500?

AWS 上的 Elasticsearch:如何修复未分配的分片?

如何修复 oneAPI 代码上的编译错误

如何修复 Spyder 上的缩进/制表符问题