如何使数据绑定类型安全并支持重构?

Posted

技术标签:

【中文标题】如何使数据绑定类型安全并支持重构?【英文标题】:How to make Databinding type safe and support refactoring? 【发布时间】:2010-11-22 16:23:32 【问题描述】:

当我希望将控件绑定到对象的属性时,我必须以字符串的形式提供属性名称。这不是很好,因为:

    如果属性被删除或重命名,我不会收到编译器警告。 如果使用重构工具重命名属性,则很可能不会更新数据绑定。 如果属性类型错误,例如将整数绑定到日期选择器,然后直到运行时才会出现错误。

是否有一种设计模式可以解决这个问题,但仍然具有数据绑定的易用性?

(这是 WinForms、ASP.NET 和 WPF 以及可能的其他系统中的问题。)

我现在找到了“workarounds for nameof() operator in C#: typesafe databinding”,它也是一个很好的解决方案起点。

如果你愿意在编译代码后使用后处理器,那么NotifyPropertyWeaver 值得一看。


当绑定是在 XML 而不是 C# 中完成时,有谁知道 WPF 的一个好的解决方案?

【问题讨论】:

更多参考:jagregory.com/writings/introduction-to-static-reflectionlostechies.com/blogs/gabrielschenker/archive/2009/02/03/…weblogs.asp.net/cazzu/archive/2006/07/06/… 引用链接的问题:这个问题现在在编译时解决了! nameof 运算符于 2015 年 7 月在 C# 6.0 和 .NET 4.6 和 VS2015 中实现。以下答案对于 C# ***.com/users/772086/mike) @MadsRavn,这并没有像您希望的那样解决,因为它不能从 XAML 中使用并且不提供类型安全。但是,当从“代码”完成绑定时,它确实允许重构。 @IanRingrose 很公平,直到我们拥有编译时类型安全性/从 XAML 等标记中使用它的能力后,问题才得以解决。但是我的主要观点是,在 C# 6.0 及更高版本中不应使用已接受答案 (BindingHelper) 中的解决方案,而使用 nameof 运算符可以实现相同的目的。答案现在反映了这一点,所以我很高兴:) 查看链接如何在编译时检测 XAML 中已损坏的绑定:***.com/questions/43208011/… 【参考方案1】:

请注意,此答案使用 WinForm,并且是在 C# 有 'NameOf()' 之前编写的

感谢 Oliver 让我开始使用,我现在有了一个既支持重构又是类型安全的解决方案。它还让我实现了 INotifyPropertyChanged,因此它可以处理重命名的属性。

它的用法是这样的:

checkBoxCanEdit.Bind(c => c.Checked, person, p => p.UserCanEdit);
textBoxName.BindEnabled(person, p => p.UserCanEdit);
checkBoxEmployed.BindEnabled(person, p => p.UserCanEdit);
trackBarAge.BindEnabled(person, p => p.UserCanEdit);

textBoxName.Bind(c => c.Text, person, d => d.Name);
checkBoxEmployed.Bind(c => c.Checked, person, d => d.Employed);
trackBarAge.Bind(c => c.Value, person, d => d.Age);

labelName.BindLabelText(person, p => p.Name);
labelEmployed.BindLabelText(person, p => p.Employed);
labelAge.BindLabelText(person, p => p.Age);

person 类展示了如何以类型安全的方式实现 INotifyPropertyChanged(或者see this answer 是实现 INotifyPropertyChanged 的​​另一种相当不错的方式,ActiveSharp - Automatic INotifyPropertyChanged 看起来也不错):

public class Person : INotifyPropertyChanged

   private bool _employed;
   public bool Employed
   
      get  return _employed; 
      set
      
         _employed = value;
         OnPropertyChanged(() => c.Employed);
      
   
    
   // etc
    
   private void OnPropertyChanged(Expression<Func<object>> property)
   
      if (PropertyChanged != null)
      
         PropertyChanged(this, 
             new PropertyChangedEventArgs(BindingHelper.Name(property)));
      
   
    
   public event PropertyChangedEventHandler PropertyChanged;

WinForms 绑定帮助程序类具有使这一切正常工作的关键:

namespace TypeSafeBinding

    public static class BindingHelper
    
        private static string GetMemberName(Expression expression)
        
            // The nameof operator was implemented in C# 6.0 with .NET 4.6
            // and VS2015 in July 2015. 
            // The following is still valid for C# < 6.0

            switch (expression.NodeType)
            
                case ExpressionType.MemberAccess:
                    var memberExpression = (MemberExpression) expression;
                    var supername = GetMemberName(memberExpression.Expression);
                    if (String.IsNullOrEmpty(supername)) return memberExpression.Member.Name;
                    return String.Concat(supername, '.', memberExpression.Member.Name);
                case ExpressionType.Call:
                    var callExpression = (MethodCallExpression) expression;
                    return callExpression.Method.Name;
                case ExpressionType.Convert:
                    var unaryExpression = (UnaryExpression) expression;
                    return GetMemberName(unaryExpression.Operand);
                case ExpressionType.Parameter:
                case ExpressionType.Constant: //Change
                    return String.Empty;
                default:
                    throw new ArgumentException("The expression is not a member access or method call expression");
            
        

        public static string Name<T, T2>(Expression<Func<T, T2>> expression)
        
            return GetMemberName(expression.Body);
        

        //NEW
        public static string Name<T>(Expression<Func<T>> expression)
        
           return GetMemberName(expression.Body);
        

        public static void Bind<TC, TD, TP>(this TC control, Expression<Func<TC, TP>> controlProperty, TD dataSource, Expression<Func<TD, TP>> dataMember) where TC : Control
        
            control.DataBindings.Add(Name(controlProperty), dataSource, Name(dataMember));
        

        public static void BindLabelText<T>(this Label control, T dataObject, Expression<Func<T, object>> dataMember)
        
            // as this is way one any type of property is ok
            control.DataBindings.Add("Text", dataObject, Name(dataMember));
        

        public static void BindEnabled<T>(this Control control, T dataObject, Expression<Func<T, bool>> dataMember)
               
           control.Bind(c => c.Enabled, dataObject, dataMember);
        
    

这利用了 C# 3.5 中的许多新内容,并展示了哪些是可能的。现在,如果我们有hygienic macros lisp 程序员可能会停止称我们为二等公民)

【讨论】:

这是否需要为每种类型实现 OnPropertyChanged 方法?如果是这样,这有点好,但并不理想,而且 OnPropertyChanged 方法通常在基类中实现并从所有派生类中调用。 戴维,没有理由不能将 OnPropertyChanged 方法(和事件)移至基类并使其受到保护。 (这是我在现实生活中期望做的) 但是从您的示例来看,它似乎依赖于 Expression> 类型的参数,是否需要为每种类型实现该方法以获取参数Expression>, Expression>, etc? 我现在已将 OnPropertyChanged 更改为 OnPropertyChanged(Expression> property),这将允许将其移动到基类中。 @macias,见msdn.microsoft.com/en-us/library/…【参考方案2】:

nameof 运算符于 2015 年 7 月在 C# 6.0 和 .NET 4.6 和 VS2015 中实现。以下对 C# 仍然有效

为了避免字符串包含属性名称,我编写了一个简单的类,使用表达式树返回成员的名称:

using System;
using System.Linq.Expressions;
using System.Reflection;

public static class Member

    private static string GetMemberName(Expression expression)
    
        switch (expression.NodeType)
        
            case ExpressionType.MemberAccess:
                var memberExpression = (MemberExpression) expression;
                var supername = GetMemberName(memberExpression.Expression);

                if (String.IsNullOrEmpty(supername))
                    return memberExpression.Member.Name;

                return String.Concat(supername, '.', memberExpression.Member.Name);

            case ExpressionType.Call:
                var callExpression = (MethodCallExpression) expression;
                return callExpression.Method.Name;

            case ExpressionType.Convert:
                var unaryExpression = (UnaryExpression) expression;
                return GetMemberName(unaryExpression.Operand);

            case ExpressionType.Parameter:
                return String.Empty;

            default:
                throw new ArgumentException("The expression is not a member access or method call expression");
        
    

    public static string Name<T>(Expression<Func<T, object>> expression)
    
        return GetMemberName(expression.Body);
    

    public static string Name<T>(Expression<Action<T>> expression)
    
        return GetMemberName(expression.Body);
    

您可以按如下方式使用此类。即使您只能在代码中使用它(所以不能在 XAML 中),它还是很有帮助的(至少对我而言),但您的代码仍然不是类型安全的。您可以使用第二个类型参数扩展方法名称,该参数定义函数的返回值,这将限制属性的类型。

var name = Member.Name<MyClass>(x => x.MyProperty); // name == "MyProperty"

到目前为止,我还没有找到任何解决数据绑定类型安全问题的方法。

【讨论】:

感谢您的出色起点,我刚刚发布了一个答案,可以扩展您的工作以提供类型安全。【参考方案3】:

Framework 4.5 为我们提供了CallerMemberNameAttribute,这使得将属性名称作为字符串传递是不必要的:

private string m_myProperty;
public string MyProperty

    get  return m_myProperty; 
    set
    
        m_myProperty = value;
        OnPropertyChanged();
    


private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")

    // ... do stuff here ...

如果您正在使用安装了KB2468871 的Framework 4.0,您可以通过nuget 安装Microsoft BCL 兼容包,它也提供此属性。

【讨论】:

【参考方案4】:

此博客article raises some good questions about the performance of this approach。您可以通过将表达式转换为字符串作为某种静态初始化的一部分来改进这些缺点。

实际的机制可能有点难看,但它仍然是类型安全的,并且性能与原始 INotifyPropertyChanged 大致相同。

有点像这样:

public class DummyViewModel : ViewModelBase

    private class DummyViewModelPropertyInfo
    
        internal readonly string Dummy;

        internal DummyViewModelPropertyInfo(DummyViewModel model)
        
            Dummy = BindingHelper.Name(() => model.Dummy);
        
    

    private static DummyViewModelPropertyInfo _propertyInfo;
    private DummyViewModelPropertyInfo PropertyInfo
    
        get  return _propertyInfo ?? (_propertyInfo = new DummyViewModelPropertyInfo(this)); 
    

    private string _dummyProperty;
    public string Dummy
    
        get
        
            return this._dummyProperty;
        
        set
        
            this._dummyProperty = value;
            OnPropertyChanged(PropertyInfo.Dummy);
        
    

【讨论】:

好点,但是在大多数软件中,在现实生活中并不容易成为问题,所以先尝试简单的方法。【参考方案5】:

如果您的绑定被破坏,获得反馈的一种方法是创建一个 DataTemplate 并将其 DataType 声明为它绑定到的 ViewModel 的类型,例如如果你有一个 PersonView 和一个 PersonViewModel 你会做以下事情:

    使用 DataType = PersonViewModel 和一个键(例如 PersonTemplate)声明一个 DataTemplate

    剪切所有 PersonView xaml 并将其粘贴到数据模板中(理想情况下,它可以位于 PersonView 的顶部。

3a。创建一个 ContentControl 并设置 ContentTemplate = PersonTemplate 并将其 Content 绑定到 PersonViewModel。

3b。另一种选择是不为 DataTemplate 提供密钥,也不设置 ContentControl 的 ContentTemplate。在这种情况下,WPF 将确定要使用的 DataTemplate,因为它知道您要绑定的对象类型。它将向上搜索树并找到您的 DataTemplate,由于它与绑定的类型匹配,它会自动将其应用为 ContentTemplate。

您最终会得到与以前基本相同的视图,但由于您将 DataTemplate 映射到底层 DataType,因此 Resharper 等工具可以向您提供反馈(通过颜色标识符 - Resharper-Options-Settings-Color Identifiers)关于您的绑定是否损坏。

您仍然不会收到编译器警告,但可以直观地检查损坏的绑定,这比在视图和视图模型之间来回检查要好。

您提供的这些附加信息的另一个优点是,它还可以用于重命名重构。据我记得,当底层 ViewModel 的属性名称发生更改时,Resharper 能够自动重命名类型化 DataTemplates 上的绑定,反之亦然。

【讨论】:

【参考方案6】:

1.如果属性被删除或重命名,我不会收到编译器警告。

2.如果使用重构工具重命名属性,很可能数据绑定不会更新。

3.如果属性的类型错误,我直到运行时才会收到错误,例如将整数绑定到日期选择器。

是的,Ian,这正是名称字符串驱动的数据绑定的问题。您要求提供设计模式。我设计了类型安全视图模型 (TVM) 模式,它是模型-视图-视图模型 (MVVM) 模式的视图模型部分的具体化。它基于类型安全的绑定,类似于您自己的答案。我刚刚发布了 WPF 的解决方案:

http://www.codeproject.com/Articles/450688/Enhanced-MVVM-Design-w-Type-Safe-View-Models-TVM

【讨论】:

很好,但似乎有很多工作要做,而且当 MSFT 所要做的就是从 XAML 实际编译绑定时,从 XAML 中的绑定到在代码隐藏中的绑定的破坏性转变。反正都是编译成 BAML 的,所以没有太多的借口。【参考方案7】:

windows 10 和 windows phone 10 中 XAML(通用应用程序)的 x:bind(也称为“编译数据绑定”)可能会解决此问题,请参阅https://channel9.msdn.com/Events/Build/2015/3-635

我找不到它的在线文档,但没有付出太多努力,因为它是我一段时间内不会使用的东西。然而,这个答案应该是对其他人的有用指针。

https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension

Difference between Binding and x:Bind

【讨论】:

【参考方案8】:

C# Markup 似乎解决了同样的问题,因此我将这个答案添加为帮助当前一代程序员的指针。

Xamarin.Forms 4.6 introduced C# Markup,一组流畅的助手和 旨在使 C# 中的 UI 开发成为一种乐趣的类。

C# 标记可帮助开发人员编写简洁的声明式 UI 标记和 将其与 UI 逻辑完全分开,全部在 C# 中。开发者可以享受 编写标记时 C# 的一流 IDE 支持。单一语言 用于标记和逻辑减少摩擦、标记分散和认知 加载;很少或不需要语言桥接机制,例如 单独的转换器、样式、资源字典、行为、 触发器和标记扩展

【讨论】:

以上是关于如何使数据绑定类型安全并支持重构?的主要内容,如果未能解决你的问题,请参考以下文章

C# 中缺少用于类型安全数据绑定的“nameof”运算符的解决方法?

如何使C#Combobox数据源和数据绑定不同

如何使用绑定变量使整个 PL/SQL 代码块动态化?

函数计算支持 MySQL 实例绑定

代码未通过安全检查,如何使我的参数绑定更好?

如何初始化片段中的绑定属性以使双向数据绑定工作