使用DataAnnotation在Xamarin中进行验证

时间:2018-02-27 10:22:50

标签: c# xamarin xamarin.forms data-annotations behavior

我正在尝试在Xamarin中添加验证。为此,我使用这篇文章作为参考点:Validation using Data Annotation。以下是我的行为。

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次。其次红色边框需要大约一秒时间才能显示出来。

4 个答案:

答案 0 :(得分:6)

这种方法看起来很神奇,并为改进提供了很多可能性。

为了不让它没有答案,我想你可以尝试创建一个组件来包装你想要处理的视图,并公开你需要在外面使用的事件和属性。 它将是可重用的,它可以解决问题。

所以,一步一步是:

  1. 创建您的包装器组件;
  2. 针对您的行为定位此控件;
  3. 公开/处理您打算使用的属性和事件;
  4. 在代码上用Entry替换简单的CheckableEntryView
  5. 以下是组件的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

    我没有在此示例代码上绑定验证消息,但您可以保持相同的想法。

    我希望它可以帮到你。

答案 1 :(得分:5)

使用Xamarin.FormsEnterprise应用程序模式电子书中的Validation in Enterprise Apps和下面的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
}

eShopOnContainers示例中可以找到所引用的类的实现,包括ValidatableObjectIsNotNullOrEmptyRuleEventToCommandBehaviorFirstValidationErrorConverter

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));
    }
}

答案 2 :(得分:3)

花了一段时间后,我提出了所有建议的混合体。 由于您呼叫FirstErrorConverter的属性已更改,因此您的ErrorsList被触发了多次。而是使用带有_errors作为后备字段的Dictionary。这是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事件,因此我选择了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 :(得分:0)

我可能会迟到一些,但是对于将来偶然发现这篇文章的人来说。

也许可以尝试一下这个图书馆:https://www.nuget.org/packages/Xamarin.AttributeValidation/

它允许您通过简单地将属性放在ViewModel中的属性上方来验证UI。而已。没什么可做的。就像在ASP.NET Core中一样。验证消息会以浮动文本气泡的形式自动显示,将鼠标悬停在条目上。与原生Android验证非常相似。

有关示例或详细说明,请查看GitHub存储库:https://github.com/kevin-mueller/Xamarin.AttributeValidation