我正在尝试在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次。其次红色边框需要大约一秒时间才能显示出来。
答案 0 :(得分:6)
这种方法看起来很神奇,并为改进提供了很多可能性。
为了不让它没有答案,我想你可以尝试创建一个组件来包装你想要处理的视图,并公开你需要在外面使用的事件和属性。 它将是可重用的,它可以解决问题。
所以,一步一步是:
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>
这就是它的样子:
我没有在此示例代码上绑定验证消息,但您可以保持相同的想法。
我希望它可以帮到你。
答案 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示例中可以找到所引用的类的实现,包括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));
}
}
答案 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>
其他都一样!
答案 3 :(得分:0)
我可能会迟到一些,但是对于将来偶然发现这篇文章的人来说。
也许可以尝试一下这个图书馆:https://www.nuget.org/packages/Xamarin.AttributeValidation/
它允许您通过简单地将属性放在ViewModel中的属性上方来验证UI。而已。没什么可做的。就像在ASP.NET Core中一样。验证消息会以浮动文本气泡的形式自动显示,将鼠标悬停在条目上。与原生Android验证非常相似。
有关示例或详细说明,请查看GitHub存储库:https://github.com/kevin-mueller/Xamarin.AttributeValidation