如何修复AttachedBehavior中DependencyPropertyDescriptor AddValueChanged的内存泄漏问题?

13

我知道需要调用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);
        }
    }
}
2个回答

20

AddValueChanged的依赖属性描述符会导致内存泄漏,正如您已经知道的那样。因此,就像这里所述,您可以创建自定义类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
}

1
谢谢你的帮助。我不知道我怎么错过了这个资源! - scuba88
4
这个解决方案似乎对我无效,我没有从PropertyChangedNotifier中获得OnPropertyChanged事件。 - Igor Meszaros
需要注意的是,当通知器类超出作用域时,它将停止工作,因此您必须在私有字段或其他地方保留实例。 - 15ee8f99-57ff-4f92-890c-b56153

9

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

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

3
这不是正确的方法。如果你把你的“TextBox”放在一个弹出窗口或类似的东西里,当弹出窗口关闭时,你会发现“TextBox”的“Unloaded”事件会被触发。 - Hosein
1
只要在“Loaded”事件中添加处理程序,这就是正确的方法。 “Loaded”和“Unloaded”应始终成对匹配。 - Miral

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接