Как сделать тип привязки данных безопасным и поддержать рефакторинг
Когда я хочу привязать элемент управления к свойству моего объекта, я должен предоставить имя свойства в виде строки. Это не очень хорошо, потому что:
- Если свойство удалено или переименовано, я не получаю предупреждение компилятора.
- Если переименовать свойство с помощью инструмента рефакторинга, вероятно, привязка данных не будет обновлена.
- Я не получаю ошибку до времени выполнения, если тип свойства неправильный, например, связывание целого числа с выбором даты.
Есть ли шаблон дизайна, который обходит это, но все еще имеет простоту использования привязки данных?
(Это проблема в WinForm, Asp.net и WPF и, скорее всего, во многих других системах)
Теперь я нашел " обходные пути для оператора nameof() в C#: типобезопасная привязка данных ", который также имеет хорошую отправную точку для решения.
Если вы готовы использовать постпроцессор после компиляции кода, стоит обратить внимание на notifypropertyweaver.
Кто-нибудь знает хорошее решение для WPF, когда привязки выполняются в XML, а не в C#?
7 ответов
Спасибо Оливеру за то, что я начал, и теперь у меня есть решение, которое поддерживает рефакторинг и безопасно для типов. Это также позволило мне реализовать 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 был реализован в C# 6.0 с.NET 4.6 и VS2015 в июле 2015 года. Следующее все еще действует для 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"
До сих пор я не нашел ничего, что решало бы проблему безопасности типов данных.
С уважением
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 ...
}
Если вы работаете на Framework 4.0 с установленным KB2468871, вы можете установить пакет совместимости Microsoft BCL через nuget, который также предоставляет этот атрибут.
Эта статья блога поднимает несколько хороших вопросов о производительности этого подхода. Вы можете исправить эти недостатки, преобразовав выражение в строку как часть статической инициализации.
Реальная механика может быть немного неприглядной, но она все еще будет безопасна от типов и будет примерно такой же производительности, что и необработанный 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);
}
}
}
1. Если свойство удалено или переименовано, я не получаю предупреждение компилятора.
2. При переименовании свойства с помощью инструмента рефакторинга, вероятно, привязка данных не будет обновлена.
3. Я не получаю ошибку до времени выполнения, если тип свойства неправильный, например, связывание целого числа с выбором даты.
Да, Ян, это именно проблемы с привязкой данных на основе именных строк. Вы просили дизайн-шаблон. Я разработал шаблон Type-Safe View Model (TVM), который представляет собой конкретную часть модели View модели Pattern-View-ViewModel (MVVM). Он основан на типобезопасной привязке, аналогичной вашему собственному ответу. Я только что опубликовал решение для WPF:
http://www.codeproject.com/Articles/450688/Enhanced-MVVM-Design-w-Type-Safe-View-Models-TVM
Один из способов получить обратную связь, если ваши привязки нарушены, - это создать DataTemplate и объявить его DataType типом ViewModel, к которому он привязывается, например, если у вас есть PersonView и PersonViewModel, вы должны сделать следующее:
Объявите DataTemplate с DataType = PersonViewModel и ключом (например, PersonTemplate)
Вырежьте все xaml PersonView и вставьте его в шаблон данных (который в идеале может находиться в верхней части PersonView.
3a. Создайте ContentControl и установите ContentTemplate = PersonTemplate и привяжите его содержимое к PersonViewModel.
3b. Другой вариант - не давать ключ к DataTemplate и не устанавливать ContentTemplate для ContentControl. В этом случае WPF выяснит, какой DataTemplate использовать, поскольку он знает, к какому типу объекта вы привязываетесь. Он выполнит поиск по дереву и найдет ваш DataTemplate, и, поскольку он соответствует типу привязки, он автоматически применит его как ContentTemplate.
В конечном итоге вы получаете практически то же представление, что и раньше, но, поскольку вы сопоставили DataTemplate с базовым DataType, такие инструменты, как Resharper, могут дать вам обратную связь (через идентификаторы цвета - Resharper-Options-Settings-Color Identifiers), чтобы избежать разрыва ваших привязок. или нет.
Вы по-прежнему не будете получать предупреждения компилятора, но можете визуально проверять наличие нарушенных привязок, что лучше, чем проверять информацию между вашим представлением и моделью представления.
Еще одно преимущество этой дополнительной информации, которую вы предоставляете, заключается в том, что она также может использоваться при переименовании рефакторингов. Насколько я помню, Resharper может автоматически переименовывать привязки для типизированных шаблонов данных при изменении имени основного ViewModel и наоборот.
x:bind (также называемый "привязкой скомпилированных данных") для XAML (универсальное приложение) в Windows 10 и Windows Phone 10 может решить эту проблему, см. https://channel9.msdn.com/Events/Build/2015/3-635
Я не могу найти онлайн документы для этого, но не приложил много усилий, потому что это то, что я не буду использовать в течение некоторого времени. Однако этот ответ должен быть полезным указателем на других людей.
Разметка C#, похоже, решает тот же набор проблем, поэтому я добавляю этот ответ в качестве указателя, чтобы помочь нынешнему поколению программистов.
В Xamarin.Forms 4.6 представлена разметка C#, набор быстрых помощников и классов, которые призваны сделать разработку пользовательского интерфейса на C# радостью.
Разметка C# помогает разработчикам писать краткую декларативную разметку пользовательского интерфейса и четко отделить ее от логики пользовательского интерфейса - все на C#. При написании разметки разработчики получают удовольствие от первоклассной поддержки IDE C#. Единый язык разметки и логики снижает трение, разброс разметки и когнитивную нагрузку; меньше или совсем нет необходимости в механизмах языкового моста, таких как отдельные конвертеры, стили, словари ресурсов, поведения, триггеры и расширения разметки