不使用 `INotifyPropertyChanged` 或 `DependencyProperty` 的最简单的 WPF 数据绑定有什么问题

What's wrong with the simplest Data-binding with WPF without using `INotifyPropertyChanged` or `DependencyProperty`

我刚开始使用 WPF 中的数据绑定(我对这一切都是新手),下面的代码是我可以开始工作的最简单的实现。代码如下:

我的问题是: 为什么我需要使用 INotifyPropertyChanged 和它附带的所有样板代码或 DependencyProperty 等当下面的简单开箱即用时? 我试图理解为什么本网站上的示例和答案比下面的示例复杂得多。

我的XAML

<TextBox Text="{Binding ConduitWidth, Mode = TwoWay}" />
<TextBox Text="{Binding ConduitWidth, Mode = TwoWay}" />

我的代码隐藏

public partial class ConduitCapacityCalculator : UserControl
{

    ConduitCapacity conduitCapacity = new ConduitCapacity();
    public ConduitCapacityCalculator()
    {
        InitializeComponent();
        this.DataContext = conduitCapacity;
        conduitCapacity.ConduitWidth = 10; //Just to check the textboxes update properly
    }
}

还有我的Class

public class ConduitCapacity
{
    private double _conduitWidth;

    public double ConduitWidth
    {
        get { return _conduitWidth; }
        set { _conduitWidth = value; } //add any conditions or Methods etc here
    }
}

这没有错。 INotify属性Changed 的​​原因是当您更改 class.

时您的 UI 会更新

例如,如果您决定稍后更新 conduitCapacity,您的 UI 仍会显示旧值。 INotifyPropertChanged Documentation

DepedencyProperties 的原因是您可以用它扩展您的 UserControl 假设您想在 MainWindow 中使用您的 Usercontrol 并给它一些 属性,例如 MaximumConduitWidth。比你会做这样的事情:

     public double MaximumConduitWidth
    {
        get { return (double)GetValue(MaximumConduitWidthProperty); }
        set { SetValue(MaximumConduitWidthProperty, value); }
    }

    public static readonly DependencyProperty MaximumConduitWidthProperty =
        DependencyProperty.Register("MaximumConduitWidth", typeof(double), typeof(ConduitCapacityCalculator), new PropertyMetadata(0));

然后你可以把这个写在你的 MainWindow.xaml

<ConduitCapacityCalculator MaximumConduitWidth=10/> 

Dependency Property

这样的绑定确实有效,但会造成内存泄漏。该框架将创建一个对源对象 ConduitCapacity 的静态引用来观察它。由于静态引用永远不符合垃圾收集器的条件,静态对象引用将使对象 ConduitCapacity 保持活动状态,从而阻止其被收集。这也适用于绑定到未实现 INotifyCollectionChanged.

的集合时

如果您担心避免内存泄漏,则数据绑定的源必须实现 INotifyPropertyChangedINotifyCollectionChanged 或 属性 必须是 DependencyProperty

DependencyProperty 提供最佳性能。这意味着当源对象是 DependencyObject 时,您应该更愿意将旨在用作绑定源的属性实现为 DependencyProperty.

更新:

不遵循 INotifyPropertyChangedDependencyProperty 绑定模式时数据绑定如何工作以及如何使用此方法启用 TwoWay 绑定

在评论区进行了一些讨论后,我觉得有必要更新问题以更多地解释背景。

结合 Microsoft Docs: How Data Binding References are Resolved 和 Microsoft 知识库文档提供的信息 KB 938416,
我们了解到 WPF 使用三种方法来建立从 DependencyProperty(绑定目标)到任何 CLR 对象(绑定源)的数据绑定:

  1. TypeDescrtiptor(组件检查)
  2. INotifyPropertyChanged
  3. DependencyProperty

原始问题与方法 1 相关):创建与源的数据绑定,该源未实现 INotifyPropertyChangedDependencyProperty。因此,框架必须使用繁重的 TypeDescriptor 来设置 属性 更改的绑定和跟踪。

从 KB 938416 文档我们了解到绑定引擎将存储对获得的 PropertyDescriptor 的静态引用(通过使用 TypeDescriptor)。由于以这种方式获取 PropertyDescriptor 非常慢,因此 PropertyDescriptor 引用存储在静态 HashTable 中(以避免连续的组件检查)。

框架使用这个 PropertyDescriptor 来监听 属性 变化。现在,因为描述符实例存储在静态 HashTable 中,它永远不会符合垃圾收集的条件。
静态引用或对象通常从不由 farbage 收集器管理。
因此内存泄漏,因为静态引用将使源对象在应用程序的生命周期内保持活动状态。

要解锁 TwoWay 绑定,我们必须明确启用支持,以便 PropertyDescriptor 了解 Binding.Source 上的 属性 更改。
我们可以通过查询 PropertyDescriptor.SupportsChangeEvents 属性 来测试这种意识。它是 true 当:

  1. 我们用DependencyObject.SetValue修饰属性(也就是说属性也是一个DependencyProperty)或者
  2. Binding.Source 提供了一个事件,必须符合以下命名模式:“[property_name]已更改”

这意味着,在没有额外事件的情况下,到普通 CLR 对象的绑定只能是 OneTimeOneWayToSource。从源到目标的初始化将始终有效。

例子

CLR 对象
绑定源,未实现 INotifyPropertyChanged,但仍支持 TwoWay 绑定。

class ClrObject
{
  public string TextProperty { get; set; }

  // Event to enable TwoWay data binding
  public event EventHandler TextPropertyChanged;

  protected virtual void OnTextPropertyChanged() 
    => this.TextPropertyChanged?.Invoke(this, EventArgs.Empty);
}

这就是 WPF 框架正在做的事情:

// Simplified. Would use reflection and binding engine lookup table to retrieve the binding.
// Example references a TextBox control named "BindingTarget" for simplicity
Binding binding = BindingOperations.GetBinding(this.BindingTarget, TextBox.TextProperty);

// Only observe Binding.Source when binding is TwoWay or OneWay
if (binding.Mode != BindingMode.OneWay 
  && binding.Mode != BindingMode.TwoWay)
{
  return;
}

object bindingSource = binding.Source ?? this.BindingTarget.DataContext;

// Use heavy TypeDescriptor inspection to obtain the object's PropertyDescriptors
PropertyDescriptorCollection descriptors = TypeDescriptor.GetProperties(bindingSource);

foreach (PropertyDescriptor descriptor in descriptors)
{
  if (descriptor.Name.Equals(binding.Path.Path, StringComparison.OrdinalIgnoreCase)
    && descriptor.SupportsChangeEvents)
  {
    // Add descriptor to static HashTable for faster lookup 
    // (e.g. in case for additional data bindings to this source object). 
    // TypeDescriptor is too slow

    // Attach a change delegate
    descriptor.AddValueChanged(bindingSource, UpdateTarget_OnSourcePropertyChanged);

    break;
  }
}

我们可以看出为什么这种数据绑定方式性能不好。 TypeDescriptor 很慢。此外,引擎必须使用更多反射来查找 TextPropertyChanged 事件以初始化 TwoWay 绑定。

我们可以得出结论,即使不是内存泄漏,我们也会避免这种解决方案,而是在 CLR 对象上实现 INotifyCollectionChanged 或更好地将属性实现为 DependencyProperty(以防万一来源是 DependencyObject) 以显着提高应用程序的性能(一个应用程序通常定义数百个绑定)。

因为 Mode = TwoWay 在您的示例中不正确。

如果没有来自源的任何信号 (INotifyPropertyChanged),您只能获得 OneWayToSource + OneTime 模式。

要对此进行测试,请添加一个按钮并使其运行:conduitCapacity.ConduitWidth = 100;

看看你的 Control 中是否有 100。