使用 DataAnnotation 在 Xamarin 中进行验证

Validation in Xamarin using DataAnnotation

提问人:Safi Mustafa 提问时间:2/27/2018 最后编辑:Tobias TheelSafi Mustafa 更新时间:4/24/2020 访问量:5986

问:

我正在尝试在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; }
    }

在我的行为中,我添加了一个背景和一个红色边框。我想自动为此条目添加标签。所以我在想在这个条目上方添加一个堆栈布局,并在其中添加一个标签和那个条目。为每个控件编写标签非常繁琐。是否有可能,或者可能是其他更好的方法?

更新的方法(效率不高):

 <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 次。其次,红色边框首次显示大约需要一秒钟。

C# Xamarin Xamarin.Forms 数据批注 行为

评论

0赞 ethane 3/2/2018
对于多标签问题,你可以把你的行为应用到像 a 这样的布局上,或者像 .你会进入这个布局。这使您能够在行为中动态地将视图/控件添加到此布局中。StackLayoutGridEntry
0赞 Safi Mustafa 3/2/2018
行为与条目相关联。我将如何访问标签?你能举一些例子吗?您认为这是进行验证的最佳方式吗?在验证的情况下,Xamarin 的文档不是很好。
0赞 Martin Zikmund 3/4/2018
转换器被执行了很多次,因为所有绑定都绑定到更改的集合,因此它们也必须更新。特定属性错误集合是否未更改并不重要,绑定无法知道这一点。但是,这应该不会妨碍性能,评估转换器应该非常轻量级。您是否尝试过单步执行代码,看看实际需要一秒钟才能完成的内容?对于任何类型的操作来说,这似乎都太多了。Errors
1赞 Tobias Theel 3/8/2018
我使用ValidationRules,它们工作得非常整洁。你试过这些吗?

答:

6赞 Diego Rafael Souza 3/9/2018 #1

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

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

因此,逐步将是:

  1. 创建包装器组件;
  2. 将此控件定位到您的行为;
  3. 公开/处理您打算使用的属性和事件;
  4. 在代码中将 simple 替换为 this。EntryCheckableEntryView

下面是组件的 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赞 memsranga 10/19/2018
这很棒,唯一的问题是属性级联从自定义视图到内部视图应该手动完成!
0赞 Diego Rafael Souza 10/19/2018
当然@Shan。它限制了内部视图的太多特征。我敢打赌,验证规则方法应该更适合,正如在问题评论中所说的那样。
3赞 memsranga 10/21/2018 #2

经过一段时间,我想出了所有建议的混合体。 由于您调用了更改属性,因此 Your 被多次触发。请改用 Dictionary 作为支持字段。ViewModelBase 的样子如下:FirstErrorConverterErrorsList_errors

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

由于每个项目添加时都会发生火灾事件,因此我使用了 SmartCollection,并添加了一个名为ObservableCollectionCollectionChangedFirstItem

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

5赞 Benl 10/22/2018 #3

使用 Xamarin.FormsEnterprise 应用程序模式电子书中的企业应用中的验证和以下组件,XAML 可能如下所示:EntryLabelView

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

视图模型:

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));
    }
}
0赞 Kevin Mueller 4/24/2020 #4

我可能有点晚了,但对于将来偶然发现这篇文章的任何人。

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

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

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