如何使数据绑定类型安全并支持重构
-
19-09-2019 - |
题
当我希望将控件绑定到对象的属性时,我必须以字符串形式提供属性名称。这不太好,因为:
- 如果属性被删除或 重命名,我没有编译器 警告。
- 如果重命名属性 使用重构工具,它是 数据绑定可能不会 更新。
- 直到 运行时,如果属性的类型 是错误的,例如将整数绑定到 日期选择器。
是否有一种设计模式可以解决这个问题,但仍然易于使用数据绑定?
(这是 WinForm、Asp.net 和 WPF 以及很可能许多其他系统中的问题)
我现在已经找到了“C# 中 nameof() 运算符的解决方法:类型安全数据绑定“这也为解决方案提供了一个良好的起点。
如果您愿意在编译代码后使用后处理器, 通知propertyweaver 非常值得一看。
当绑定通过 XML 而不是 C# 完成时,任何人都知道 WPF 的良好解决方案吗?
解决方案
感谢 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 程序员可能不再称我们为二等公民)
其他提示
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 中),但它非常有帮助(至少对我来说),但您的代码仍然不是类型安全的。您可以使用第二个类型参数来扩展方法 Name,该参数定义函数的返回值,这将限制属性的类型。
var name = Member.Name<MyClass>(x => x.MyProperty); // name == "MyProperty"
到目前为止,我还没有找到任何可以解决数据绑定类型安全问题的方法。
此致
框架 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 ...
}
如果您正在使用 Framework 4.0 KB2468871 已安装,您可以安装 微软 BCL 兼容包 通过 努盖特, ,它也提供了这个属性。
这个博客 文章对这种方法的性能提出了一些很好的问题. 。您可以通过将表达式转换为字符串作为某种静态初始化的一部分来改进这些缺点。
实际的机制可能有点难看,但它仍然是类型安全的,并且性能与原始 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);
}
}
}
如果绑定被破坏,获得反馈的一种方法是创建一个 DataTemplate 并将其 DataType 声明为它绑定到的 ViewModel 的类型,例如如果您有 PersonView 和 PersonViewModel 您将执行以下操作:
声明一个 DataTemplate,其中 DataType = PersonViewModel 和一个键(例如人员模板)
剪切所有 PersonView xaml 并将其粘贴到数据模板中(理想情况下可以位于 PersonView 的顶部)。
3a.创建一个 ContentControl 并设置 ContentTemplate = PersonTemplate 并将其内容绑定到 PersonViewModel。
3b.另一种选择是不向 DataTemplate 提供密钥,也不设置 ContentControl 的 ContentTemplate。在这种情况下,WPF 将确定要使用什么 DataTemplate,因为它知道您要绑定到什么类型的对象。它将搜索树并找到您的 DataTemplate,并且由于它与绑定的类型匹配,因此它会自动将其应用为 ContentTemplate。
您最终得到的视图与以前基本相同,但由于您将 DataTemplate 映射到基础 DataType,Resharper 等工具可以为您提供反馈(通过颜色标识符 - Resharper-Options-Settings-Color Identifiers),了解您的绑定是否已损坏或不。
您仍然不会收到编译器警告,但可以直观地检查损坏的绑定,这比必须在视图和视图模型之间来回检查要好。
您提供的附加信息的另一个优点是,它也可以用于重命名重构。据我记得,当底层 ViewModel 的属性名称更改时,Resharper 能够自动重命名类型化 DataTemplate 上的绑定,反之亦然。
1.如果该属性被删除或重命名,我不会收到编译器警告。
2.如果使用重构工具重命名属性,则数据绑定可能不会更新。
3.如果属性的类型错误,例如,直到运行时我才会收到错误。将整数绑定到日期选择器。
是的,Ian,这正是名称字符串驱动的数据绑定的问题。您要求设计模式。我设计了类型安全视图模型 (TVM) 模式,它是模型-视图-视图模型 (MVVM) 模式的视图模型部分的具体化。它基于类型安全的绑定,类似于您自己的答案。我刚刚发布了 WPF 的解决方案:
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
我找不到它的在线文档,但没有付出太多努力,因为我一段时间不会使用它。然而,这个答案应该对其他人有用。