// 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 // Original file header: // **************************************************************************** // // Copyright © GalaSoft Laurent Bugnion 2011-2016 // // **************************************************************************** // Laurent Bugnion // laurent@galasoft.ch // 10.4.2011 // GalaSoft.MvvmLight.Messaging // http://www.mvvmlight.net // // See license.txt in this project or http://www.galasoft.ch/license_MIT.txt // // **************************************************************************** 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 { /// /// 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 if needed. /// /// (optional) The name of the property that changed. protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null!) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } /// /// Raises the event if needed. /// /// (optional) The name of the property that changed. protected virtual void OnPropertyChanging([CallerMemberName] string propertyName = null!) { this.PropertyChanging?.Invoke(this, 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 Set(ref T field, T newValue, [CallerMemberName] string propertyName = null!) { if (EqualityComparer.Default.Equals(field, newValue)) { return false; } this.OnPropertyChanging(propertyName); field = newValue; this.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. /// Here is a sample property declaration using this method: /// /// private Task myTask; /// /// public Task MyTask /// { /// get => myTask; /// private set => SetAndNotifyOnCompletion(ref myTask, () => myTask, value); /// } /// /// /// The type of to set and monitor. /// The field storing the property's value. /// /// An returning the field to update. This is needed to be /// able to raise the to notify the completion of the input task. /// /// 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 SetAndNotifyOnCompletion(ref TTask? field, Expression> fieldExpression, TTask? newValue, [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; this.OnPropertyChanging(propertyName); field = newValue; OnPropertyChanged(propertyName); if (isAlreadyCompletedOrNull) { 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(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); } } MonitorTask(); return true; } /// /// Throws an when a given is invalid for a property field. /// [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowArgumentExceptionForInvalidFieldExpression() { throw new ArgumentException("The given expression must be in the form () => field"); } } }