Проверка в 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
Этот подход выглядит потрясающе и открывает множество возможностей для улучшения.
Просто, чтобы не позволить этому без ответа, я думаю, вы можете попытаться создать компонент, который обертывает взгляды, которые вы хотите обработать, и выставлять события и свойства, необходимые для использования снаружи. Это будет многоразово, и это трюк.
Итак, шаг за шагом это будет:
- Создайте свой оберточный компонент;
- Направьте этот контроль на свое поведение;
- Выставлять/обрабатывать свойства и события, которые вы собираетесь использовать;
- Замените простой
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>
Вот как это выглядит:
Я не связал сообщение проверки на этом примере кода, но вы можете сохранить ту же идею.
Надеюсь, это поможет вам.
Ответ 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>
Все остальное же!
Ответ 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));
}
}