WPF 和 UWP 中的强类型依赖属性注册
为 WPF 和 UWP 实现的强类型依赖属性注册
引言
几乎所有 Microsoft UI 框架都使用基于 DependencyObjects
和 DependencyProperties
的相同架构模型。 同样的概念也用于 WPF 和 UWP,以及过时的框架,如 Silverlight 和 Windows Phone,甚至 Xamarin 和其他类似 CSHTML5 的好奇事物 (http://cshtml5.com/)。 DependencyObject
是一个基类,代表参与依赖属性系统的对象。 它由 UI 控件、几何图形、模板、样式等实现。DependencyProperty
(Xamarin 中的 BindableProperty
) 是一个属性的“后备存储”,用于启用动画、样式、数据绑定等。
添加依赖属性的最快方法是使用代码片段。 只要键入“propdp
”并按 Tab 键,就会出现以下代码
public int MyProperty
{
get { return (int)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(int),
typeof(ownerclass), new PropertyMetadata(0));
这种实现的缺点一目了然。 首先,属性名称被硬编码为 string
。 随着 nameof
运算符的引入,这个问题已不复存在。 如今,上述代码应如下所示
DependencyProperty.Register(nameof(MyProperty),
typeof(int), typeof(ownerclass), new PropertyMetadata(0));
从现在开始,与从 lambda 表达式中提取属性名称相关的技巧不再需要。 如今,几乎没有人会以这种方式触发 PropertyChanged
事件
RaisePropertyChanged(() => MyProperty);
可以这样做
RaisePropertyChanged(nameof(MyProperty));
或者这样做(感谢 CallerMemberNameAttribute
)
RaisePropertyChanged();
然而,注册依赖属性的主要问题是类型不匹配的危险。 如果我在上面的例子中用 double
替换 int
怎么办? 代码看起来是正确的,但它会抛出一个运行时异常,因为在 PropertyMetadata
中指定的默认值是一个整数
new PropertyMetadata(0));
为了避免异常,我们必须这样写
new PropertyMetadata(0d));
这个问题已经在 这篇文章 中指出了,这篇文章被我认为得到了不公平的评价。 评论区的人主要是在反驳文章的标题,并且他们关注的是会抛出运行时异常这一事实,这正是问题的所在——在我看来,这源于对强类型目的的明显误解,而这正是这篇文章的重点。
我在这里想要提出的解决方案与 Xamarin 之前提出的解决方案非常相似
public static readonly BindableProperty SomePropertyProp = BindableProperty.Create<CustomControl,
int>(c => c. SomeProperty, 1);
public int SomeProperty
{
get { return (int)GetValue(SomePropertyProp); }
set { SetValue(SomePropertyProp, value); }
}
由于不明原因,Xamarin 在引入 nameof
运算符后不久就撤回了此实现。 正如我之前提到的,nameof
运算符消除了与硬编码属性名称相关的问题,但与类型安全相关的问题仍然存在。 如果您知道此更改的原因,请在评论区写下。
在本文中,我将为 WPF 和 UWP 提出一个类似的实现,该实现用于开源乐谱控制库 Manufaktura.Controls
(之前在 本文 中描述过),以及一个代码片段,使它更易于使用。
如何使用
首先,您可以导入附加到 Manufaktura.Controls.WPF
库的代码片段。 可以在 Snippets 文件夹中找到它。 要在 Visual Studio 中导入代码片段,请转到 Tools\Code Snippet Manager…\Import… 并选择附加到项目的 .snippet 文件。 现在您可以键入“propdpx
”并按 Tab 键,就会出现以下代码
public static readonly DependencyProperty MyPropertyProperty = DependencyPropertyEx.Register<MyDepObject, string>(v => v.MyProperty, default(string), (depObject, oldValue, newValue) => { }); public string MyProperty { get { return (string)GetValue(MyPropertyProperty); } set { SetValue(MyPropertyProperty, value); } }
此代码类似于与 Dependency.Register
一起使用的代码,但类型安全性由泛型类型参数提供。 名称安全性由 lambda 提供(如今可以用 nameof
替换)。 还有一个默认的属性更改处理程序
(depObject, oldValue, newValue) => {
//Provide implementation here
});
它比旧的属性更改处理程序更好,因为每个参数(依赖对象、旧值和新值)都具有适当的类型。
它是如何工作的
实现很简单。 它依赖于调用 DependencyProperty.Register
并将回调参数转换为适当的类型
public static class DependencyPropertyEx
{
public delegate void PropertyChangedCallback<TControl, TProperty>
(TControl control, TProperty oldValue, TProperty newValue)
where TControl : DependencyObject;
public static DependencyProperty Register<TControl, TProperty>
(Expression<Func<TControl, TProperty>> property, TProperty defaultValue,
PropertyChangedCallback<TControl, TProperty> propertyChangedCallback = null)
where TControl : DependencyObject
{
if (propertyChangedCallback == null)
return DependencyProperty.Register(GetPropertyName(property), typeof(TProperty),
typeof(TControl), new PropertyMetadata(defaultValue));
return DependencyProperty.Register(GetPropertyName(property), typeof(TProperty),
typeof(TControl), new PropertyMetadata(defaultValue, (obj, args) =>
{
propertyChangedCallback(obj as TControl, (TProperty)args.OldValue,
(TProperty)args.NewValue);
}));
}
private static string GetPropertyName<TControl, TProperty>
(Expression<Func<TControl, TProperty>> propertyLambda)
{
var memberExpression = propertyLambda.Body as MemberExpression;
if (memberExpression == null)
throw new Exception("Lambda expression should be
in the following format: control => control.Property.");
return memberExpression.Member.Name;
}
}
代码片段
为了更容易地插入依赖属性,我创建了以下代码片段
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Typed dependency property</Title>
<Author>Manufaktura programów Jacek Salamon</Author>
<Description>Inserts strongly typed dependency property registration</Description>
<Shortcut>propdpx</Shortcut>
</Header>
<Snippet>
<Imports>
<Import>
<Namespace>System.Windows</Namespace>
</Import>
<Import>
<Namespace>Manufaktura.Controls.WPF.Bindings</Namespace>
</Import>
</Imports>
<Declarations>
<Literal>
<ID>PropertyName</ID>
<ToolTip>Define property name.</ToolTip>
<Default>MyProperty</Default>
</Literal>
<Literal>
<ID>PropertyType</ID>
<ToolTip>Define property type.</ToolTip>
<Default>string</Default>
</Literal>
<Literal>
<ID>PropertyTarget</ID>
<ToolTip>Define property target.</ToolTip>
<Default>MyDepObject</Default>
</Literal>
</Declarations>
<Code Language="CSharp">
<![CDATA[public static readonly DependencyProperty
$PropertyName$Property = DependencyPropertyEx.Register<$PropertyTarget$,
$PropertyType$>(v => v.$PropertyName$, default($PropertyType$),
(depObject, oldValue, newValue) => {});
public $PropertyType$ $PropertyName$
{
get { return ($PropertyType$)GetValue($PropertyName$Property); }
set { SetValue($PropertyName$Property, value); }
}]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
在 Visual Studio 中创建代码片段的过程在 这篇文章 中有介绍。
Manufaktura.Controls 中的其他有趣的功能
ResetableLazy
ResetableLazy
只是一个围绕 Lazy
类的门面,它允许您使用 Reset
方法重新初始化 Lazy
对象
public class ResetableLazy<T>
{
private readonly Func<T> valueFactory;
private Lazy<T> innerLazy;
public ResetableLazy(Func<T> valueFactory)
{
this.valueFactory = valueFactory;
innerLazy = new Lazy<T>(valueFactory);
}
public bool IsValueCreated => innerLazy.IsValueCreated;
public T Value => innerLazy.Value;
public void Reset()
{
lock (innerLazy)
{
innerLazy = new Lazy<T>(valueFactory);
}
}
public override string ToString() => innerLazy.ToString();
public void Touch()
{
var x = Value;
}
}
还有一个有用的 Touch
方法,它允许您在不返回的情况下初始化值。 它应该提高代码的清晰度。
公式绑定
FormulaBinding
是一种在 XAML 框架中进行数据绑定的新方法,它将绑定带入 JavaScript 框架的简单性和灵活性。 这在 这篇文章 中进行了描述。