不使用 `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/>
这样的绑定确实有效,但会造成内存泄漏。该框架将创建一个对源对象 ConduitCapacity
的静态引用来观察它。由于静态引用永远不符合垃圾收集器的条件,静态对象引用将使对象 ConduitCapacity
保持活动状态,从而阻止其被收集。这也适用于绑定到未实现 INotifyCollectionChanged
.
的集合时
如果您担心避免内存泄漏,则数据绑定的源必须实现 INotifyPropertyChanged
、INotifyCollectionChanged
或 属性 必须是 DependencyProperty
。
DependencyProperty
提供最佳性能。这意味着当源对象是 DependencyObject
时,您应该更愿意将旨在用作绑定源的属性实现为 DependencyProperty
.
更新:
不遵循 INotifyPropertyChanged
或 DependencyProperty
绑定模式时数据绑定如何工作以及如何使用此方法启用 TwoWay
绑定
在评论区进行了一些讨论后,我觉得有必要更新问题以更多地解释背景。
结合 Microsoft Docs: How Data Binding References are Resolved 和 Microsoft 知识库文档提供的信息
KB 938416,
我们了解到 WPF 使用三种方法来建立从 DependencyProperty
(绑定目标)到任何 CLR 对象(绑定源)的数据绑定:
TypeDescrtiptor
(组件检查)
INotifyPropertyChanged
DependencyProperty
原始问题与方法 1 相关):创建与源的数据绑定,该源未实现 INotifyPropertyChanged
或 DependencyProperty
。因此,框架必须使用繁重的 TypeDescriptor
来设置 属性 更改的绑定和跟踪。
从 KB 938416 文档我们了解到绑定引擎将存储对获得的 PropertyDescriptor
的静态引用(通过使用 TypeDescriptor
)。由于以这种方式获取 PropertyDescriptor
非常慢,因此 PropertyDescriptor
引用存储在静态 HashTable
中(以避免连续的组件检查)。
框架使用这个 PropertyDescriptor
来监听 属性 变化。现在,因为描述符实例存储在静态 HashTable
中,它永远不会符合垃圾收集的条件。
静态引用或对象通常从不由 farbage 收集器管理。
因此内存泄漏,因为静态引用将使源对象在应用程序的生命周期内保持活动状态。
要解锁 TwoWay
绑定,我们必须明确启用支持,以便 PropertyDescriptor
了解 Binding.Source
上的 属性 更改。
我们可以通过查询 PropertyDescriptor.SupportsChangeEvents
属性 来测试这种意识。它是 true
当:
- 我们用
DependencyObject.SetValue
修饰属性(也就是说属性也是一个DependencyProperty
)或者
Binding.Source
提供了一个事件,必须符合以下命名模式:“[property_name]已更改”
这意味着,在没有额外事件的情况下,到普通 CLR 对象的绑定只能是 OneTime
或 OneWayToSource
。从源到目标的初始化将始终有效。
例子
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。
我刚开始使用 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/>
这样的绑定确实有效,但会造成内存泄漏。该框架将创建一个对源对象 ConduitCapacity
的静态引用来观察它。由于静态引用永远不符合垃圾收集器的条件,静态对象引用将使对象 ConduitCapacity
保持活动状态,从而阻止其被收集。这也适用于绑定到未实现 INotifyCollectionChanged
.
如果您担心避免内存泄漏,则数据绑定的源必须实现 INotifyPropertyChanged
、INotifyCollectionChanged
或 属性 必须是 DependencyProperty
。
DependencyProperty
提供最佳性能。这意味着当源对象是 DependencyObject
时,您应该更愿意将旨在用作绑定源的属性实现为 DependencyProperty
.
更新:
不遵循 INotifyPropertyChanged
或 DependencyProperty
绑定模式时数据绑定如何工作以及如何使用此方法启用 TwoWay
绑定
在评论区进行了一些讨论后,我觉得有必要更新问题以更多地解释背景。
结合 Microsoft Docs: How Data Binding References are Resolved 和 Microsoft 知识库文档提供的信息
KB 938416,
我们了解到 WPF 使用三种方法来建立从 DependencyProperty
(绑定目标)到任何 CLR 对象(绑定源)的数据绑定:
TypeDescrtiptor
(组件检查)INotifyPropertyChanged
DependencyProperty
原始问题与方法 1 相关):创建与源的数据绑定,该源未实现 INotifyPropertyChanged
或 DependencyProperty
。因此,框架必须使用繁重的 TypeDescriptor
来设置 属性 更改的绑定和跟踪。
从 KB 938416 文档我们了解到绑定引擎将存储对获得的 PropertyDescriptor
的静态引用(通过使用 TypeDescriptor
)。由于以这种方式获取 PropertyDescriptor
非常慢,因此 PropertyDescriptor
引用存储在静态 HashTable
中(以避免连续的组件检查)。
框架使用这个 PropertyDescriptor
来监听 属性 变化。现在,因为描述符实例存储在静态 HashTable
中,它永远不会符合垃圾收集的条件。
静态引用或对象通常从不由 farbage 收集器管理。
因此内存泄漏,因为静态引用将使源对象在应用程序的生命周期内保持活动状态。
要解锁 TwoWay
绑定,我们必须明确启用支持,以便 PropertyDescriptor
了解 Binding.Source
上的 属性 更改。
我们可以通过查询 PropertyDescriptor.SupportsChangeEvents
属性 来测试这种意识。它是 true
当:
- 我们用
DependencyObject.SetValue
修饰属性(也就是说属性也是一个DependencyProperty
)或者 Binding.Source
提供了一个事件,必须符合以下命名模式:“[property_name]已更改”
这意味着,在没有额外事件的情况下,到普通 CLR 对象的绑定只能是 OneTime
或 OneWayToSource
。从源到目标的初始化将始终有效。
例子
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。