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

时间:2009-08-25 15:44:02

标签: c# .net data-binding refactoring type-safety

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

  1. 如果财产被删除或 重命名,我没有得到编译器 警告。
  2. 如果重命名该属性 使用重构工具,它是 可能数据绑定不会 更新。
  3. 直到我才收到错误 运行时如果属性的类型 是错的,例如将整数绑定到 日期选择器。
  4. 是否有一个设计模式可以解决这个问题,但是仍然可以轻松使用数据绑定?

    (这是WinForm,Asp.net和WPF中的问题,很可能是很多其他系统)

    我现在发现“workarounds for nameof() operator in C#: typesafe databinding”也有一个很好的解决方案起点。

    如果您在编译代码后愿意使用后期处理器,notifypropertyweaver非常值得关注。


    当用XML而不是C#完成绑定时,任何人都知道WPF的良好解决方案吗?

8 个答案:

答案 0 :(得分:51)

感谢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程序员可能会停止称我们为二等公民)

答案 1 :(得分:27)

nameof运算符是在2015年7月使用.NET 4.6和VS2015在C#6.0中实现的。以下内容对C#&lt; 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"

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

最好的问候

答案 2 :(得分:24)

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兼容包,它也提供此属性。< / p>

答案 3 :(得分:5)

此博客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);
        }
    }
}

答案 4 :(得分:3)

得到的反馈,如果你的绑定被打破,一种方法是创建一个DataTemplate并声明它的数据类型是视图模型的,它结合到例如类型如果您有PersonView和PersonViewModel,您将执行以下操作:

  1. 使用DataType = PersonViewModel和密钥(例如PersonTemplate)声明DataTemplate

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

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

    3B。另一个选择是不给DataTemplate一个键,也不设置ContentControl的ContentTemplate。在这种情况下,WPF将确定要使用的DataTemplate,因为它知道您要绑定的对象类型。它将搜索树并找到您的DataTemplate,因为它匹配绑定的类型,它将自动将其应用为ContentTemplate。

    您使用与以前基本相同的观点结束,但因为你映射的DataTemplate到一个基本数据类型,如ReSharper的工具,可以给你的反馈(通过颜色标识 - ReSharper的 - 选项 - 设置 - 颜色标识),以还判定是否你绑定是否破坏。

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

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

答案 5 :(得分:3)

  

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

     

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

     

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

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

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

答案 6 :(得分:1)

对于Windows 10和Windows Phone 10中的XAML(通用应用),

x(绑定(也称为#34;编译数据绑定&#34;)可以解决此问题,请参阅https://channel9.msdn.com/Events/Build/2015/3-635

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

答案 7 :(得分:0)

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

Xamarin.Forms 4.6 introduced C# Markup,一组熟练的助手和 旨在使C#中的UI开发高兴的类。

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