@@ -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