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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1329138/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 15:11:52  来源:igfitidea点击:

How to make Databinding type safe and support refactoring?

c#.netdata-bindingrefactoringtype-safety

提问by Ian Ringrose

When I wish to bind a control to a property of my object, I have to provide the name of the property as a string. This is not very good because:

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

  1. If the property is removed or renamed, then I don't get a compiler warning.
  2. If a rename the property with a refactoring tool, then it is likely the data binding will not be updated.
  3. If the type of the property is wrong, e.g. binding an integer to a date chooser, then I don't get an error until runtime.
  1. 如果该属性被删除或重命名,那么我不会收到编译器警告。
  2. 如果使用重构工具重命名属性,则数据绑定可能不会更新。
  3. 如果属性的类型错误,例如将整数绑定到日期选择器,那么直到运行时我才会收到错误消息。

Is there a design-pattern that gets round this, but still has the ease of use of data-binding?

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

(This is a problem in WinForms, ASP.NET, and WPF and possibly other systems.)

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

I have now found "workarounds for nameof() operator in C#: typesafe databinding" that also has a good starting point for a solution.

我现在找到了“ C# 中 nameof() 运算符的解决方法:类型安全数据绑定”,它也有一个很好的解决方案起点。

If you are willing to use a post processor after compiling your code, then NotifyPropertyWeaveris worth looking at.

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



Does anyone know of a good solution for WPF when the bindings are done in XML rather than C#?

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

采纳答案by Ian Ringrose

Thanks to Oliver for getting me started I now have a solution that both supports refactoring and is type safe. It also let me implement INotifyPropertyChanged so it copes with properties being renamed.

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

It's usage looks like:

它的用法如下:

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);

The person class shows how to implemented INotifyPropertyChanged in a type safe way (or see this answerfor a other rather nice way of implementing INotifyPropertyChanged, ActiveSharp - Automatic INotifyPropertyChangedalso looks good ):

person 类显示了如何以类型安全的方式实现 INotifyPropertyChanged(或查看此答案以了解其他相当不错的实现 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;
}

The WinForms binding helper class has the meat in it that makes it all work:

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);
        }
    }
}

This makes use of a lot of the new stuff in C# 3.5 and shows just what is possible. Now if only we had hygienic macroslisp programmer may stop calling us second class citizens)

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

回答by Oliver Hanappi

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

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

To avoid strings which contain property names, I've written a simple class using expression trees to return the name of the member:

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

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);
    }
}

You can use this class as follows. Even though you can use it only in code (so not in XAML), it is quite helpful (at least for me), but your code is still not typesafe. You could extend the method Name with a second type argument which defines the return value of the function, which would constrain the type of the property.

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

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

Until now I haven't found anything which solves the databinding typesafety issue.

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

Best Regards

此致

回答by nedruod

This blog article raises some good questions about the performance of this approach. You could improve upon those shortcomings by converting the expression to a string as part of some kind of static initialization.

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

The actual mechanics might be a little unsightly, but it would still be type-safe, and approximately equal performance to the raw INotifyPropertyChanged.

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

Something kind of like this:

有点像这样:

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);
        }
    }
}

回答by Thorsten Lorenz

One way to get feedback if your bindings are broken, is to create a DataTemplate and declare its DataType to be the type of the ViewModel that it binds to e.g. if you have a PersonView and a PersonViewModel you would do the following:

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

  1. Declare a DataTemplate with DataType = PersonViewModel and a key (e.g. PersonTemplate)

  2. Cut all PersonView xaml and paste it into the data template (which ideally can just be at the top of the PersonView.

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

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

3a. Create a ContentControl and set the ContentTemplate = PersonTemplate and bind its Content to the PersonViewModel.

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

3b. Another option is to not give a key to the DataTemplate and not set the ContentTemplate of the ContentControl. In this case WPF will figure out what DataTemplate to use, since it knows what type of object you are binding to. It will search up the tree and find your DataTemplate and since it matches the type of the binding, it will automatically apply it as the ContentTemplate.

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

You end up with essentially the same view as before, but since you mapped the DataTemplate to an underlying DataType, tools like Resharper can give you feedback (via Color identifiers - Resharper-Options-Settings-Color Identifiers) as to wether your bindings are broken or not.

您最终会得到与以前基本相同的视图,但是由于您将 DataTemplate 映射到基础数据类型,因此 Resharper 之类的工具可以为您提供反馈(通过颜色标识符 - Resharper-Options-Settings-Color Identifiers)关于您的绑定是否被破坏或不。

You still won't get compiler warnings, but can visually check for broken bindings, which is better than having to check back and forth between your view and viewmodel.

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

Another advantage of this additional information you give, is, that it can also be used in renaming refactorings. As far as I remember Resharper is able to automatically rename bindings on typed DataTemplates when the underlying ViewModel's property name is changed and vice versa.

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

回答by Harry von Borstel

1.If the property is removed or renamed, I don't get a compiler warning.

2.If a rename the property with a refactoring tool, it is likely the data binding will not be updated.

3.I don't get an error until runtime if the type of the property is wrong, e.g. binding an integer to a date chooser.

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

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

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

Yes, Ian, that are exactly the problems with name-string driven data binding. You asked for a design-pattern. I designed the Type-Safe View Model (TVM) pattern that is a concretion of the View Model part of the Model-View-ViewModel (MVVM) pattern. It is based on a type-safe binding, similar to your own answer. I've just posted a solution for WPF:

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

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

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

回答by takrl

The Framework 4.5 provides us with the CallerMemberNameAttribute, which makes passing the property name as a string unnecessary:

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 ...
}

If you're working on Framework 4.0 with KB2468871installed, you can install the Microsoft BCL Compatibility Packvia nuget, which also provides this attribute.

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

回答by Ian Ringrose

x:bind (also called "compiled data bindings") for XAML (universal app) in windows 10 and windows phone 10 may solve this problem, see https://channel9.msdn.com/Events/Build/2015/3-635

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

I can't find the on line docs for it, but have not put much effort in, as it is something I will not be using for some time. However this answer should be a useful pointer to other people.

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

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

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

Difference between Binding and x:Bind

绑定和 x:Bind 的区别