c# - mvc - Validación en Xamarin utilizando DataAnnotation
datatype dataannotations (3)
Después de pasar algún tiempo, he encontrado un híbrido de todas las sugerencias. Su FirstErrorConverter
se FirstErrorConverter
varias veces desde que llama a la propiedad ErrorsList
modificada. En su lugar, use un Diccionario con _errors
como campo de respaldo. Así es como se ve 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();
}
}
Desde que ObservableCollection
SmartCollection el evento CollectionChanged
en cada elemento agregado, fui con SmartCollection con una propiedad adicional llamada 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);
}
}
Así es como se ve mi 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>
Todo lo demás es igual !!
Estoy tratando de agregar validaciones en Xamarin. Para eso he usado esta publicación como punto de referencia: Validación usando Anotación de Datos . Siguiente es mi comportamiento.
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; }
}
En mi comportamiento agrego un fondo y un borde en rojo. Quiero agregar automáticamente una etiqueta a esta entrada. Así que estaba pensando en agregar un stacklayout sobre esta entrada y agregar una etiqueta y esa entrada en ella. Es muy tedioso escribir una etiqueta para cada control. ¿Es posible o puede ser de otra manera mejor?
Método actualizado (no eficiente):
<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>
Convertidor
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();
}
}
Validador
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();
}
}
}
El problema con esta solución es que se llama a FirstErrorConverter para cada propiedad en una página cada vez que cambia alguna de las propiedades. Entonces, por ejemplo, hay 10 propiedades que necesitan ser validadas. El método se llamará 10 veces. En segundo lugar, el borde rojo tarda aproximadamente un segundo en mostrarse por primera vez.
Ese enfoque se ve increíble y abre muchas posibilidades de mejora.
Solo para no dejarlo sin una respuesta, creo que puedes intentar crear un componente que envuelva las vistas que quieras manejar y exponer los eventos y propiedades que necesitas usar en el exterior. Será reutilizable y hará el truco.
Entonces, paso a paso sería:
- Crea tu componente envoltorio;
- Apunta este control en tu comportamiento;
- Exponer / manejar las propiedades y eventos que pretende utilizar;
- Reemplace la
Entry
simple por esteCheckableEntryView
en su código.
Aquí está el código XAML del componente:
<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>
Y es el código detrás:
[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));
}
}
He cambiado la lógica de eventos del comportamiento para hacerlo más simple. Sólo para su información, es:
void _associatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
if(e.NewTextValue == "test")
((CheckableEntryView)sender).ShowValidationMessage();
else
((CheckableEntryView)sender).HideValidationMessage();
}
Para usarlo haces básicamente lo mismo que hiciste antes:
<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>
Así es como se vería:
No vinculé el mensaje de validación en este código de ejemplo, pero puede mantener la misma idea.
Espero que te ayude.
Usando la validación en aplicaciones empresariales del libro electrónico Xamarin.FormsEnterprise Application Patterns y el componente EntryLabelView
continuación, el XAML puede tener el siguiente aspecto:
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
}
Las implementaciones de las clases a las que se hace referencia, incluidos ValidatableObject
, IsNotNullOrEmptyRule
, EventToCommandBehavior
y FirstValidationErrorConverter
se pueden encontrar en el ejemplo de eShopOnContainers .
EntryLabelView.xaml
: (tenga en cuenta el uso de 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
: (tenga en cuenta el uso de 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));
}
}