forked from CommunityToolkit/WindowsCommunityToolkit
-
Notifications
You must be signed in to change notification settings - Fork 3
/
ObservableObject.cs
457 lines (404 loc) · 25.5 KB
/
ObservableObject.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma warning disable SA1512
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
// more info in ThirdPartyNotices.txt in the root of the project.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace Microsoft.Toolkit.Mvvm.ComponentModel
{
/// <summary>
/// A base class for objects of which the properties must be observable.
/// </summary>
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
{
/// <inheritdoc cref="INotifyPropertyChanged.PropertyChanged"/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc cref="INotifyPropertyChanging.PropertyChanging"/>
public event PropertyChangingEventHandler? PropertyChanging;
/// <summary>
/// Performs the required configuration when a property has changed, and then
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
/// </summary>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <remarks>The base implementation only raises the <see cref="PropertyChanged"/> event.</remarks>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Performs the required configuration when a property is changing, and then
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
/// </summary>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <remarks>The base implementation only raises the <see cref="PropertyChanging"/> event.</remarks>
protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
/// value, then raises the <see cref="PropertyChanged"/> event.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
/// if the current and new value for the target property are the same.
/// </remarks>
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string? propertyName = null)
{
// We duplicate the code here instead of calling the overload because we can't
// guarantee that the invoked SetProperty<T> will be inlined, and we need the JIT
// to be able to see the full EqualityComparer<T>.Default.Equals call, so that
// it'll use the intrinsics version of it and just replace the whole invocation
// with a direct comparison when possible (eg. for primitive numeric types).
// This is the fastest SetProperty<T> overload so we particularly care about
// the codegen quality here, and the code is small and simple enough so that
// duplicating it still doesn't make the whole class harder to maintain.
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
field = newValue;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
/// value, then raises the <see cref="PropertyChanged"/> event.
/// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,string)"/>.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, [CallerMemberName] string? propertyName = null)
{
if (comparer.Equals(field, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
field = newValue;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
/// value, then raises the <see cref="PropertyChanged"/> event.
/// This overload is much less efficient than <see cref="SetProperty{T}(ref T,T,string)"/> and it
/// should only be used when the former is not viable (eg. when the target property being
/// updated does not directly expose a backing field that can be passed by reference).
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="oldValue">The current property value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="callback">A callback to invoke to update the property value.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
/// if the current and new value for the target property are the same.
/// </remarks>
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, [CallerMemberName] string? propertyName = null)
{
return SetProperty(oldValue, newValue, EqualityComparer<T>.Default, callback, propertyName);
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
/// value, then raises the <see cref="PropertyChanged"/> event.
/// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},string)"/>.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="oldValue">The current property value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="callback">A callback to invoke to update the property value.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, [CallerMemberName] string? propertyName = null)
{
if (comparer.Equals(oldValue, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
callback(newValue);
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Compares the current and new values for a given nested property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property and then raises the
/// <see cref="PropertyChanged"/> event. The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>,
/// with the difference being that this method is used to relay properties from a wrapped model in the
/// current instance. This type is useful when creating wrapping, bindable objects that operate over
/// models that lack support for notification (eg. for CRUD operations).
/// Suppose we have this model (eg. for a database row in a table):
/// <code>
/// public class Person
/// {
/// public string Name { get; set; }
/// }
/// </code>
/// We can then use a property to wrap instances of this type into our observable model (which supports
/// notifications), injecting the notification to the properties of that model, like so:
/// <code>
/// public class BindablePerson : ObservableObject
/// {
/// public Model { get; }
///
/// public BindablePerson(Person model)
/// {
/// Model = model;
/// }
///
/// public string Name
/// {
/// get => Model.Name;
/// set => Set(() => Model.Name, value);
/// }
/// }
/// </code>
/// This way we can then use the wrapping object in our application, and all those "proxy" properties will
/// also raise notifications when changed. Note that this method is not meant to be a replacement for
/// <see cref="SetProperty{T}(ref T,T,string)"/>, which offers better performance and less memory usage. Only use this
/// overload when relaying properties to a model that doesn't support notifications, and only if you can't
/// implement notifications to that model directly (eg. by having it inherit from <see cref="ObservableObject"/>).
/// </summary>
/// <typeparam name="T">The type of property to set.</typeparam>
/// <param name="propertyExpression">An <see cref="Expression{TDelegate}"/> returning the property to update.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
/// if the current and new value for the target property are the same. Additionally, <paramref name="propertyExpression"/>
/// must return a property from a model that is stored as another property in the current instance.
/// This method only supports one level of indirection: <paramref name="propertyExpression"/> can only
/// be used to access properties of a model that is directly stored as a property of the current instance.
/// Additionally, this method can only be used if the wrapped item is a reference type.
/// </remarks>
protected bool SetProperty<T>(Expression<Func<T>> propertyExpression, T newValue, [CallerMemberName] string? propertyName = null)
{
return SetProperty(propertyExpression, newValue, EqualityComparer<T>.Default, out _, propertyName);
}
/// <summary>
/// Compares the current and new values for a given nested property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property and then raises the
/// <see cref="PropertyChanged"/> event. The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>,
/// with the difference being that this method is used to relay properties from a wrapped model in the
/// current instance. See additional notes about this overload in <see cref="SetProperty{T}(Expression{Func{T}},T,string)"/>.
/// </summary>
/// <typeparam name="T">The type of property to set.</typeparam>
/// <param name="propertyExpression">An <see cref="Expression{TDelegate}"/> returning the property to update.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
protected bool SetProperty<T>(Expression<Func<T>> propertyExpression, T newValue, IEqualityComparer<T> comparer, [CallerMemberName] string? propertyName = null)
{
return SetProperty(propertyExpression, newValue, comparer, out _, propertyName);
}
/// <summary>
/// Implements the shared logic for <see cref="SetProperty{T}(Expression{Func{T}},T,IEqualityComparer{T},string)"/>
/// </summary>
/// <typeparam name="T">The type of property to set.</typeparam>
/// <param name="propertyExpression">An <see cref="Expression{TDelegate}"/> returning the property to update.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="oldValue">The resulting initial value for the target property.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
private protected bool SetProperty<T>(Expression<Func<T>> propertyExpression, T newValue, IEqualityComparer<T> comparer, out T oldValue, [CallerMemberName] string? propertyName = null)
{
PropertyInfo? parentPropertyInfo;
FieldInfo? parentFieldInfo = null;
// Get the target property info
if (!(propertyExpression.Body is MemberExpression targetExpression &&
targetExpression.Member is PropertyInfo targetPropertyInfo &&
targetExpression.Expression is MemberExpression parentExpression &&
(!((parentPropertyInfo = parentExpression.Member as PropertyInfo) is null) ||
!((parentFieldInfo = parentExpression.Member as FieldInfo) is null)) &&
parentExpression.Expression is ConstantExpression instanceExpression &&
instanceExpression.Value is object instance))
{
ThrowArgumentExceptionForInvalidPropertyExpression();
// This is never executed, as the method above always throws
oldValue = default!;
return false;
}
object parent = parentPropertyInfo is null
? parentFieldInfo!.GetValue(instance)
: parentPropertyInfo.GetValue(instance);
oldValue = (T)targetPropertyInfo.GetValue(parent);
if (comparer.Equals(oldValue, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
targetPropertyInfo.SetValue(parent, newValue);
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Compares the current and new values for a given field (which should be the backing
/// field for a property). If the value has changed, raises the <see cref="PropertyChanging"/>
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
/// The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>, with the difference being that this method
/// will also monitor the new value of the property (a generic <see cref="Task"/>) and will also
/// raise the <see cref="PropertyChanged"/> again for the target property when it completes.
/// This can be used to update bindings observing that <see cref="Task"/> or any of its properties.
/// Here is a sample property declaration using this method:
/// <code>
/// private Task myTask;
///
/// public Task MyTask
/// {
/// get => myTask;
/// private set => SetAndNotifyOnCompletion(ref myTask, () => myTask, value);
/// }
/// </code>
/// </summary>
/// <typeparam name="TTask">The type of <see cref="Task"/> to set and monitor.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="fieldExpression">
/// An <see cref="Expression{TDelegate}"/> returning the field to update. This is needed to be
/// able to raise the <see cref="PropertyChanged"/> to notify the completion of the input task.
/// </param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised if the current
/// and new value for the target property are the same. The return value being <see langword="true"/> only
/// indicates that the new value being assigned to <paramref name="field"/> is different than the previous one,
/// and it does not mean the new <typeparamref name="TTask"/> instance passed as argument is in any particular state.
/// </remarks>
protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Expression<Func<TTask?>> fieldExpression, TTask? newValue, [CallerMemberName] string? propertyName = null)
where TTask : Task
{
// We invoke the overload with a callback here to avoid code duplication, and simply pass an empty callback.
// The lambda expression here is transformed by the C# compiler into an empty closure class with a
// static singleton field containing a closure instance, and another caching the instantiated Action<TTask>
// instance. This will result in no further allocations after the first time this method is called for a given
// generic type. We only pay the cost of the virtual call to the delegate, but this is not performance critical
// code and that overhead would still be much lower than the rest of the method anyway, so that's fine.
return SetPropertyAndNotifyOnCompletion(ref field, fieldExpression, newValue, _ => { }, propertyName);
}
/// <summary>
/// Compares the current and new values for a given field (which should be the backing
/// field for a property). If the value has changed, raises the <see cref="PropertyChanging"/>
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
/// This method is just like <see cref="SetPropertyAndNotifyOnCompletion{TTask}(ref TTask,Expression{Func{TTask}},TTask,string)"/>,
/// with the difference being an extra <see cref="Action{T}"/> parameter with a callback being invoked
/// either immediately, if the new task has already completed or is <see langword="null"/>, or upon completion.
/// </summary>
/// <typeparam name="TTask">The type of <see cref="Task"/> to set and monitor.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="fieldExpression">
/// An <see cref="Expression{TDelegate}"/> returning the field to update.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="callback">A callback to invoke to update the property value.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
/// if the current and new value for the target property are the same.
/// </remarks>
protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Expression<Func<TTask?>> fieldExpression, TTask? newValue, Action<TTask?> callback, [CallerMemberName] string? propertyName = null)
where TTask : Task
{
if (ReferenceEquals(field, newValue))
{
return false;
}
// Check the status of the new task before assigning it to the
// target field. This is so that in case the task is either
// null or already completed, we can avoid the overhead of
// scheduling the method to monitor its completion.
bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true;
OnPropertyChanging(propertyName);
field = newValue;
OnPropertyChanged(propertyName);
// If the input task is either null or already completed, we don't need to
// execute the additional logic to monitor its completion, so we can just bypass
// the rest of the method and return that the field changed here. The return value
// does not indicate that the task itself has completed, but just that the property
// value itself has changed (ie. the referenced task instance has changed).
// This mirrors the return value of all the other synchronous Set methods as well.
if (isAlreadyCompletedOrNull)
{
callback(newValue);
return true;
}
// Get the target field to set. This is needed because we can't
// capture the ref field in a closure (for the async method).
if (!((fieldExpression.Body as MemberExpression)?.Member is FieldInfo fieldInfo))
{
ThrowArgumentExceptionForInvalidFieldExpression();
// This is never executed, as the method above always throws
return false;
}
// We use a local async function here so that the main method can
// remain synchronous and return a value that can be immediately
// used by the caller. This mirrors Set<T>(ref T, T, string).
// We use an async void function instead of a Task-returning function
// so that if a binding update caused by the property change notification
// causes a crash, it is immediately reported in the application instead of
// the exception being ignored (as the returned task wouldn't be awaited),
// which would result in a confusing behavior for users.
async void MonitorTask()
{
try
{
// Await the task and ignore any exceptions
await newValue!;
}
catch
{
}
TTask? currentTask = (TTask?)fieldInfo.GetValue(this);
// Only notify if the property hasn't changed
if (ReferenceEquals(newValue, currentTask))
{
OnPropertyChanged(propertyName);
}
callback(newValue);
}
MonitorTask();
return true;
}
/// <summary>
/// Throws an <see cref="ArgumentException"/> when a given <see cref="Expression{TDelegate}"/> is invalid for a property.
/// </summary>
private static void ThrowArgumentExceptionForInvalidPropertyExpression()
{
throw new ArgumentException("The given expression must be in the form () => MyModel.MyProperty");
}
/// <summary>
/// Throws an <see cref="ArgumentException"/> when a given <see cref="Expression{TDelegate}"/> is invalid for a property field.
/// </summary>
private static void ThrowArgumentExceptionForInvalidFieldExpression()
{
throw new ArgumentException("The given expression must be in the form () => field");
}
}
}