如何使Databindingtypes安全并支持重构
当我希望将控件绑定到对象的属性时,必须将该属性的名称作为string提供。 这不是很好,因为:
- 如果该属性被删除或重命名,我没有得到一个编译器警告。
- 如果使用重构工具重命名属性,可能不会更新数据绑定。
- 如果属性的types是错误的,直到运行时才会出现错误,例如将整数绑定到dateselect器。
有没有一种devise模式可以解决这个问题,但仍然具有数据绑定的易用性?
(这是WinForm,Asp.net和WPF以及很多其他系统的问题)
我现在已经find了“ 在C#中使用nameof()运算符的解决方法:types安全的数据绑定 ”,这对解决scheme也有很好的起点。
如果你愿意在编译你的代码后使用后处理器, notifypropertyweaver是值得一看的。
任何人都知道一个很好的WPF解决scheme时,绑定是在XML而不是C#完成?
感谢Oliver让我开始我现在有一个解决scheme,支持重构和types安全。 它也让我实现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类显示了如何以types安全的方式实现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中使用了很多新的东西,并且展示了什么是可能的。 现在只要我们有卫生的macros, lisp程序员可能会停止给我们打电话给二等公民)
运算符的名称是在C#6.0中用.NET 4.6和VS2015在2015年7月实现的。以下对于C#仍然有效<6.0
为了避免包含属性名称的string,我写了一个简单的类,使用expression式树来返回成员的名称:
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中),它也是非常有用的(至less对我而言),但是你的代码仍然不是types安全的。 你可以使用第二个types参数来扩展Name方法,该参数定义函数的返回值,这将限制属性的types。
var name = Member.Name<MyClass>(x => x.MyProperty); // name == "MyProperty"
到目前为止,我还没有find任何解决数据绑定types安全问题的东西。
最好的祝福
Framework 4.5为我们提供了CallerMemberNameAttribute
,它使得将属性名称作为string传递是不必要的:
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兼容包 ,该包也提供了此属性。
这篇博客文章提出了关于这种方法的一些很好的问题 。 通过将expression式转换为string作为某种静态初始化的一部分,可以改善这些缺陷。
实际的机制可能有点难看,但它仍然是types安全的,并且与原始的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); } } }
如果绑定被破坏,获得反馈的一种方法是创build一个DataTemplate并将其DataType声明为它所绑定的ViewModel的types,例如,如果您有一个PersonView和一个PersonViewModel,您可以执行以下操作:
-
用DataType = PersonViewModel和一个键(例如PersonTemplate)声明一个DataTemplate
-
剪切所有PersonView xaml并将其粘贴到数据模板中(理想情况下可以位于PersonView的顶部。
3A。 创build一个ContentControl并设置ContentTemplate = PersonTemplate并将其内容绑定到PersonViewModel。
3B。 另一种select是不给DataTemplate的键,也不设置ContentControl的ContentTemplate。 在这种情况下,WPF将找出使用哪个DataTemplate,因为它知道你绑定到什么types的对象。 它会search树并find你的DataTemplate,因为它匹配的绑定types,它会自动将其作为ContentTemplate应用。
您最终得到的视图基本上与之前相同,但是由于您将DataTemplate映射到基础数据types,所以像Resharper这样的工具可以给您反馈(通过颜色标识符 – resharper-Options-Settings-Color Identifiers),以确定您的绑定是否被破坏或不。
你仍然不会得到编译器的警告,但可以直观地检查破坏的绑定,这比在视图和视图模型之间来回检查要好。
您给出的这个附加信息的另一个优点是,它也可以用于重命名重构。 据我记得Resharper能够自动重命名绑定的typesDataTemplates当基础ViewModel的属性名称被改变,反之亦然。
1.如果该属性被删除或重命名,我没有得到编译器警告。
2.如果使用重构工具重命名该属性,则数据绑定可能不会更新。
3.如果属性的types是错误的,例如绑定一个整数到一个dateselect器,我不会得到一个错误。
是的,伊恩,这正是名称string驱动的数据绑定的问题。 你问了一个devise模式。 我devise了Type-Safe View Model(TVM)模式,它是Model-View-ViewModel(MVVM)模式的View模型部分的一个结构。 它基于一个types安全的绑定,类似于你自己的答案。 我刚刚为WPF发布了一个解决scheme:
http://www.codeproject.com/Articles/450688/Enhanced-MVVM-Design-w-Type-Safe-View-Models-TVM
对于Windows 10和Windows Phone 10中的XAML(通用应用程序),x:bind(也称为“编译数据绑定”)可以解决此问题,请参阅https://channel9.msdn.com/Events/Build/2015/3-635
我找不到在线的文档,但没有付出太多的努力,因为这是我不会使用一段时间的东西。 但是,这个答案应该是一个有用的指针,其他人。