Шаблон ошибки проверки отображается в ContentControl вместо TextBox

Проблема

Я пытаюсь создать визуальную обратную связь о результате проверки элемента управления вводом TextBox со следующими требованиями:

  • Вокруг границы текстового поля отображается красная рамка.
  • Любые сообщения об ошибках отображаются чуть ниже текстового поля.

Соответствующий код проверки, похоже, работает: функция, выполняющая проверку, запущена, сообщения об ошибках накапливаются, вызывается OnErrorsChanged, а WPF делает... что-то... вокруг всего моего UserControl появляется красная рамка.

Прочитав множество связанных с SO вопросов/ответов и приняв во внимание тот факт, что рисуется красный прямоугольник, я изначально почувствовал, что это связано с размещением декоративного элемента. Однако я пробовал ставить AdornerDecorator на каждом слое, но безрезультатно. Я также проверил во время выполнения, есть ли у элементов управления текстовым полем доступный для них слой оформления, и они есть. Он создается на основе ContentControl хостинга моего представления (View1.xaml). Кажется, это объясняет, почему я вижу красную рамку вокруг всего ContentControl, но меня смущает, почему TextBoxValidation.ErrorTemplate не контролирует размещение украшения.

Схема управления

  • View1.xaml
    • UserControl
      • Grid
        • GroupBox
          • StackPanel (вертикальная планировка)
            • StackPanel (горизонтальное расположение — повторяется для всех элементов управления текстовым полем)
              • Label
              • Textbox (подтвержденный элемент управления — ErrorTemplate здесь)

Код

View1.xaml

<UserControl x:Class = "View1"
             ....namespaces....
             xmlns:Fluent = "urn:fluent-ribbon"
             xmlns:vm = "clr-namespace:MyNamespace.ViewModel"
             xmlns:view = "clr-namespace:MyNamespace.View"
             Name = "MyView1"
             DataContext = "{StaticResource g_MainWindowViewModel}"
             Validation.ErrorTemplate = "{x:Null}">
<DockPanel LastChildFill = "True">
    <ContentControl Name = "NewSessionFormContentControl" 
                    Content = "{Binding m_SessionFormViewModel}" 
                    DockPanel.Dock = "Left"/>
</DockPanel>

С ControlTemplate, взятым из ответа BionicCode здесь:

<ControlTemplate x:Key = "ValidationErrorTemplate">
    <DockPanel LastChildFill = "True">
    <Border BorderBrush = "Red" BorderThickness = "1">
        <AdornedElementPlaceholder x:Name = "AdornedElement"/>
    </Border>
    <Border Background = "White" 
            BorderBrush = "Red" 
            Padding = "4"
            BorderThickness = "1,0,1,1" 
            HorizontalAlignment = "Left">
        <ItemsControl ItemsSource = "{Binding}"
                        HorizontalAlignment = "Left"
                        DockPanel.Dock = "Right">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                <TextBlock Text = "{Binding ErrorContent}" 
                            Foreground = "Red"
                            DockPanel.Dock = "Right"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Border>
    </DockPanel>
</ControlTemplate>

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

<TextBox Name = "Name1"
        Width = "350" 
        Validation.ErrorTemplate = "{DynamicResource ValidationErrorTemplate}"
        Text = "{Binding Name, Mode=OneWayToSource, ValidatesOnNotifyDataErrors=True,UpdateSourceTrigger=Explicit}"/>

Это не связано с AdornerDecorator. Неважно, какой элемент содержит AdornerLayer. Механизм привязки просто добавит Adorner к первому родителю AdornerLayer, который он найдет в текущем визуальном дереве. Это Adorner отображает значение Validation.ErrorTemplate. Символ Adorner украшает фактический элемент, имеющий целевое свойство, для которого привязка сообщает об ошибке проверки (TextBox.Text). Отображение ошибок проверки основано на привязке. Каждая отдельная привязка отслеживает и управляет собственными ошибками проверки. Механизм привязки по умолчанию всегда украшает цель привязки.

BionicCode 10.07.2024 23:52

Вам придется установить свойство Validation.ValidationAdornerSite, чтобы заставить механизм привязки украшать другой элемент. Например, UserControl состоит из нескольких элементов управления, каждый из которых привязан к источнику данных. И если одна из этих привязок не проходит проверку, вы хотите, чтобы UserControl отображался как недействительный и предоставлял визуальную обратную связь об ошибке. Затем автор элемента управления установил Validation.ValidationAdornerSiteFor для дочернего элемента, чтобы он ссылался на родителя UserControl. Механизм привязки теперь выберет значение Validation.ValidationAdornerSite в качестве украшаемого элемента.

BionicCode 10.07.2024 23:53

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

BionicCode 10.07.2024 23:54

Проблема, которую вы описываете, обычно является результатом привязки дочернего элемента управления к свойству родительского элемента UserControl, в то время как это свойство UserControl привязывается к фактическому источнику данных (это было бы правильной конструкцией элемента управления). Если это не так, например, потому что ваш дочерний элемент контролирует все привязки непосредственно к DataContext, было бы неплохо, если бы вы могли опубликовать контекст XAML TextBox и то, как UserControl используется/настраивается. Немного больше контекста для проверки кода. Таким образом, мы можем узнать, что переопределяет поведение по умолчанию.

BionicCode 10.07.2024 23:56

Код модели представления также не имеет значения в этом случае. Сама проверка работает правильно. Только отрисовка шаблона ошибки не такая как хотелось. Модель представления (или реализация INotifyDataErrorInfo в целом) не может иметь никакого влияния на сайт рендеринга. Проблема должна быть связана с целью привязки (UserControl и TextBox). Итак, если TextBox не является обязательным следующим образом: TextBox ==> UserControl ==> data source, опубликуйте контекст, связанный с представлением. В противном случае сообщите мне, что ваш путь привязки соответствует шаблону, и я немедленно опубликую исправление.

BionicCode 11.07.2024 00:00

Спасибо за подробные комментарии! Подлежащий проверке TextBox привязан непосредственно к источнику данных (в моем исходном вопросе есть образец TextBox, с моей точки зрения) через OneWayToSource. Я обновлю вопрос, добавив больше кода из представления, а также некоторые подробности о потоке данных.

user1229658 11.07.2024 14:13

Спасибо за обновление вопроса. Исходная привязка отображала только {Binding Name}, поэтому я не был уверен, является ли Name свойством, определенным UserControl, которое называется так же, как свойство модели представления. Это не было бы редкостью. Итак, я спросил. Как правило, вы не используете внедрение зависимостей для назначения экземпляров модели представления представлениям. Обычно у вас будет, например. MainViewModel, который предоставляет несколько классов дочерних моделей представлений. Вы, вероятно, вводите MainViewModel в MainWindow. Отсюда все ссылки на XAML.

BionicCode 11.07.2024 15:55

Затем вы можете ссылаться на MainViewModel, чтобы назначить более специализированный DataContext отдельным элементам управления или областям элементов управления, но всегда внешне. Например, в вашем случае назначение DataContext должно выглядеть следующим образом: Установить извне! <Window DataContext = "{StaticResource g_MainWindowViewModel}"> <View1 DataContext = "{Binding m_ViewModel}" /> </Window>. Кроме того, вы можете использовать наследование DataContext.

BionicCode 11.07.2024 15:56

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

BionicCode 11.07.2024 15:56

Большое спасибо за предоставленную вами дополнительную информацию. К сожалению, этого недостаточно, чтобы воспроизвести поведение. Судя по тому, что вы опубликовали, поведение должно быть таким, как вы ожидаете: красная рамка вокруг TextBox, а не View1 элемента управления. Не могли бы вы создать минимальный воспроизводимый пример? Возможно, вы могли бы начать с пустого проекта, а затем добавлять код, пока не сможете воспроизвести поведение. Никаких дополнительных услуг, только минимум. Спасибо!

BionicCode 11.07.2024 16:02

Я также понял, что ваша привязка TextBox — OneWayToSource. Это не должно работать именно так, как вы хотите: OneWayToSource запускает проверку, как ожидалось, но не запускает представление для отображения визуальной обратной связи об ошибке. Это связано с тем, что информация, связанная с ошибкой, назначается цели привязки через прикрепленные свойства. Но целевой элемент будет обновляться только тогда, когда его обновит механизм привязки, что возможно только в привязках TwoWay, OneWay или OneTime.

BionicCode 11.07.2024 16:50

OneWay и OneTime не обновляют источник (чтобы запустить проверку данных), поэтому они дисквалифицируются. Остался только TwoWay. Чтобы получить проверку привязки, привязка должна быть TwoWay или OneWayToSource. Чтобы получить обратную связь об ошибке, привязка должна быть двусторонней. Несмотря на это, вам каким-то образом удалось показать обратную связь об ошибках, хотя и не на том управлении. Вот почему я думаю, что проблема может быть связана с тем, как вы явно обновляете привязку. Однако наличие минимально воспроизводимого примера внесет ясность.

BionicCode 11.07.2024 16:50

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

user1229658 11.07.2024 22:20
UpdateSourceTrigger=Explicitна всех проверенных элементах управления просто вызывать проверку явным образом является ошибкой. В вашей модели представления должен быть метод, который будет проверять все свойства и уведомлять об ошибках.
Liero 11.07.2024 22:43

@Liero Изначально я хотел обновить информацию об изменении поля (в средстве установки свойств для каждого элемента данных, привязанного к текстовому полю), но это вызвало проверку при создании экземпляра данных резервной модели представления, поэтому я не был уверен, как этого избежать.

user1229658 11.07.2024 22:53

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

BionicCode 12.07.2024 11:17
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
16
92
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

1 Отсутствует рендеринг ошибки проверки для TextBox

Как я уже упоминал в комментариях, вы должны установить Binding.Mode на BindingMode.TwoWay, если хотите, чтобы цель привязки показывала ошибки проверки.

Объяснение

Если вам нужна только простая проверка свойства, тогда BindingMode.OneWayToSource достаточно. Наборы механизмов привязки прикрепленные свойства, такие как свойство Validation.Errors на целевой элемент. Это происходит после обновления цели свойство. Цель обновляется только в том случае, если Binding.Mode есть. OneWay, OneTime или TwoWay. Поскольку вы хотите вызвать проверка данных (которая требует обновления источника привязки), единственный возможный режим — TwoWay.

Исправить

SessionFromView.xaml
Установите Binding.Mode привязки TextBox на BindingMode.TwoWay

<TextBox Name = "SessionName"
         Validation.ErrorTemplate = "{DynamicResource ValidationErrorTemplate}"
         Text = "{Binding Name, Mode=TwoWay, ValidatesOnNotifyDataErrors=True}" />

2 Ошибка в ViewModelBase.ClearErrors

Вы пропустили событие INotifyDataErrorInfo.ErrorsChanged в случае, если параметр propertyName равен null или пуст.

Объяснение

Механизм привязки подписывается на событие INotifyDataErrorInfo.ErrorsChanged для обновления привязки (на основе настроенного Binding.Mode). Если вы вызываете ViewModelbase.ClearErrors без указания имени свойства, как вы это делаете, то очистка ошибок не повлияет на пользовательский интерфейс, поскольку механизм привязки никогда не запускается для повторной оценки привязки, связанной с событием. В результате шаблон ошибки не удаляется после устранения ошибок.

Исправить

ВьюМоделБасе.cs

protected bool ClearErrors(string propertyName = "")
{
  if (string.IsNullOrEmpty(propertyName))
  {
    Errors.Clear();

    // FIX::Raise ErrrorsChanged event
    OnErrorsChanged(propertyName);

    return true;
  }

  if (Errors.Remove(propertyName))
  {
    OnErrorsChanged(propertyName);

    return true;
  }

  return false;
}

3. Граница ошибки проверки вокруг родителя TextBox

У вас есть три возможных варианта исправить это:

  1. Явно установите от Binding.ValidatesOnNotifyDataErrors до false (рекомендуется).
  2. установите Validation.ErrorTemplate на null (не рекомендуется). Хотя я бы не рекомендовал это решение (поскольку привязка по-прежнему будет занята обработкой ошибок проверки, что приводит к накладным расходам и снижению производительности), это решение более удобно, поскольку вы можете использовать глобальные неявные стили для установки Validation.ErrorTemplate в {x:Null}.
  3. Измените реализацию INotifyDataErrorInfo.GetErrors (не рекомендуется). Это исключит возможность получения всех ошибок источника привязки. WPF активно использует эту функцию (что в вашем случае приводит к красной рамке вокруг UserControl (или родительского хостинга ContentControl, если быть точным).

Объяснение

Значение по умолчанию для Binding.ValidatesOnNotifyDataErrorstrue. Кроме того, прикрепленное свойство Validation.ErrorTemplate возвращает шаблон по умолчанию, который просто рисует красную рамку вокруг целевого элемента.

Если элемент управления привязывается к объекту источника данных, а не к свойству этого объекта, механизм привязки передает пустую строку вместо определенного имени свойства в метод INotifyDataErrorInfo.GetErrors. Если этот метод реализован для возврата всех текущих ошибок экземпляра, как это делает ваш ViewModelBase (и это рекомендуемая реализация),
тогда цель имеет ошибки для отображения:

<!-- 
  The Content property binding targets the whole data source object 
  and not a property of the data source. As a result, the binding engine can't query the errors 
  for a particular property and instead tries to get all errors of the source object.
  And because ValidatesOnNotifyDataErrors is TRUE by default, the binding on the Content property 
  will be validated.

  The following two bindings are equivalaent
-->

<ContentControl Name = "NewSessionFormContentControl"
                Content = "{Binding m_SessionFormViewModel}" />


<ContentControl Name = "NewSessionFormContentControl"
                Content = "{Binding m_SessionFormViewModel, ValidatesOnNotifyDataErrors=True}" />
// ViewModelBase: 
// This is were all errors of the source object are returned 
// in case the binding engine passes in an empty string
public System.Collections.IEnumerable GetErrors(string propertyName)
  => string.IsNullOrWhiteSpace(propertyName)
    ? Errors.SelectMany(entry => entry.Value) // Return all errors
    : Errors.TryGetValue(propertyName, out IList<object> errors)
      ? (IEnumerable<object>)errors
      : new List<object>();

Исправление № 1 (рекомендуется)

Сеансвиев.xaml

<!-- Disable validation for the particular binding and avoid related overhead -->
<ContentControl Name = "NewSessionFormContentControl"
                Content = "{Binding m_SessionFormViewModel, ValidatesOnNotifyDataErrors=False}" />

Исправление №2

Сеансвиев.xaml

<!-- 
  Set Validation.ErrorTemplate to NULL. 
  The binding will still perform all the validation related operations
  but won't show the visual error feedback .
-->
<ContentControl Name = "NewSessionFormContentControl"
                Validation.ErrorTemplate = "{x:Null}"
                Content = "{Binding m_SessionFormViewModel}" />

Исправление №3

ВьюМоделБасе.xaml

// Don't support NULL and empty string queries
public System.Collections.IEnumerable GetErrors(string propertyName)
  => string.IsNullOrWhiteSpace(propertyName)
    ? new List<object>() // Return no errors
    : Errors.TryGetValue(propertyName, out IList<object> errors)
      ? (IEnumerable<object>)errors
      : new List<object>();

Другие вопросы по теме