Skip to content

Commit ffecfc4

Browse files
committed
Added ObservableValidator.TrySetProperty methods
1 parent e4116ca commit ffecfc4

File tree

1 file changed

+164
-0
lines changed

1 file changed

+164
-0
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,115 @@ protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<
217217
return propertyChanged;
218218
}
219219

220+
/// <summary>
221+
/// Tries to validate a new value for a specified property. If the validation is successful,
222+
/// <see cref="ObservableObject.SetProperty{T}(ref T,T,string?)"/> is called, otherwise no state change is performed.
223+
/// </summary>
224+
/// <typeparam name="T">The type of the property that changed.</typeparam>
225+
/// <param name="field">The field storing the property's value.</param>
226+
/// <param name="newValue">The property's value after the change occurred.</param>
227+
/// <param name="errors">The resulting validation errors, if any.</param>
228+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
229+
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
230+
protected bool TrySetProperty<T>(ref T field, T newValue, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
231+
{
232+
return TryValidateProperty(newValue, propertyName, out errors) &&
233+
SetProperty(ref field, newValue, propertyName);
234+
}
235+
236+
/// <summary>
237+
/// Tries to validate a new value for a specified property. If the validation is successful,
238+
/// <see cref="ObservableObject.SetProperty{T}(ref T,T,IEqualityComparer{T},string?)"/> is called, otherwise no state change is performed.
239+
/// </summary>
240+
/// <typeparam name="T">The type of the property that changed.</typeparam>
241+
/// <param name="field">The field storing the property's value.</param>
242+
/// <param name="newValue">The property's value after the change occurred.</param>
243+
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
244+
/// <param name="errors">The resulting validation errors, if any.</param>
245+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
246+
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
247+
protected bool TrySetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
248+
{
249+
return TryValidateProperty(newValue, propertyName, out errors) &&
250+
SetProperty(ref field, newValue, comparer, propertyName);
251+
}
252+
253+
/// <summary>
254+
/// Tries to validate a new value for a specified property. If the validation is successful,
255+
/// <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string?)"/> is called, otherwise no state change is performed.
256+
/// </summary>
257+
/// <typeparam name="T">The type of the property that changed.</typeparam>
258+
/// <param name="oldValue">The current property value.</param>
259+
/// <param name="newValue">The property's value after the change occurred.</param>
260+
/// <param name="callback">A callback to invoke to update the property value.</param>
261+
/// <param name="errors">The resulting validation errors, if any.</param>
262+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
263+
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
264+
protected bool TrySetProperty<T>(T oldValue, T newValue, Action<T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
265+
{
266+
return TryValidateProperty(newValue, propertyName, out errors) &&
267+
SetProperty(oldValue, newValue, callback, propertyName);
268+
}
269+
270+
/// <summary>
271+
/// Tries to validate a new value for a specified property. If the validation is successful,
272+
/// <see cref="ObservableObject.SetProperty{T}(T,T,IEqualityComparer{T},Action{T},string?)"/> is called, otherwise no state change is performed.
273+
/// </summary>
274+
/// <typeparam name="T">The type of the property that changed.</typeparam>
275+
/// <param name="oldValue">The current property value.</param>
276+
/// <param name="newValue">The property's value after the change occurred.</param>
277+
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
278+
/// <param name="callback">A callback to invoke to update the property value.</param>
279+
/// <param name="errors">The resulting validation errors, if any.</param>
280+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
281+
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
282+
protected bool TrySetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
283+
{
284+
return TryValidateProperty(newValue, propertyName, out errors) &&
285+
SetProperty(oldValue, newValue, comparer, callback, propertyName);
286+
}
287+
288+
/// <summary>
289+
/// Tries to validate a new value for a specified property. If the validation is successful,
290+
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string?)"/> is called, otherwise no state change is performed.
291+
/// </summary>
292+
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
293+
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
294+
/// <param name="oldValue">The current property value.</param>
295+
/// <param name="newValue">The property's value after the change occurred.</param>
296+
/// <param name="model">The model </param>
297+
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
298+
/// <param name="errors">The resulting validation errors, if any.</param>
299+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
300+
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
301+
protected bool TrySetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
302+
where TModel : class
303+
{
304+
return TryValidateProperty(newValue, propertyName, out errors) &&
305+
SetProperty(oldValue, newValue, model, callback, propertyName);
306+
}
307+
308+
/// <summary>
309+
/// Tries to validate a new value for a specified property. If the validation is successful,
310+
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string?)"/> is called, otherwise no state change is performed.
311+
/// </summary>
312+
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
313+
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
314+
/// <param name="oldValue">The current property value.</param>
315+
/// <param name="newValue">The property's value after the change occurred.</param>
316+
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
317+
/// <param name="model">The model </param>
318+
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
319+
/// <param name="errors">The resulting validation errors, if any.</param>
320+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
321+
/// <returns>Whether the validation was successful and the property value changed as well.</returns>
322+
protected bool TrySetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, out IReadOnlyCollection<ValidationResult> errors, [CallerMemberName] string? propertyName = null)
323+
where TModel : class
324+
{
325+
return TryValidateProperty(newValue, propertyName, out errors) &&
326+
SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
327+
}
328+
220329
/// <inheritdoc/>
221330
[Pure]
222331
public IEnumerable GetErrors(string? propertyName)
@@ -329,6 +438,61 @@ private void ValidateProperty(object? value, string? propertyName)
329438
}
330439
}
331440

441+
/// <summary>
442+
/// Tries to validate a property with a specified name and a given input value, and returns
443+
/// the computed errors, if any. If the property is valid, it is assumed that its value is
444+
/// about to be set in the current object. Otherwise, no observable local state is modified.
445+
/// </summary>
446+
/// <param name="value">The value to test for the specified property.</param>
447+
/// <param name="propertyName">The name of the property to validate.</param>
448+
/// <param name="errors">The resulting validation errors, if any.</param>
449+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="propertyName"/> is <see langword="null"/>.</exception>
450+
private bool TryValidateProperty(object? value, string? propertyName, out IReadOnlyCollection<ValidationResult> errors)
451+
{
452+
if (propertyName is null)
453+
{
454+
ThrowArgumentNullExceptionForNullPropertyName();
455+
}
456+
457+
// Add the cached errors list for later use.
458+
if (!this.errors.TryGetValue(propertyName!, out List<ValidationResult>? propertyErrors))
459+
{
460+
propertyErrors = new List<ValidationResult>();
461+
462+
this.errors.Add(propertyName!, propertyErrors);
463+
}
464+
465+
bool hasErrors = propertyErrors.Count > 0;
466+
467+
List<ValidationResult> localErrors = new List<ValidationResult>();
468+
469+
// Validate the property, by adding new errors to the local list
470+
bool isValid = Validator.TryValidateProperty(
471+
value,
472+
new ValidationContext(this, null, null) { MemberName = propertyName },
473+
localErrors);
474+
475+
// We only modify the state if the property is valid and it wasn't so before. In this case, we
476+
// clear the cached list of errors (which is visible to consumers) and raise the necessary events.
477+
if (isValid && hasErrors)
478+
{
479+
propertyErrors.Clear();
480+
481+
this.totalErrors--;
482+
483+
if (this.totalErrors == 0)
484+
{
485+
OnPropertyChanged(HasErrorsChangedEventArgs);
486+
}
487+
488+
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
489+
}
490+
491+
errors = localErrors;
492+
493+
return isValid;
494+
}
495+
332496
#pragma warning disable SA1204
333497
/// <summary>
334498
/// Throws an <see cref="ArgumentNullException"/> when a property name given as input is <see langword="null"/>.

0 commit comments

Comments
 (0)