65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 和 UWP 中的强类型依赖属性注册

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.11/5 (7投票s)

2018年8月5日

MIT

4分钟阅读

viewsIcon

15205

downloadIcon

93

为 WPF 和 UWP 实现的强类型依赖属性注册

引言

几乎所有 Microsoft UI 框架都使用基于 DependencyObjectsDependencyProperties 的相同架构模型。 同样的概念也用于 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 框架的简单性和灵活性。 这在 这篇文章 中进行了描述。

© . All rights reserved.