71

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

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

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

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

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

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


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

4

8 回答 8

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 - 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 中的许多新内容,并展示了可能的情况。现在,如果我们有卫生的宏, lisp 程序员可能会停止称我们为二等公民)

于 2009-08-26T10:55:34.743 回答
29

nameof运算符于 2015 年 7 月在 C# 6.0 和 .NET 4.6 和 VS2015 中实现。以下对于 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 中),它还是很有帮助的(至少对我而言),但您的代码仍然不是类型安全的。您可以使用第二个类型参数扩展方法名称,该参数定义函数的返回值,这将限制属性的类型。

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

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

于 2009-08-25T16:07:15.627 回答
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 兼容包,它也提供此属性。

于 2012-09-13T14:29:31.293 回答
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);
        }
    }
}
于 2010-04-24T22:29:37.593 回答
3

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

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

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

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

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

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

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

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

于 2011-02-12T20:41:05.010 回答
3

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

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

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

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

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

于 2012-09-04T07:51:38.280 回答
1

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

Binding 和 x:Bind 之间的区别

于 2015-07-31T17:40:55.333 回答
0

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

Xamarin.Forms 4.6 引入了 C# Markup,这是一组流畅的帮助程序和类,旨在使 C# 中的 UI 开发成为一种乐趣。

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

于 2020-07-16T11:12:33.617 回答