提问人:Safi Mustafa 提问时间:2/27/2018 最后编辑:Tobias TheelSafi Mustafa 更新时间:4/24/2020 访问量:5986
使用 DataAnnotation 在 Xamarin 中进行验证
Validation in Xamarin using 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; }
}
在我的行为中,我添加了一个背景和一个红色边框。我想自动为此条目添加标签。所以我在想在这个条目上方添加一个堆栈布局,并在其中添加一个标签和那个条目。为每个控件编写标签非常繁琐。是否有可能,或者可能是其他更好的方法?
更新的方法(效率不高):
<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 次。其次,红色边框首次显示大约需要一秒钟。
答:
这种方法看起来很棒,并且为改进提供了许多可能性。
只是为了不让它没有答案,我认为您可以尝试创建一个组件来包装您想要处理的视图并公开您需要在外部使用的事件和属性。 它将是可重复使用的,它可以解决问题。
因此,逐步将是:
- 创建包装器组件;
- 将此控件定位到您的行为;
- 公开/处理您打算使用的属性和事件;
- 在代码中将 simple 替换为 this。
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>
这是它的样子:
我没有在此示例代码上绑定验证消息,但您可以保持相同的想法。
我希望它对你有所帮助。
评论
经过一段时间,我想出了所有建议的混合体。
由于您调用了更改属性,因此 Your 被多次触发。请改用 Dictionary 作为支持字段。ViewModelBase 的样子如下:FirstErrorConverter
ErrorsList
_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,并添加了一个名为ObservableCollection
CollectionChanged
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>
其他一切都是一样的!
使用 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 示例中找到。ValidatableObject
IsNotNullOrEmptyRule
EventToCommandBehavior
FirstValidationErrorConverter
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));
}
}
我可能有点晚了,但对于将来偶然发现这篇文章的任何人。
也许可以尝试一下这个库:https://www.nuget.org/packages/Xamarin.AttributeValidation/
它允许您通过简单地将 Attributes 放在 ViewModel 中的属性上方来验证 UI。就是这样。没什么可做的了。就像你在 ASP.NET Core 中所做的那样。验证消息自动以小浮动文本气泡的形式显示,悬停在条目上。与原生 Android 验证非常相似。
有关示例或详细说明,请查看 GitHub 存储库:https://github.com/kevin-mueller/Xamarin.AttributeValidation
评论
StackLayout
Grid
Entry
Errors