Проверка в Xamarin с использованием DataAnnotation

Я пытаюсь добавить проверки в Xamarin. Для этого я использовал этот пост в качестве ориентира: Проверка с использованием аннотации данных. Следующее - это мое поведение.

public class EntryValidationBehavior : Behavior<Entry>
    {
        private Entry _associatedObject;

        protected override void OnAttachedTo(Entry bindable)
        {
            base.OnAttachedTo(bindable);
            // Perform setup       

            _associatedObject = bindable;

            _associatedObject.TextChanged += _associatedObject_TextChanged;
        }

        void _associatedObject_TextChanged(object sender, TextChangedEventArgs e)
        {
            var source = _associatedObject.BindingContext as ValidationBase;
            if (source != null && !string.IsNullOrEmpty(PropertyName))
            {
                var errors = source.GetErrors(PropertyName).Cast<string>();
                if (errors != null && errors.Any())
                {
                    var borderEffect = _associatedObject.Effects.FirstOrDefault(eff => eff is BorderEffect);
                    if (borderEffect == null)
                    {
                        _associatedObject.Effects.Add(new BorderEffect());
                    }

                    if (Device.OS != TargetPlatform.Windows)
                    {
                        //_associatedObject.BackgroundColor = Color.Red;
                    }
                }
                else
                {
                    var borderEffect = _associatedObject.Effects.FirstOrDefault(eff => eff is BorderEffect);
                    if (borderEffect != null)
                    {
                        _associatedObject.Effects.Remove(borderEffect);
                    }

                    if (Device.OS != TargetPlatform.Windows)
                    {
                        _associatedObject.BackgroundColor = Color.Default;
                    }
                }
            }
        }

        protected override void OnDetachingFrom(Entry bindable)
        {
            base.OnDetachingFrom(bindable);
            // Perform clean up

            _associatedObject.TextChanged -= _associatedObject_TextChanged;

            _associatedObject = null;
        }

        public string PropertyName { get; set; }
    }

В моем Поведении я добавляю фон и границу как красный. Я хочу автоматически добавить ярлык к этой записи. Поэтому я подумал о том, чтобы добавить stacklayout над этой записью и добавить ярлык и эту запись в нем. Очень утомительно писать этикетку для каждого элемента управления. Возможно ли это или может быть каким-то другим способом?

Обновленный метод (неэффективен):

 <Entry Text="{Binding Email}" Placeholder="Enter Email ID" Keyboard="Email" HorizontalTextAlignment="Center">
            <Entry.Behaviors>
                <validation:EntryValidationBehavior PropertyName="Email" />
            </Entry.Behaviors>
        </Entry>
        <Label Text="{Binding Errors[Email], Converter={StaticResource FirstErrorConverter}" 
               IsVisible="{Binding Errors[Email], Converter={StaticResource ErrorLabelVisibilityConverter}"  
               FontSize="Small" 
               TextColor="Red" />
        <Entry Text="{Binding Password}" Placeholder="Enter Password" Keyboard="Text" IsPassword="true" HorizontalTextAlignment="Center">
            <Entry.Behaviors>
                <validation:EntryValidationBehavior PropertyName="Password" />
            </Entry.Behaviors>
        </Entry>
        <Label Text="{Binding Errors[Password], Converter={StaticResource FirstErrorConverter}" 
               IsVisible="{Binding Errors[Password], Converter={StaticResource ErrorLabelVisibilityConverter}"  
               FontSize="Small" 
               TextColor="Red" />
        <Entry Text="{Binding ConfirmPassword}" Placeholder="Confirm Password" Keyboard="Text" IsPassword="true" HorizontalTextAlignment="Center">
            <Entry.Behaviors>
                <validation:EntryValidationBehavior PropertyName="ConfirmPassword" />
            </Entry.Behaviors>
        </Entry>

конвертер

public class FirstErrorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            ICollection<string> errors = value as ICollection<string>;
            return errors != null && errors.Count > 0 ? errors.ElementAt(0) : null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

Оценщик:

public class ValidationBase : BindableBase, INotifyDataErrorInfo
    {
        private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
        public Dictionary<string, List<string>> Errors
        {
            get { return _errors; }
        }


        public ValidationBase()
        {
            ErrorsChanged += ValidationBase_ErrorsChanged;
        }

        private void ValidationBase_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
        {
            OnPropertyChanged("HasErrors");
            OnPropertyChanged("Errors");
            OnPropertyChanged("ErrorsList");
        }

        #region INotifyDataErrorInfo Members

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public IEnumerable GetErrors(string propertyName)
        {
            if (!string.IsNullOrEmpty(propertyName))
            {
                if (_errors.ContainsKey(propertyName) && (_errors[propertyName].Any()))
                {
                    return _errors[propertyName].ToList();
                }
                else
                {
                    return new List<string>();
                }
            }
            else
            {
                return _errors.SelectMany(err => err.Value.ToList()).ToList();
            }
        }

        public bool HasErrors
        {
            get
            {
                return _errors.Any(propErrors => propErrors.Value.Any());
            }
        }

        #endregion

        protected virtual void ValidateProperty(object value, [CallerMemberName] string propertyName = null)
        {
            var validationContext = new ValidationContext(this, null)
            {
                MemberName = propertyName
            };

            var validationResults = new List<ValidationResult>();
            Validator.TryValidateProperty(value, validationContext, validationResults);

            RemoveErrorsByPropertyName(propertyName);

            HandleValidationResults(validationResults);
            RaiseErrorsChanged(propertyName);
        }

        private void RemoveErrorsByPropertyName(string propertyName)
        {
            if (_errors.ContainsKey(propertyName))
            {
                _errors.Remove(propertyName);
            }

           // RaiseErrorsChanged(propertyName);
        }

        private void HandleValidationResults(List<ValidationResult> validationResults)
        {
            var resultsByPropertyName = from results in validationResults
                                        from memberNames in results.MemberNames
                                        group results by memberNames into groups
                                        select groups;

            foreach (var property in resultsByPropertyName)
            {
                _errors.Add(property.Key, property.Select(r => r.ErrorMessage).ToList());
               // RaiseErrorsChanged(property.Key);
            }
        }

        private void RaiseErrorsChanged(string propertyName)
        {
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }

        public IList<string> ErrorsList
        {
            get
            {
                return GetErrors(string.Empty).Cast<string>().ToList();
            }
        }
    }

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

Ответы

Ответ 1

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

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

Итак, шаг за шагом это будет:

  1. Создайте свой оберточный компонент;
  2. Направьте этот контроль на свое поведение;
  3. Выставлять/обрабатывать свойства и события, которые вы собираетесь использовать;
  4. Замените простой CheckableEntryView Entry этим CheckableEntryView на свой код.

Вот код компонента XAML:

<ContentView xmlns="http://xamarin.com/schemas/2014/forms" 
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         x:Class="MyApp.CheckableEntryView">
<ContentView.Content>
    <StackLayout>
        <Label x:Name="lblContraintText" 
               Text="This is not valid"
               TextColor="Red"
               AnchorX="0"
               AnchorY="0"
               IsVisible="False"/>
        <Entry x:Name="txtEntry"
               Text="Value"/>
    </StackLayout>
</ContentView.Content>

И это код:

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CheckableEntryView : ContentView
{
    public event EventHandler<TextChangedEventArgs> TextChanged;

    private BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(CheckableEntryView), string.Empty);
    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue( TextProperty, value); }
    }

    public CheckableEntryView ()
    {
        InitializeComponent();

        txtEntry.TextChanged += OnTextChanged;
        txtEntry.SetBinding(Entry.TextProperty, new Binding(nameof(Text), BindingMode.Default, null, null, null, this));
    }

    protected virtual void OnTextChanged(object sender, TextChangedEventArgs args)
    {
        TextChanged?.Invoke(this, args);
    }

    public Task ShowValidationMessage()
    {
        Task.Yield();
        lblContraintText.IsVisible = true;
        return lblContraintText.ScaleTo(1, 250, Easing.SinInOut);
    }

    public Task HideValidationMessage()
    {
        Task.Yield();
        return lblContraintText.ScaleTo(0, 250, Easing.SinInOut)
            .ContinueWith(t => 
                Device.BeginInvokeOnMainThread(() => lblContraintText.IsVisible = false));
    }
}

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

void _associatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
    if(e.NewTextValue == "test")
        ((CheckableEntryView)sender).ShowValidationMessage();
    else
        ((CheckableEntryView)sender).HideValidationMessage();
}

Чтобы использовать его, вы делаете в основном то же самое, что и раньше:

<local:CheckableEntryView HorizontalOptions="FillAndExpand">
    <local:CheckableEntryView.Behaviors>
        <local:EntryValidationBehavior PropertyName="Test"/><!-- this property is not being used on this example -->
    </local:CheckableEntryView.Behaviors>
</local:CheckableEntryView>

Вот как это выглядит:

gif sample

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

Надеюсь, это поможет вам.

Ответ 2

Проведя когда-нибудь, я придумал гибрид всех предложений. Ваш FirstErrorConverter запускается несколько раз, так как вы вызываете свойство ErrorsList. Вместо этого используйте словарь с _errors качестве поля поддержки. Вот как выглядит ViewModelBase:

public ViewModelBase()
{
    PropertyInfo[] properties = GetType().GetProperties();
    foreach (PropertyInfo property in properties)
    {
        var attrs = property.GetCustomAttributes(true);
        if (attrs?.Length > 0)
        {
            Errors[property.Name] = new SmartCollection<ValidationResult>();
        }
    }
}

private Dictionary<string, SmartCollection<ValidationResult>> _errors = new Dictionary<string, SmartCollection<ValidationResult>>();
public Dictionary<string, SmartCollection<ValidationResult>> Errors
{
    get => _errors;
    set => SetProperty(ref _errors, value);
}

protected void Validate(string propertyName, string propertyValue)
{
    var validationContext = new ValidationContext(this, null)
    {
        MemberName = propertyName
    };

    var validationResults = new List<ValidationResult>();
    var isValid = Validator.TryValidateProperty(propertyValue, validationContext, validationResults);

    if (!isValid)
    {
        Errors[propertyName].Reset(validationResults);
    }
    else
    {
        Errors[propertyName].Clear();
    }
}

Поскольку ObservableCollection запускает событие CollectionChanged для каждого элемента add, я пошел с SmartCollection с дополнительным свойством FirstItem

public class SmartCollection<T> : ObservableCollection<T>
{
    public T FirstItem => Items.Count > 0 ? Items[0] : default(T);

    public SmartCollection()
        : base()
    {
    }

    public SmartCollection(IEnumerable<T> collection)
        : base(collection)
    {
    }

    public SmartCollection(List<T> list)
        : base(list)
    {
    }

    public void AddRange(IEnumerable<T> range)
    {
        foreach (var item in range)
        {
            Items.Add(item);
        }

        this.OnPropertyChanged(new PropertyChangedEventArgs("FirstItem"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    public void Reset(IEnumerable<T> range)
    {
        this.Items.Clear();

        AddRange(range);
    }
}

Вот как выглядит мой xaml:

<StackLayout Orientation="Vertical">
    <Entry Placeholder="Email" Text="{Binding Email}">
        <Entry.Behaviors>
            <behaviors:EntryValidatorBehavior PropertyName="Email" />
        </Entry.Behaviors>
    </Entry>
    <Label Text="{Binding Errors[Email].FirstItem, Converter={StaticResource firstErrorToTextConverter}}"
           IsVisible="{Binding Errors[Email].Count, Converter={StaticResource errorToBoolConverter}}" />

    <Entry Placeholder="Password" Text="{Binding Password}">
        <Entry.Behaviors>
            <behaviors:EntryValidatorBehavior PropertyName="Password" />
        </Entry.Behaviors>
    </Entry>
    <Label Text="{Binding Errors[Password].FirstItem, Converter={StaticResource firstErrorToTextConverter}}"
           IsVisible="{Binding Errors[Password].Count, Converter={StaticResource errorToBoolConverter}}" />
</StackLayout>

Все остальное же!

look gif

Ответ 3

Использование проверки в корпоративных приложениях из электронной книги приложений EntryLabelView компонента EntryLabelView ниже, XAML может выглядеть следующим образом:

xmlns:local="clr-namespace:View"
...
<local:EntryLabelView ValidatableObject="{Binding MyValue, Mode=TwoWay}"
                      ValidateCommand="{Binding ValidateValueCommand}" />

ViewModel:

private ValidatableObject<string> _myValue;

public ViewModel()
{
  _myValue = new ValidatableObject<string>();

  _myValue.Validations.Add(new IsNotNullOrEmptyRule<string> { ValidationMessage = "A value is required." });
}

public ValidatableObject<string> MyValue
{
  get { return _myValue; }
  set
  {
      _myValue = value;
      OnPropertyChanged(nameof(MyValue));
  }
}

public ICommand ValidateValueCommand => new Command(() => ValidateValue());

private bool ValidateValue()
{
  return _myValue.Validate(); //updates ValidatableObject.Errors
}

Реализации классов, на которые ссылаются, включая ValidatableObject, IsNotNullOrEmptyRule, EventToCommandBehavior и FirstValidationErrorConverter можно найти в образце eShopOnContainers.

EntryLabelView.xaml: (Обратите внимание на использование Source={x:Reference view})

<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:converters="clr-namespace:Toolkit.Converters;assembly=Toolkit"
         xmlns:behaviors="clr-namespace:Toolkit.Behaviors;assembly=Toolkit"
         x:Name="view"
         x:Class="View.EntryLabelView">
  <ContentView.Resources>
    <converters:FirstValidationErrorConverter x:Key="FirstValidationErrorConverter" />
  </ContentView.Resources>
  <ContentView.Content>
    <StackLayout>
      <Entry Text="{Binding ValidatableObject.Value, Mode=TwoWay, Source={x:Reference view}}">
        <Entry.Behaviors>
          <behaviors:EventToCommandBehavior 
                            EventName="TextChanged"
                            Command="{Binding ValidateCommand, Source={x:Reference view}}" />
        </Entry.Behaviors>
      </Entry>
      <Label Text="{Binding ValidatableObject.Errors, Source={x:Reference view},
                        Converter={StaticResource FirstValidationErrorConverter}}" />
    </StackLayout>
  </ContentView.Content>
</ContentView>

EntryLabelView.xaml.cs: (Обратите внимание на использование OnPropertyChanged).

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class EntryLabelView : ContentView
{
    public EntryLabelView ()
    {
        InitializeComponent ();
    }

    public static readonly BindableProperty ValidatableObjectProperty = BindableProperty.Create(
        nameof(ValidatableObject), typeof(ValidatableObject<string>), typeof(EntryLabelView), default(ValidatableObject<string>),
        BindingMode.TwoWay,
        propertyChanged: (b, o, n) => ((EntryLabelView)b).ValidatableObjectChanged(o, n));

    public ValidatableObject<string> ValidatableObject
    {
        get { return (ValidatableObject<string>)GetValue(ValidatableObjectProperty); }
        set { SetValue(ValidatableObjectProperty, value); }
    }

    void ValidatableObjectChanged(object o, object n)
    {
        ValidatableObject = (ValidatableObject<string>)n;
        OnPropertyChanged(nameof(ValidatableObject));
    }

    public static readonly BindableProperty ValidateCommandProperty = BindableProperty.Create(
        nameof(Command), typeof(ICommand), typeof(EntryLabelView), null,
        propertyChanged: (b, o, n) => ((EntryLabelView)b).CommandChanged(o, n));

    public ICommand ValidateCommand
    {
        get { return (ICommand)GetValue(ValidateCommandProperty); }
        set { SetValue(ValidateCommandProperty, value); }
    }

    void CommandChanged(object o, object n)
    {
        ValidateCommand = (ICommand)n;
        OnPropertyChanged(nameof(ValidateCommand));
    }
}