Подтвердить что ты не робот

Правильная проверка с помощью MVVM

Предупреждение: очень длинный и подробный пост.

Хорошо, проверка в WPF при использовании MVVM. Я прочитал много вещей сейчас, посмотрел на многие вопросы SO и перепробовал много подходов, но в какой-то момент все выглядит несколько странно, и я действительно не уверен, как сделать это правильно ™.

В идеале я хочу, чтобы вся проверка происходила в модели представления с использованием IDataErrorInfo; так вот что я сделал. Однако существуют различные аспекты, которые делают это решение не полным решением для всей темы проверки.

Ситуация

Примем следующую простую форму. Как видите, ничего особенного. У нас просто есть два текстовых поля, которые связываются со string и свойством int в модели представления каждого. Кроме того, у нас есть кнопка, связанная с ICommand.

Simple form with only a string and integer input

Итак, для проверки у нас теперь есть два варианта:

  1. Мы можем запустить проверку автоматически каждый раз, когда изменяется значение текстового поля. Таким образом, пользователь получает мгновенный ответ, когда вводит что-то недействительное.
    • Мы можем сделать еще один шаг, чтобы отключить кнопку при возникновении ошибок.
  2. Или мы можем запустить проверку только в явном виде, когда нажата кнопка, а затем показаны все ошибки, если это применимо. Очевидно, мы не можем отключить кнопку на ошибки здесь.

В идеале я хочу реализовать вариант 1. Для обычных привязок данных с активированными ValidatesOnDataErrors это поведение по умолчанию. Поэтому, когда текст изменяется, привязка обновляет источник и запускает проверку IDataErrorInfo для этого свойства; об ошибках сообщается в обратном виде. Все идет нормально.

Статус проверки в модели представления

Интересным моментом является информирование модели представления или кнопки в этом случае о наличии ошибок. Как работает IDataErrorInfo, он в основном предназначен для сообщения об ошибках обратно в представление. Таким образом, представление может легко увидеть, есть ли какие-либо ошибки, отобразить их и даже показать аннотации с использованием Validation.Errors. Кроме того, проверка всегда происходит, глядя на одно свойство.

Таким образом, иметь представление о модели представления, когда есть какие-либо ошибки или если проверка прошла успешно, сложно. Распространенное решение - просто запустить проверку IDataErrorInfo для всех свойств в самой модели представления. Это часто делается с использованием отдельного свойства IsValid. Преимущество состоит в том, что это также может быть легко использовано для отключения команды. Недостатком является то, что это может запускать проверку всех свойств слишком часто, но большинство проверок должно быть достаточно просто, чтобы не ухудшить производительность. Другим решением было бы вспомнить, какие свойства вызывали ошибки при использовании проверки, и проверять только их, но в большинстве случаев это кажется слишком сложным и ненужным.

Суть в том, что это может работать нормально. IDataErrorInfo обеспечивает проверку всех свойств, и мы можем просто использовать этот интерфейс в самой модели представления, чтобы выполнить проверку там также для всего объекта. Представляя проблему:

Обязательные исключения

Модель представления использует фактические типы для своих свойств. Таким образом, в нашем примере, целое свойство является актуальной int. Однако текстовое поле, используемое в представлении, поддерживает только текст. Таким образом, при привязке к int в модели представления механизм привязки данных автоматически выполнит преобразования типов - или, по крайней мере, попытается. Если вы можете ввести текст в текстовое поле, предназначенное для чисел, высока вероятность того, что внутри не всегда будут действительные числа: поэтому механизм привязки данных не сможет преобразовать и FormatException.

Data binding engine throws an exception and thats displayed in the view

Со стороны взгляда мы можем это легко увидеть. Исключения из механизма привязки автоматически перехватываются WPF и отображаются как ошибки - даже нет необходимости включать Binding.ValidatesOnExceptions которые потребуются для исключений, Binding.ValidatesOnExceptions в установщике. Сообщения об ошибках имеют общий текст, поэтому это может быть проблемой. Я решил это для себя, используя обработчик Binding.UpdateSourceExceptionFilter, Binding.UpdateSourceExceptionFilter выбрасываемое исключение и просматривая свойство источника, а затем генерируя менее общее сообщение об ошибке. Все это скрыто в моем собственном расширении разметки Binding, поэтому у меня могут быть все необходимые значения по умолчанию.

Так что мнение в порядке. Пользователь делает ошибку, видит какую-то ошибку и может исправить ее. Модель представления однако потеряна. Поскольку механизм привязки выдал исключение, источник никогда не обновлялся. Таким образом, модель представления все еще имеет старое значение, которое не отображается пользователю, и проверка IDataErrorInfo очевидно, неприменима.

Что еще хуже, у модели представления нет хорошего способа узнать это. По крайней мере, я еще не нашел хорошего решения для этого. То, что было бы возможно, это вернуть отчет о представлении в модель представления о том, что произошла ошибка. Это может быть сделано путем привязки данных свойства Validation.HasError к модели представления (что невозможно напрямую), поэтому модель представления может сначала проверить состояние представлений.

Другой вариант - передать исключение, обработанное в Binding.UpdateSourceExceptionFilter в модель представления, чтобы оно также Binding.UpdateSourceExceptionFilter уведомление об этом. Модель представления может даже предоставить некоторый интерфейс для привязки, чтобы сообщать об этих вещах, допуская настраиваемые сообщения об ошибках вместо общих для каждого типа сообщений. Но это создаст более сильную связь между представлением и моделью представления, чего я обычно хочу избегать.

Другое "решение" - избавиться от всех типизированных свойств, использовать свойства простой string и вместо этого выполнить преобразование в модели представления. Это, очевидно, переместило бы всю проверку в модель представления, но также означало бы невероятное количество дублирований вещей, о которых обычно заботится механизм привязки данных. Кроме того, это изменило бы семантику модели представления. Для меня представление построено для модели представления, а не наоборот - конечно, дизайн модели представления зависит от того, что мы представляем для представления, но есть еще общая свобода, как это делает представление. Таким образом, модель представления определяет свойство int потому что есть число; Теперь представление может использовать текстовое поле (разрешающее все эти проблемы) или использовать то, что изначально работает с числами. Так что нет, изменение типов свойств на string для меня не вариант.

В конце концов, это проблема зрения. Представление (и его механизм привязки данных) отвечает за предоставление корректных значений модели представления для работы. Но в этом случае, похоже, нет хорошего способа сказать модели представления, что она должна сделать недействительным старое значение свойства.

BindingGroups

Связывающие группы - один из способов, которым я пытался заняться этим. Группы связывания имеют возможность группировать все проверки, включая IDataErrorInfo и IDataErrorInfo исключения. Если они доступны для модели представления, у них даже есть возможность проверить состояние проверки для всех этих источников проверки, например, используя CommitEdit.

По умолчанию группы связывания реализуют вариант 2 сверху. Они явно обновляют привязки, по существу добавляя дополнительное незафиксированное состояние. Таким образом, при нажатии кнопки команда может зафиксировать эти изменения, вызвать обновления источника и все проверки и получить единый результат в случае успеха. Таким образом, действие команды может быть таким:

 if (bindingGroup.CommitEdit())
     SaveEverything();

CommitEdit вернет true только если все проверки CommitEdit успешно. Он будет учитывать 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 повторно IDataErrorInfo свойство, я знаю, что привязка сработала, и я могу удалить свойство из хеш-набора.

В модели представления я затем могу проверить, содержит ли набор хэшей какие-либо элементы, и прервать любое действие, требующее полной проверки данных. Это может быть не самым лучшим решением из-за связи между представлением и моделью представления, но использование этого интерфейса, по крайней мере, несколько меньше проблем.

4b9b3361

Ответ 1

Предупреждение: длинный ответ также

Я использую интерфейс 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;
    }
}

Таким образом, вы сможете выполнить свои требования, но у вас будет много дополнительного кода для добавления.

Ответ 2

Недостатком является то, что это может привести к проверке всех свойств a бит слишком часто, но большинство валидаций должны быть достаточно просто, чтобы не повредить производительность. Другим решением было бы помнить, какие свойства порождали ошибки с использованием проверки и проверяли только те, но это кажется немного сложным и ненужным в большинстве случаев.

Вам не нужно отслеживать, какие свойства имеют ошибки; вам нужно только знать, что существуют ошибки. Модель просмотра может содержать список ошибок (также полезно для отображения сводки ошибок), а свойство IsValid может быть просто отражением того, есть ли в списке что-либо. Вам не нужно проверять все каждый раз, когда вызывается IsValid, пока вы уверены, что сводка ошибок является текущей и что IsValid обновляется каждый раз, когда она изменяется.


В конце концов, это проблема представления. Вид (и его данные механизм привязки) отвечает за предоставление правильных значений модели представления работать с. Но в этом случае, кажется, нет хорошего способа сказать модель представления, что она должна аннулировать значение старого свойства.

Вы можете прослушивать ошибки в контейнере, привязанные к модели представления:

container.AddHandler(Validation.ErrorEvent, Container_Error);

...

void Container_Error(object sender, ValidationErrorEventArgs e) {
    ...
}

Это уведомляет вас о том, когда ошибки добавляются или удаляются, и вы можете идентифицировать исключения привязки, существует ли e.Error.Exception, поэтому ваше представление может поддерживать список исключений привязки и информировать его о модели представления.

Но любое решение этой проблемы всегда будет хаком, потому что представление не заполняет свою роль должным образом, что дает пользователю средство для чтения и обновления структуры модели представления. Это должно рассматриваться как временное решение, пока вы не представите пользователю некое текстовое поле "целочисленное поле".

Ответ 3

По моему мнению, проблема заключается в том, что валидация происходит в слишком многих местах. Я также хотел написать весь мой логин для проверки в 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.

Ответ 4

Хорошо, я считаю, что нашел ответ, который вы искали...
Это будет нелегко объяснить - но..
Очень легко понять, как только объяснялось...
Я действительно думаю, что он является наиболее точным/ "сертифицированным" для MVVM, который рассматривается как "стандартный" или, по крайней мере, для стандартного образца.

Но прежде чем мы начнем... вам нужно изменить концепцию, к которой вы привыкли относиться к MVVM:

"Кроме того, это изменит семантику модели представления. Для меня,  для модели просмотра создается представление, а не наоборот, дизайн модели взгляда зависит от того, что мы представляем себе,  но theres все еще общая свобода как взгляд делает то "

Этот пункт является источником вашей проблемы.. - почему?

Потому что вы заявляете, что View-Model не имеет никакой роли, чтобы приспособиться к представлению.
Это неправильно во многих отношениях - так как я докажу вам очень просто.

Если у вас есть свойство, например:

public Visibility MyPresenter { get...

Что такое Visibility, если не что-то, что служит представлению?
Сам тип и имя, которое будет присвоено свойству, определенно составлены для представления.

В MVVM есть две отличимые категории View-Models в зависимости от моего опыта:

  • Модель представления презентатора - для подключения к кнопкам, меню, вкладкам и т.д.
  • Модель представления объектов - которая должна быть запущена для элементов управления, которые приносят данные сущности на экран.

Это две разные проблемы - совершенно разные.

И теперь к решению:

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.

И дайте мир вашему коду относительно валидации с этого момента:)

Ответ 5

Вот попытка упростить вещи, если вы не хотите реализовывать тонны дополнительного кода...

Сценарий состоит в том, что у вас есть свойство int в вашей модели представления (может быть десятичным или другим нестроковым типом), и вы привязываете текстовое поле к нему в своем представлении.

У вас есть проверка в вашей viewmodel, которая срабатывает в установщике свойства.

В представлении пользователь вводит 123abc, и логика представления выделяет ошибку в представлении, но не может установить свойство, поскольку значение имеет неправильный тип. Сеттер никогда не вызывается.

Самое простое решение состоит в том, чтобы изменить свойство int в viewmodel на строковое свойство и преобразовать значения в него и из него из модели. Это позволяет некорректному тексту попадать в установщик вашего свойства, и ваш код проверки может затем проверить данные и отклонить их соответствующим образом.

Проверка IMHO в WPF не работает, как видно из сложных (и изобретательных) способов, которыми люди пытались обойти проблему, указанную ранее. Для меня я не хочу добавлять огромное количество дополнительного кода или реализовывать свои собственные классы типов, чтобы позволить текстовому полю проверять, поэтому я могу жить с этими строками на строках, даже если это немного похоже на ляп.

Microsoft должна попытаться исправить это, чтобы сценарий некорректного ввода данных пользователем в текстовом поле, привязанном к свойству int или decimal, мог каким-то образом элегантно сообщить этот факт модели представления. Должна быть возможность, например, для них создать новое связанное свойство для элемента управления XAML, чтобы сообщать об ошибках проверки логики представления свойству в модели представления.

Спасибо и уважение другим ребятам, которые предоставили подробные ответы на эту тему.