【问题标题】:What's wrong with the simplest Data-binding with WPF without using `INotifyPropertyChanged` or `DependencyProperty`不使用 INotifyPropertyChanged 或 DependencyProperty 的 WPF 最简单的数据绑定有什么问题
【发布时间】:2021-06-06 07:39:03
【问题描述】:

我刚刚在 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
    }
}

还有我的班级

public class ConduitCapacity
{
    private double _conduitWidth;

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

【问题讨论】:

  • 除了答案中所说的之外,您还应该避免在控件中使用私有视图模型。 UserControl 应改为公开依赖属性 ConduitWidth,该属性绑定在控件的 XAML 中,如 Text="{Binding ConduitWidth, Mode=TwoWay, RelativeSource={RelativeSource AncestorType=UserControl}}"。然后不得显式设置控件的 DataContext。
  • 请阅读我更新的答案以了解如何启用双向绑定。经过一番讨论,我决定在我的答案中添加更多细节。

标签: c# wpf data-binding inotifypropertychanged


【解决方案1】:

这没有错。 INotifyPropertyChanged 的​​原因是当你改变你的类时你的 UI 会更新。

例如,如果您决定稍后更新管道容量,您的 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

【讨论】:

    【解决方案2】:

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

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

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

    更新:

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

    在评论区讨论了一些之后,我觉得有必要更新问题来解释一下背景。

    结合Microsoft Docs: How Data Binding References are Resolved 提供的信息和微软知识库文档 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]Changed”

    这意味着,如果没有额外的事件,与普通 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(如果源是@987654371 @) 显着提高应用程序的性能(一个应用程序通常定义数百个绑定)。

    【讨论】:

    • 你有那个“对源对象的静态引用”的引用吗?这对我来说听起来很奇怪。
    • @HenkHolterman 我相信这就是该页面在第一个“绑定泄漏”部分中所指的内容:blog.jetbrains.com/dotnet/2014/09/04/…
    • @HenkHolterman KB 938416.
    • @StayOnTarget 谢谢。此问题已由 Microsoft 在KB 938416 下正式注册。
    • @BionicCode 确实,这是一个更好的参考。谢谢
    【解决方案3】:

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

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

    要对此进行测试,请添加一个按钮并执行此操作:conduitCapacity.ConduitWidth = 100;

    看看你的 Control 中是否有 100 个。

    【讨论】:

    • 你确实是对的。该按钮不会按照您的建议更改 UI。添加 INotifyPropertyChanged 和样板代码后,该按钮可以正常工作。我没有得到的是,没有 INotifyPropertyChanged,当我在第一个文本框中键入一个数字时,它将触发 setter,但是如果第二个文本框使用 OneWayToSource + OneTime 模式,它如何接收更新的值。这就是诱使我认为我不需要 INotifyPropertyChanged 的​​原因。
    • 你会得到 2 个显然相互触发的 Binding 对象。我不完全确定它是如何工作的。但是没有从 DataContext 触发(单独)。
    猜你喜欢
    • 1970-01-01
    • 2013-07-31
    • 2011-03-20
    • 2011-03-27
    • 2023-03-12
    • 1970-01-01
    • 2010-10-27
    • 2011-08-31
    • 1970-01-01
    相关资源
    最近更新 更多