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


71

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

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

是否有一种设计模式可以解决此问题,但仍易于使用数据绑定?

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

现在,我发现“ C#中的nameof()运算符的变通办法:typesafe数据绑定”也为解决方案提供了良好的起点。

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


当绑定是用XML而不是C#完成时,有人知道WPF的一种好的解决方案吗?



引用链接的问题:现在可以在编译时解决此问题!nameof运算符已于2015年7月在带有C#6.0的.NET 4.6和VS2015中实现。以下答案对于C#<6.0仍然有效。迈克- (stackoverflow.com/users/772086/mike
的Mads Ravn的

@MadsRavn不能解决您所希望的那样,因为它不能从XAML使用,并且不提供类型安全性。但是,当从“代码”完成绑定时,它确实允许重构。
伊恩·林格罗斯

1
@IanRingrose很公平,直到我们具有编译时类型安全性/能够从XAML之类的标记中使用它的能力,问题才能解决。但是,我的主要观点是,在C#6.0及更高版本中,不应使用接受的答案(BindingHelper)中的解决方案,在该解决方案中,可以使用nameof运算符实现相同的解决方案。现在的答案反映了这一点,所以我很高兴:)
Mads Ravn

请参阅链接,了解如何在编译时已在XAML中检测到损坏的绑定stackoverflow.com/questions/43208011/…–
Rekshino

Answers:


53

请注意,此答案使用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(或查看此答案以了解实现INotifyPropertyChanged的另一种不错的方法,ActiveSharp-自动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中的许多新内容,并显示了可能的结果。现在,如果只有卫生宏,那么Lisp程序员可能会停止称我们为二等公民)


这是否要求为每种类型实现OnPropertyChanged方法?如果是这样,那会有些不错,但并不理想,通常,OnPropertyChanged方法是在基类中实现的,并从所有派生类中调用。
戴维

戴维,没有理由不能将OnPropertyChanged方法(和事件)仅移至基类并使其受到保护。(这就是我期望在现实生活中所做的事情)
Ian Ringrose,

但是从您的示例来看,似乎它依赖于类型为Expression <Func <Person,object >>的参数,不需要为每个类型实现该方法以采用类型Expression <Func <Foo的参数, object >>,Expression <Func <Bar,object >>等?
2009年

1
现在,我将OnPropertyChanged更改为OnPropertyChanged(Expression <Func <object >>属性),将其移动到基类中。
伊恩·林格罗斯


29

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

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

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中使用它),它还是很有帮助的(至少对我而言),但是您的代码仍然不是类型安全的。您可以使用第二个类型参数扩展Name方法,该参数定义函数的返回值,这将约束属性的类型。

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

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

最好的祝福


3
感谢您提供了一个很好的起点,我刚刚发布了一个答案,该答案将您的工作扩展到提供类型安全性。
伊恩·林格罗斯

27

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兼容性包,该也提供了此属性。


5

这篇博客文章对这种方法的性能提出了一些很好的问题。您可以通过将表达式转换为字符串作为某种静态初始化的一部分来改善这些缺点。

实际的机制可能有点难看,但是它仍然是类型安全的,并且与原始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);
        }
    }
}

很好,但是在大多数软件中,现实生活中并不是一个麻烦的问题,因此请首先尝试简单的方法。
伊恩·林罗斯

3

如果绑定被破坏,一种获取反馈的方法是创建一个DataTemplate并将其DataType声明为其绑定的ViewModel的类型,例如,如果您有PersonView和PersonViewModel,则可以执行以下操作:

  1. 声明一个DataTemplate,其DataType = PersonViewModel和一个键(例如PersonTemplate)

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

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

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

最后,您得到的视图基本上与以前相同,但是由于将DataTemplate映射到基础DataType,因此诸如Resharper之类的工具可以通过绑定标识符给您反馈(通过颜色标识符-Resharper-Options-Settings-Color标识符)或不。

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

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


3

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

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

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

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

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


很好,但似乎需要大量工作,而且从XAML绑定到Code Behind绑定的颠覆性转变,而所有MSFT要做的工作就是从XAML实际编译绑定。无论如何,它都被编译为BAML,因此那里没有太多借口。
Bruno Brant 2015年

1

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

我找不到它的在线文档,但是并没有投入太多精力,因为这是我一段时间不会使用的东西。但是,此答案应该是对其他人的有用指示。

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

绑定和x:Bind之间的区别


0

C#标记似乎正在解决同一组问题,因此,我将此答案添加为一个指针,以帮助当前一代的程序员。

Xamarin.Forms 4.6引入了C#标记,这是一组流畅的帮助程序和类,旨在使用C#进行UI开发感到高兴。

C#标记可帮助开发人员编写简洁的声明性UI标记,并将其与UI逻辑清晰地分开,所有这些都在C#中实现。编写标记时,开发人员可以享受C#的一流IDE支持。标记和逻辑的单一语言可减少摩擦,标记分散和认知负担;很少或不需要语言桥接机制,例如单独的转换器,样式,资源字典,行为,触发器和标记扩展

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.