Предупреждение: очень длинный и подробный пост.
Хорошо, проверка в WPF при использовании MVVM. Я прочитал много вещей сейчас, посмотрел на многие вопросы SO и перепробовал много подходов, но в какой-то момент все выглядит несколько странно, и я действительно не уверен, как сделать это правильно ™.
В идеале я хочу, чтобы вся проверка происходила в модели представления с использованием IDataErrorInfo
; так вот что я сделал. Однако существуют различные аспекты, которые делают это решение не полным решением для всей темы проверки.
Ситуация
Примем следующую простую форму. Как видите, ничего особенного. У нас просто есть два текстовых поля, которые связываются со string
и свойством int
в модели представления каждого. Кроме того, у нас есть кнопка, связанная с ICommand
.
Итак, для проверки у нас теперь есть два варианта:
- Мы можем запустить проверку автоматически каждый раз, когда изменяется значение текстового поля. Таким образом, пользователь получает мгновенный ответ, когда вводит что-то недействительное.
- Мы можем сделать еще один шаг, чтобы отключить кнопку при возникновении ошибок.
- Или мы можем запустить проверку только в явном виде, когда нажата кнопка, а затем показаны все ошибки, если это применимо. Очевидно, мы не можем отключить кнопку на ошибки здесь.
В идеале я хочу реализовать вариант 1. Для обычных привязок данных с активированными ValidatesOnDataErrors
это поведение по умолчанию. Поэтому, когда текст изменяется, привязка обновляет источник и запускает проверку IDataErrorInfo
для этого свойства; об ошибках сообщается в обратном виде. Все идет нормально.
Статус проверки в модели представления
Интересным моментом является информирование модели представления или кнопки в этом случае о наличии ошибок. Как работает IDataErrorInfo
, он в основном предназначен для сообщения об ошибках обратно в представление. Таким образом, представление может легко увидеть, есть ли какие-либо ошибки, отобразить их и даже показать аннотации с использованием Validation.Errors
. Кроме того, проверка всегда происходит, глядя на одно свойство.
Таким образом, иметь представление о модели представления, когда есть какие-либо ошибки или если проверка прошла успешно, сложно. Распространенное решение - просто запустить проверку IDataErrorInfo
для всех свойств в самой модели представления. Это часто делается с использованием отдельного свойства IsValid
. Преимущество состоит в том, что это также может быть легко использовано для отключения команды. Недостатком является то, что это может запускать проверку всех свойств слишком часто, но большинство проверок должно быть достаточно просто, чтобы не ухудшить производительность. Другим решением было бы вспомнить, какие свойства вызывали ошибки при использовании проверки, и проверять только их, но в большинстве случаев это кажется слишком сложным и ненужным.
Суть в том, что это может работать нормально. IDataErrorInfo
обеспечивает проверку всех свойств, и мы можем просто использовать этот интерфейс в самой модели представления, чтобы выполнить проверку там также для всего объекта. Представляя проблему:
Обязательные исключения
Модель представления использует фактические типы для своих свойств. Таким образом, в нашем примере, целое свойство является актуальной int
. Однако текстовое поле, используемое в представлении, поддерживает только текст. Таким образом, при привязке к int
в модели представления механизм привязки данных автоматически выполнит преобразования типов - или, по крайней мере, попытается. Если вы можете ввести текст в текстовое поле, предназначенное для чисел, высока вероятность того, что внутри не всегда будут действительные числа: поэтому механизм привязки данных не сможет преобразовать и FormatException
.
Со стороны взгляда мы можем это легко увидеть. Исключения из механизма привязки автоматически перехватываются 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
свойство, я знаю, что привязка сработала, и я могу удалить свойство из хеш-набора.
В модели представления я затем могу проверить, содержит ли набор хэшей какие-либо элементы, и прервать любое действие, требующее полной проверки данных. Это может быть не самым лучшим решением из-за связи между представлением и моделью представления, но использование этого интерфейса, по крайней мере, несколько меньше проблем.