Form Behavior that groups validators #227
Replies: 2 comments 5 replies
-
Update:This is not needed. I can just bind to the ValueProperty. This is still valuable specially the implementation of the FormValidationBehavior below since I can build on that to bind to the ValueProperty and avoid all these changes. It's funny how you don't get these simple ideas sometimes. Experimenting with a BaseValidationBehavior with Custom Bindings (using abstract classes)So, I've experimented a little on my side with allowing a more complex binding and I seem to get the behavior I want when I combine it with an Attachable BindableProperty. What I have so far is the same ValidationBehavior base class except that I rename it BaseValidationBehavior and remove the binding configuration and the DefaultPropertyName and they get replaced with: /// <summary>
/// You can call this method to override how the default binding is done for the ValueProperty.
/// </summary>
protected abstract BindingBase CreateBinding();
/// <summary>
/// You can override this method to change how the ValueProperty binding is done.
/// </summary>
protected virtual void ConfigureValueBinding()
{
if (IsBound(ValueProperty, _defaultValueBinding))
{
_defaultValueBinding = null;
return;
}
_defaultValueBinding = CreateBinding();
SetBinding(ValueProperty, _defaultValueBinding);
} Those are two abstract methods that handle how it binds the ValueProperty, it allows for the FormValidationBehavior (or whatever name if approved) to override What happens to the old ValidationBehavior?In the /// <summary>
/// Backing BindableProperty for the <see cref="ValuePropertyName"/> property.
/// </summary>
public static readonly BindableProperty ValuePropertyNameProperty =
BindableProperty.Create(nameof(ValuePropertyName), typeof(string), typeof(ValidationBehavior), defaultValueCreator: GetDefaultValuePropertyName, propertyChanged: OnValuePropertyNamePropertyChanged);
/// <summary>
/// Allows the user to override the property that will be used as the value to validate. This is a bindable property.
/// </summary>
public string? ValuePropertyName
{
get => (string?)GetValue(ValuePropertyNameProperty);
set => SetValue(ValuePropertyNameProperty, value);
}
/// <summary>
/// Default value property name
/// </summary>
protected virtual string DefaultValuePropertyName { get; } = Entry.TextProperty.PropertyName;
static void OnValuePropertyNamePropertyChanged(BindableObject bindable, object oldValue, object newValue)
=> ((ValidationBehavior)bindable).OnValuePropertyNamePropertyChanged();
static object GetDefaultValuePropertyName(BindableObject bindable)
=> ((ValidationBehavior)bindable).DefaultValuePropertyName; And finally it overrides the ///<inheritdoc/>
protected override BindingBase CreateBinding()
{
return new Binding
{
Path = ValuePropertyName,
Source = View
};
} And I create a wrapper that calls the base's ConfigureValueBinding when the ValuePropertyName is changed. void OnValuePropertyNamePropertyChanged()
{
base.ConfigureValueBinding();
} Why do I think these changes are worth it?All these changes mean more flexibility but at the end everything is pretty much the same to the end user. Limited Testing Done So FarI've done a little testing and doing the changes above do not affect existing behaviors. I am actually using some on a personal project of mine and using a private fork with these changes seems to be running well. I haven't been able to run the tests since they were updated to MAUI Preview 11 and I am waiting for it to come with Visual Studio to avoid futur conflicts. Thoughts?I still think it can be improved of course, and I am open to suggestions that's why I opened this discussion. Implementing FormValidationBehaviorSo now that I can custom binding more than just to a property name, I can use a And I created a prototype like this: public class FormValidationBehavior : BaseValidationBehavior
{
///<inheritdoc/>
public FormValidationBehavior() : base()
{
DefaultVariableMultiValueConverter = new VariableMultiValueConverter { ConditionType = MultiBindingCondition.All };
ConfigurationValueBindingRequired += OnConfigurationRequired;
}
/// <summary>
/// Default VariableMultiValueConverter
/// </summary>
protected virtual VariableMultiValueConverter DefaultVariableMultiValueConverter { get; }
/// <summary>
/// Backing BindableProperty for the <see cref="VariableMultiValueConverter"/> property.
/// </summary>
public static readonly BindableProperty VariableMultiValueConverterProperty =
BindableProperty.Create(nameof(VariableMultiValueConverter), typeof(VariableMultiValueConverter), typeof(FormValidationBehavior), defaultValueCreator: CreateDefaultConverter);
static object CreateDefaultConverter(BindableObject bindable) => ((FormValidationBehavior)bindable).DefaultVariableMultiValueConverter;
/// <summary>
/// Converter in charge of converting all the IsValid from the individual behaviors to a single boolean that shows in the IsValid.
/// </summary>
public VariableMultiValueConverter VariableMultiValueConverter
{
get => (VariableMultiValueConverter)GetValue(VariableMultiValueConverterProperty);
set => SetValue(VariableMultiValueConverterProperty, value);
}
/// <summary>
/// This is the backing field of the References which will hold references to all the behaviors.
/// </summary>
readonly ObservableCollection<BaseValidationBehavior> _behaviors = new();
/// <summary>
/// All behaviors that are part of this <see cref="FormValidationBehavior"/>. This is a bindable property.
/// </summary>
public IList<BaseValidationBehavior> Behaviors => _behaviors;
public static readonly BindableProperty RequiredProperty =
BindableProperty.CreateAttached("Required", typeof(bool), typeof(FormValidationBehavior), false, propertyChanged: OnRequiredPropertyChanged);
/// <summary>
/// Get's the RequiredProperty.
/// </summary>
/// <param name="bindable"></param>
/// <returns></returns>
public static bool GetRequired(BindableObject bindable)
{
return (bool)bindable.GetValue(RequiredProperty);
}
/// <summary>
/// Method to set the the required property on the attached property for a behavior/>.
/// </summary>
/// <param name="bindable">The <see cref="ValidationBehavior"/> on which we set the attached Error property value</param>
/// <param name="value">The value to set</param>
public static void SetRequired(BindableObject bindable, bool value)
{
bindable.SetValue(RequiredProperty, value);
}
static void OnRequiredPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
switch (bindable)
{
case BaseValidationBehavior vb:
ConfigurationValueBindingRequired?.Invoke(vb, EventArgs.Empty);
break;
}
}
/// <summary>
/// Event Handler used to tell the class that the binding needs to be updated.
/// </summary>
public static event EventHandler ConfigurationValueBindingRequired;
private void OnConfigurationRequired(object sender, EventArgs e)
{
Behaviors.Add((BaseValidationBehavior)sender);
ConfigureValueBinding();
}
protected override void ConfigureValueBinding()
{
base.ConfigureValueBinding();
}
///<inheritdoc/>
protected override BindingBase CreateBinding()
{
var bindings = new List<BindingBase>();
foreach (var behavior in Behaviors)
{
bindings.Add(new Binding
{
Source = behavior,
Path = nameof(behavior.IsValid)
});
}
return new MultiBinding
{
Bindings = bindings,
Converter = VariableMultiValueConverter
};
}
protected override ValueTask<bool> ValidateAsync(object? value, CancellationToken token)
{
if (value is bool convertedResult)
{
return new ValueTask<bool>(convertedResult);
}
return new ValueTask<bool>(false);
}
} And it doesn't even have to live inside the project. Anyone could just inherit the And I can use it like this: <Entry Text="{Binding SmtpEmailUser.Name}" x:Name="nameTxtBox" Style="{StaticResource BaseEntry}">
<Entry.Behaviors>
<mctbehaviors:TextValidationBehavior MinimumLength="1"
MaximumLength="255"
Flags="ValidateOnValueChanged"
behaviors:FormValidationBehavior.Required="True"/>
</Entry.Behaviors>
</Entry>
<Label Text="Email Address" />
<Entry x:Name="emailTxtBox"
Text="{Binding SmtpEmailUser.EmailAddress}"
Style="{StaticResource BaseEntry}">
<Entry.Behaviors>
<mctbehaviors:EmailValidationBehavior ValidStyle="{StaticResource ValidEntry}"
behaviors:FormValidationBehavior.Required="True"
InvalidStyle="{StaticResource InvalidEntry} "
Flags="ValidateOnValueChanged"/>
</Entry.Behaviors>
</Entry>
<Button Text="Save Email Settings"
x:Name="EmailSaveButton" Style="{StaticResource LeftSideButton}"
IsEnabled="{Binding IsValid, Source={x:Reference FormValidator}, Mode=TwoWay}">
<Button.Behaviors>
<behaviors:FormValidationBehavior Flags="ValidateOnValueChanged" x:Name="FormValidator" />
</Button.Behaviors>
</Button>
And you wouldn't be able to click that button until all the validators with FormValidationBehavior.Required="True" had passed their validation. That's the feature I'd like to bring to the MAUI community toolkit. I would of course like to add more like Error Messages support using a similar attachable property, and even a FormattedErrorMessage that will join them into a string where each Error Message is a line in the string. Improvements? Thoughts?Any ideas on how to make this better? Simpler maybe? I'm open to it. |
Beta Was this translation helpful? Give feedback.
-
Closed as per the Community Stand-up discussion here: https://youtu.be/N9wMcBP4jtg?t=2889 |
Beta Was this translation helpful? Give feedback.
-
Hi,
I wanted to put forward a new feature that I think could be useful.
So right now we have the MultiValidationBehavior that works great for multiple behaviors on one entry. But I think it would be nice to have a similar behavior but that instead of attaching itself to an entry could attach itself to a layout component and kind of "add up" all the results of the descendant validators.
The objective of this feature is to be able do the same thing MultiValidationBehavior allows but for multiple entries.
I know we can do something similar by using MultiBinding and naming each validator, but wouldn't it be nice to be able to do this automatically, and have a single property we could bind "Submit" buttons to that handles all this logic for us?
Right now, the abstract ValidatorBehavior binds the ValueProperty here:
Maui/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs
Line 311 in 83fd89e
And by default it uses this binding:
where ValuePropertyName is just
Entry.TextProperty.PropertyName
by default.What I think would work would be if this Binding was more customizable than just changing the property name.
Say you could override how the binding happens you could use MultiBinding in here and bind yourself to the IsValid property of all descendant ValidatorBehavior, and use the VariableMultiValueConverter of your choice to convert all the booleans to a single bool which would be the value.
What do you all think about this?
Beta Was this translation helpful? Give feedback.
All reactions