Skip to content

Commit 1cb4df2

Browse files
committed
Speed/memory improvements to SetPropertyAndNotifyOnCompletion
1 parent f429a78 commit 1cb4df2

File tree

4 files changed

+24
-40
lines changed

4 files changed

+24
-40
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -312,27 +312,22 @@ parentExpression.Expression is ConstantExpression instanceExpression &&
312312
/// public Task MyTask
313313
/// {
314314
/// get => myTask;
315-
/// private set => SetAndNotifyOnCompletion(ref myTask, () => myTask, value);
315+
/// private set => SetAndNotifyOnCompletion(() => ref myTask, value);
316316
/// }
317317
/// </code>
318318
/// </summary>
319319
/// <typeparam name="TTask">The type of <see cref="Task"/> to set and monitor.</typeparam>
320-
/// <param name="field">The field storing the property's value.</param>
321-
/// <param name="fieldExpression">
322-
/// An <see cref="Expression{TDelegate}"/> returning the field to update. This is needed to be
323-
/// able to raise the <see cref="PropertyChanged"/> to notify the completion of the input task.
324-
/// </param>
320+
/// <param name="fieldAccessor">The <see cref="FieldAccessor{T}"/> instance to access the backing field for the property.</param>
325321
/// <param name="newValue">The property's value after the change occurred.</param>
326322
/// <param name="propertyName">(optional) The name of the property that changed.</param>
327323
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
328324
/// <remarks>
329-
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised if the current
330-
/// and new value for the target property are the same. The return value being <see langword="true"/> only
331-
/// indicates that the new value being assigned to <paramref name="field"/> is different than the previous one,
325+
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised if the current and new
326+
/// value for the target property are the same. The return value being <see langword="true"/> only indicates that the
327+
/// new value being assigned to field provided by <paramref name="fieldAccessor"/> is different than the previous one,
332328
/// and it does not mean the new <typeparamref name="TTask"/> instance passed as argument is in any particular state.
333329
/// </remarks>
334-
/// <exception cref="ArgumentException">Thrown when <paramref name="fieldExpression"/> is not valid.</exception>
335-
protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Expression<Func<TTask?>> fieldExpression, TTask? newValue, [CallerMemberName] string? propertyName = null)
330+
protected bool SetPropertyAndNotifyOnCompletion<TTask>(FieldAccessor<TTask?> fieldAccessor, TTask? newValue, [CallerMemberName] string? propertyName = null)
336331
where TTask : Task
337332
{
338333
// We invoke the overload with a callback here to avoid code duplication, and simply pass an empty callback.
@@ -341,21 +336,19 @@ protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Express
341336
// instance. This will result in no further allocations after the first time this method is called for a given
342337
// generic type. We only pay the cost of the virtual call to the delegate, but this is not performance critical
343338
// code and that overhead would still be much lower than the rest of the method anyway, so that's fine.
344-
return SetPropertyAndNotifyOnCompletion(ref field, fieldExpression, newValue, _ => { }, propertyName);
339+
return SetPropertyAndNotifyOnCompletion(fieldAccessor, newValue, _ => { }, propertyName);
345340
}
346341

347342
/// <summary>
348343
/// Compares the current and new values for a given field (which should be the backing
349344
/// field for a property). If the value has changed, raises the <see cref="PropertyChanging"/>
350345
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
351-
/// This method is just like <see cref="SetPropertyAndNotifyOnCompletion{TTask}(ref TTask,Expression{Func{TTask}},TTask,string)"/>,
346+
/// This method is just like <see cref="SetPropertyAndNotifyOnCompletion{TTask}(FieldAccessor{TTask},TTask,string)"/>,
352347
/// with the difference being an extra <see cref="Action{T}"/> parameter with a callback being invoked
353348
/// either immediately, if the new task has already completed or is <see langword="null"/>, or upon completion.
354349
/// </summary>
355350
/// <typeparam name="TTask">The type of <see cref="Task"/> to set and monitor.</typeparam>
356-
/// <param name="field">The field storing the property's value.</param>
357-
/// <param name="fieldExpression">
358-
/// An <see cref="Expression{TDelegate}"/> returning the field to update.</param>
351+
/// <param name="fieldAccessor">The <see cref="FieldAccessor{T}"/> instance to access the backing field for the property.</param>
359352
/// <param name="newValue">The property's value after the change occurred.</param>
360353
/// <param name="callback">A callback to invoke to update the property value.</param>
361354
/// <param name="propertyName">(optional) The name of the property that changed.</param>
@@ -364,10 +357,12 @@ protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Express
364357
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
365358
/// if the current and new value for the target property are the same.
366359
/// </remarks>
367-
/// <exception cref="ArgumentException">Thrown when <paramref name="fieldExpression"/> is not valid.</exception>
368-
protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Expression<Func<TTask?>> fieldExpression, TTask? newValue, Action<TTask?> callback, [CallerMemberName] string? propertyName = null)
360+
protected bool SetPropertyAndNotifyOnCompletion<TTask>(FieldAccessor<TTask?> fieldAccessor, TTask? newValue, Action<TTask?> callback, [CallerMemberName] string? propertyName = null)
369361
where TTask : Task
370362
{
363+
// Invoke the accessor once to get a field reference for the synchronous part
364+
ref TTask? field = ref fieldAccessor();
365+
371366
if (ReferenceEquals(field, newValue))
372367
{
373368
return false;
@@ -398,16 +393,6 @@ protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Express
398393
return true;
399394
}
400395

401-
// Get the target field to set. This is needed because we can't
402-
// capture the ref field in a closure (for the async method).
403-
if (!((fieldExpression.Body as MemberExpression)?.Member is FieldInfo fieldInfo))
404-
{
405-
ThrowArgumentExceptionForInvalidFieldExpression();
406-
407-
// This is never executed, as the method above always throws
408-
return false;
409-
}
410-
411396
// We use a local async function here so that the main method can
412397
// remain synchronous and return a value that can be immediately
413398
// used by the caller. This mirrors Set<T>(ref T, T, string).
@@ -427,7 +412,7 @@ async void MonitorTask()
427412
{
428413
}
429414

430-
TTask? currentTask = (TTask?)fieldInfo.GetValue(this);
415+
TTask? currentTask = fieldAccessor();
431416

432417
// Only notify if the property hasn't changed
433418
if (ReferenceEquals(newValue, currentTask))
@@ -444,19 +429,18 @@ async void MonitorTask()
444429
}
445430

446431
/// <summary>
447-
/// Throws an <see cref="ArgumentException"/> when a given <see cref="Expression{TDelegate}"/> is invalid for a property.
432+
/// A custom <see langword="delegate"/> that returns a reference to a backing field to a property.
448433
/// </summary>
449-
private static void ThrowArgumentExceptionForInvalidPropertyExpression()
450-
{
451-
throw new ArgumentException("The given expression must be in the form () => MyModel.MyProperty");
452-
}
434+
/// <typeparam name="T">The type of reference to return.</typeparam>
435+
/// <returns>A reference to the backing field of a property.</returns>
436+
protected delegate ref T FieldAccessor<T>();
453437

454438
/// <summary>
455-
/// Throws an <see cref="ArgumentException"/> when a given <see cref="Expression{TDelegate}"/> is invalid for a property field.
439+
/// Throws an <see cref="ArgumentException"/> when a given <see cref="Expression{TDelegate}"/> is invalid for a property.
456440
/// </summary>
457-
private static void ThrowArgumentExceptionForInvalidFieldExpression()
441+
private static void ThrowArgumentExceptionForInvalidPropertyExpression()
458442
{
459-
throw new ArgumentException("The given expression must be in the form () => field");
443+
throw new ArgumentException("The given expression must be in the form () => MyModel.MyProperty");
460444
}
461445
}
462446
}

Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public Task? ExecutionTask
5858
get => this.executionTask;
5959
private set
6060
{
61-
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, () => this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
61+
if (SetPropertyAndNotifyOnCompletion(() => ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
6262
{
6363
OnPropertyChanged(nameof(IsRunning));
6464
}

Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public Task? ExecutionTask
5858
get => this.executionTask;
5959
private set
6060
{
61-
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, () => this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
61+
if (SetPropertyAndNotifyOnCompletion(() => ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
6262
{
6363
OnPropertyChanged(nameof(IsRunning));
6464
}

UnitTests/UnitTests.Shared/Mvvm/Test_ObservableObject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ public class SampleModelWithTask<T> : ObservableObject
226226
public Task<T> Data
227227
{
228228
get => data;
229-
set => SetPropertyAndNotifyOnCompletion(ref data, () => data, value);
229+
set => SetPropertyAndNotifyOnCompletion(() => ref data, value);
230230
}
231231
}
232232
}

0 commit comments

Comments
 (0)