I am trying to add validations in Xamarin. For that I have used this post as a reference point: Validation using Data Annotation. Following is my Behavior.
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; }
}
In my Behavior I add a background and a border as red. I want to automatically add a label to this Entry. So I was thinking to Add a stacklayout above this Entry and add a label and that Entry in it. Its very tedious to write a label for every control. Is it possible or may be some other better way?
Updated Method (Not Efficient):
<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>
Converter
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();
}
}
Validator:
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();
}
}
}
The problem with this solution is that FirstErrorConverter is called for each property in a page every time any one of the property changes. So for example there are 10 properties that need to be validated. The method will be called 10 times. Secondly The Red Border takes about a second to display for the first time.
PropertyChanged—the form validates a data field when its value changes. Manually (the default value)—the form validates data fields when the Validate() method is called. You can also call the Validate(String) method to validate a specific data field.
Setup Xamarin Community ToolkitOpen an existing project, or create a new project using the Blank Forms App template. In the Solution Explorer panel, right click on your project name and select Manage NuGet Packages. Search for Xamarin. CommunityToolkit, and choose the desired NuGet Package from the list.
That approach looks amazing, and open a lot of possibilities for improvements.
Just to don't let it without an answer, I think you can try to create a component that wraps the views you wanna handle and expose the events and properties you need to use outside. It'll be reusable and it does the trick.
So, step-by-step it would be:
Entry
by this CheckableEntryView
on your code.Here is the component's XAML code:
<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>
And it's code-behind:
[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));
}
}
I've changed the behavior's event logic to make it simpler. Just for your information, it is:
void _associatedObject_TextChanged(object sender, TextChangedEventArgs e)
{
if(e.NewTextValue == "test")
((CheckableEntryView)sender).ShowValidationMessage();
else
((CheckableEntryView)sender).HideValidationMessage();
}
To use it you do basically the same thing you did before:
<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>
This is how it would looks like:
I didn't bound the validation message on this sample code, but you can keep the same idea.
I hope it helps you.
Using Validation in Enterprise Apps from the Xamarin.FormsEnterprise Application Patterns eBook and the EntryLabelView
component below, the XAML might look as follows:
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
}
Implementations of classes referenced, including ValidatableObject
, IsNotNullOrEmptyRule
, EventToCommandBehavior
, and FirstValidationErrorConverter
can be found in the eShopOnContainers sample.
EntryLabelView.xaml
: (Please note the use of 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
: (Please note the use of 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));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With