Использование INotifyDataErrorInfo с дочерними объектами в модели, которые требуют пользовательской проверки

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

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

Простой пример

Мой пример модели для медиа-мероприятия, где будут присутствовать докладчики и гости. При планировании медиа-события пользователь вводит имя, минимальное и максимальное количество докладчиков, а также минимальное и максимальное количество гостей. Как правило, в СМИ должно быть не менее 1 докладчика и не более 5, а также не менее 10 гостей и не более 50 человек.

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

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace NotifyDataErrorInfo
{
    public class ValidatableModel : INotifyDataErrorInfo, INotifyPropertyChanged
    {
        public ConcurrentDictionary<string, List<string>> _errors = new ConcurrentDictionary<string, List<string>>();

        public event PropertyChangedEventHandler PropertyChanged;

        public void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }

            ValidateAsync();
        }

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public void OnErrorsChanged(string propertyName)
        {
            var handler = ErrorsChanged;

            if (handler != null)
            {
                handler(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        public IEnumerable GetErrors(string propertyName)
        {
            if (propertyName == null) return null;

            List<string> errorsForName;
            _errors.TryGetValue(propertyName, out errorsForName);

            return errorsForName;
        }

        public bool HasErrors
        {
            get
            {
                return _errors.Any(kv => kv.Value != null && kv.Value.Count > 0);
            }
        }

        public Task ValidateAsync()
        {
            return Task.Run(() => Validate());
        }

        private object _lock = new object();
        public void Validate()
        {
            lock (_lock)
            {
                var validationContext = new ValidationContext(this, null, null);
                var validationResults = new List<ValidationResult>();

                Validator.TryValidateObject(this, validationContext, validationResults, true);

                foreach (var kv in _errors.ToList())
                {
                    if (validationResults.All(r => r.MemberNames.All(m => m != kv.Key)))
                    {
                        List<string> outLi;
                        _errors.TryRemove(kv.Key, out outLi);
                        OnErrorsChanged(kv.Key);
                    }
                }

                var q = from r in validationResults
                        from m in r.MemberNames
                        group r by m into g
                        select g;

                foreach (var prop in q)
                {
                    var messages = prop.Select(r => r.ErrorMessage).ToList();

                    if (_errors.ContainsKey(prop.Key))
                    {
                        List<string> outLi;
                        _errors.TryRemove(prop.Key, out outLi);
                    }

                    _errors.TryAdd(prop.Key, messages);
                    OnErrorsChanged(prop.Key);
                }
            }
        }
    }
}

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

namespace NotifyDataErrorInfo
{
    public class MinMaxValues : ValidatableModel
    {
        private int min;
        private int max;

        public int Min
        {
            get
            {
                return min;
            }

            set
            {
                if (!min.Equals(value))
                {
                    min = value;
                    RaisePropertyChanged(nameof(Min));
                    OnErrorsChanged(nameof(Min));
                }
            }
        }

        public int Max
        {
            get
            {
                return max;
            }

            set
            {
                if (!max.Equals(value))
                {
                    max = value;
                    RaisePropertyChanged(nameof(Max));
                    OnErrorsChanged(nameof(Max));
                }
            }
        }

        public MinMaxValues()
        {
            Min = 0;
            Max = 0;
        }
    }
}

Это мой класс MediaEvent, который, как вы можете видеть, использует класс MinMaxValues ​​для MinMaxPresenters и MinMaxGhest.

using System.ComponentModel.DataAnnotations;

namespace NotifyDataErrorInfo
{
    public class MediaEvent: ValidatableModel
    {
        private string name;
        private MinMaxValues minMaxPresenters;
        private MinMaxValues minMaxGuests;

        public MediaEvent()
        {
            name = string.Empty;
            minMaxPresenters = new MinMaxValues();
            minMaxGuests = new MinMaxValues();

            this.Validate();
            this.minMaxPresenters.Validate();
            this.minMaxGuests.Validate();            }
        }

        [Required]
        [StringLength(10, MinimumLength = 5)]
        public string Name
        {
            get
            {
                return name;
            }

            set
            {
                if(!name.Equals(value))
                {
                    name = value;
                    RaisePropertyChanged(nameof(Name));
                }
            }
        }

        public MinMaxValues MinMaxPresenters
        {
            get
            {
                return minMaxPresenters;
            }

            set
            {
                if (!minMaxPresenters.Equals(value))
                {
                    minMaxPresenters = value;
                    RaisePropertyChanged(nameof(MinMaxPresenters));
                }
            }
        }

        public MinMaxValues MinMaxGuests
        {
            get
            {
                return minMaxGuests;
            }

            set
            {
                if (!minMaxGuests.Equals(value))
                {
                    minMaxGuests = value;
                    RaisePropertyChanged(nameof(MinMaxGuests));
                }
            }
        }
    }
}

Это XAML для моего главного окна

<Window 
    x:Class="NotifyDataErrorInfo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:NotifyDataErrorInfo"
    mc:Ignorable="d"
    Title="MainWindow"
    Height="209" Width="525"
    ResizeMode="NoResize">

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    <Grid Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="42*"/>
            <RowDefinition Height="43*"/>
            <RowDefinition Height="42*"/>
            <RowDefinition Height="43*"/>
        </Grid.RowDefinitions>

        <Label
            Content="Meeting Name: "
            Grid.Row="0" Grid.Column="0"/>

        <TextBox
            Text="{Binding Name}"
            Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3"/>

        <Label
            Content="Min Presenters: "
            Grid.Row="1" Grid.Column="0"/>

        <TextBox
            Text="{Binding MinMaxPresenters.Min}"
            Grid.Row="1" Grid.Column="1"/>

        <Label
            Content="Max Presenters: "
            Grid.Row="1" Grid.Column="2"/>

        <TextBox
            Text="{Binding MinMaxPresenters.Max}"
            Grid.Row="1" Grid.Column="3"/>

        <Label
            Content="Min Guests: "
            Grid.Row="2" Grid.Column="0"/>

        <TextBox
            Text="{Binding MinMaxGuests.Min}"
            Grid.Row="2" Grid.Column="1"/>

        <Label
            Content="Max Guests: "
            Grid.Row="2" Grid.Column="2"/>

        <TextBox
            Text="{Binding MinMaxGuests.Max}"
            Grid.Row="2" Grid.Column="3"/>

        <Button
            x:Name="TestButton"
            Content="TEST"
            Click="TestButton_Click"
            Grid.Row="3" Grid.Column="3"/>
    </Grid>
</Window>

Который загружается в App.xaml.cs с помощью

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    var mainWindow = new MainWindow();
    var mediaEvent = new MediaEvent();

    mainWindow.DataContext = mediaEvent;
    mainWindow.Show();
}

В классе MediaEvent я украсил свойство Name атрибутами [Required] и [StringLength(10, MinimumLength = 5)]. Эти работы, как и ожидалось. Когда вводится имя менее 5 символов или более 10 символов, я вижу красную рамку вокруг текстового поля имени, показывающую, что есть ошибка.

Что я не могу понять

Теперь я не уверен, как сделать проверку для MinMaxPresenters.Min, MinMaxPresenters.Max, MinMaxGhest.Min и MinMaxGhest.Max

Если я украслю свойство Min в классе MinMaxValues ​​чем-то вроде [Range(1, 5)], я могу подтвердить, что проверка происходит, и пользовательский интерфейс обновляется соответствующим образом.

Проблема заключается в том, что проверка применяется к значению Min для докладчиков и гостей. Мне нужно проверить разные значения Min для докладчиков и гостей.

Что я пробовал

В MediaEvent я подключился к событию PropertyChanged minMaxPresenters. В этом обработчике событий я пытался проверить значения Min и Max на основе правил для докладчиков (диапазон от 1 до 5). Если проверка не удалась, я попытался добавить в коллекцию _errors.

В моем конструкторе я добавил

minMaxPresenters.PropertyChanged += MinMaxPresenters_PropertyChanged;

а затем создал следующее

private void MinMaxPresenters_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Min")
    {
        if (minMaxPresenters.Min < 1)
        {
            _errors.TryAdd("MinMaxPresenters.Min", new List<string> { "A media event requires at least 1 presenter" });
            OnErrorsChanged("MinMaxPresenters.Min");
        }
    }
    else if (e.PropertyName == "Max")
    {
        if (minMaxPresenters.Max <= minMaxPresenters.Min)
        {
            _errors.TryAdd("MinMaxPresenters.Max", new List<string> { "The max presenters must be greater than the min" });
            OnErrorsChanged("MinMaxPresenters.Max");
        }
        else if (minMaxPresenters.Max > 5)
        {
            _errors.TryAdd("MinMaxPresenters.Max", new List<string> { "A media event can't have more than 5 presenters" });
            OnErrorsChanged("MinMaxPresenters.Max");
        }
    }
}

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

Я рядом? Я все об этом ошибаюсь?

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

редактировать

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

Кроме того, на случай, если кто-то прокомментирует опубликование _errors, это будет простой способ добавить ошибки. Я бы в идеале создал бы методы AddError и RemoveError.

1 ответ

Ваша проблема здесь

_errors.TryAdd("MinMaxPresenters.Min", new List<string> 
      { "A media event requires at least 1 presenter" });

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

MinMaxPresenters._errors.TryAdd("Min", new List<string>
      { "A media event requires at least 1 presenter" });

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

В моей структуре, которую я разработал, я могу сделать то, что вы изначально пробовали, но я анализирую строку ошибки "MinMaxPresenters.Min", а затем ищу свойства с именем "MinMaxPresenters" и автоматически пересылаю ошибку проверки на подчиненные объекты.

Моя реализация AddErrors является

    public void AddErrors(string path, IEnumerable<Exception> errors, bool nest = true)
    {
        var exceptions = errors as IList<Exception> ?? errors.ToList();

        var nestedPath = path.Split('.').ToList();
        if (nestedPath.Count > 1 && nest)
        {
            var tail = string.Join(".", nestedPath.Skip(1));
            // Try and get a child property as Maybe<INotifyDataExceptionInfo> 
            // and if it exists pass the error
            // downwards after stripping off the first part of
            // the path.
            var notifyDataExceptionInfo = this.TryGet<INotifyDataExceptionInfo,INotifyDataExceptionInfo>(nestedPath[0]);
            if(notifyDataExceptionInfo.IsSome)
                notifyDataExceptionInfo.Value.AddErrors(tail, exceptions);
        }

        _Errors.RemoveKey(path);
        foreach (var error in exceptions)
        {
            _Errors.Add(path, error);
        }

        RaiseErrorEvents(path);

    }

** TryGet - метод получения значения свойства по ссылке

** Полная реализация может быть найдена в этом месте.

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