Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
public string this[string columnName]
{
get
{
switch (columnName)
{
case nameof(Id):
break;
case nameof(Make):
return Make == "ModelT"
? "Too Old"
: CheckMakeAndColor();
case nameof(Color):
return CheckMakeAndColor();
case nameof(PetName):
break;
}
return string.Empty;
}
}
internal string CheckMakeAndColor()
{
if (Make == "Chevy" && Color == "Pink")
{
return $"{Make}'s don't come in {Color}";
}
return string.Empty;
}
Запустите приложение, выберите автомобиль Red Rider (Ford) и измените значение в поле Make (Производитель) на ModelT. После того, как фокус покинет поле, появится декоратор ошибки красного цвета. Выберите в поле со списком автомобиль Kit (Chevy) и щелкните на кнопке Change Color, чтобы изменить его цвет на Pink. Вокруг поля Color незамедлительно появится декоратор ошибки красного цвета, но возле поля Make он будет отсутствовать. Измените значение в поле Make на Ford и переместите фокус из этого поля; декоратор ошибки красного цвета не появляется!
Причина в том, что индексатор выполняется, только когда для свойства сгенерировано событие PropertyChanged. Как обсуждалось в разделе "Система уведомлений привязки WPF" ранее в главе, событие PropertyChanged инициируется при изменении исходного значения свойства объекта, что происходит либо через код (вроде обработчика события Click для кнопки Change Color), либо через взаимодействие с пользователем (синхронизируется с помощью UpdateSourceTrigger). При изменении цвета свойство Make не изменяется, а потому событие PropertyChanged для него не генерируется. Поскольку событие не генерируется, индексатор не вызывается и проверка достоверности для свойства Make не выполняется.
Решить проблему можно двумя путями. Первый предусматривает изменение объекта PropertyChangedEventArgs, которое обеспечит обновление всех привязанных свойств, за счет передачи его конструктору значения string.Empty вместо имени поля. Как упоминалось ранее, это заставит механизм привязки обновить каждое свойство в данном экземпляре. Добавьте метод OnPropertyChanged() со следующим кодом:
protected virtual void OnPropertyChanged([CallerMemberName]
string propertyName = "")
{
if (propertyName != nameof(IsChanged))
{
IsChanged = true;
}
//PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(string.Empty));
}
Теперь при выполнении того же самого теста текстовые поля Make и Color декорируются с помощью шаблона отображения ошибки, когда одно из них обновляется. Так почему бы ни генерировать событие всегда в такой манере? В значительной степени причиной является производительность. Вполне возможно, что обновление каждого свойства объекта приведет к снижению производительности. Разумеется, без тестирования об этом утверждать нельзя, и конкретные ситуации могут (и вероятно будут) варьироваться.
Другое решение предполагает генерацию события PropertyChanged для зависимого поля (полей), когда одно из полей изменяется. Недостаток такого приема в том, что вы (или другие разработчики, сопровождающие ваше приложение) должны знать о взаимосвязи между свойствами Make и Color через код проверки достоверности.
Интерфейс INotifyDataErrorInfo
Интерфейс INotifyDataErrorInfo, появившийся в версии .NET 4.5, построен на основе интерфейса IDataErrorInfo и предлагает дополнительные возможности для проверки достоверности. Конечно, возросшая мощь сопровождается дополнительной работой! По разительному контрасту с предшествующими приемами проверки достоверности, которые вы видели до сих пор, свойство привязки ValidatesOnNotifyDataErrors имеет стандартное значение true, поэтому добавлять его к операторам привязки не обязательно.
Интерфейс INotifyDataErrorInfo чрезвычайно мал, но, как вскоре будет показано, для обеспечения своей эффективности требует написания порядочного объема связующего кода. Ниже приведено определение интерфейса INotifyDataErrorInfo:
public interface INotifyDataErrorInfo
{
bool HasErrors { get; }
event EventHandler<DataErrorsChangedEventArgs>
ErrorsChanged;
IEnumerable GetErrors(string propertyName);
}
Свойство HasErrors используется механизмом привязки для выяснения, есть ли какие-нибудь ошибки в любых свойствах экземпляра. Если метод GetErrors() вызывается со значением null или пустой строкой в параметре propertyName, то он возвращает все ошибки, существующие в экземпляре. Если методу передан параметр propertyName, тогда возвращаются только ошибки, относящиеся к конкретному свойству. Событие ErrorsChanged (подобно событиям PropertyChanged и CollectionChanged) уведомляет механизм привязки о необходимости обновления пользовательского интерфейса для текущего списка ошибок.
Реализация поддерживающего кода
При реализации INotifyDataErrorInfo большая часть кода обычно помещается в базовый класс модели, поэтому она пишется только один раз. Начните с замены IDataErrorInfo интерфейсом INotifyDataErrorInfo в файле класса CarPartial.cs (код для IDataErrorInfo в классе можете оставить; вы обновите его позже).
public partial class Car: INotifyDataErrorInfo, IDataErrorInfo
{
...
public IEnumerable GetErrors(string propertyName)
{
throw new NotImplementedException();
}
public bool HasErrors { get; }
public event
EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
}
Добавьте закрытое поле типа Dictionary<string,List<string>>, которое будет хранить сведения о любых ошибках, сгруппированные по именам свойств. Понадобится также добавить оператор using для пространства имен System.Collections.Generic. Вот как выглядит код:
using System.Collections.Generic;
private readonly Dictionary<string,List<string>> _errors
= new Dictionary<string, List<string>>();
Свойство HasErrors должно возвращать true, если в словаре присутствуют любые ошибки, что легко достигается следующим образом:
public bool HasErrors => _errors.Any();
Создайте вспомогательный метод для инициирования события ErrorsChanged (подобно инициированию события PropertyChanged):
private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this,
new DataErrorsChangedEventArgs(propertyName));
}
Как упоминалось ранее, метод GetErrors() должен возвращать