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

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

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

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

Ситуация

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

Простая форма с только строковым и целочисленным вводом

Итак, для проверки мы имеем два варианта:

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

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

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

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

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

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

Исключительные исключения

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

Механизм привязки данных генерирует исключение и отображается в представлении

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

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

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

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

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

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

BindingGroups

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

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

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

CommitEdit вернет true только в том случае, если все проверки выполнены успешно. Он учитывает IDataErrorInfo а также проверяет исключения привязки. Это, кажется, идеальное решение для выбора 2. Единственное, что немного хлопот, – это управлять группой привязки с привязками, но я создал то, что в основном позаботится об этом ( связанном ).

Если для привязки присутствует группа привязки, привязка по умолчанию будет иметь явный UpdateSourceTrigger . Чтобы реализовать выбор 1 сверху, используя связывающие группы, мы в основном должны изменить триггер. Поскольку в любом случае у меня есть расширение для обязательного связывания, это довольно просто, я просто установил его для LostFocus для всех.

Итак, теперь привязки будут обновляться всякий раз, когда изменяется текстовое поле. Если источник может быть обновлен (привязка двигателя не вызывает исключения), тогда IDataErrorInfo будет работать как обычно. Если он не может быть обновлен, вид все еще может его увидеть. И если мы нажмем на нашу кнопку, основная команда может вызвать CommitEdit (хотя ничего не нужно делать) и получить итоговый результат проверки, чтобы увидеть, можно ли продолжить.

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

Решение?

Так что в целом это кажется идеальным решением. В чем моя проблема? Честно говоря, я не совсем уверен. Связывающие группы представляют собой сложную вещь, которая, как правило, используется в небольших группах, возможно, имеющих несколько групп привязки в одном представлении. Используя одну большую группу привязки для всего представления, чтобы обеспечить мою проверку, мне кажется, что я злоупотребляю ею. И я просто продолжаю думать, что должен быть лучший способ решить всю эту ситуацию, потому что, конечно, я не могу быть единственным, у кого есть эти проблемы. И пока я не видел, чтобы многие люди использовали привязывающие группы для проверки с MVVM вообще, так что это просто странно.

Итак, что именно является правильным способом проверки в WPF с MVVM, имея возможность проверить наличие ограничений на переключение?


Мое решение (/ взломать)

Прежде всего, спасибо за ваш вклад! Как я уже писал выше, я уже использую IDataErrorInfo для проверки своих данных, и я лично считаю, что это самая удобная утилита для выполнения задания валидации. Я использую утилиты, похожие на то, что предложил Шеридан в своем ответе ниже, поэтому поддержание тоже прекрасное.

В конце концов, моя проблема сводилась к проблеме исключения привязки, когда модель представления просто не знала о том, когда это произошло. Хотя я мог бы справиться с этим со связанными группами, как описано выше, я все же решил против него, так как я просто не чувствовал себя так комфортно с ним. Так что же я сделал вместо этого?

Как я уже упоминал выше, я обнаруживаю исключения привязки на стороне просмотра, слушая UpdateSourceExceptionFilter связывания. Там я могу получить ссылку на модель представления из DataItem выражения 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 bindingErrors = new HashSet(); void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception) { bindingErrors.Add(path); } 

И всякий раз, когда IDataErrorInfo свойство, я знаю, что сработала привязка, и я могу очистить свойство от хеш-набора.

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

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

Я использую интерфейс IDataErrorInfo для проверки, но я настроил его на свои нужды. Я думаю, что вы обнаружите, что он решает некоторые из ваших проблем. Одно из отличий от вашего вопроса в том, что я реализую его в своем базовом classе типов данных.

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

 protected ObservableCollection errors = new ObservableCollection(); public virtual ObservableCollection Errors { get { return errors; } } 

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

 protected ObservableCollection externalErrors = new ObservableCollection(); public ObservableCollection ExternalErrors { get { return externalErrors; } } 

У меня есть свойство HasError которое смотрит на мою коллекцию:

 public virtual bool HasError { get { return Errors != null && Errors.Count > 0; } } 

Это позволяет мне привязать это к Grid.Visibility с помощью пользовательского BoolToVisibilityConverter , например. чтобы показать Grid с элементом управления коллекцией внутри, который показывает ошибки, когда они есть. Это также позволяет мне менять Brush to Red чтобы выделить ошибку (используя другой Converter ), но, я думаю, вы поняли эту идею.

Затем в каждом типе данных или classе модели я переопределяю свойство Errors и реализую индексатор Item (упрощенный в этом примере):

 public override ObservableCollection Errors { get { errors = new ObservableCollection(); 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 , я могу проверить, что я не могу проверить в classе данных:

 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 интерфейса 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 , единственный способ увидеть, как вы получаете эти ошибки в своем classе данных, – это объявить все свои свойства как 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; } } 

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

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

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


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

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

 container.AddHandler(Validation.ErrorEvent, Container_Error); ... void Container_Error(object sender, ValidationErrorEventArgs e) { ... } 

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

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

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

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

Тип отказоустойчивого типа

Я начал с создания универсального типа, который бы изящно поддерживал неудачные конверсии:

 public struct Failable { 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 : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value.GetType() != typeof(Failable)) throw new InvalidOperationException("Invalid value type."); if (targetType != typeof(string)) throw new InvalidOperationException("Invalid target type."); var rawValue = (Failable)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)) throw new InvalidOperationException("Invalid target type."); return new Failable(value as string); } } 

Конвертеры XAML Handy

Поскольку создание и использование экземпляров дженериков – это боль в XAML, позволяет создавать статические экземпляры обычных преобразователей:

 public static class Failable { public static StringToFailableConverter Int32Converter { get; private set; } public static StringToFailableConverter DoubleConverter { get; private set; } static Failable() { Int32Converter = new StringToFailableConverter(); DoubleConverter = new StringToFailableConverter(); } } 

Другие типы значений могут быть легко расширены.

Применение

Использование довольно просто, просто нужно изменить тип от int до Failable :

ViewModel

 public Failable NumberValue { //Custom logic along with validation //using IsValid property } 

XAML

  

Таким образом, вы можете использовать один и тот же механизм проверки ( IDataErrorInfo или INotifyDataErrorInfo или что-то еще) в ViewModel , установив свойство IsValid . Если IsValid истинно, вы можете напрямую использовать Value .

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

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

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

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

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

Если у вас есть свойство, такое как:

public Visibility MyPresenter { get...

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

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

  • Presenter View Model – которая должна быть подключена к кнопкам, меню, вкладкам и т. Д ….
  • Entity View Model – которая должна быть запущена для элементов управления, которая выводит данные сущности на экран.

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

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

 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 ErrorsChanged; //will hold the errors found in validation. public Dictionary ValidationErrors = new Dictionary(); //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.

И пусть мир будет с вашего кода относительно валидации с этого момента 🙂

  • Закрыть окно из ViewModel
  • Содержимое стиля кнопки отображается только в одном экземпляре Button
  • Установка свойства с помощью EventTrigger
  • Что означает «Захват мыши» в WPF?
  • Различные шаблоны представлений / данных на основе переменной-члена
  • WPF TextBox не будет заполнять StackPanel
  • Конфликты Datacontext
  • Как обрабатывать сообщения WndProc в WPF?
  • DataTrigger не изменяет свойство Text
  • Размер времени разработки пользовательского интерфейса WPF
  • CommandParameters в ContextMenu в WPF
  • Давайте будем гением компьютера.