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