// 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.
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
// more info in ThirdPartyNotices.txt in the root of the project.
// ================================== NOTE ==================================
// This file is mirrored in the trimmed-down INotifyPropertyChanged file in
// the source generator project, to be used with the [INotifyPropertyChanged],
// attribute, along with the ObservableObject annotated copy (for debugging info).
// If any changes are made to this file, they should also be appropriately
// ported to that file as well to keep the behavior consistent.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel.__Internals;
#pragma warning disable CS0618
namespace CommunityToolkit.Mvvm.ComponentModel;
///
/// A base class for objects of which the properties must be observable.
///
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
{
///
public event PropertyChangedEventHandler? PropertyChanged;
///
public event PropertyChangingEventHandler? PropertyChanging;
///
/// Raises the event.
///
/// The input instance.
/// Thrown if is .
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
ArgumentNullException.ThrowIfNull(e);
PropertyChanged?.Invoke(this, e);
}
///
/// Raises the event.
///
/// The input instance.
/// Thrown if is .
protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
{
ArgumentNullException.ThrowIfNull(e);
// When support is disabled, just do nothing
if (!FeatureSwitches.EnableINotifyPropertyChangingSupport)
{
return;
}
PropertyChanging?.Invoke(this, e);
}
///
/// Raises the event.
///
/// (optional) The name of the property that changed.
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
///
/// Raises the event.
///
/// (optional) The name of the property that changed.
protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
// When support is disabled, avoid instantiating the event args entirely
if (!FeatureSwitches.EnableINotifyPropertyChangingSupport)
{
return;
}
OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
}
///
/// Compares the current and new values for a given property. If the value has changed,
/// raises the event, updates the property with the new
/// value, then raises the event.
///
/// The type of the property that changed.
/// The field storing the property's value.
/// The property's value after the change occurred.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
///
/// The and events are not raised
/// if the current and new value for the target property are the same.
///
protected bool SetProperty([NotNullIfNotNull(nameof(newValue))] 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 will be inlined, and we need the JIT
// to be able to see the full EqualityComparer.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 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.Default.Equals(field, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
field = newValue;
OnPropertyChanged(propertyName);
return true;
}
///
/// Compares the current and new values for a given property. If the value has changed,
/// raises the event, updates the property with the new
/// value, then raises the event.
/// See additional notes about this overload in .
///
/// The type of the property that changed.
/// The field storing the property's value.
/// The property's value after the change occurred.
/// The instance to use to compare the input values.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
/// Thrown if is .
protected bool SetProperty([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null)
{
ArgumentNullException.ThrowIfNull(comparer);
if (comparer.Equals(field, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
field = newValue;
OnPropertyChanged(propertyName);
return true;
}
///
/// Compares the current and new values for a given property. If the value has changed,
/// raises the event, updates the property with the new
/// value, then raises the event.
/// This overload is much less efficient than 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).
/// For performance reasons, it is recommended to use a stateful callback if possible through
/// the whenever possible
/// instead of this overload, as that will allow the C# compiler to cache the input callback and
/// reduce the memory allocations. More info on that overload are available in the related XML
/// docs. This overload is here for completeness and in cases where that is not applicable.
///
/// The type of the property that changed.
/// The current property value.
/// The property's value after the change occurred.
/// A callback to invoke to update the property value.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
///
/// The and events are not raised
/// if the current and new value for the target property are the same.
///
/// Thrown if is .
protected bool SetProperty(T oldValue, T newValue, Action callback, [CallerMemberName] string? propertyName = null)
{
ArgumentNullException.ThrowIfNull(callback);
// We avoid calling the overload again to ensure the comparison is inlined
if (EqualityComparer.Default.Equals(oldValue, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
callback(newValue);
OnPropertyChanged(propertyName);
return true;
}
///
/// Compares the current and new values for a given property. If the value has changed,
/// raises the event, updates the property with the new
/// value, then raises the event.
/// See additional notes about this overload in .
///
/// The type of the property that changed.
/// The current property value.
/// The property's value after the change occurred.
/// The instance to use to compare the input values.
/// A callback to invoke to update the property value.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
/// Thrown if or are .
protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, [CallerMemberName] string? propertyName = null)
{
ArgumentNullException.ThrowIfNull(comparer);
ArgumentNullException.ThrowIfNull(callback);
if (comparer.Equals(oldValue, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
callback(newValue);
OnPropertyChanged(propertyName);
return true;
}
///
/// Compares the current and new values for a given nested property. If the value has changed,
/// raises the event, updates the property and then raises the
/// event. The behavior mirrors that of ,
/// 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):
///
/// public class Person
/// {
/// public string Name { get; set; }
/// }
///
/// 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:
///
/// public class BindablePerson : ObservableObject
/// {
/// public Model { get; }
///
/// public BindablePerson(Person model)
/// {
/// Model = model;
/// }
///
/// public string Name
/// {
/// get => Model.Name;
/// set => Set(Model.Name, value, Model, (model, name) => model.Name = name);
/// }
/// }
///
/// 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
/// , and it should only be used 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 ). The syntax relies on passing the target model and a stateless callback
/// to allow the C# compiler to cache the function, which results in much better performance and no memory usage.
///
/// The type of model whose property (or field) to set.
/// The type of property (or field) to set.
/// The current property value.
/// The property's value after the change occurred.
/// The model containing the property being updated.
/// The callback to invoke to set the target property value, if a change has occurred.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
///
/// The and events are not
/// raised if the current and new value for the target property are the same.
///
/// Thrown if or are .
protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, [CallerMemberName] string? propertyName = null)
where TModel : class
{
ArgumentNullException.ThrowIfNull(model);
ArgumentNullException.ThrowIfNull(callback);
if (EqualityComparer.Default.Equals(oldValue, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
callback(model, newValue);
OnPropertyChanged(propertyName);
return true;
}
///
/// Compares the current and new values for a given nested property. If the value has changed,
/// raises the event, updates the property and then raises the
/// event. The behavior mirrors that of ,
/// 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 .
///
/// The type of model whose property (or field) to set.
/// The type of property (or field) to set.
/// The current property value.
/// The property's value after the change occurred.
/// The instance to use to compare the input values.
/// The model containing the property being updated.
/// The callback to invoke to set the target property value, if a change has occurred.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
/// Thrown if , or are .
protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, [CallerMemberName] string? propertyName = null)
where TModel : class
{
ArgumentNullException.ThrowIfNull(comparer);
ArgumentNullException.ThrowIfNull(model);
ArgumentNullException.ThrowIfNull(callback);
if (comparer.Equals(oldValue, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
callback(model, newValue);
OnPropertyChanged(propertyName);
return true;
}
///
/// 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
/// event, updates the field and then raises the event.
/// The behavior mirrors that of , with the difference being that
/// this method will also monitor the new value of the property (a generic ) and will also
/// raise the again for the target property when it completes.
/// This can be used to update bindings observing that or any of its properties.
/// This method and its overload specifically rely on the type, which needs
/// to be used in the backing field for the target property. The field doesn't need to be
/// initialized, as this method will take care of doing that automatically. The
/// type also includes an implicit operator, so it can be assigned to any instance directly.
/// Here is a sample property declaration using this method:
///
/// private TaskNotifier myTask;
///
/// public Task MyTask
/// {
/// get => myTask;
/// private set => SetAndNotifyOnCompletion(ref myTask, value);
/// }
///
///
/// The field notifier to modify.
/// The property's value after the change occurred.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
///
/// The and events are not raised if the current
/// and new value for the target property are the same. The return value being only
/// indicates that the new value being assigned to is different than the previous one,
/// and it does not mean the new instance passed as argument is in any particular state.
///
protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
{
// 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
// 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(taskNotifier ??= new TaskNotifier(), newValue, null, propertyName);
}
///
/// 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
/// event, updates the field and then raises the event.
/// This method is just like ,
/// with the difference being an extra parameter with a callback being invoked
/// either immediately, if the new task has already completed or is , or upon completion.
///
/// The field notifier to modify.
/// The property's value after the change occurred.
/// A callback to invoke to update the property value.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
///
/// The and events are not raised
/// if the current and new value for the target property are the same.
///
/// Thrown if is .
protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null)
{
ArgumentNullException.ThrowIfNull(callback);
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, callback, propertyName);
}
///
/// 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
/// event, updates the field and then raises the event.
/// The behavior mirrors that of , with the difference being that
/// this method will also monitor the new value of the property (a generic ) and will also
/// raise the again for the target property when it completes.
/// This can be used to update bindings observing that or any of its properties.
/// This method and its overload specifically rely on the type, which needs
/// to be used in the backing field for the target property. The field doesn't need to be
/// initialized, as this method will take care of doing that automatically. The
/// type also includes an implicit operator, so it can be assigned to any instance directly.
/// Here is a sample property declaration using this method:
///
/// private TaskNotifier<int> myTask;
///
/// public Task<int> MyTask
/// {
/// get => myTask;
/// private set => SetAndNotifyOnCompletion(ref myTask, value);
/// }
///
///
/// The type of result for the to set and monitor.
/// The field notifier to modify.
/// The property's value after the change occurred.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
///
/// The and events are not raised if the current
/// and new value for the target property are the same. The return value being only
/// indicates that the new value being assigned to is different than the previous one,
/// and it does not mean the new instance passed as argument is in any particular state.
///
protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null)
{
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, null, propertyName);
}
///
/// 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
/// event, updates the field and then raises the event.
/// This method is just like ,
/// with the difference being an extra parameter with a callback being invoked
/// either immediately, if the new task has already completed or is , or upon completion.
///
/// The type of result for the to set and monitor.
/// The field notifier to modify.
/// The property's value after the change occurred.
/// A callback to invoke to update the property value.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
///
/// The and events are not raised
/// if the current and new value for the target property are the same.
///
/// Thrown if is .
protected bool SetPropertyAndNotifyOnCompletion([NotNull] ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null)
{
ArgumentNullException.ThrowIfNull(callback);
return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new TaskNotifier(), newValue, callback, propertyName);
}
///
/// Implements the notification logic for the related methods.
///
/// The type of to set and monitor.
/// The field notifier.
/// The property's value after the change occurred.
/// (optional) A callback to invoke to update the property value.
/// (optional) The name of the property that changed.
/// if the property was changed, otherwise.
private bool SetPropertyAndNotifyOnCompletion(ITaskNotifier taskNotifier, TTask? newValue, Action? callback, [CallerMemberName] string? propertyName = null)
where TTask : Task
{
if (ReferenceEquals(taskNotifier.Task, 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);
taskNotifier.Task = 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)
{
if (callback is not null)
{
callback(newValue);
}
return true;
}
// 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(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()
{
// Await the task and ignore any exceptions
await newValue!.GetAwaitableWithoutEndValidation();
// Only notify if the property hasn't changed
if (ReferenceEquals(taskNotifier.Task, newValue))
{
OnPropertyChanged(propertyName);
}
if (callback is not null)
{
callback(newValue);
}
}
MonitorTask();
return true;
}
///
/// An interface for task notifiers of a specified type.
///
/// The type of value to store.
private interface ITaskNotifier
where TTask : Task
{
///
/// Gets or sets the wrapped value.
///
TTask? Task { get; set; }
}
///
/// A wrapping class that can hold a value.
///
protected sealed class TaskNotifier : ITaskNotifier
{
///
/// Initializes a new instance of the class.
///
internal TaskNotifier()
{
}
private Task? task;
///
Task? ITaskNotifier.Task
{
get => this.task;
set => this.task = value;
}
///
/// Unwraps the value stored in the current instance.
///
/// The input instance.
public static implicit operator Task?(TaskNotifier? notifier)
{
return notifier?.task;
}
}
///
/// A wrapping class that can hold a value.
///
/// The type of value for the wrapped instance.
protected sealed class TaskNotifier : ITaskNotifier>
{
///
/// Initializes a new instance of the class.
///
internal TaskNotifier()
{
}
private Task? task;
///
Task? ITaskNotifier>.Task
{
get => this.task;
set => this.task = value;
}
///
/// Unwraps the value stored in the current instance.
///
/// The input instance.
public static implicit operator Task?(TaskNotifier? notifier)
{
return notifier?.task;
}
}
}