Skip to content

Commit 28b1977

Browse files
committed
Code refactoring in ObservableValidator
1 parent e57ba22 commit 28b1977

File tree

2 files changed

+183
-30
lines changed

2 files changed

+183
-30
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs

Lines changed: 162 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,167 @@ public abstract class ObservableValidator : ObservableObject, INotifyDataErrorIn
3737
/// <inheritdoc/>
3838
public bool HasErrors => this.totalErrors > 0;
3939

40+
/// <summary>
41+
/// Compares the current and new values for a given property. If the value has changed,
42+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
43+
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
44+
/// </summary>
45+
/// <typeparam name="T">The type of the property that changed.</typeparam>
46+
/// <param name="field">The field storing the property's value.</param>
47+
/// <param name="newValue">The property's value after the change occurred.</param>
48+
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
49+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
50+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
51+
/// <remarks>
52+
/// This method is just like <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/>, just with the addition
53+
/// of the <paramref name="validate"/> parameter. If that is set to <see langword="true"/>, the new value will be
54+
/// validated and <see cref="ErrorsChanged"/> will be raised if needed. Following the behavior of the base method,
55+
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
56+
/// are not raised if the current and new value for the target property are the same.
57+
/// </remarks>
58+
protected bool SetProperty<T>(ref T field, T newValue, bool validate, [CallerMemberName] string? propertyName = null)
59+
{
60+
if (validate)
61+
{
62+
ValidateProperty(newValue, propertyName);
63+
}
64+
65+
return SetProperty(ref field, newValue, propertyName);
66+
}
67+
68+
/// <summary>
69+
/// Compares the current and new values for a given property. If the value has changed,
70+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
71+
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
72+
/// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,bool,string)"/>.
73+
/// </summary>
74+
/// <typeparam name="T">The type of the property that changed.</typeparam>
75+
/// <param name="field">The field storing the property's value.</param>
76+
/// <param name="newValue">The property's value after the change occurred.</param>
77+
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
78+
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
79+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
80+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
81+
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, bool validate, [CallerMemberName] string? propertyName = null)
82+
{
83+
if (validate)
84+
{
85+
ValidateProperty(newValue, propertyName);
86+
}
87+
88+
return SetProperty(ref field, newValue, comparer, propertyName);
89+
}
90+
91+
/// <summary>
92+
/// Compares the current and new values for a given property. If the value has changed,
93+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
94+
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event. Similarly to
95+
/// the <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/> method, this overload should only be
96+
/// used when <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/> can't be used directly.
97+
/// </summary>
98+
/// <typeparam name="T">The type of the property that changed.</typeparam>
99+
/// <param name="oldValue">The current property value.</param>
100+
/// <param name="newValue">The property's value after the change occurred.</param>
101+
/// <param name="callback">A callback to invoke to update the property value.</param>
102+
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
103+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
104+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
105+
/// <remarks>
106+
/// This method is just like <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/>, just with the addition
107+
/// of the <paramref name="validate"/> parameter. As such, following the behavior of the base method,
108+
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
109+
/// are not raised if the current and new value for the target property are the same.
110+
/// </remarks>
111+
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool validate, [CallerMemberName] string? propertyName = null)
112+
{
113+
if (validate)
114+
{
115+
ValidateProperty(newValue, propertyName);
116+
}
117+
118+
return SetProperty(oldValue, newValue, callback, propertyName);
119+
}
120+
121+
/// <summary>
122+
/// Compares the current and new values for a given property. If the value has changed,
123+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
124+
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
125+
/// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},bool,string)"/>.
126+
/// </summary>
127+
/// <typeparam name="T">The type of the property that changed.</typeparam>
128+
/// <param name="oldValue">The current property value.</param>
129+
/// <param name="newValue">The property's value after the change occurred.</param>
130+
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
131+
/// <param name="callback">A callback to invoke to update the property value.</param>
132+
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
133+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
134+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
135+
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool validate, [CallerMemberName] string? propertyName = null)
136+
{
137+
if (validate)
138+
{
139+
ValidateProperty(newValue, propertyName);
140+
}
141+
142+
return SetProperty(oldValue, newValue, comparer, callback, propertyName);
143+
}
144+
145+
/// <summary>
146+
/// Compares the current and new values for a given nested property. If the value has changed,
147+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
148+
/// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
149+
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>, with the difference being that this
150+
/// method is used to relay properties from a wrapped model in the current instance. For more info, see the docs for
151+
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>.
152+
/// </summary>
153+
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
154+
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
155+
/// <param name="oldValue">The current property value.</param>
156+
/// <param name="newValue">The property's value after the change occurred.</param>
157+
/// <param name="model">The model </param>
158+
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
159+
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
160+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
161+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
162+
protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string? propertyName = null)
163+
{
164+
if (validate)
165+
{
166+
ValidateProperty(newValue, propertyName);
167+
}
168+
169+
return SetProperty(oldValue, newValue, model, callback, propertyName);
170+
}
171+
172+
/// <summary>
173+
/// Compares the current and new values for a given nested property. If the value has changed,
174+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
175+
/// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
176+
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>,
177+
/// with the difference being that this method is used to relay properties from a wrapped model in the
178+
/// current instance. For more info, see the docs for
179+
/// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>.
180+
/// </summary>
181+
/// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
182+
/// <typeparam name="T">The type of property (or field) to set.</typeparam>
183+
/// <param name="oldValue">The current property value.</param>
184+
/// <param name="newValue">The property's value after the change occurred.</param>
185+
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
186+
/// <param name="model">The model </param>
187+
/// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
188+
/// <param name="validate">If <see langword="true"/>, <paramref name="newValue"/> will also be validated.</param>
189+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
190+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
191+
protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, bool validate, [CallerMemberName] string? propertyName = null)
192+
{
193+
if (validate)
194+
{
195+
ValidateProperty(newValue, propertyName);
196+
}
197+
198+
return SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
199+
}
200+
40201
/// <inheritdoc/>
41202
[Pure]
42203
public IEnumerable GetErrors(string? propertyName)
@@ -77,9 +238,8 @@ private IEnumerable GetAllErrors()
77238
/// </summary>
78239
/// <param name="value">The value to test for the specified property.</param>
79240
/// <param name="propertyName">The name of the property to validate.</param>
80-
/// <returns>Whether or not the validation was successful.</returns>
81241
/// <exception cref="ArgumentNullException">Thrown when <paramref name="propertyName"/> is <see langword="null"/>.</exception>
82-
protected bool ValidateProperty(object? value, [CallerMemberName] string? propertyName = null)
242+
private void ValidateProperty(object? value, string? propertyName)
83243
{
84244
if (propertyName is null)
85245
{
@@ -138,8 +298,6 @@ protected bool ValidateProperty(object? value, [CallerMemberName] string? proper
138298
{
139299
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
140300
}
141-
142-
return isValid;
143301
}
144302

145303
/// <summary>

UnitTests/UnitTests.Shared/Mvvm/Test_ObservableValidator.cs

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -119,17 +119,27 @@ public void Test_ObservableValidator_GetErrors()
119119

120120
[TestCategory("Mvvm")]
121121
[TestMethod]
122-
public void Test_ObservableValidator_ValidateReturn()
122+
[DataRow(null, false)]
123+
[DataRow("", false)]
124+
[DataRow("No", false)]
125+
[DataRow("This text is really, really too long for the target property", false)]
126+
[DataRow("1234", true)]
127+
[DataRow("01234567890123456789", true)]
128+
[DataRow("Hello world", true)]
129+
public void Test_ObservableValidator_ValidateReturn(string value, bool isValid)
123130
{
124-
var model = new Person();
131+
var model = new Person { Name = value };
132+
133+
Assert.AreEqual(model.HasErrors, !isValid);
125134

126-
Assert.IsFalse(model.ValidateName(null));
127-
Assert.IsFalse(model.ValidateName(string.Empty));
128-
Assert.IsFalse(model.ValidateName("No"));
129-
Assert.IsFalse(model.ValidateName("This text is really, really too long for the target property"));
130-
Assert.IsTrue(model.ValidateName("1234"));
131-
Assert.IsTrue(model.ValidateName("01234567890123456789"));
132-
Assert.IsTrue(model.ValidateName("Hello world"));
135+
if (isValid)
136+
{
137+
Assert.IsTrue(!model.GetErrors(nameof(Person.Name)).Cast<object>().Any());
138+
}
139+
else
140+
{
141+
Assert.IsTrue(model.GetErrors(nameof(Person.Name)).Cast<object>().Any());
142+
}
133143
}
134144

135145
public class Person : ObservableValidator
@@ -142,17 +152,7 @@ public class Person : ObservableValidator
142152
public string Name
143153
{
144154
get => this.name;
145-
set
146-
{
147-
ValidateProperty(value);
148-
149-
SetProperty(ref this.name, value);
150-
}
151-
}
152-
153-
public bool ValidateName(string value)
154-
{
155-
return ValidateProperty(value, nameof(Name));
155+
set => SetProperty(ref this.name, value, true);
156156
}
157157

158158
private int age;
@@ -161,12 +161,7 @@ public bool ValidateName(string value)
161161
public int Age
162162
{
163163
get => this.age;
164-
set
165-
{
166-
ValidateProperty(value);
167-
168-
SetProperty(ref this.age, value);
169-
}
164+
set => SetProperty(ref this.age, value, true);
170165
}
171166
}
172167
}

0 commit comments

Comments
 (0)