Skip to content
This repository was archived by the owner on May 1, 2024. It is now read-only.

Commit 3d439c8

Browse files
[Breaking Changes] ValidationBehavior Async checks support (#912)
* Refactored validation behavior * Refactored to ValueTask * added configure await false * Fixed isRunning
1 parent 1fa5b5c commit 3d439c8

8 files changed

+119
-50
lines changed

src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/CharactersValidationBehavior.shared.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
46
using Xamarin.Forms;
57

68
namespace Xamarin.CommunityToolkit.Behaviors
@@ -63,8 +65,9 @@ public int MaximumCharacterCount
6365
set => SetValue(MaximumCharacterCountProperty, value);
6466
}
6567

66-
protected override bool Validate(object value)
67-
=> base.Validate(value) && Validate(value?.ToString());
68+
protected override async ValueTask<bool> ValidateAsync(object value, CancellationToken token)
69+
=> await base.ValidateAsync(value, token).ConfigureAwait(false)
70+
&& Validate(value?.ToString());
6871

6972
static void OnCharacterTypePropertyChanged(BindableObject bindable, object oldValue, object newValue)
7073
{

src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/EmailValidationBehavior.shared.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ protected override string DefaultRegexPattern
2121

2222
protected override RegexOptions DefaultRegexOptions => RegexOptions.IgnoreCase;
2323

24-
protected override object DecorateValue()
24+
protected override object Decorate(object value)
2525
{
26-
var value = base.DecorateValue()?.ToString();
26+
var stringValue = base.Decorate(value)?.ToString();
2727
#if NETSTANDARD1_0
28-
return value;
28+
return stringValue;
2929
#else
30-
if (string.IsNullOrWhiteSpace(value))
31-
return value;
30+
if (string.IsNullOrWhiteSpace(stringValue))
31+
return stringValue;
3232

3333
try
3434
{
@@ -43,11 +43,11 @@ static string DomainMapper(Match match)
4343
}
4444

4545
// Normalize the domain
46-
return normalizerRegex.Replace(value, DomainMapper);
46+
return normalizerRegex.Replace(stringValue, DomainMapper);
4747
}
4848
catch (ArgumentException)
4949
{
50-
return value;
50+
return stringValue;
5151
}
5252
#endif
5353
}

src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/MultiValidationBehavior.shared.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Collections.ObjectModel;
33
using System.Collections.Specialized;
44
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
57
using Xamarin.CommunityToolkit.Behaviors.Internals;
68
using Xamarin.Forms;
79

@@ -62,14 +64,18 @@ public static object GetError(BindableObject bindable)
6264
public static void SetError(BindableObject bindable, object value)
6365
=> bindable.SetValue(ErrorProperty, value);
6466

65-
protected override bool Validate(object value)
67+
protected override async ValueTask<bool> ValidateAsync(object value, CancellationToken token)
6668
{
67-
var errors = children.Where(c =>
69+
await Task.WhenAll(children.Select(c =>
6870
{
6971
c.Value = value;
70-
c.ForceValidate();
71-
return c.IsNotValid;
72-
}).Select(c => GetError(c));
72+
return c.ValidateNestedAsync(token).AsTask();
73+
})).ConfigureAwait(false);
74+
75+
if (token.IsCancellationRequested)
76+
return IsValid;
77+
78+
var errors = children.Where(c => c.IsNotValid).Select(c => GetError(c));
7379

7480
if (!errors.Any())
7581
{

src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/NumericValidationBehavior.shared.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.Globalization;
2+
using System.Threading;
3+
using System.Threading.Tasks;
24
using Xamarin.CommunityToolkit.Behaviors.Internals;
35
using Xamarin.Forms;
46

@@ -69,30 +71,31 @@ public int MaximumDecimalPlaces
6971
set => SetValue(MaximumDecimalPlacesProperty, value);
7072
}
7173

72-
protected override object DecorateValue()
73-
=> base.DecorateValue()?.ToString()?.Trim();
74+
protected override object Decorate(object value)
75+
=> base.Decorate(value)?.ToString()?.Trim();
7476

75-
protected override bool Validate(object value)
77+
protected override ValueTask<bool> ValidateAsync(object value, CancellationToken token)
7678
{
7779
var valueString = value as string;
7880
if (!(double.TryParse(valueString, out var numeric)
7981
&& numeric >= MinimumValue
8082
&& numeric <= MaximumValue))
81-
return false;
83+
return new ValueTask<bool>(false);
8284

8385
var decimalDelimeterIndex = valueString.IndexOf(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);
8486
var hasDecimalDelimeter = decimalDelimeterIndex >= 0;
8587

8688
// If MaximumDecimalPlaces equals zero, ".5" or "14." should be considered as invalid inputs.
8789
if (hasDecimalDelimeter && MaximumDecimalPlaces == 0)
88-
return false;
90+
return new ValueTask<bool>(false);
8991

9092
var decimalPlaces = hasDecimalDelimeter
9193
? valueString.Substring(decimalDelimeterIndex + 1, valueString.Length - decimalDelimeterIndex - 1).Length
9294
: 0;
9395

94-
return decimalPlaces >= MinimumDecimalPlaces
95-
&& decimalPlaces <= MaximumDecimalPlaces;
96+
return new ValueTask<bool>(
97+
decimalPlaces >= MinimumDecimalPlaces &&
98+
decimalPlaces <= MaximumDecimalPlaces);
9699
}
97100
}
98101
}

src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/RequiredStringValidationBehavior.shared.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Xamarin.CommunityToolkit.Behaviors.Internals;
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Xamarin.CommunityToolkit.Behaviors.Internals;
24
using Xamarin.Forms;
35

46
namespace Xamarin.CommunityToolkit.Behaviors
@@ -23,7 +25,7 @@ public string RequiredString
2325
set => SetValue(RequiredStringProperty, value);
2426
}
2527

26-
protected override bool Validate(object value)
27-
=> value?.ToString() == RequiredString;
28+
protected override ValueTask<bool> ValidateAsync(object value, CancellationToken token)
29+
=> new ValueTask<bool>(value?.ToString() == RequiredString);
2830
}
2931
}

src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/TextValidationBehavior.shared.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Text;
22
using System.Text.RegularExpressions;
3+
using System.Threading;
4+
using System.Threading.Tasks;
35
using Xamarin.CommunityToolkit.Behaviors.Internals;
46
using Xamarin.Forms;
57

@@ -96,39 +98,37 @@ public RegexOptions RegexOptions
9698

9799
protected virtual RegexOptions DefaultRegexOptions => RegexOptions.None;
98100

99-
protected override object DecorateValue()
101+
protected override object Decorate(object value)
100102
{
101-
var value = base.DecorateValue()?.ToString();
103+
var stringValue = base.Decorate(value)?.ToString();
102104
var flags = DecorationFlags;
103105

104106
if (flags.HasFlag(TextDecorationFlags.NullToEmpty))
105-
value ??= string.Empty;
107+
stringValue ??= string.Empty;
106108

107-
if (value == null)
109+
if (stringValue == null)
108110
return null;
109111

110112
if (flags.HasFlag(TextDecorationFlags.TrimStart))
111-
value = value.TrimStart();
113+
stringValue = stringValue.TrimStart();
112114

113115
if (flags.HasFlag(TextDecorationFlags.TrimEnd))
114-
value = value.TrimEnd();
116+
stringValue = stringValue.TrimEnd();
115117

116118
if (flags.HasFlag(TextDecorationFlags.NormalizeWhiteSpace))
117-
value = NormalizeWhiteSpace(value);
119+
stringValue = NormalizeWhiteSpace(stringValue);
118120

119-
return value;
121+
return stringValue;
120122
}
121123

122-
protected override bool Validate(object value)
124+
protected override ValueTask<bool> ValidateAsync(object value, CancellationToken token)
123125
{
124126
var text = value?.ToString();
125-
if (text == null)
126-
return false;
127-
128-
var length = text.Length;
129-
return length >= MinimumLength &&
130-
length <= MaximumLength &&
131-
(regex?.IsMatch(text) ?? false);
127+
return new ValueTask<bool>(
128+
text != null &&
129+
text.Length >= MinimumLength &&
130+
text.Length <= MaximumLength &&
131+
(regex?.IsMatch(text) ?? false));
132132
}
133133

134134
static void OnRegexPropertyChanged(BindableObject bindable, object oldValue, object newValue)

src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/UriValidationBehavior.shared.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
24
using Xamarin.Forms;
35

46
namespace Xamarin.CommunityToolkit.Behaviors
@@ -23,8 +25,8 @@ public UriKind UriKind
2325
set => SetValue(UriKindProperty, value);
2426
}
2527

26-
protected override bool Validate(object value)
27-
=> base.Validate(value)
28+
protected override async ValueTask<bool> ValidateAsync(object value, CancellationToken token)
29+
=> await base.ValidateAsync(value, token).ConfigureAwait(false)
2830
&& Uri.IsWellFormedUriString(value?.ToString(), UriKind);
2931
}
3032
}

src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System.ComponentModel;
2+
using System.Threading;
3+
using System.Threading.Tasks;
24
using System.Windows.Input;
35
using Xamarin.Forms;
46

@@ -21,6 +23,12 @@ public abstract class ValidationBehavior : BaseBehavior<VisualElement>
2123
public static readonly BindableProperty IsValidProperty =
2224
BindableProperty.Create(nameof(IsValid), typeof(bool), typeof(ValidationBehavior), true, BindingMode.OneWayToSource, propertyChanged: OnIsValidPropertyChanged);
2325

26+
/// <summary>
27+
/// Backing BindableProperty for the <see cref="IsRunning"/> property.
28+
/// </summary>
29+
public static readonly BindableProperty IsRunningProperty =
30+
BindableProperty.Create(nameof(IsRunning), typeof(bool), typeof(ValidationBehavior), false, BindingMode.OneWayToSource);
31+
2432
/// <summary>
2533
/// Backing BindableProperty for the <see cref="ValidStyle"/> property.
2634
/// </summary>
@@ -63,6 +71,8 @@ public abstract class ValidationBehavior : BaseBehavior<VisualElement>
6371

6472
BindingBase defaultValueBinding;
6573

74+
CancellationTokenSource validationTokenSource;
75+
6676
/// <summary>
6777
/// Indicates whether or not the current value is considered valid. This is a bindable property.
6878
/// </summary>
@@ -72,6 +82,15 @@ public bool IsValid
7282
set => SetValue(IsValidProperty, value);
7383
}
7484

85+
/// <summary>
86+
/// Indicates whether or not the validation is in progress now (waiting for an asynchronous call is finished).
87+
/// </summary>
88+
public bool IsRunning
89+
{
90+
get => (bool)GetValue(IsRunningProperty);
91+
set => SetValue(IsRunningProperty, value);
92+
}
93+
7594
/// <summary>
7695
/// Indicates whether or not the current value is considered not valid. This is a bindable property.
7796
/// </summary>
@@ -142,11 +161,13 @@ public ICommand ForceValidateCommand
142161
/// <summary>
143162
/// Forces the behavior to make a validation pass.
144163
/// </summary>
145-
public void ForceValidate() => UpdateState(true);
164+
public void ForceValidate() => _ = UpdateStateAsync(true);
146165

147-
protected virtual object DecorateValue() => Value;
166+
internal ValueTask ValidateNestedAsync(CancellationToken token) => UpdateStateAsync(true, token);
148167

149-
protected abstract bool Validate(object value);
168+
protected virtual object Decorate(object value) => value;
169+
170+
protected abstract ValueTask<bool> ValidateAsync(object value, CancellationToken token);
150171

151172
protected override void OnAttachedTo(VisualElement bindable)
152173
{
@@ -156,7 +177,7 @@ protected override void OnAttachedTo(VisualElement bindable)
156177
currentStatus = ValidationFlags.ValidateOnAttaching;
157178

158179
OnValuePropertyNamePropertyChanged();
159-
UpdateState(false);
180+
_ = UpdateStateAsync(false);
160181
isAttaching = false;
161182
}
162183

@@ -180,12 +201,12 @@ protected override void OnViewPropertyChanged(object sender, PropertyChangedEven
180201
currentStatus = View.IsFocused
181202
? ValidationFlags.ValidateOnFocusing
182203
: ValidationFlags.ValidateOnUnfocusing;
183-
UpdateState(false);
204+
_ = UpdateStateAsync(false);
184205
}
185206
}
186207

187208
protected static void OnValidationPropertyChanged(BindableObject bindable, object oldValue, object newValue)
188-
=> ((ValidationBehavior)bindable).UpdateState(false);
209+
=> _ = ((ValidationBehavior)bindable).UpdateStateAsync(false);
189210

190211
static void OnIsValidPropertyChanged(BindableObject bindable, object oldValue, object newValue)
191212
=> ((ValidationBehavior)bindable).OnIsValidPropertyChanged();
@@ -232,12 +253,38 @@ void OnValuePropertyNamePropertyChanged()
232253
SetBinding(ValueProperty, defaultValueBinding);
233254
}
234255

235-
void UpdateState(bool isForced)
256+
async ValueTask UpdateStateAsync(bool isForced, CancellationToken? parentToken = null)
236257
{
237258
if ((View?.IsFocused ?? false) && Flags.HasFlag(ValidationFlags.ForceMakeValidWhenFocused))
259+
{
260+
IsRunning = true;
261+
ResetValidationTokenSource(null);
238262
IsValid = true;
263+
IsRunning = false;
264+
}
239265
else if (isForced || (currentStatus != ValidationFlags.None && Flags.HasFlag(currentStatus)))
240-
IsValid = Validate(DecorateValue());
266+
{
267+
IsRunning = true;
268+
using var tokenSource = new CancellationTokenSource();
269+
var token = parentToken ?? tokenSource.Token;
270+
ResetValidationTokenSource(tokenSource);
271+
272+
try
273+
{
274+
var isValid = await ValidateAsync(Decorate(Value), token).ConfigureAwait(false);
275+
276+
if (token.IsCancellationRequested)
277+
return;
278+
279+
validationTokenSource = null;
280+
IsValid = isValid;
281+
IsRunning = false;
282+
}
283+
catch (TaskCanceledException)
284+
{
285+
return;
286+
}
287+
}
241288

242289
UpdateStyle();
243290
}
@@ -251,5 +298,11 @@ void UpdateStyle()
251298
? ValidStyle
252299
: InvalidStyle;
253300
}
301+
302+
void ResetValidationTokenSource(CancellationTokenSource newTokenSource)
303+
{
304+
validationTokenSource?.Cancel();
305+
validationTokenSource = newTokenSource;
306+
}
254307
}
255308
}

0 commit comments

Comments
 (0)