Правильная проверка с MVVM
Предупреждение: очень длинный и подробный пост.
Хорошо, проверка в WPF при использовании MVVM. Я прочитал много вещей сейчас, посмотрел на многие вопросы SO и перепробовал много подходов, но в какой-то момент все выглядит несколько странно, и я действительно не уверен, как сделать это правильно ™.
В идеале я хочу, чтобы вся проверка происходила в модели представления с использованием IDataErrorInfo
; вот что я сделал. Однако существуют различные аспекты, которые делают это решение не полным решением для всей темы проверки.
Ситуация
Давайте примем следующую простую форму. Как видите, ничего особенного. У нас просто есть два текстовых поля, которые связаны с string
а также int
Свойство в представлении модели каждого. Кроме того, у нас есть кнопка, которая связана с ICommand
,
Итак, для проверки у нас теперь есть два варианта:
- Мы можем запустить проверку автоматически каждый раз, когда изменяется значение текстового поля. Таким образом, пользователь получает мгновенный ответ, когда вводит что-то недействительное.
- Мы можем сделать еще один шаг, чтобы отключить кнопку при возникновении ошибок.
- Или мы можем запустить проверку только в явном виде, когда нажата кнопка, а затем показаны все ошибки, если это применимо. Очевидно, мы не можем отключить кнопку на ошибки здесь.
В идеале я хочу реализовать вариант 1. Для обычных привязок данных с активированным ValidatesOnDataErrors
это поведение по умолчанию. Поэтому, когда текст изменяется, привязка обновляет источник и запускает IDataErrorInfo
проверка для этого свойства; об ошибках сообщается в обратном виде. Все идет нормально.
Статус проверки в модели представления
Интересным моментом является информирование модели представления или кнопки в этом случае о наличии ошибок. Путь IDataErrorInfo
работает, это в основном там, чтобы сообщить об ошибках обратно в представление. Таким образом, представление может легко увидеть, есть ли какие-либо ошибки, отобразить их и даже показать аннотации, используя Validation.Errors
, Кроме того, проверка всегда происходит, глядя на одно свойство.
Таким образом, иметь представление о модели представления, когда есть какие-либо ошибки или если проверка прошла успешно, сложно. Общее решение состоит в том, чтобы просто вызвать IDataErrorInfo
проверка всех свойств в самой модели представления. Это часто делается с помощью отдельного IsValid
имущество. Преимущество состоит в том, что это также может быть легко использовано для отключения команды. Недостатком является то, что это может запускать проверку всех свойств слишком часто, но большинство проверок должно быть достаточно просто, чтобы не ухудшить производительность. Другим решением было бы вспомнить, какие свойства вызывали ошибки при использовании проверки, и проверять только их, но в большинстве случаев это кажется слишком сложным и ненужным.
Суть в том, что это может работать нормально. IDataErrorInfo
обеспечивает проверку всех свойств, и мы можем просто использовать этот интерфейс в самой модели представления, чтобы выполнить проверку там же для всего объекта. Представляя проблему:
Обязательные исключения
Модель представления использует фактические типы для своих свойств. Так что в нашем примере целочисленное свойство является фактическим int
, Однако текстовое поле, используемое в представлении, поддерживает только текст. Так что при привязке к int
в модели представления механизм привязки данных автоматически выполнит преобразования типов или, по крайней мере, попытается. Если вы можете ввести текст в текстовое поле, предназначенное для чисел, высока вероятность того, что внутри не всегда будут действительные числа: поэтому механизм привязки данных не сможет преобразовать и выбросить FormatException
,
Со стороны взгляда мы можем это легко увидеть. Исключения из механизма привязки автоматически перехватываются WPF и отображаются как ошибки - даже нет необходимости включать Binding.ValidatesOnExceptions
что будет необходимо для исключений, генерируемых в сеттере. Сообщения об ошибках имеют общий текст, поэтому это может быть проблемой. Я решил это для себя, используя Binding.UpdateSourceExceptionFilter
обработчик, проверяющий генерируемое исключение и просматривающий исходное свойство, а затем генерирующий менее общее сообщение об ошибке. Все это скрыто в моем собственном расширении разметки Binding, поэтому у меня могут быть все необходимые значения по умолчанию.
Так что мнение в порядке. Пользователь делает ошибку, видит какую-то ошибку и может исправить ее. Модель представления однако потеряна. Поскольку механизм привязки выдал исключение, источник никогда не обновлялся. Таким образом, модель представления все еще имеет старое значение, а не то, что отображается пользователю, а IDataErrorInfo
проверка, очевидно, не применяется.
Что еще хуже, у модели представления нет хорошего способа узнать это. По крайней мере, я еще не нашел хорошего решения для этого. То, что было бы возможно, это вернуть отчет о представлении в модель представления о том, что произошла ошибка. Это может быть сделано путем привязки данных Validation.HasError
свойство возвращается к модели представления (что невозможно напрямую), поэтому модель представления может сначала проверить состояние представления.
Другой вариант - передать исключение, обработанное в Binding.UpdateSourceExceptionFilter
на модель представления, поэтому она также будет уведомлена об этом. Модель представления может даже предоставить некоторый интерфейс для привязки, чтобы сообщать об этих вещах, допуская настраиваемые сообщения об ошибках вместо общих для каждого типа сообщений. Но это создаст более сильную связь между представлением и моделью представления, чего я обычно хочу избегать.
Другим "решением" было бы избавиться от всех типизированных свойств, используя обычный string
Свойства и сделать преобразование в модели представления вместо. Это, очевидно, переместило бы всю проверку в модель представления, но также означало бы невероятное количество дублирований вещей, о которых обычно заботится механизм привязки данных. Кроме того, это изменило бы семантику модели представления. Для меня представление построено для модели представления, а не наоборот - конечно, дизайн модели представления зависит от того, что мы представляем для представления, но все же есть общая свобода, как это делает представление. Таким образом, модель представления определяет int
свойство, потому что есть номер; Теперь представление может использовать текстовое поле (разрешающее все эти проблемы) или использовать то, что изначально работает с числами. Так что нет, изменение типов свойств на string
это не вариант для меня.
В конце концов, это проблема зрения. Представление (и его механизм привязки данных) отвечает за предоставление корректных значений модели представления для работы. Но в этом случае, похоже, нет хорошего способа сказать модели представления, что она должна сделать недействительным старое значение свойства.
BindingGroups
Связывающие группы - один из способов, которым я пытался заняться этим. Связующие группы имеют возможность группировать все проверки, включая IDataErrorInfo
и бросили исключения. Если они доступны для модели представления, у них даже есть возможность проверить состояние проверки для всех этих источников проверки, например, используя CommitEdit
,
По умолчанию группы связывания реализуют вариант 2 сверху. Они явно обновляют привязки, по существу добавляя дополнительное незафиксированное состояние. Таким образом, при нажатии кнопки команда может зафиксировать эти изменения, вызвать обновления источника и все проверки и получить единый результат в случае успеха. Таким образом, действие команды может быть таким:
if (bindingGroup.CommitEdit())
SaveEverything();
CommitEdit
вернет true, только если все проверки пройдены успешно. Это займет IDataErrorInfo
а также проверить обязательные исключения. Похоже, это идеальное решение для выбора 2. Единственное, что доставляет немало хлопот - это управление группой привязок с помощью привязок, но я сам создал нечто, что в основном заботится об этом ( связанно).
Если для привязки присутствует группа привязки, привязка по умолчанию будет явной UpdateSourceTrigger
, Чтобы реализовать вариант 1 сверху с использованием групп привязок, нам в основном нужно изменить триггер. В любом случае, у меня есть собственное расширение привязки, это довольно просто, я просто установил его LostFocus
для всех.
Так что теперь привязки будут обновляться при изменении текстового поля. Если источник может быть обновлен (механизм связывания не выдает исключений), тогда IDataErrorInfo
будет работать как обычно. Если это не могло быть обновлено, представление все еще может видеть это. И если мы нажмем нашу кнопку, основная команда может вызвать CommitEdit
(хотя ничего не нужно фиксировать) и получите общий результат проверки, чтобы увидеть, можно ли продолжить.
Возможно, мы не сможем легко отключить кнопку таким способом. По крайней мере, не с точки зрения модели. Проверять валидацию снова и снова - не очень хорошая идея, просто обновлять статус команды, и модель представления не уведомляется, когда в любом случае выдается исключение механизма привязки (которое должно отключить кнопку) - или когда оно уходит в снова включите кнопку. Мы все еще можем добавить триггер, чтобы отключить кнопку в представлении, используя Validation.HasError
так что это не невозможно.
Решение?
В общем, это, кажется, идеальное решение. В чем моя проблема, хотя? Если честно, я не совсем уверен. Связывающие группы - сложная вещь, которая, как представляется, обычно используется в небольших группах, возможно, с несколькими группами связывания в одном представлении. Используя одну большую группу связывания для всего представления просто для того, чтобы обеспечить мою проверку, мне кажется, что я злоупотребляю ею. И я просто продолжаю думать, что должен быть лучший способ решить всю эту ситуацию, потому что, конечно, я не могу быть единственным, у кого есть эти проблемы. И до сих пор я действительно не видел, чтобы многие люди использовали группы связывания для проверки с MVVM, так что это просто странно.
Итак, что именно является правильным способом проверки в WPF с MVVM, в то же время проверяя наличие исключений механизма связывания?
Мое решение (/ взломать)
Прежде всего, спасибо за ваш вклад! Как я уже писал выше, я использую IDataErrorInfo
я уже проверил мои данные, и я лично считаю, что это самая удобная утилита для выполнения работы по валидации. Я использую утилиты, аналогичные тем, что Шеридан предложил в своем ответе ниже, так что обслуживание тоже работает нормально.
В конце концов, моя проблема сводилась к проблеме обязательных исключений, когда модель представления просто не знала, когда это произошло. Хотя я мог справиться с этим с помощью обязательных групп, как описано выше, я все же решил не делать этого, поскольку мне просто было не очень комфортно с этим. Так что я сделал вместо этого?
Как я упоминал выше, я обнаруживаю исключения привязки на стороне просмотра, прослушивая привязку UpdateSourceExceptionFilter
, Там я могу получить ссылку на модель представления из выражения привязки DataItem
, Тогда у меня есть интерфейс IReceivesBindingErrorInformation
который регистрирует модель представления в качестве возможного получателя информации об ошибках привязки. Затем я использую это для передачи пути привязки и исключения в модель представления:
object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception)
{
BindingExpression expr = (bindExpression as BindingExpression);
if (expr.DataItem is IReceivesBindingErrorInformation)
{
((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception);
}
// check for FormatException and produce a nicer error
// ...
}
В модели представления я вспоминаю всякий раз, когда мне сообщают о выражении привязки пути:
HashSet<string> bindingErrors = new HashSet<string>();
void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception)
{
bindingErrors.Add(path);
}
И всякий раз, когда IDataErrorInfo
повторно проверяет свойство, я знаю, что привязка сработала, и я могу очистить свойство из хэш-набора.
В модели представления я затем могу проверить, содержит ли набор хэшей какие-либо элементы, и прервать любое действие, требующее полной проверки данных. Это может быть не самым лучшим решением из-за связи между представлением и моделью представления, но использование этого интерфейса, по крайней мере, несколько меньше проблем.
4 ответа
Предупреждение: длинный ответ также
Я использую IDataErrorInfo
интерфейс для проверки, но я настроил его под свои нужды. Я думаю, вы обнаружите, что это решает и некоторые ваши проблемы. Одно из отличий вашего вопроса состоит в том, что я реализую его в своем базовом классе типов данных.
Как вы указали, этот интерфейс работает только с одним свойством за раз, но ясно, что в наше время это не хорошо. Поэтому я просто добавил свойство коллекции для использования вместо:
protected ObservableCollection<string> errors = new ObservableCollection<string>();
public virtual ObservableCollection<string> Errors
{
get { return errors; }
}
Чтобы решить вашу проблему невозможности отображения внешних ошибок (в вашем случае из представления, но в моем из модели представления), я просто добавил другое свойство коллекции:
protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();
public ObservableCollection<string> ExternalErrors
{
get { return externalErrors; }
}
у меня есть HasError
свойство, которое смотрит на мою коллекцию:
public virtual bool HasError
{
get { return Errors != null && Errors.Count > 0; }
}
Это позволяет мне связать это с Grid.Visibility
используя обычай BoolToVisibilityConverter
например показать Grid
с элементом управления коллекцией внутри, который показывает ошибки, когда они есть. Это также позволяет мне изменить Brush
в Red
выделить ошибку (используя другую Converter
), но я думаю, вы поняли.
Затем в каждом типе данных или классе модели я переопределяю Errors
собственность и реализовать Item
индексатор (упрощенно в этом примере):
public override ObservableCollection<string> Errors
{
get
{
errors = new ObservableCollection<string>();
errors.AddUniqueIfNotEmpty(this["Name"]);
errors.AddUniqueIfNotEmpty(this["EmailAddresses"]);
errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]);
errors.AddRange(ExternalErrors);
return errors;
}
}
public override string this[string propertyName]
{
get
{
string error = string.Empty;
if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field.";
else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field.";
else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field.";
return error;
}
}
AddUniqueIfNotEmpty
метод является обычаем extension
метод и "делает то, что говорит на жестяной банке". Обратите внимание, как он будет вызывать каждое свойство, которое я хочу проверить по очереди, и компилировать коллекцию из них, игнорируя повторяющиеся ошибки.
С использованием ExternalErrors
коллекция, я могу проверить вещи, которые я не могу проверить в классе данных:
private void ValidateUniqueName(Genre genre)
{
string errorMessage = "The genre name must be unique";
if (!IsGenreNameUnique(genre))
{
if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage);
}
else genre.ExternalErrors.Remove(errorMessage);
}
Чтобы ответить на ваш вопрос относительно ситуации, когда пользователь вводит алфавитный символ в int
поле, я склонен использовать обычай IsNumeric AttachedProperty
для TextBox
например Я не позволяю им делать подобные ошибки. Я всегда чувствую, что лучше остановить это, чем позволить этому случиться, а затем исправить это.
В целом, я действительно доволен моими способностями проверки в WPF, и мне совсем не хочется.
Чтобы закончить и для полноты, я чувствовал, что должен предупредить вас о том, что сейчас INotifyDataErrorInfo
интерфейс, который включает в себя некоторые из этих дополнительных функций. Вы можете узнать больше из INotifyDataErrorInfo
Страница интерфейса на MSDN.
ОБНОВЛЕНИЕ >>>
Да, ExternalErrors
свойство, просто давайте я добавлю ошибки, которые относятся к объекту данных извне этого объекта... извините, мой пример не был завершен... если бы я показал вам IsGenreNameUnique
метод, вы бы видели, что он использует LinQ
на всех Genre
Элементы данных в коллекции, чтобы определить, является ли имя объекта уникальным или нет:
private bool IsGenreNameUnique(Genre genre)
{
return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1;
}
Что касается вашего int
/string
проблема, единственный способ увидеть, как вы получаете эти ошибки в вашем классе данных, это если вы объявите все свои свойства как object
, но тогда у вас будет очень много кастинга. Возможно, вы могли бы удвоить свои свойства, как это:
public object FooObject { get; set; } // Implement INotifyPropertyChanged
public int Foo
{
get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; }
}
Тогда если Foo
был использован в коде и FooObject
был использован в Binding
Вы могли бы сделать это:
public override string this[string propertyName]
{
get
{
string error = string.Empty;
if (propertyName == "FooObject" && FooObject.GetType() != typeof(int))
error = "Please enter a whole number for the Foo field.";
...
return error;
}
}
Таким образом, вы сможете выполнить свои требования, но у вас будет много дополнительного кода для добавления.
На мой взгляд, проблема заключается в том, что проверка происходит в слишком многих местах. Я также хотел написать все мои проверки логин в ViewModel
но все эти числовые привязки делали мой ViewModel
псих.
Я решил эту проблему, создав привязку, которая никогда не выходит из строя. Очевидно, что если привязка всегда успешна, то сам тип должен корректно обрабатывать условия ошибки.
Тип недопустимого значения
Я начал с создания универсального типа, который бы изящно поддерживал неудачные преобразования:
public struct Failable<T>
{
public T Value { get; private set; }
public string Text { get; private set; }
public bool IsValid { get; private set; }
public Failable(T value)
{
Value = value;
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
Text = converter.ConvertToString(value);
IsValid = true;
}
catch
{
Text = String.Empty;
IsValid = false;
}
}
public Failable(string text)
{
Text = text;
try
{
var converter = TypeDescriptor.GetConverter(typeof(T));
Value = (T)converter.ConvertFromString(text);
IsValid = true;
}
catch
{
Value = default(T);
IsValid = false;
}
}
}
Обратите внимание, что даже если тип не удается инициализировать из-за неверной входной строки (второй конструктор), он спокойно сохраняет недопустимое состояние вместе с недопустимым текстом. Это необходимо для поддержки двусторонней привязки даже в случае неправильного ввода.
Универсальный конвертер значений
Универсальный конвертер значений может быть написан с использованием вышеуказанного типа:
public class StringToFailableConverter<T> : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() != typeof(Failable<T>))
throw new InvalidOperationException("Invalid value type.");
if (targetType != typeof(string))
throw new InvalidOperationException("Invalid target type.");
var rawValue = (Failable<T>)value;
return rawValue.Text;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value.GetType() != typeof(string))
throw new InvalidOperationException("Invalid value type.");
if (targetType != typeof(Failable<T>))
throw new InvalidOperationException("Invalid target type.");
return new Failable<T>(value as string);
}
}
XAML Handy Converters
Поскольку создание и использование экземпляров универсальных шаблонов является проблемой в XAML, давайте создадим статические экземпляры общих конвертеров:
public static class Failable
{
public static StringToFailableConverter<Int32> Int32Converter { get; private set; }
public static StringToFailableConverter<double> DoubleConverter { get; private set; }
static Failable()
{
Int32Converter = new StringToFailableConverter<Int32>();
DoubleConverter = new StringToFailableConverter<Double>();
}
}
Другие типы значений могут быть легко расширены.
использование
Использование довольно просто, просто нужно изменить тип с int
в Failable<int>
:
ViewModel
public Failable<int> NumberValue
{
//Custom logic along with validation
//using IsValid property
}
XAML
<TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/>
Таким образом, вы можете использовать тот же механизм проверки (IDataErrorInfo
или же INotifyDataErrorInfo
или что-нибудь еще) в ViewModel
проверяя IsValid
имущество. Если IsValid
это правда, вы можете напрямую использовать Value
,
Хорошо, я думаю, что нашел ответ, который вы искали...
Это будет нелегко объяснить - но..
Очень легко понять, когда-то объяснил...
Я думаю, что он наиболее точен /"сертифицирован" для MVVM, который рассматривается как "стандарт" или, по крайней мере, стандарт.
Но прежде чем мы начнем... вам нужно изменить концепцию, к которой вы привыкли, относительно MVVM:
"Кроме того, это изменило бы семантику модели представления. Для меня представление построено для модели представления, а не наоборот - конечно, дизайн модели представления зависит от того, что мы представляем для представления, но все же есть общие свобода, как это делает вид "
Этот абзац является источником вашей проблемы.. - почему?
Поскольку вы заявляете, что View-Model не имеет никакой роли, чтобы приспособиться к View.
Это неправильно во многих отношениях - как я докажу вам очень просто..
Если у вас есть свойство, такое как:
public Visibility MyPresenter { get...
Что такое Visibility
если не то, что служит представлению?
Сам тип и имя, которое будет дано свойству, определенно составлены для представления.
По моему опыту, в MVVM есть две категории View-Models:
- Модель Presenter View - которая должна быть привязана к кнопкам, меню, элементам табуляции и т. Д.
- Модель Entity View - которая должна быть привязана к элементам управления, которые выводят данные объекта на экран.
Это две разные - совершенно разные проблемы.
А теперь к решению:
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo
{
//This one is part of INotifyDataErrorInfo interface which I will not use,
//perhaps in more complicated scenarios it could be used to let some other VM know validation changed.
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
//will hold the errors found in validation.
public Dictionary<string, string> ValidationErrors = new Dictionary<string, string>();
//the actual value - notice it is 'int' and not 'string'..
private int storageCapacityInBytes;
//this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it.
//we want to consume what the user throw at us and validate it - right? :)
private string storageCapacityInBytesWrapper;
//This is a property to be served by the View.. important to understand the tactic used inside!
public string StorageCapacityInBytes
{
get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); }
set
{
int result;
var isValid = int.TryParse(value, out result);
if (isValid)
{
storageCapacityInBytes = result;
storageCapacityInBytesWrapper = null;
RaisePropertyChanged();
}
else
storageCapacityInBytesWrapper = value;
HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number.");
}
}
//Manager for the dictionary
private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription)
{
if (!string.IsNullOrEmpty(propertyName))
{
if (isValid)
{
if (ValidationErrors.ContainsKey(propertyName))
ValidationErrors.Remove(propertyName);
}
else
{
if (!ValidationErrors.ContainsKey(propertyName))
ValidationErrors.Add(propertyName, validationErrorDescription);
else
ValidationErrors[propertyName] = validationErrorDescription;
}
}
}
// this is another part of the interface - will be called automatically
public IEnumerable GetErrors(string propertyName)
{
return ValidationErrors.ContainsKey(propertyName)
? ValidationErrors[propertyName]
: null;
}
// same here, another part of the interface - will be called automatically
public bool HasErrors
{
get
{
return ValidationErrors.Count > 0;
}
}
}
И теперь где-то в вашем коде - ваш метод команды CanExecute может добавить в свою реализацию вызов VmEntity.HasErrors.
И пусть мир будет на вашем коде относительно проверки теперь:)
Недостатком является то, что это может запускать проверку всех свойств слишком часто, но большинство проверок должно быть достаточно просто, чтобы не ухудшить производительность. Другим решением было бы вспомнить, какие свойства вызывали ошибки при использовании проверки, и проверять только их, но в большинстве случаев это кажется слишком сложным и ненужным.
Вам не нужно отслеживать, какие свойства имеют ошибки; Вам нужно только знать, что ошибки существуют. Модель представления может вести список ошибок (также полезно для отображения сводки ошибок), а также IsValid
свойство может быть просто отражением того, есть ли в списке что-либо. Вам не нужно каждый раз проверять все IsValid
вызывается, если вы уверены, что сводка ошибок является текущей и что IsValid
обновляется каждый раз, когда меняется.
В конце концов, это проблема зрения. Представление (и его механизм привязки данных) отвечает за предоставление корректных значений модели представления для работы. Но в этом случае, похоже, нет хорошего способа сказать модели представления, что она должна сделать недействительным старое значение свойства.
Вы можете прослушивать ошибки в контейнере, который связан с моделью представления:
container.AddHandler(Validation.ErrorEvent, Container_Error);
...
void Container_Error(object sender, ValidationErrorEventArgs e) {
...
}
Это уведомляет вас, когда ошибки добавляются или удаляются, и вы можете определить обязательные исключения по e.Error.Exception
существует, поэтому ваше представление может вести список исключений привязки и информировать модель представления об этом.
Но любое решение этой проблемы всегда будет взломом, потому что представление не выполняет свою роль должным образом, что дает пользователю средство для чтения и обновления структуры модели представления. Это следует рассматривать как временное решение, пока вы правильно не представите пользователю какое-то "целочисленное поле" вместо текстового поля.
Вот попытка упростить вещи, если вы не хотите реализовывать тонны дополнительного кода...
Сценарий заключается в том, что у вас есть свойство int в вашей модели представления (может быть десятичным или другим нестроковым типом), и вы привязываете текстовое поле к нему в своем представлении.
У вас есть проверка в вашей viewmodel, которая срабатывает в установщике свойства.
В представлении пользователь вводит 123abc, и логика представления выделяет ошибку в представлении, но не может установить свойство, поскольку значение имеет неправильный тип. Сеттер никогда не вызывается.
Самое простое решение состоит в том, чтобы изменить свойство int в viewmodel на строковое свойство и преобразовать значения в него и из него из модели. Это позволяет некорректному тексту попасть в установщик вашего свойства, и ваш код проверки может затем проверить данные и отклонить их соответствующим образом.
Проверка IMHO в WPF не работает, как видно из сложных (и гениальных) способов, которыми люди пытались обойти проблему, указанную ранее. Для меня я не хочу добавлять огромное количество дополнительного кода или реализовывать свои собственные классы типов, чтобы позволить текстовому полю проверять, поэтому я могу жить с этими строками на строках, даже если это немного похоже на ляп.
Microsoft должна попытаться исправить это, чтобы сценарий некорректного ввода пользователя в текстовом поле, привязанном к свойству int или decimal, мог каким-то образом элегантно сообщить об этом факте модели представления. Для них должна быть возможность, например, создать новое связанное свойство для элемента управления XAML, чтобы сообщать об ошибках проверки логики представления свойству в модели представления.
Спасибо и уважение другим ребятам, которые предоставили подробные ответы на эту тему.