// 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; } } }