using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; namespace Swan { /// <summary> /// A base class for implementing models that fire notifications when their properties change. /// This class is ideal for implementing MVVM driven UIs. /// </summary> /// <seealso cref="INotifyPropertyChanged" /> public abstract class ViewModelBase : INotifyPropertyChanged { private readonly ConcurrentDictionary<string, bool> _queuedNotifications = new ConcurrentDictionary<string, bool>(); private readonly bool _useDeferredNotifications; /// <summary> /// Initializes a new instance of the <see cref="ViewModelBase"/> class. /// </summary> /// <param name="useDeferredNotifications">Set to <c>true</c> to use deferred notifications in the background.</param> protected ViewModelBase(bool useDeferredNotifications) { _useDeferredNotifications = useDeferredNotifications; } /// <summary> /// Initializes a new instance of the <see cref="ViewModelBase"/> class. /// </summary> protected ViewModelBase() : this(false) { // placeholder } /// <inheritdoc /> public event PropertyChangedEventHandler PropertyChanged; /// <summary>Checks if a property already matches a desired value. Sets the property and /// notifies listeners only when necessary.</summary> /// <typeparam name="T">Type of the property.</typeparam> /// <param name="storage">Reference to a property with both getter and setter.</param> /// <param name="value">Desired value for the property.</param> /// <param name="propertyName">Name of the property used to notify listeners. This /// value is optional and can be provided automatically when invoked from compilers that /// support CallerMemberName.</param> /// <param name="notifyAlso">An array of property names to notify in addition to notifying the changes on the current property name.</param> /// <returns>True if the value was changed, false if the existing value matched the /// desired value.</returns> protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = "", string[] notifyAlso = null) { if (EqualityComparer<T>.Default.Equals(storage, value)) return false; storage = value; NotifyPropertyChanged(propertyName, notifyAlso); return true; } /// <summary> /// Notifies one or more properties changed. /// </summary> /// <param name="propertyNames">The property names.</param> protected void NotifyPropertyChanged(params string[] propertyNames) => NotifyPropertyChanged(null, propertyNames); /// <summary> /// Notifies one or more properties changed. /// </summary> /// <param name="mainProperty">The main property.</param> /// <param name="auxiliaryProperties">The auxiliary properties.</param> private void NotifyPropertyChanged(string mainProperty, string[] auxiliaryProperties) { // Queue property notification if (string.IsNullOrWhiteSpace(mainProperty) == false) _queuedNotifications[mainProperty] = true; // Set the state for notification properties if (auxiliaryProperties != null) { foreach (var property in auxiliaryProperties) { if (string.IsNullOrWhiteSpace(property) == false) _queuedNotifications[property] = true; } } // Depending on operation mode, either fire the notifications in the background // or fire them immediately if (_useDeferredNotifications) Task.Run(NotifyQueuedProperties); else NotifyQueuedProperties(); } /// <summary> /// Notifies the queued properties and resets the property name to a non-queued stated. /// </summary> private void NotifyQueuedProperties() { // get a snapshot of property names. var propertyNames = _queuedNotifications.Keys.ToArray(); // Iterate through the properties foreach (var property in propertyNames) { // don't notify if we don't have a change if (!_queuedNotifications[property]) continue; // notify and reset queued state to false try { OnPropertyChanged(property); } finally { _queuedNotifications[property] = false; } } } /// <summary> /// Called when a property changes its backing value. /// </summary> /// <param name="propertyName">Name of the property.</param> private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? string.Empty)); } }