1
0
mirror of https://github.com/chylex/.NET-Community-Toolkit.git synced 2025-04-13 20:15:45 +02:00

Merge branch 'master' into improvement/high-performance-tweaks

This commit is contained in:
Sergio Pedri 2020-08-07 15:41:59 +02:00 committed by GitHub
commit 5907963f71
55 changed files with 5066 additions and 22 deletions

View File

@ -87,7 +87,7 @@ public static Box<T> GetFrom(object obj)
/// <param name="obj">The input <see cref="object"/> instance, representing a boxed <typeparamref name="T"/> value.</param>
/// <returns>A <see cref="Box{T}"/> reference pointing to <paramref name="obj"/>.</returns>
/// <remarks>
/// This method doesn't check the actual type of <paramref name="obj"/>, so it is responsability of the caller
/// This method doesn't check the actual type of <paramref name="obj"/>, so it is responsibility of the caller
/// to ensure it actually represents a boxed <typeparamref name="T"/> value and not some other instance.
/// </remarks>
[Pure]

View File

@ -61,7 +61,7 @@ public static ref T DangerousGetReference<T>(this T[,] array)
/// <remarks>
/// This method doesn't do any bounds checks, therefore it is responsibility of the caller to ensure the <paramref name="i"/>
/// and <paramref name="j"/> parameters are valid. Furthermore, this extension will ignore the lower bounds for the input
/// array, and will just assume that the input index is 0-based. It is responsability of the caller to adjust the input
/// array, and will just assume that the input index is 0-based. It is responsibility of the caller to adjust the input
/// indices to account for the actual lower bounds, if the input array has either axis not starting at 0.
/// </remarks>
[Pure]
@ -234,7 +234,7 @@ public static Array2DColumnEnumerable<T> GetColumn<T>(this T[,] array, int colum
#if SPAN_RUNTIME_SUPPORT
/// <summary>
/// Cretes a new <see cref="Span{T}"/> over an input 2D <typeparamref name="T"/> array.
/// Creates a new <see cref="Span{T}"/> over an input 2D <typeparamref name="T"/> array.
/// </summary>
/// <typeparam name="T">The type of elements in the input 2D <typeparamref name="T"/> array instance.</typeparam>
/// <param name="array">The input 2D <typeparamref name="T"/> array instance.</param>

View File

@ -22,7 +22,7 @@ public static class MemoryExtensions
/// <returns>A <see cref="Stream"/> wrapping the data within <paramref name="memory"/>.</returns>
/// <remarks>
/// Since this method only receives a <see cref="Memory{T}"/> instance, which does not track
/// the lifetime of its underlying buffer, it is responsability of the caller to manage that.
/// the lifetime of its underlying buffer, it is responsibility of the caller to manage that.
/// In particular, the caller must ensure that the target buffer is not disposed as long
/// as the returned <see cref="Stream"/> is in use, to avoid unexpected issues.
/// </remarks>

View File

@ -24,7 +24,7 @@ public static class ObjectExtensions
/// The <see cref="IntPtr"/> value representing the offset to the target field from the start of the object data
/// for the parameter <paramref name="obj"/>. The offset is in relation to the first usable byte after the method table.
/// </returns>
/// <remarks>The input parameters are not validated, and it's responsability of the caller to ensure that
/// <remarks>The input parameters are not validated, and it's responsibility of the caller to ensure that
/// the <paramref name="data"/> reference is actually pointing to a memory location within <paramref name="obj"/>.
/// </remarks>
[Pure]
@ -46,7 +46,7 @@ public static IntPtr DangerousGetObjectDataByteOffset<T>(this object obj, ref T
/// <param name="offset">The input byte offset for the <typeparamref name="T"/> reference to retrieve.</param>
/// <returns>A <typeparamref name="T"/> reference at a specified offset within <paramref name="obj"/>.</returns>
/// <remarks>
/// None of the input arguments is validated, and it is responsability of the caller to ensure they are valid.
/// None of the input arguments is validated, and it is responsibility of the caller to ensure they are valid.
/// In particular, using an invalid offset might cause the retrieved reference to be misaligned with the
/// desired data, which would break the type system. Or, if the offset causes the retrieved reference to point
/// to a memory location outside of the input <see cref="object"/> instance, that might lead to runtime crashes.

View File

@ -22,7 +22,7 @@ public static class ReadOnlyMemoryExtensions
/// <returns>A <see cref="Stream"/> wrapping the data within <paramref name="memory"/>.</returns>
/// <remarks>
/// Since this method only receives a <see cref="Memory{T}"/> instance, which does not track
/// the lifetime of its underlying buffer, it is responsability of the caller to manage that.
/// the lifetime of its underlying buffer, it is responsibility of the caller to manage that.
/// In particular, the caller must ensure that the target buffer is not disposed as long
/// as the returned <see cref="Stream"/> is in use, to avoid unexpected issues.
/// </remarks>

View File

@ -279,7 +279,7 @@ public static ReadOnlySpanTokenizer<T> Tokenize<T>(this ReadOnlySpan<T> span, T
/// even long sequences of values. For the reference implementation, see: <see href="http://www.cse.yorku.ca/~oz/hash.html"/>.
/// For details on the used constants, see the details provided in this StackOverflow answer (as well as the accepted one):
/// <see href="https://stackoverflow.com/questions/10696223/reason-for-5381-number-in-djb-hash-function/13809282#13809282"/>.
/// Additionally, a comparison between some common hashing algoriths can be found in the reply to this StackExchange question:
/// Additionally, a comparison between some common hashing algorithms can be found in the reply to this StackExchange question:
/// <see href="https://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed"/>.
/// Note that the exact implementation is slightly different in this method when it is not called on a sequence of <see cref="byte"/>
/// values: in this case the <see cref="object.GetHashCode"/> method will be invoked for each <typeparamref name="T"/> value in

View File

@ -78,9 +78,9 @@
<!-- NETCORE_RUNTIME: to avoid issues with APIs that assume a specific memory layout, we define a
.NET Core runtime constant to indicate the either .NET Core 2.1 or .NET Core 3.1. These are
runtimes with the same overall memory layout for objects (in particular: strings, SZ arrays
runtimes with the same overall memory layout for objects (in particular: strings, SZ arrays,
and 2D arrays). We can use this constant to make sure that APIs that are exclusively available
for .NET Standard targets do not make any assumtpion of any internals of the runtime being
for .NET Standard targets do not make any assumption of any internals of the runtime being
actually used by the consumers. -->
<DefineConstants>NETSTANDARD2_1_OR_GREATER;SPAN_RUNTIME_SUPPORT;NETCORE_RUNTIME</DefineConstants>
</PropertyGroup>

View File

@ -70,7 +70,7 @@ public static implicit operator ReadOnlyRef<T>(Ref<T> reference)
/// </summary>
/// <param name="owner">The owner <see cref="object"/> to create a portable reference for.</param>
/// <param name="offset">The target offset within <paramref name="owner"/> for the target reference.</param>
/// <remarks>The <paramref name="offset"/> parameter is not validated, and it's responsability of the caller to ensure it's valid.</remarks>
/// <remarks>The <paramref name="offset"/> parameter is not validated, and it's responsibility of the caller to ensure it's valid.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ReadOnlyRef(object owner, IntPtr offset)
{
@ -83,7 +83,7 @@ private ReadOnlyRef(object owner, IntPtr offset)
/// </summary>
/// <param name="owner">The owner <see cref="object"/> to create a portable reference for.</param>
/// <param name="value">The target reference to point to (it must be within <paramref name="owner"/>).</param>
/// <remarks>The <paramref name="value"/> parameter is not validated, and it's responsability of the caller to ensure it's valid.</remarks>
/// <remarks>The <paramref name="value"/> parameter is not validated, and it's responsibility of the caller to ensure it's valid.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlyRef(object owner, in T value)
{

View File

@ -62,7 +62,7 @@ public ref T Value
/// </summary>
/// <param name="owner">The owner <see cref="object"/> to create a portable reference for.</param>
/// <param name="value">The target reference to point to (it must be within <paramref name="owner"/>).</param>
/// <remarks>The <paramref name="value"/> parameter is not validated, and it's responsability of the caller to ensure it's valid.</remarks>
/// <remarks>The <paramref name="value"/> parameter is not validated, and it's responsibility of the caller to ensure it's valid.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ref(object owner, ref T value)
{

View File

@ -0,0 +1,421 @@
// 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
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
// more info in ThirdPartyNotices.txt in the root of the project.
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
{
/// <summary>
/// A base class for objects of which the properties must be observable.
/// </summary>
public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
{
/// <inheritdoc cref="INotifyPropertyChanged.PropertyChanged"/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc cref="INotifyPropertyChanging.PropertyChanging"/>
public event PropertyChangingEventHandler? PropertyChanging;
/// <summary>
/// Performs the required configuration when a property has changed, and then
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
/// </summary>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <remarks>The base implementation only raises the <see cref="PropertyChanged"/> event.</remarks>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Performs the required configuration when a property is changing, and then
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
/// </summary>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <remarks>The base implementation only raises the <see cref="PropertyChanging"/> event.</remarks>
protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
/// value, then raises the <see cref="PropertyChanged"/> event.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
/// if the current and new value for the target property are the same.
/// </remarks>
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string? propertyName = null)
{
return SetProperty(ref field, newValue, EqualityComparer<T>.Default, propertyName);
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
/// value, then raises the <see cref="PropertyChanged"/> event.
/// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,string)"/>.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, [CallerMemberName] string? propertyName = null)
{
if (comparer.Equals(field, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
field = newValue;
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
/// value, then raises the <see cref="PropertyChanged"/> event.
/// This overload is much less efficient than <see cref="SetProperty{T}(ref T,T,string)"/> 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).
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="oldValue">The current property value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="callback">A callback to invoke to update the property value.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
/// if the current and new value for the target property are the same.
/// </remarks>
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, [CallerMemberName] string? propertyName = null)
{
return SetProperty(oldValue, newValue, EqualityComparer<T>.Default, callback, propertyName);
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property with the new
/// value, then raises the <see cref="PropertyChanged"/> event.
/// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},string)"/>.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="oldValue">The current property value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="callback">A callback to invoke to update the property value.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, [CallerMemberName] string? propertyName = null)
{
if (comparer.Equals(oldValue, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
callback(newValue);
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Compares the current and new values for a given nested property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property and then raises the
/// <see cref="PropertyChanged"/> event. The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>,
/// 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):
/// <code>
/// public class Person
/// {
/// public string Name { get; set; }
/// }
/// </code>
/// 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:
/// <code>
/// public class BindablePerson : ObservableObject
/// {
/// public Model { get; }
///
/// public BindablePerson(Person model)
/// {
/// Model = model;
/// }
///
/// public string Name
/// {
/// get => Model.Name;
/// set => Set(() => Model.Name, value);
/// }
/// }
/// </code>
/// 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
/// <see cref="SetProperty{T}(ref T,T,string)"/>, which offers better performance and less memory usage. Only use this
/// overload 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 <see cref="ObservableObject"/>).
/// </summary>
/// <typeparam name="T">The type of property to set.</typeparam>
/// <param name="propertyExpression">An <see cref="Expression{TDelegate}"/> returning the property to update.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
/// if the current and new value for the target property are the same. Additionally, <paramref name="propertyExpression"/>
/// must return a property from a model that is stored as another property in the current instance.
/// This method only supports one level of indirection: <paramref name="propertyExpression"/> can only
/// be used to access properties of a model that is directly stored as a property of the current instance.
/// Additionally, this method can only be used if the wrapped item is a reference type.
/// </remarks>
protected bool SetProperty<T>(Expression<Func<T>> propertyExpression, T newValue, [CallerMemberName] string? propertyName = null)
{
return SetProperty(propertyExpression, newValue, EqualityComparer<T>.Default, propertyName);
}
/// <summary>
/// Compares the current and new values for a given nested property. If the value has changed,
/// raises the <see cref="PropertyChanging"/> event, updates the property and then raises the
/// <see cref="PropertyChanged"/> event. The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>,
/// 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 <see cref="SetProperty{T}(Expression{Func{T}},T,string)"/>.
/// </summary>
/// <typeparam name="T">The type of property to set.</typeparam>
/// <param name="propertyExpression">An <see cref="Expression{TDelegate}"/> returning the property to update.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
protected bool SetProperty<T>(Expression<Func<T>> propertyExpression, T newValue, IEqualityComparer<T> comparer, [CallerMemberName] string? propertyName = null)
{
PropertyInfo? parentPropertyInfo;
FieldInfo? parentFieldInfo = null;
// Get the target property info
if (!(propertyExpression.Body is MemberExpression targetExpression &&
targetExpression.Member is PropertyInfo targetPropertyInfo &&
targetExpression.Expression is MemberExpression parentExpression &&
(!((parentPropertyInfo = parentExpression.Member as PropertyInfo) is null) ||
!((parentFieldInfo = parentExpression.Member as FieldInfo) is null)) &&
parentExpression.Expression is ConstantExpression instanceExpression &&
instanceExpression.Value is object instance))
{
ThrowArgumentExceptionForInvalidPropertyExpression();
// This is never executed, as the method above always throws
return false;
}
object parent = parentPropertyInfo is null
? parentFieldInfo!.GetValue(instance)
: parentPropertyInfo.GetValue(instance);
T oldValue = (T)targetPropertyInfo.GetValue(parent);
if (comparer.Equals(oldValue, newValue))
{
return false;
}
OnPropertyChanging(propertyName);
targetPropertyInfo.SetValue(parent, newValue);
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// 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 <see cref="PropertyChanging"/>
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
/// The behavior mirrors that of <see cref="SetProperty{T}(ref T,T,string)"/>, with the difference being that this method
/// will also monitor the new value of the property (a generic <see cref="Task"/>) and will also
/// raise the <see cref="PropertyChanged"/> again for the target property when it completes.
/// This can be used to update bindings observing that <see cref="Task"/> or any of its properties.
/// Here is a sample property declaration using this method:
/// <code>
/// private Task myTask;
///
/// public Task MyTask
/// {
/// get => myTask;
/// private set => SetAndNotifyOnCompletion(ref myTask, () => myTask, value);
/// }
/// </code>
/// </summary>
/// <typeparam name="TTask">The type of <see cref="Task"/> to set and monitor.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="fieldExpression">
/// An <see cref="Expression{TDelegate}"/> returning the field to update. This is needed to be
/// able to raise the <see cref="PropertyChanged"/> to notify the completion of the input task.
/// </param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised if the current
/// and new value for the target property are the same. The return value being <see langword="true"/> only
/// indicates that the new value being assigned to <paramref name="field"/> is different than the previous one,
/// and it does not mean the new <typeparamref name="TTask"/> instance passed as argument is in any particular state.
/// </remarks>
protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Expression<Func<TTask?>> fieldExpression, TTask? newValue, [CallerMemberName] string? propertyName = null)
where TTask : Task
{
// 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<TTask>
// 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(ref field, fieldExpression, newValue, _ => { }, propertyName);
}
/// <summary>
/// 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 <see cref="PropertyChanging"/>
/// event, updates the field and then raises the <see cref="PropertyChanged"/> event.
/// This method is just like <see cref="SetPropertyAndNotifyOnCompletion{TTask}(ref TTask,Expression{Func{TTask}},TTask,string)"/>,
/// with the difference being an extra <see cref="Action{T}"/> parameter with a callback being invoked
/// either immediately, if the new task has already completed or is <see langword="null"/>, or upon completion.
/// </summary>
/// <typeparam name="TTask">The type of <see cref="Task"/> to set and monitor.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="fieldExpression">
/// An <see cref="Expression{TDelegate}"/> returning the field to update.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="callback">A callback to invoke to update the property value.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// The <see cref="PropertyChanging"/> and <see cref="PropertyChanged"/> events are not raised
/// if the current and new value for the target property are the same.
/// </remarks>
protected bool SetPropertyAndNotifyOnCompletion<TTask>(ref TTask? field, Expression<Func<TTask?>> fieldExpression, TTask? newValue, Action<TTask?> callback, [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;
OnPropertyChanging(propertyName);
field = 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)
{
callback(newValue);
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<T>(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);
}
callback(newValue);
}
MonitorTask();
return true;
}
/// <summary>
/// Throws an <see cref="ArgumentException"/> when a given <see cref="Expression{TDelegate}"/> is invalid for a property.
/// </summary>
private static void ThrowArgumentExceptionForInvalidPropertyExpression()
{
throw new ArgumentException("The given expression must be in the form () => MyModel.MyProperty");
}
/// <summary>
/// Throws an <see cref="ArgumentException"/> when a given <see cref="Expression{TDelegate}"/> is invalid for a property field.
/// </summary>
private static void ThrowArgumentExceptionForInvalidFieldExpression()
{
throw new ArgumentException("The given expression must be in the form () => field");
}
}
}

View File

@ -0,0 +1,234 @@
// 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
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
// more info in ThirdPartyNotices.txt in the root of the project.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Mvvm.Messaging.Messages;
namespace Microsoft.Toolkit.Mvvm.ComponentModel
{
/// <summary>
/// A base class for observable objects that also acts as recipients for messages. This class is an extension of
/// <see cref="ObservableObject"/> which also provides built-in support to use the <see cref="IMessenger"/> type.
/// </summary>
public abstract class ObservableRecipient : ObservableObject
{
/// <summary>
/// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
/// </summary>
/// <remarks>
/// This constructor will produce an instance that will use the <see cref="Messaging.Messenger.Default"/> instance
/// to perform requested operations. It will also be available locally through the <see cref="Messenger"/> property.
/// </remarks>
protected ObservableRecipient()
: this(Messaging.Messenger.Default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
/// </summary>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send messages.</param>
protected ObservableRecipient(IMessenger messenger)
{
Messenger = messenger;
}
/// <summary>
/// Gets the <see cref="IMessenger"/> instance in use.
/// </summary>
protected IMessenger Messenger { get; }
private bool isActive;
/// <summary>
/// Gets or sets a value indicating whether the current view model is currently active.
/// </summary>
public bool IsActive
{
get => this.isActive;
set
{
if (SetProperty(ref this.isActive, value, true))
{
if (value)
{
OnActivated();
}
else
{
OnDeactivated();
}
}
}
}
/// <summary>
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="true"/>.
/// Use this method to register to messages and do other initialization for this instance.
/// </summary>
/// <remarks>
/// The base implementation registers all messages for this recipients that have been declared
/// explicitly through the <see cref="IRecipient{TMessage}"/> interface, using the default channel.
/// For more details on how this works, see the <see cref="MessengerExtensions.RegisterAll"/> method.
/// If you need more fine tuned control, want to register messages individually or just prefer
/// the lambda-style syntax for message registration, override this method and register manually.
/// </remarks>
protected virtual void OnActivated()
{
Messenger.RegisterAll(this);
}
/// <summary>
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="false"/>.
/// Use this method to unregister from messages and do general cleanup for this instance.
/// </summary>
/// <remarks>
/// The base implementation unregisters all messages for this recipient. It does so by
/// invoking <see cref="IMessenger.UnregisterAll"/>, which removes all registered
/// handlers for a given subscriber, regardless of what token was used to register them.
/// That is, all registered handlers across all subscription channels will be removed.
/// </remarks>
protected virtual void OnDeactivated()
{
Messenger.UnregisterAll(this);
}
/// <summary>
/// Broadcasts a <see cref="PropertyChangedMessage{T}"/> with the specified
/// parameters, without using any particular token (so using the default channel).
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="oldValue">The value of the property before it changed.</param>
/// <param name="newValue">The value of the property after it changed.</param>
/// <param name="propertyName">The name of the property that changed.</param>
/// <remarks>
/// You should override this method if you wish to customize the channel being
/// used to send the message (eg. if you need to use a specific token for the channel).
/// </remarks>
protected virtual void Broadcast<T>(T oldValue, T newValue, string? propertyName)
{
var message = new PropertyChangedMessage<T>(this, propertyName, oldValue, newValue);
Messenger.Send(message);
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// This method is just like <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/>, just with the addition
/// of the <paramref name="broadcast"/> parameter. As such, following the behavior of the base method,
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
/// are not raised if the current and new value for the target property are the same.
/// </remarks>
protected bool SetProperty<T>(ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null)
{
return SetProperty(ref field, newValue, EqualityComparer<T>.Default, broadcast, propertyName);
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
/// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,bool,string)"/>.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="field">The field storing the property's value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, bool broadcast, [CallerMemberName] string? propertyName = null)
{
if (!broadcast)
{
return SetProperty(ref field, newValue, comparer, propertyName);
}
T oldValue = field;
if (SetProperty(ref field, newValue, comparer, propertyName))
{
Broadcast(oldValue, newValue, propertyName);
return true;
}
return false;
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event. Similarly to
/// the <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/> method, this overload should only be
/// used when <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/> can't be used directly.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="oldValue">The current property value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="callback">A callback to invoke to update the property value.</param>
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// This method is just like <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/>, just with the addition
/// of the <paramref name="broadcast"/> parameter. As such, following the behavior of the base method,
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
/// are not raised if the current and new value for the target property are the same.
/// </remarks>
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
{
return SetProperty(oldValue, newValue, EqualityComparer<T>.Default, callback, broadcast, propertyName);
}
/// <summary>
/// Compares the current and new values for a given property. If the value has changed,
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
/// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},bool,string)"/>.
/// </summary>
/// <typeparam name="T">The type of the property that changed.</typeparam>
/// <param name="oldValue">The current property value.</param>
/// <param name="newValue">The property's value after the change occurred.</param>
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
/// <param name="callback">A callback to invoke to update the property value.</param>
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
/// <param name="propertyName">(optional) The name of the property that changed.</param>
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
{
if (!broadcast)
{
return SetProperty(oldValue, newValue, comparer, callback, propertyName);
}
if (SetProperty(oldValue, newValue, comparer, callback, propertyName))
{
Broadcast(oldValue, newValue, propertyName);
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,145 @@
// 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.
using System;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
#nullable enable
namespace Microsoft.Toolkit.Mvvm.DependencyInjection
{
/// <summary>
/// A type that facilitates the use of the <see cref="IServiceProvider"/> type.
/// The <see cref="Ioc"/> provides the ability to configure services in a singleton, thread-safe
/// service provider instance, which can then be used to resolve service instances.
/// The first step to use this feature is to declare some services, for instance:
/// <code>
/// public interface ILogger
/// {
/// void Log(string text);
/// }
/// </code>
/// <code>
/// public class ConsoleLogger : ILogger
/// {
/// void Log(string text) => Console.WriteLine(text);
/// }
/// </code>
/// Then the services configuration should then be done at startup, by calling one of
/// the available <see cref="ConfigureServices(IServiceCollection)"/> overloads, like so:
/// <code>
/// Ioc.Default.ConfigureServices(services =>
/// {
/// services.AddSingleton&lt;ILogger, Logger&gt;();
/// });
/// </code>
/// Finally, you can use the <see cref="Ioc"/> instance (which implements <see cref="IServiceProvider"/>)
/// to retrieve the service instances from anywhere in your application, by doing as follows:
/// <code>
/// Ioc.Default.GetService&lt;ILogger&gt;().Log("Hello world!");
/// </code>
/// </summary>
public sealed class Ioc : IServiceProvider
{
/// <summary>
/// Gets the default <see cref="Ioc"/> instance.
/// </summary>
public static Ioc Default { get; } = new Ioc();
/// <summary>
/// The <see cref="ServiceProvider"/> instance to use, if initialized.
/// </summary>
private ServiceProvider? serviceProvider;
/// <inheritdoc/>
object? IServiceProvider.GetService(Type serviceType)
{
// As per section I.12.6.6 of the official CLI ECMA-335 spec:
// "[...] read and write access to properly aligned memory locations no larger than the native
// word size is atomic when all the write accesses to a location are the same size. Atomic writes
// shall alter no bits other than those written. Unless explicit layout control is used [...],
// data elements no larger than the natural word size [...] shall be properly aligned.
// Object references shall be treated as though they are stored in the native word size."
// The field being accessed here is of native int size (reference type), and is only ever accessed
// directly and atomically by a compare exchange instruction (see below), or here. We can therefore
// assume this read is thread safe with respect to accesses to this property or to invocations to one
// of the available configuration methods. So we can just read the field directly and make the necessary
// check with our local copy, without the need of paying the locking overhead from this get accessor.
ServiceProvider? provider = this.serviceProvider;
if (provider is null)
{
ThrowInvalidOperationExceptionForMissingInitialization();
}
return provider!.GetService(serviceType);
}
/// <summary>
/// Initializes the shared <see cref="IServiceProvider"/> instance.
/// </summary>
/// <param name="setup">The configuration delegate to use to add services.</param>
public void ConfigureServices(Action<IServiceCollection> setup)
{
ConfigureServices(setup, new ServiceProviderOptions());
}
/// <summary>
/// Initializes the shared <see cref="IServiceProvider"/> instance.
/// </summary>
/// <param name="setup">The configuration delegate to use to add services.</param>
/// <param name="options">The <see cref="ServiceProviderOptions"/> instance to configure the service provider behaviors.</param>
public void ConfigureServices(Action<IServiceCollection> setup, ServiceProviderOptions options)
{
var collection = new ServiceCollection();
setup(collection);
ConfigureServices(collection, options);
}
/// <summary>
/// Initializes the shared <see cref="IServiceProvider"/> instance.
/// </summary>
/// <param name="services">The input <see cref="IServiceCollection"/> instance to use.</param>
public void ConfigureServices(IServiceCollection services)
{
ConfigureServices(services, new ServiceProviderOptions());
}
/// <summary>
/// Initializes the shared <see cref="IServiceProvider"/> instance.
/// </summary>
/// <param name="services">The input <see cref="IServiceCollection"/> instance to use.</param>
/// <param name="options">The <see cref="ServiceProviderOptions"/> instance to configure the service provider behaviors.</param>
public void ConfigureServices(IServiceCollection services, ServiceProviderOptions options)
{
ServiceProvider newServices = services.BuildServiceProvider(options);
ServiceProvider? oldServices = Interlocked.CompareExchange(ref this.serviceProvider, newServices, null);
if (!(oldServices is null))
{
ThrowInvalidOperationExceptionForRepeatedConfiguration();
}
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when the <see cref="ServiceProvider"/> property is used before initialization.
/// </summary>
private static void ThrowInvalidOperationExceptionForMissingInitialization()
{
throw new InvalidOperationException("The service provider has not been configured yet");
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when a configuration is attempted more than once.
/// </summary>
private static void ThrowInvalidOperationExceptionForRepeatedConfiguration()
{
throw new InvalidOperationException("The default service provider has already been configured");
}
}
}

View File

@ -0,0 +1,101 @@
// 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.
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Toolkit.Mvvm.ComponentModel;
namespace Microsoft.Toolkit.Mvvm.Input
{
/// <summary>
/// A command that mirrors the functionality of <see cref="RelayCommand"/>, with the addition of
/// accepting a <see cref="Func{TResult}"/> returning a <see cref="Task"/> as the execute
/// action, and providing an <see cref="ExecutionTask"/> property that notifies changes when
/// <see cref="ExecuteAsync"/> is invoked and when the returned <see cref="Task"/> completes.
/// </summary>
public sealed class AsyncRelayCommand : ObservableObject, IAsyncRelayCommand
{
/// <summary>
/// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute"/> is used.
/// </summary>
private readonly Func<Task> execute;
/// <summary>
/// The optional action to invoke when <see cref="CanExecute"/> is used.
/// </summary>
private readonly Func<bool>? canExecute;
/// <inheritdoc/>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public AsyncRelayCommand(Func<Task> execute)
{
this.execute = execute;
}
/// <summary>
/// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public AsyncRelayCommand(Func<Task> execute, Func<bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
private Task? executionTask;
/// <inheritdoc/>
public Task? ExecutionTask
{
get => this.executionTask;
private set
{
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, () => this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
{
OnPropertyChanged(nameof(IsRunning));
}
}
}
/// <inheritdoc/>
public bool IsRunning => ExecutionTask?.IsCompleted == false;
/// <inheritdoc/>
public void NotifyCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CanExecute(object? parameter)
{
return this.canExecute?.Invoke() != false;
}
/// <inheritdoc/>
public void Execute(object? parameter)
{
ExecuteAsync(parameter);
}
/// <inheritdoc/>
public Task ExecuteAsync(object? parameter)
{
if (CanExecute(parameter))
{
return ExecutionTask = this.execute();
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,128 @@
// 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.
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Toolkit.Mvvm.ComponentModel;
namespace Microsoft.Toolkit.Mvvm.Input
{
/// <summary>
/// A generic command that provides a more specific version of <see cref="AsyncRelayCommand"/>.
/// </summary>
/// <typeparam name="T">The type of parameter being passed as input to the callbacks.</typeparam>
public sealed class AsyncRelayCommand<T> : ObservableObject, IAsyncRelayCommand<T>
{
/// <summary>
/// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute(T)"/> is used.
/// </summary>
private readonly Func<T, Task> execute;
/// <summary>
/// The optional action to invoke when <see cref="CanExecute(T)"/> is used.
/// </summary>
private readonly Func<T, bool>? canExecute;
/// <inheritdoc/>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Initializes a new instance of the <see cref="AsyncRelayCommand{T}"/> class that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <remarks>See notes in <see cref="RelayCommand{T}(Action{T})"/>.</remarks>
public AsyncRelayCommand(Func<T, Task> execute)
{
this.execute = execute;
}
/// <summary>
/// Initializes a new instance of the <see cref="AsyncRelayCommand{T}"/> class that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <remarks>See notes in <see cref="RelayCommand{T}(Action{T})"/>.</remarks>
public AsyncRelayCommand(Func<T, Task> execute, Func<T, bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
private Task? executionTask;
/// <inheritdoc/>
public Task? ExecutionTask
{
get => this.executionTask;
private set
{
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, () => this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
{
OnPropertyChanged(nameof(IsRunning));
}
}
}
/// <inheritdoc/>
public bool IsRunning => ExecutionTask?.IsCompleted == false;
/// <inheritdoc/>
public void NotifyCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CanExecute(T parameter)
{
return this.canExecute?.Invoke(parameter) != false;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CanExecute(object? parameter)
{
if (typeof(T).IsValueType &&
parameter is null &&
this.canExecute is null)
{
return true;
}
return CanExecute((T)parameter!);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Execute(T parameter)
{
ExecuteAsync(parameter);
}
/// <inheritdoc/>
public void Execute(object? parameter)
{
ExecuteAsync((T)parameter!);
}
/// <inheritdoc/>
public Task ExecuteAsync(T parameter)
{
if (CanExecute(parameter))
{
return ExecutionTask = this.execute(parameter);
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public Task ExecuteAsync(object? parameter)
{
return ExecuteAsync((T)parameter!);
}
}
}

View File

@ -0,0 +1,34 @@
// 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.
using System.ComponentModel;
using System.Threading.Tasks;
namespace Microsoft.Toolkit.Mvvm.Input
{
/// <summary>
/// An interface expanding <see cref="IRelayCommand"/> to support asynchronous operations.
/// </summary>
public interface IAsyncRelayCommand : IRelayCommand, INotifyPropertyChanged
{
/// <summary>
/// Gets the last scheduled <see cref="Task"/>, if available.
/// This property notifies a change when the <see cref="Task"/> completes.
/// </summary>
Task? ExecutionTask { get; }
/// <summary>
/// Gets a value indicating whether the command currently has a pending operation being executed.
/// </summary>
bool IsRunning { get; }
/// <summary>
/// Provides a more specific version of <see cref="System.Windows.Input.ICommand.Execute"/>,
/// also returning the <see cref="Task"/> representing the async operation being executed.
/// </summary>
/// <param name="parameter">The input parameter.</param>
/// <returns>The <see cref="Task"/> representing the async operation being executed.</returns>
Task ExecuteAsync(object? parameter);
}
}

View File

@ -0,0 +1,23 @@
// 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.
using System.Threading.Tasks;
namespace Microsoft.Toolkit.Mvvm.Input
{
/// <summary>
/// A generic interface representing a more specific version of <see cref="IAsyncRelayCommand"/>.
/// </summary>
/// <typeparam name="T">The type used as argument for the interface methods.</typeparam>
/// <remarks>This interface is needed to solve the diamond problem with base classes.</remarks>
public interface IAsyncRelayCommand<in T> : IAsyncRelayCommand, IRelayCommand<T>
{
/// <summary>
/// Provides a strongly-typed variant of <see cref="IAsyncRelayCommand.ExecuteAsync"/>.
/// </summary>
/// <param name="parameter">The input parameter.</param>
/// <returns>The <see cref="Task"/> representing the async operation being executed.</returns>
Task ExecuteAsync(T parameter);
}
}

View File

@ -0,0 +1,20 @@
// 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.
using System.Windows.Input;
namespace Microsoft.Toolkit.Mvvm.Input
{
/// <summary>
/// An interface expanding <see cref="ICommand"/> with the ability to raise
/// the <see cref="ICommand.CanExecuteChanged"/> event externally.
/// </summary>
public interface IRelayCommand : ICommand
{
/// <summary>
/// Notifies that the <see cref="ICommand.CanExecute"/> property has changed.
/// </summary>
void NotifyCanExecuteChanged();
}
}

View File

@ -0,0 +1,30 @@
// 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.
using System.Windows.Input;
namespace Microsoft.Toolkit.Mvvm.Input
{
/// <summary>
/// A generic interface representing a more specific version of <see cref="IRelayCommand"/>.
/// </summary>
/// <typeparam name="T">The type used as argument for the interface methods.</typeparam>
public interface IRelayCommand<in T> : IRelayCommand
{
/// <summary>
/// Provides a strongly-typed variant of <see cref="ICommand.CanExecute(object)"/>.
/// </summary>
/// <param name="parameter">The input parameter.</param>
/// <returns>Whether or not the current command can be executed.</returns>
/// <remarks>Use this overload to avoid boxing, if <typeparamref name="T"/> is a value type.</remarks>
bool CanExecute(T parameter);
/// <summary>
/// Provides a strongly-typed variant of <see cref="ICommand.Execute(object)"/>.
/// </summary>
/// <param name="parameter">The input parameter.</param>
/// <remarks>Use this overload to avoid boxing, if <typeparamref name="T"/> is a value type.</remarks>
void Execute(T parameter);
}
}

View File

@ -0,0 +1,78 @@
// 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
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
// more info in ThirdPartyNotices.txt in the root of the project.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.Mvvm.Input
{
/// <summary>
/// A command whose sole purpose is to relay its functionality to other
/// objects by invoking delegates. The default return value for the <see cref="CanExecute"/>
/// method is <see langword="true"/>. This type does not allow you to accept command parameters
/// in the <see cref="Execute"/> and <see cref="CanExecute"/> callback methods.
/// </summary>
public sealed class RelayCommand : IRelayCommand
{
/// <summary>
/// The <see cref="Action"/> to invoke when <see cref="Execute"/> is used.
/// </summary>
private readonly Action execute;
/// <summary>
/// The optional action to invoke when <see cref="CanExecute"/> is used.
/// </summary>
private readonly Func<bool>? canExecute;
/// <inheritdoc/>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Initializes a new instance of the <see cref="RelayCommand"/> class that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
public RelayCommand(Action execute)
{
this.execute = execute;
}
/// <summary>
/// Initializes a new instance of the <see cref="RelayCommand"/> class.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action execute, Func<bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
/// <inheritdoc/>
public void NotifyCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CanExecute(object? parameter)
{
return this.canExecute?.Invoke() != false;
}
/// <inheritdoc/>
public void Execute(object? parameter)
{
if (CanExecute(parameter))
{
this.execute();
}
}
}
}

View File

@ -0,0 +1,105 @@
// 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
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
// more info in ThirdPartyNotices.txt in the root of the project.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.Mvvm.Input
{
/// <summary>
/// A generic command whose sole purpose is to relay its functionality to other
/// objects by invoking delegates. The default return value for the CanExecute
/// method is <see langword="true"/>. This class allows you to accept command parameters
/// in the <see cref="Execute(T)"/> and <see cref="CanExecute(T)"/> callback methods.
/// </summary>
/// <typeparam name="T">The type of parameter being passed as input to the callbacks.</typeparam>
public sealed class RelayCommand<T> : IRelayCommand<T>
{
/// <summary>
/// The <see cref="Action"/> to invoke when <see cref="Execute(T)"/> is used.
/// </summary>
private readonly Action<T> execute;
/// <summary>
/// The optional action to invoke when <see cref="CanExecute(T)"/> is used.
/// </summary>
private readonly Func<T, bool>? canExecute;
/// <inheritdoc/>
public event EventHandler? CanExecuteChanged;
/// <summary>
/// Initializes a new instance of the <see cref="RelayCommand{T}"/> class that can always execute.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <remarks>
/// Due to the fact that the <see cref="System.Windows.Input.ICommand"/> interface exposes methods that accept a
/// nullable <see cref="object"/> parameter, it is recommended that if <typeparamref name="T"/> is a reference type,
/// you should always declare it as nullable, and to always perform checks within <paramref name="execute"/>.
/// </remarks>
public RelayCommand(Action<T> execute)
{
this.execute = execute;
}
/// <summary>
/// Initializes a new instance of the <see cref="RelayCommand{T}"/> class.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <remarks>See notes in <see cref="RelayCommand{T}(Action{T})"/>.</remarks>
public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
/// <inheritdoc/>
public void NotifyCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CanExecute(T parameter)
{
return this.canExecute?.Invoke(parameter) != false;
}
/// <inheritdoc/>
public bool CanExecute(object? parameter)
{
if (typeof(T).IsValueType &&
parameter is null &&
this.canExecute is null)
{
return true;
}
return CanExecute((T)parameter!);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Execute(T parameter)
{
if (CanExecute(parameter))
{
this.execute(parameter);
}
}
/// <inheritdoc/>
public void Execute(object? parameter)
{
Execute((T)parameter!);
}
}
}

View File

@ -0,0 +1,91 @@
// 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.
using System;
using System.Diagnostics.Contracts;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// An interface for a type providing the ability to exchange messages between different objects.
/// </summary>
public interface IMessenger
{
/// <summary>
/// Checks whether or not a given recipient has already been registered for a message.
/// </summary>
/// <typeparam name="TMessage">The type of message to check for the given recipient.</typeparam>
/// <typeparam name="TToken">The type of token to check the channel for.</typeparam>
/// <param name="recipient">The target recipient to check the registration for.</param>
/// <param name="token">The token used to identify the target channel to check.</param>
/// <returns>Whether or not <paramref name="recipient"/> has already been registered for the specified message.</returns>
[Pure]
bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>;
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="token">A token used to determine the receiving channel to use.</param>
/// <param name="action">The <see cref="Action{T}"/> to invoke when a message is received.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
void Register<TMessage, TToken>(object recipient, TToken token, Action<TMessage> action)
where TMessage : class
where TToken : IEquatable<TToken>;
/// <summary>
/// Unregisters a recipient from all registered messages.
/// </summary>
/// <param name="recipient">The recipient to unregister.</param>
/// <remarks>
/// This method will unregister the target recipient across all channels.
/// Use this method as an easy way to lose all references to a target recipient.
/// If the recipient has no registered handler, this method does nothing.
/// </remarks>
void UnregisterAll(object recipient);
/// <summary>
/// Unregisters a recipient from all messages on a specific channel.
/// </summary>
/// <typeparam name="TToken">The type of token to identify what channel to unregister from.</typeparam>
/// <param name="recipient">The recipient to unregister.</param>
/// <param name="token">The token to use to identify which handlers to unregister.</param>
/// <remarks>If the recipient has no registered handler, this method does nothing.</remarks>
void UnregisterAll<TToken>(object recipient, TToken token)
where TToken : IEquatable<TToken>;
/// <summary>
/// Unregisters a recipient from messages of a given type.
/// </summary>
/// <typeparam name="TMessage">The type of message to stop receiving.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to unregister from.</typeparam>
/// <param name="recipient">The recipient to unregister.</param>
/// <param name="token">The token to use to identify which handlers to unregister.</param>
/// <remarks>If the recipient has no registered handler, this method does nothing.</remarks>
void Unregister<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>;
/// <summary>
/// Sends a message of the specified type to all registered recipients.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
/// <param name="message">The message to send.</param>
/// <param name="token">The token indicating what channel to use.</param>
/// <returns>The message that was sent (ie. <paramref name="message"/>).</returns>
TMessage Send<TMessage, TToken>(TMessage message, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>;
/// <summary>
/// Resets the <see cref="IMessenger"/> instance and unregisters all the existing recipients.
/// </summary>
void Reset();
}
}

View File

@ -0,0 +1,20 @@
// 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.
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// An interface for a recipient that declares a registration for a specific message type.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
public interface IRecipient<in TMessage>
where TMessage : class
{
/// <summary>
/// Receives a given <typeparamref name="TMessage"/> message instance.
/// </summary>
/// <param name="message">The message being received.</param>
void Receive(TMessage message);
}
}

View File

@ -0,0 +1,143 @@
// 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.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
{
/// <summary>
/// A <see langword="class"/> for request messages that can receive multiple replies, which can either be used directly or through derived classes.
/// </summary>
/// <typeparam name="T">The type of request to make.</typeparam>
public class AsyncCollectionRequestMessage<T> : IAsyncEnumerable<T>
{
/// <summary>
/// The collection of received replies. We accept both <see cref="Task{TResult}"/> instance, representing already running
/// operations that can be executed in parallel, or <see cref="Func{T,TResult}"/> instances, which can be used so that multiple
/// asynchronous operations are only started sequentially from <see cref="GetAsyncEnumerator"/> and do not overlap in time.
/// </summary>
private readonly List<(Task<T>?, Func<CancellationToken, Task<T>>?)> responses = new List<(Task<T>?, Func<CancellationToken, Task<T>>?)>();
/// <summary>
/// The <see cref="CancellationTokenSource"/> instance used to link the token passed to
/// <see cref="GetAsyncEnumerator"/> and the one passed to all subscribers to the message.
/// </summary>
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
/// <summary>
/// Gets the <see cref="System.Threading.CancellationToken"/> instance that will be linked to the
/// one used to asynchronously enumerate the received responses. This can be used to cancel asynchronous
/// replies that are still being processed, if no new items are needed from this request message.
/// Consider the following example, where we define a message to retrieve the currently opened documents:
/// <code>
/// public class OpenDocumentsRequestMessage : AsyncCollectionRequestMessage&lt;XmlDocument&gt; { }
/// </code>
/// We can then request and enumerate the results like so:
/// <code>
/// await foreach (var document in Messenger.Default.Send&lt;OpenDocumentsRequestMessage&gt;())
/// {
/// // Process each document here...
/// }
/// </code>
/// If we also want to control the cancellation of the token passed to each subscriber to the message,
/// we can do so by passing a token we control to the returned message before starting the enumeration
/// (<see cref="TaskAsyncEnumerableExtensions.WithCancellation{T}(IAsyncEnumerable{T},CancellationToken)"/>).
/// The previous snippet with this additional change looks as follows:
/// <code>
/// await foreach (var document in Messenger.Default.Send&lt;OpenDocumentsRequestMessage&gt;().WithCancellation(cts.Token))
/// {
/// // Process each document here...
/// }
/// </code>
/// When no more new items are needed (or for any other reason depending on the situation), the token
/// passed to the enumerator can be canceled (by calling <see cref="CancellationTokenSource.Cancel()"/>),
/// and that will also notify the remaining tasks in the request message. The token exposed by the message
/// itself will automatically be linked and canceled with the one passed to the enumerator.
/// </summary>
public CancellationToken CancellationToken => this.cancellationTokenSource.Token;
/// <summary>
/// Replies to the current request message.
/// </summary>
/// <param name="response">The response to use to reply to the request message.</param>
public void Reply(T response)
{
Reply(Task.FromResult(response));
}
/// <summary>
/// Replies to the current request message.
/// </summary>
/// <param name="response">The response to use to reply to the request message.</param>
public void Reply(Task<T> response)
{
this.responses.Add((response, null));
}
/// <summary>
/// Replies to the current request message.
/// </summary>
/// <param name="response">The response to use to reply to the request message.</param>
public void Reply(Func<CancellationToken, Task<T>> response)
{
this.responses.Add((null, response));
}
/// <summary>
/// Gets the collection of received response items.
/// </summary>
/// <param name="cancellationToken">A <see cref="System.Threading.CancellationToken"/> value to stop the operation.</param>
/// <returns>The collection of received response items.</returns>
[Pure]
public async Task<IReadOnlyCollection<T>> GetResponsesAsync(CancellationToken cancellationToken = default)
{
if (cancellationToken.CanBeCanceled)
{
cancellationToken.Register(this.cancellationTokenSource.Cancel);
}
List<T> results = new List<T>(this.responses.Count);
await foreach (var response in this.WithCancellation(cancellationToken))
{
results.Add(response);
}
return results;
}
/// <inheritdoc/>
[Pure]
[EditorBrowsable(EditorBrowsableState.Never)]
public async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
if (cancellationToken.CanBeCanceled)
{
cancellationToken.Register(this.cancellationTokenSource.Cancel);
}
foreach (var (task, func) in this.responses)
{
if (cancellationToken.IsCancellationRequested)
{
yield break;
}
if (!(task is null))
{
yield return await task.ConfigureAwait(false);
}
else
{
yield return await func!(cancellationToken).ConfigureAwait(false);
}
}
}
}
}

View File

@ -0,0 +1,95 @@
// 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.
using System;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
{
/// <summary>
/// A <see langword="class"/> for async request messages, which can either be used directly or through derived classes.
/// </summary>
/// <typeparam name="T">The type of request to make.</typeparam>
public class AsyncRequestMessage<T>
{
private Task<T>? response;
/// <summary>
/// Gets the message response.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when <see cref="HasReceivedResponse"/> is <see langword="false"/>.</exception>
public Task<T> Response
{
get
{
if (!HasReceivedResponse)
{
ThrowInvalidOperationExceptionForNoResponseReceived();
}
return this.response!;
}
}
/// <summary>
/// Gets a value indicating whether a response has already been assigned to this instance.
/// </summary>
public bool HasReceivedResponse { get; private set; }
/// <summary>
/// Replies to the current request message.
/// </summary>
/// <param name="response">The response to use to reply to the request message.</param>
/// <exception cref="InvalidOperationException">Thrown if <see cref="Response"/> has already been set.</exception>
public void Reply(T response)
{
Reply(Task.FromResult(response));
}
/// <summary>
/// Replies to the current request message.
/// </summary>
/// <param name="response">The response to use to reply to the request message.</param>
/// <exception cref="InvalidOperationException">Thrown if <see cref="Response"/> has already been set.</exception>
public void Reply(Task<T> response)
{
if (HasReceivedResponse)
{
ThrowInvalidOperationExceptionForDuplicateReply();
}
HasReceivedResponse = true;
this.response = response;
}
/// <inheritdoc cref="Task{T}.GetAwaiter"/>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[EditorBrowsable(EditorBrowsableState.Never)]
public TaskAwaiter<T> GetAwaiter()
{
return this.Response.GetAwaiter();
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when a response is not available.
/// </summary>
private static void ThrowInvalidOperationExceptionForNoResponseReceived()
{
throw new InvalidOperationException("No response was received for the given request message");
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when <see cref="Reply(T)"/> or <see cref="Reply(Task{T})"/> are called twice.
/// </summary>
private static void ThrowInvalidOperationExceptionForDuplicateReply()
{
throw new InvalidOperationException("A response has already been issued for the current message");
}
}
}

View File

@ -0,0 +1,50 @@
// 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.
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
{
/// <summary>
/// A <see langword="class"/> for request messages that can receive multiple replies, which can either be used directly or through derived classes.
/// </summary>
/// <typeparam name="T">The type of request to make.</typeparam>
public class CollectionRequestMessage<T> : IEnumerable<T>
{
private readonly List<T> responses = new List<T>();
/// <summary>
/// Gets the message responses.
/// </summary>
public IReadOnlyCollection<T> Responses => this.responses;
/// <summary>
/// Replies to the current request message.
/// </summary>
/// <param name="response">The response to use to reply to the request message.</param>
public void Reply(T response)
{
this.responses.Add(response);
}
/// <inheritdoc/>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[EditorBrowsable(EditorBrowsableState.Never)]
public IEnumerator<T> GetEnumerator()
{
return this.responses.GetEnumerator();
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
}

View File

@ -0,0 +1,53 @@
// 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
// This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
// more info in ThirdPartyNotices.txt in the root of the project.
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
{
/// <summary>
/// A message used to broadcast property changes in observable objects.
/// </summary>
/// <typeparam name="T">The type of the property to broadcast the change for.</typeparam>
public class PropertyChangedMessage<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="PropertyChangedMessage{T}"/> class.
/// </summary>
/// <param name="sender">The original sender of the broadcast message.</param>
/// <param name="propertyName">The name of the property that changed.</param>
/// <param name="oldValue">The value that the property had before the change.</param>
/// <param name="newValue">The value that the property has after the change.</param>
public PropertyChangedMessage(object sender, string? propertyName, T oldValue, T newValue)
{
Sender = sender;
PropertyName = propertyName;
OldValue = oldValue;
NewValue = newValue;
}
/// <summary>
/// Gets the original sender of the broadcast message.
/// </summary>
public object Sender { get; }
/// <summary>
/// Gets the name of the property that changed.
/// </summary>
public string? PropertyName { get; }
/// <summary>
/// Gets the value that the property had before the change.
/// </summary>
public T OldValue { get; }
/// <summary>
/// Gets the value that the property has after the change.
/// </summary>
public T NewValue { get; }
}
}

View File

@ -0,0 +1,84 @@
// 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.
using System;
#pragma warning disable CS8618
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
{
/// <summary>
/// A <see langword="class"/> for request messages, which can either be used directly or through derived classes.
/// </summary>
/// <typeparam name="T">The type of request to make.</typeparam>
public class RequestMessage<T>
{
private T response;
/// <summary>
/// Gets the message response.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when <see cref="HasReceivedResponse"/> is <see langword="false"/>.</exception>
public T Response
{
get
{
if (!HasReceivedResponse)
{
ThrowInvalidOperationExceptionForNoResponseReceived();
}
return this.response;
}
}
/// <summary>
/// Gets a value indicating whether a response has already been assigned to this instance.
/// </summary>
public bool HasReceivedResponse { get; private set; }
/// <summary>
/// Replies to the current request message.
/// </summary>
/// <param name="response">The response to use to reply to the request message.</param>
/// <exception cref="InvalidOperationException">Thrown if <see cref="Response"/> has already been set.</exception>
public void Reply(T response)
{
if (HasReceivedResponse)
{
ThrowInvalidOperationExceptionForDuplicateReply();
}
HasReceivedResponse = true;
this.response = response;
}
/// <summary>
/// Implicitly gets the response from a given <see cref="RequestMessage{T}"/> instance.
/// </summary>
/// <param name="message">The input <see cref="RequestMessage{T}"/> instance.</param>
/// <exception cref="InvalidOperationException">Thrown when <see cref="HasReceivedResponse"/> is <see langword="false"/>.</exception>
public static implicit operator T(RequestMessage<T> message)
{
return message.Response;
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when a response is not available.
/// </summary>
private static void ThrowInvalidOperationExceptionForNoResponseReceived()
{
throw new InvalidOperationException("No response was received for the given request message");
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when <see cref="Reply"/> is called twice.
/// </summary>
private static void ThrowInvalidOperationExceptionForDuplicateReply()
{
throw new InvalidOperationException("A response has already been issued for the current message");
}
}
}

View File

@ -0,0 +1,27 @@
// 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.
namespace Microsoft.Toolkit.Mvvm.Messaging.Messages
{
/// <summary>
/// A base message that signals whenever a specific value has changed.
/// </summary>
/// <typeparam name="T">The type of value that has changed.</typeparam>
public class ValueChangedMessage<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="ValueChangedMessage{T}"/> class.
/// </summary>
/// <param name="value">The value that has changed.</param>
public ValueChangedMessage(T value)
{
Value = value;
}
/// <summary>
/// Gets the value that has changed.
/// </summary>
public T Value { get; }
}
}

View File

@ -0,0 +1,680 @@
// 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.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Collections.Extensions;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// A type that can be used to exchange messages between different objects.
/// This can be useful to decouple different modules of an application without having to keep strong
/// references to types being referenced. It is also possible to send messages to specific channels, uniquely
/// identified by a token, and to have different messengers in different sections of an applications.
/// In order to use the <see cref="IMessenger"/> functionalities, first define a message type, like so:
/// <code>
/// public sealed class LoginCompletedMessage { }
/// </code>
/// Then, register your a recipient for this message:
/// <code>
/// Messenger.Default.Register&lt;LoginCompletedMessage&gt;(this, m =>
/// {
/// // Handle the message here...
/// });
/// </code>
/// Finally, send a message when needed, like so:
/// <code>
/// Messenger.Default.Send&lt;LoginCompletedMessage&gt;();
/// </code>
/// Additionally, the method group syntax can also be used to specify the action
/// to invoke when receiving a message, if a method with the right signature is available
/// in the current scope. This is helpful to keep the registration and handling logic separate.
/// Following up from the previous example, consider a class having this method:
/// <code>
/// private void Receive(LoginCompletedMessage message)
/// {
/// // Handle the message there
/// }
/// </code>
/// The registration can then be performed in a single line like so:
/// <code>
/// Messenger.Default.Register&lt;LoginCompletedMessage&gt;(this, Receive);
/// </code>
/// The C# compiler will automatically convert that expression to an <see cref="Action{T}"/> instance
/// compatible with the <see cref="MessengerExtensions.Register{T}(IMessenger,object,Action{T})"/> method.
/// This will also work if multiple overloads of that method are available, each handling a different
/// message type: the C# compiler will automatically pick the right one for the current message type.
/// For info on the other available features, check the <see cref="IMessenger"/> interface.
/// </summary>
public sealed class Messenger : IMessenger
{
// The Messenger class uses the following logic to link stored instances together:
// --------------------------------------------------------------------------------------------------------
// DictionarySlim<Recipient, HashSet<IMapping>> recipientsMap;
// | \________________[*]IDictionarySlim<Recipient, IDictionarySlim<TToken>>
// | \___ / / /
// | ________(recipients registrations)___________\________/ / __/
// | / _______(channel registrations)_____\___________________/ /
// | / / \ /
// DictionarySlim<Recipient, DictionarySlim<TToken, Action<TMessage>>> mapping = Mapping<TMessage, TToken>
// / / \ / /
// ___(Type2.tToken)____/ / \______/___________________/
// /________________(Type2.tMessage)____/ /
// / ________________________________________/
// / /
// DictionarySlim<Type2, IMapping> typesMap;
// --------------------------------------------------------------------------------------------------------
// Each combination of <TMessage, TToken> results in a concrete Mapping<TMessage, TToken> type, which holds
// the references from registered recipients to handlers. The handlers are stored in a <TToken, Action<TMessage>>
// dictionary, so that each recipient can have up to one registered handler for a given token, for each
// message type. Each mapping is stored in the types map, which associates each pair of concrete types to its
// mapping instance. Mapping instances are exposed as IMapping items, as each will be a closed type over
// a different combination of TMessage and TToken generic type parameters. Each existing recipient is also stored in
// the main recipients map, along with a set of all the existing dictionaries of handlers for that recipient (for all
// message types and token types). A recipient is stored in the main map as long as it has at least one
// registered handler in any of the existing mappings for every message/token type combination.
// The shared map is used to access the set of all registered handlers for a given recipient, without having
// to know in advance the type of message or token being used for the registration, and without having to
// use reflection. This is the same approach used in the types map, as we expose saved items as IMapping values too.
// Note that each mapping stored in the associated set for each recipient also indirectly implements
// IDictionarySlim<Recipient, Token>, with any token type currently in use by that recipient. This allows to retrieve
// the type-closed mappings of registered handlers with a given token type, for any message type, for every receiver,
// again without having to use reflection. This shared map is used to unregister messages from a given recipients
// either unconditionally, by message type, by token, or for a specific pair of message type and token value.
/// <summary>
/// The collection of currently registered recipients, with a link to their linked message receivers.
/// </summary>
/// <remarks>
/// This collection is used to allow reflection-free access to all the existing
/// registered recipients from <see cref="UnregisterAll"/> and other methods in this type,
/// so that all the existing handlers can be removed without having to dynamically create
/// the generic types for the containers of the various dictionaries mapping the handlers.
/// </remarks>
private readonly DictionarySlim<Recipient, HashSet<IMapping>> recipientsMap = new DictionarySlim<Recipient, HashSet<IMapping>>();
/// <summary>
/// The <see cref="Mapping{TMessage,TToken}"/> instance for types combination.
/// </summary>
/// <remarks>
/// The values are just of type <see cref="IDictionarySlim{T}"/> as we don't know the type parameters in advance.
/// Each method relies on <see cref="GetOrAddMapping{TMessage,TToken}"/> to get the type-safe instance
/// of the <see cref="Mapping{TMessage,TToken}"/> class for each pair of generic arguments in use.
/// </remarks>
private readonly DictionarySlim<Type2, IMapping> typesMap = new DictionarySlim<Type2, IMapping>();
/// <summary>
/// Gets the default <see cref="Messenger"/> instance.
/// </summary>
public static Messenger Default { get; } = new Messenger();
/// <inheritdoc/>
public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
{
return false;
}
var key = new Recipient(recipient);
return mapping!.ContainsKey(key);
}
}
/// <inheritdoc/>
public void Register<TMessage, TToken>(object recipient, TToken token, Action<TMessage> action)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
// Get the <TMessage, TToken> registration list for this recipient
Mapping<TMessage, TToken> mapping = GetOrAddMapping<TMessage, TToken>();
var key = new Recipient(recipient);
ref DictionarySlim<TToken, Action<TMessage>>? map = ref mapping.GetOrAddValueRef(key);
map ??= new DictionarySlim<TToken, Action<TMessage>>();
// Add the new registration entry
ref Action<TMessage>? handler = ref map.GetOrAddValueRef(token);
if (!(handler is null))
{
ThrowInvalidOperationExceptionForDuplicateRegistration();
}
handler = action;
// Update the total counter for handlers for the current type parameters
mapping.TotalHandlersCount++;
// Make sure this registration map is tracked for the current recipient
ref HashSet<IMapping>? set = ref this.recipientsMap.GetOrAddValueRef(key);
set ??= new HashSet<IMapping>();
set.Add(mapping);
}
}
/// <inheritdoc/>
public void UnregisterAll(object recipient)
{
lock (this.recipientsMap)
{
// If the recipient has no registered messages at all, ignore
var key = new Recipient(recipient);
if (!this.recipientsMap.TryGetValue(key, out HashSet<IMapping>? set))
{
return;
}
// Removes all the lists of registered handlers for the recipient
foreach (IMapping mapping in set!)
{
if (mapping.TryRemove(key, out object? handlersMap))
{
// If this branch is taken, it means the target recipient to unregister
// had at least one registered handler for the current <TToken, TMessage>
// pair of type parameters, which here is masked out by the IMapping interface.
// Before removing the handlers, we need to retrieve the count of how many handlers
// are being removed, in order to update the total counter for the mapping.
// Just casting the dictionary to the base interface and accessing the Count
// property directly gives us O(1) access time to retrieve this count.
// The handlers map is the IDictionary<TToken, TMessage> instance for the mapping.
int handlersCount = Unsafe.As<IDictionarySlim>(handlersMap).Count;
mapping.TotalHandlersCount -= handlersCount;
if (mapping.Count == 0)
{
// Maps here are really of type Mapping<,> and with unknown type arguments.
// If after removing the current recipient a given map becomes empty, it means
// that there are no registered recipients at all for a given pair of message
// and token types. In that case, we also remove the map from the types map.
// The reason for keeping a key in each mapping is that removing items from a
// dictionary (a hashed collection) only costs O(1) in the best case, while
// if we had tried to iterate the whole dictionary every time we would have
// paid an O(n) minimum cost for each single remove operation.
this.typesMap.Remove(mapping.TypeArguments);
}
}
}
// Remove the associated set in the recipients map
this.recipientsMap.Remove(key);
}
}
/// <inheritdoc/>
public void UnregisterAll<TToken>(object recipient, TToken token)
where TToken : IEquatable<TToken>
{
bool lockTaken = false;
IDictionarySlim<Recipient, IDictionarySlim<TToken>>[]? maps = null;
int i = 0;
// We use an explicit try/finally block here instead of the lock syntax so that we can use a single
// one both to release the lock and to clear the rented buffer and return it to the pool. The reason
// why we're declaring the buffer here and clearing and returning it in this outer finally block is
// that doing so doesn't require the lock to be kept, and releasing it before performing this last
// step reduces the total time spent while the lock is acquired, which in turn reduces the lock
// contention in multi-threaded scenarios where this method is invoked concurrently.
try
{
Monitor.Enter(this.recipientsMap, ref lockTaken);
// Get the shared set of mappings for the recipient, if present
var key = new Recipient(recipient);
if (!this.recipientsMap.TryGetValue(key, out HashSet<IMapping>? set))
{
return;
}
// Copy the candidate mappings for the target recipient to a local
// array, as we can't modify the contents of the set while iterating it.
// The rented buffer is oversized and will also include mappings for
// handlers of messages that are registered through a different token.
maps = ArrayPool<IDictionarySlim<Recipient, IDictionarySlim<TToken>>>.Shared.Rent(set!.Count);
foreach (IMapping item in set)
{
// Select all mappings using the same token type
if (item is IDictionarySlim<Recipient, IDictionarySlim<TToken>> mapping)
{
maps[i++] = mapping;
}
}
// Iterate through all the local maps. These are all the currently
// existing maps of handlers for messages of any given type, with a token
// of the current type, for the target recipient. We heavily rely on
// interfaces here to be able to iterate through all the available mappings
// without having to know the concrete type in advance, and without having
// to deal with reflection: we can just check if the type of the closed interface
// matches with the token type currently in use, and operate on those instances.
foreach (IDictionarySlim<Recipient, IDictionarySlim<TToken>> map in maps.AsSpan(0, i))
{
// We don't need whether or not the map contains the recipient, as the
// sequence of maps has already been copied from the set containing all
// the mappings for the target recipients: it is guaranteed to be here.
IDictionarySlim<TToken> holder = map[key];
// Try to remove the registered handler for the input token,
// for the current message type (unknown from here).
if (holder.Remove(token))
{
// As above, we need to update the total number of registered handlers for the map.
// In this case we also know that the current TToken type parameter is of interest
// for the current method, as we're only unsubscribing handlers using that token.
// This is because we're already working on the final <TToken, TMessage> mapping,
// which associates a single handler with a given token, for a given recipient.
// This means that we don't have to retrieve the count to subtract in this case,
// we're just removing a single handler at a time. So, we just decrement the total.
Unsafe.As<IMapping>(map).TotalHandlersCount--;
if (holder.Count == 0)
{
// If the map is empty, remove the recipient entirely from its container
map.Remove(key);
if (map.Count == 0)
{
// If no handlers are left at all for the recipient, across all
// message types and token types, remove the set of mappings
// entirely for the current recipient, and lost the strong
// reference to it as well. This is the same situation that
// would've been achieved by just calling UnregisterAll(recipient).
set.Remove(Unsafe.As<IMapping>(map));
if (set.Count == 0)
{
this.recipientsMap.Remove(key);
}
}
}
}
}
}
finally
{
// Release the lock, if we did acquire it
if (lockTaken)
{
Monitor.Exit(this.recipientsMap);
}
// If we got to renting the array of maps, return it to the shared pool.
// Remove references to avoid leaks coming from the shared memory pool.
// We manually create a span and clear it as a small optimization, as
// arrays rented from the pool can be larger than the requested size.
if (!(maps is null))
{
maps.AsSpan(0, i).Clear();
ArrayPool<IDictionarySlim<Recipient, IDictionarySlim<TToken>>>.Shared.Return(maps);
}
}
}
/// <inheritdoc/>
public void Unregister<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
// Get the registration list, if available
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
{
return;
}
var key = new Recipient(recipient);
if (!mapping!.TryGetValue(key, out DictionarySlim<TToken, Action<TMessage>>? dictionary))
{
return;
}
// Remove the target handler
if (dictionary!.Remove(token))
{
// Decrement the total count, as above
mapping.TotalHandlersCount--;
// If the map is empty, it means that the current recipient has no remaining
// registered handlers for the current <TMessage, TToken> combination, regardless,
// of the specific token value (ie. the channel used to receive messages of that type).
// We can remove the map entirely from this container, and remove the link to the map itself
// to the current mapping between existing registered recipients (or entire recipients too).
if (dictionary.Count == 0)
{
mapping.Remove(key);
HashSet<IMapping> set = this.recipientsMap[key];
set.Remove(mapping);
if (set.Count == 0)
{
this.recipientsMap.Remove(key);
}
}
}
}
}
/// <inheritdoc/>
public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
Action<TMessage>[] entries;
int i = 0;
lock (this.recipientsMap)
{
// Check whether there are any registered recipients
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
{
return message;
}
// We need to make a local copy of the currently registered handlers,
// since users might try to unregister (or register) new handlers from
// inside one of the currently existing handlers. We can use memory pooling
// to reuse arrays, to minimize the average memory usage. In practice,
// we usually just need to pay the small overhead of copying the items.
entries = ArrayPool<Action<TMessage>>.Shared.Rent(mapping!.TotalHandlersCount);
// Copy the handlers to the local collection.
// Both types being enumerate expose a struct enumerator,
// so we're not actually allocating the enumerator here.
// The array is oversized at this point, since it also includes
// handlers for different tokens. We can reuse the same variable
// to count the number of matching handlers to invoke later on.
// This will be the array slice with valid actions in the rented buffer.
var mappingEnumerator = mapping.GetEnumerator();
// Explicit enumerator usage here as we're using a custom one
// that doesn't expose the single standard Current property.
while (mappingEnumerator.MoveNext())
{
var pairsEnumerator = mappingEnumerator.Value.GetEnumerator();
while (pairsEnumerator.MoveNext())
{
// Only select the ones with a matching token
if (pairsEnumerator.Key.Equals(token))
{
entries[i++] = pairsEnumerator.Value;
}
}
}
}
try
{
// Invoke all the necessary handlers on the local copy of entries
foreach (var entry in entries.AsSpan(0, i))
{
entry(message);
}
}
finally
{
// As before, we also need to clear it first to avoid having potentially long
// lasting memory leaks due to leftover references being stored in the pool.
entries.AsSpan(0, i).Clear();
ArrayPool<Action<TMessage>>.Shared.Return(entries);
}
return message;
}
/// <inheritdoc/>
public void Reset()
{
lock (this.recipientsMap)
{
this.recipientsMap.Clear();
this.typesMap.Clear();
}
}
/// <summary>
/// Tries to get the <see cref="Mapping{TMessage,TToken}"/> instance of currently registered recipients
/// for the combination of types <typeparamref name="TMessage"/> and <typeparamref name="TToken"/>.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
/// <param name="mapping">The resulting <see cref="Mapping{TMessage,TToken}"/> instance, if found.</param>
/// <returns>Whether or not the required <see cref="Mapping{TMessage,TToken}"/> instance was found.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryGetMapping<TMessage, TToken>(out Mapping<TMessage, TToken>? mapping)
where TMessage : class
where TToken : IEquatable<TToken>
{
var key = new Type2(typeof(TMessage), typeof(TToken));
if (this.typesMap.TryGetValue(key, out IMapping? target))
{
// This method and the ones above are the only ones handling values in the types map,
// and here we are sure that the object reference we have points to an instance of the
// right type. Using an unsafe cast skips two conditional branches and is faster.
mapping = Unsafe.As<Mapping<TMessage, TToken>>(target);
return true;
}
mapping = null;
return false;
}
/// <summary>
/// Gets the <see cref="Mapping{TMessage,TToken}"/> instance of currently registered recipients
/// for the combination of types <typeparamref name="TMessage"/> and <typeparamref name="TToken"/>.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
/// <returns>A <see cref="Mapping{TMessage,TToken}"/> instance with the requested type arguments.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private Mapping<TMessage, TToken> GetOrAddMapping<TMessage, TToken>()
where TMessage : class
where TToken : IEquatable<TToken>
{
var key = new Type2(typeof(TMessage), typeof(TToken));
ref IMapping? target = ref this.typesMap.GetOrAddValueRef(key);
target ??= new Mapping<TMessage, TToken>();
return Unsafe.As<Mapping<TMessage, TToken>>(target);
}
/// <summary>
/// A mapping type representing a link to recipients and their view of handlers per communication channel.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
/// <remarks>
/// This type is defined for simplicity and as a workaround for the lack of support for using type aliases
/// over open generic types in C# (using type aliases can only be used for concrete, closed types).
/// </remarks>
private sealed class Mapping<TMessage, TToken> : DictionarySlim<Recipient, DictionarySlim<TToken, Action<TMessage>>>, IMapping
where TMessage : class
where TToken : IEquatable<TToken>
{
/// <summary>
/// Initializes a new instance of the <see cref="Mapping{TMessage, TToken}"/> class.
/// </summary>
public Mapping()
{
TypeArguments = new Type2(typeof(TMessage), typeof(TToken));
}
/// <inheritdoc/>
public Type2 TypeArguments { get; }
/// <inheritdoc/>
public int TotalHandlersCount { get; set; }
}
/// <summary>
/// An interface for the <see cref="Mapping{TMessage,TToken}"/> type which allows to retrieve the type
/// arguments from a given generic instance without having any prior knowledge about those arguments.
/// </summary>
private interface IMapping : IDictionarySlim<Recipient>
{
/// <summary>
/// Gets the <see cref="Type2"/> instance representing the current type arguments.
/// </summary>
Type2 TypeArguments { get; }
/// <summary>
/// Gets or sets the total number of handlers in the current instance.
/// </summary>
int TotalHandlersCount { get; set; }
}
/// <summary>
/// A simple type representing a recipient.
/// </summary>
/// <remarks>
/// This type is used to enable fast indexing in each mapping dictionary,
/// since it acts as an external override for the <see cref="GetHashCode"/> and
/// <see cref="Equals(object?)"/> methods for arbitrary objects, removing both
/// the virtual call and preventing instances overriding those methods in this context.
/// Using this type guarantees that all the equality operations are always only done
/// based on reference equality for each registered recipient, regardless of its type.
/// </remarks>
private readonly struct Recipient : IEquatable<Recipient>
{
/// <summary>
/// The registered recipient.
/// </summary>
private readonly object target;
/// <summary>
/// Initializes a new instance of the <see cref="Recipient"/> struct.
/// </summary>
/// <param name="target">The target recipient instance.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Recipient(object target)
{
this.target = target;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Recipient other)
{
return ReferenceEquals(this.target, other.target);
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Recipient other && Equals(other);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
return RuntimeHelpers.GetHashCode(this.target);
}
}
/// <summary>
/// A simple type representing an immutable pair of types.
/// </summary>
/// <remarks>
/// This type replaces a simple <see cref="ValueTuple{T1,T2}"/> as it's faster in its
/// <see cref="GetHashCode"/> and <see cref="IEquatable{T}.Equals(T)"/> methods, and because
/// unlike a value tuple it exposes its fields as immutable. Additionally, the
/// <see cref="tMessage"/> and <see cref="tToken"/> fields provide additional clarity reading
/// the code compared to <see cref="ValueTuple{T1,T2}.Item1"/> and <see cref="ValueTuple{T1,T2}.Item2"/>.
/// </remarks>
private readonly struct Type2 : IEquatable<Type2>
{
/// <summary>
/// The type of registered message.
/// </summary>
private readonly Type tMessage;
/// <summary>
/// The type of registration token.
/// </summary>
private readonly Type tToken;
/// <summary>
/// Initializes a new instance of the <see cref="Type2"/> struct.
/// </summary>
/// <param name="tMessage">The type of registered message.</param>
/// <param name="tToken">The type of registration token.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Type2(Type tMessage, Type tToken)
{
this.tMessage = tMessage;
this.tToken = tToken;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Type2 other)
{
return
ReferenceEquals(this.tMessage, other.tMessage) &&
ReferenceEquals(this.tToken, other.tToken);
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Type2 other && Equals(other);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
unchecked
{
// To combine the two hashes, we can simply use the fast djb2 hash algorithm.
// This is not a problem in this case since we already know that the base
// RuntimeHelpers.GetHashCode method is providing hashes with a good enough distribution.
int hash = RuntimeHelpers.GetHashCode(this.tMessage);
hash = (hash << 5) + hash;
hash += RuntimeHelpers.GetHashCode(this.tToken);
return hash;
}
}
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when trying to add a duplicate handler.
/// </summary>
private static void ThrowInvalidOperationExceptionForDuplicateRegistration()
{
throw new InvalidOperationException("The target recipient has already subscribed to the target message");
}
}
}

View File

@ -0,0 +1,41 @@
// 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.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// Extensions for the <see cref="IMessenger"/> type.
/// </summary>
public static partial class MessengerExtensions
{
/// <summary>
/// An empty type representing a generic token with no specific value.
/// </summary>
private readonly struct Unit : IEquatable<Unit>
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Unit other)
{
return true;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Unit;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
return 0;
}
}
}
}

View File

@ -0,0 +1,266 @@
// 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.
using System;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// Extensions for the <see cref="IMessenger"/> type.
/// </summary>
public static partial class MessengerExtensions
{
/// <summary>
/// The <see cref="MethodInfo"/> instance associated with <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/>.
/// </summary>
private static readonly MethodInfo RegisterIRecipientMethodInfo;
/// <summary>
/// A class that acts as a static container to associate a <see cref="ConditionalWeakTable{TKey,TValue}"/> instance to each
/// <typeparamref name="TToken"/> type in use. This is done because we can only use a single type as key, but we need to track
/// associations of each recipient type also across different communication channels, each identified by a token.
/// Since the token is actually a compile-time parameter, we can use a wrapping class to let the runtime handle a different
/// instance for each generic type instantiation. This lets us only worry about the recipient type being inspected.
/// </summary>
/// <typeparam name="TToken">The token indicating what channel to use.</typeparam>
private static class DiscoveredRecipients<TToken>
where TToken : IEquatable<TToken>
{
/// <summary>
/// The <see cref="ConditionalWeakTable{TKey,TValue}"/> instance used to track the preloaded registration actions for each recipient.
/// </summary>
public static readonly ConditionalWeakTable<Type, Action<IMessenger, object, TToken>[]> RegistrationMethods
= new ConditionalWeakTable<Type, Action<IMessenger, object, TToken>[]>();
}
/// <summary>
/// Initializes static members of the <see cref="MessengerExtensions"/> class.
/// </summary>
static MessengerExtensions()
{
RegisterIRecipientMethodInfo = (
from methodInfo in typeof(MessengerExtensions).GetMethods()
where methodInfo.Name == nameof(Register) &&
methodInfo.IsGenericMethod &&
methodInfo.GetGenericArguments().Length == 2
let parameters = methodInfo.GetParameters()
where parameters.Length == 3 &&
parameters[1].ParameterType.IsGenericType &&
parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(IRecipient<>)
select methodInfo).First();
}
/// <summary>
/// Checks whether or not a given recipient has already been registered for a message.
/// </summary>
/// <typeparam name="TMessage">The type of message to check for the given recipient.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to check the registration.</param>
/// <param name="recipient">The target recipient to check the registration for.</param>
/// <returns>Whether or not <paramref name="recipient"/> has already been registered for the specified message.</returns>
/// <remarks>This method will use the default channel to check for the requested registration.</remarks>
[Pure]
public static bool IsRegistered<TMessage>(this IMessenger messenger, object recipient)
where TMessage : class
{
return messenger.IsRegistered<TMessage, Unit>(recipient, default);
}
/// <summary>
/// Registers all declared message handlers for a given recipient, using the default channel.
/// </summary>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <remarks>See notes for <see cref="RegisterAll{TToken}(IMessenger,object,TToken)"/> for more info.</remarks>
public static void RegisterAll(this IMessenger messenger, object recipient)
{
messenger.RegisterAll(recipient, default(Unit));
}
/// <summary>
/// Registers all declared message handlers for a given recipient.
/// </summary>
/// <typeparam name="TToken">The type of token to identify what channel to use to receive messages.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="token">The token indicating what channel to use.</param>
/// <remarks>
/// This method will register all messages corresponding to the <see cref="IRecipient{TMessage}"/> interfaces
/// being implemented by <paramref name="recipient"/>. If none are present, this method will do nothing.
/// Note that unlike all other extensions, this method will use reflection to find the handlers to register.
/// Once the registration is complete though, the performance will be exactly the same as with handlers
/// registered directly through any of the other generic extensions for the <see cref="IMessenger"/> interface.
/// </remarks>
public static void RegisterAll<TToken>(this IMessenger messenger, object recipient, TToken token)
where TToken : IEquatable<TToken>
{
// We use this method as a callback for the conditional weak table, which will both
// handle thread-safety for us, as well as avoiding all the LINQ codegen bloat here.
// This method is only invoked once per recipient type and token type, so we're not
// worried about making it super efficient, and we can use the LINQ code for clarity.
static Action<IMessenger, object, TToken>[] LoadRegistrationMethodsForType(Type type)
{
return (
from interfaceType in type.GetInterfaces()
where interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>)
let messageType = interfaceType.GenericTypeArguments[0]
let registrationMethod = RegisterIRecipientMethodInfo.MakeGenericMethod(messageType, typeof(TToken))
let registrationAction = GetRegistrationAction(type, registrationMethod)
select registrationAction).ToArray();
}
// Helper method to build and compile an expression tree to a message handler to use for the registration
// This is used to reduce the overhead of repeated calls to MethodInfo.Invoke (which is over 10 times slower).
static Action<IMessenger, object, TToken> GetRegistrationAction(Type type, MethodInfo methodInfo)
{
// Input parameters (IMessenger instance, non-generic recipient, token)
ParameterExpression
arg0 = Expression.Parameter(typeof(IMessenger)),
arg1 = Expression.Parameter(typeof(object)),
arg2 = Expression.Parameter(typeof(TToken));
// Cast the recipient and invoke the registration method
MethodCallExpression body = Expression.Call(null, methodInfo, new Expression[]
{
arg0,
Expression.Convert(arg1, type),
arg2
});
// Create the expression tree and compile to a target delegate
return Expression.Lambda<Action<IMessenger, object, TToken>>(body, arg0, arg1, arg2).Compile();
}
// Get or compute the registration methods for the current recipient type.
// As in Microsoft.Toolkit.Extensions.TypeExtensions.ToTypeString, we use a lambda
// expression instead of a method group expression to leverage the statically initialized
// delegate and avoid repeated allocations for each invocation of this method.
// For more info on this, see the related issue at https://github.com/dotnet/roslyn/issues/5835.
Action<IMessenger, object, TToken>[] registrationActions = DiscoveredRecipients<TToken>.RegistrationMethods.GetValue(
recipient.GetType(),
t => LoadRegistrationMethodsForType(t));
foreach (Action<IMessenger, object, TToken> registrationAction in registrationActions)
{
registrationAction(messenger, recipient, token);
}
}
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
public static void Register<TMessage>(this IMessenger messenger, IRecipient<TMessage> recipient)
where TMessage : class
{
messenger.Register<TMessage, Unit>(recipient, default, recipient.Receive);
}
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to use to receive messages.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="token">The token indicating what channel to use.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
public static void Register<TMessage, TToken>(this IMessenger messenger, IRecipient<TMessage> recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
messenger.Register<TMessage, TToken>(recipient, token, recipient.Receive);
}
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="action">The <see cref="Action{T}"/> to invoke when a message is received.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
public static void Register<TMessage>(this IMessenger messenger, object recipient, Action<TMessage> action)
where TMessage : class
{
messenger.Register(recipient, default(Unit), action);
}
/// <summary>
/// Unregisters a recipient from messages of a given type.
/// </summary>
/// <typeparam name="TMessage">The type of message to stop receiving.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to unregister the recipient.</param>
/// <param name="recipient">The recipient to unregister.</param>
/// <remarks>
/// This method will unregister the target recipient only from the default channel.
/// If the recipient has no registered handler, this method does nothing.
/// </remarks>
public static void Unregister<TMessage>(this IMessenger messenger, object recipient)
where TMessage : class
{
messenger.Unregister<TMessage, Unit>(recipient, default);
}
/// <summary>
/// Sends a message of the specified type to all registered recipients.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
/// <returns>The message that has been sent.</returns>
/// <remarks>
/// This method is a shorthand for <see cref="Send{TMessage}(IMessenger,TMessage)"/> when the
/// message type exposes a parameterless constructor: it will automatically create
/// a new <typeparamref name="TMessage"/> instance and send that to its recipients.
/// </remarks>
public static TMessage Send<TMessage>(this IMessenger messenger)
where TMessage : class, new()
{
return messenger.Send(new TMessage(), default(Unit));
}
/// <summary>
/// Sends a message of the specified type to all registered recipients.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
/// <param name="message">The message to send.</param>
/// <returns>The message that was sent (ie. <paramref name="message"/>).</returns>
public static TMessage Send<TMessage>(this IMessenger messenger, TMessage message)
where TMessage : class
{
return messenger.Send(message, default(Unit));
}
/// <summary>
/// Sends a message of the specified type to all registered recipients.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
/// <param name="token">The token indicating what channel to use.</param>
/// <returns>The message that has been sen.</returns>
/// <remarks>
/// This method will automatically create a new <typeparamref name="TMessage"/> instance
/// just like <see cref="Send{TMessage}(IMessenger)"/>, and then send it to the right recipients.
/// </remarks>
public static TMessage Send<TMessage, TToken>(this IMessenger messenger, TToken token)
where TMessage : class, new()
where TToken : IEquatable<TToken>
{
return messenger.Send(new TMessage(), token);
}
}
}

View File

@ -0,0 +1,413 @@
// 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
// The DictionarySlim<TKey, TValue> type is originally from CoreFX labs, see
// the source repository on GitHub at https://github.com/dotnet/corefxlab.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
namespace Microsoft.Collections.Extensions
{
/// <summary>
/// A lightweight Dictionary with three principal differences compared to <see cref="Dictionary{TKey, TValue}"/>
///
/// 1) It is possible to do "get or add" in a single lookup. For value types, this also saves a copy of the value.
/// 2) It assumes it is cheap to equate values.
/// 3) It assumes the keys implement <see cref="IEquatable{TKey}"/> and they are cheap and sufficient.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
/// <remarks>
/// 1) This avoids having to do separate lookups (<see cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
/// followed by <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/>.
/// There is not currently an API exposed to get a value by ref without adding if the key is not present.
/// 2) This means it can save space by not storing hash codes.
/// 3) This means it can avoid storing a comparer, and avoid the likely virtual call to a comparer.
/// </remarks>
[DebuggerDisplay("Count = {Count}")]
internal class DictionarySlim<TKey, TValue> : IDictionarySlim<TKey, TValue>
where TKey : IEquatable<TKey>
where TValue : class
{
/// <summary>
/// A reusable array of <see cref="Entry"/> items with a single value.
/// This is used when a new <see cref="DictionarySlim{TKey,TValue}"/> instance is
/// created, or when one is cleared. The first item being added to this collection
/// will immediately cause the first resize (see <see cref="AddKey"/> for more info).
/// </summary>
private static readonly Entry[] InitialEntries = new Entry[1];
/// <summary>
/// The current number of items stored in the map.
/// </summary>
private int count;
/// <summary>
/// The 1-based index for the start of the free list within <see cref="entries"/>.
/// </summary>
private int freeList = -1;
/// <summary>
/// The array of 1-based indices for the <see cref="Entry"/> items stored in <see cref="entries"/>.
/// </summary>
private int[] buckets;
/// <summary>
/// The array of currently stored key-value pairs (ie. the lists for each hash group).
/// </summary>
private Entry[] entries;
/// <summary>
/// A type representing a map entry, ie. a node in a given list.
/// </summary>
private struct Entry
{
/// <summary>
/// The key for the value in the current node.
/// </summary>
public TKey Key;
/// <summary>
/// The value in the current node, if present.
/// </summary>
public TValue? Value;
/// <summary>
/// The 0-based index for the next node in the current list.
/// </summary>
public int Next;
}
/// <summary>
/// Initializes a new instance of the <see cref="DictionarySlim{TKey, TValue}"/> class.
/// </summary>
public DictionarySlim()
{
this.buckets = HashHelpers.SizeOneIntArray;
this.entries = InitialEntries;
}
/// <inheritdoc/>
public int Count => this.count;
/// <inheritdoc/>
public TValue this[TKey key]
{
get
{
Entry[] entries = this.entries;
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
(uint)i < (uint)entries.Length;
i = entries[i].Next)
{
if (key.Equals(entries[i].Key))
{
return entries[i].Value!;
}
}
ThrowArgumentExceptionForKeyNotFound(key);
return default!;
}
}
/// <inheritdoc/>
public void Clear()
{
this.count = 0;
this.freeList = -1;
this.buckets = HashHelpers.SizeOneIntArray;
this.entries = InitialEntries;
}
/// <inheritdoc cref="Dictionary{TKey,TValue}.ContainsKey"/>
public bool ContainsKey(TKey key)
{
Entry[] entries = this.entries;
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
(uint)i < (uint)entries.Length;
i = entries[i].Next)
{
if (key.Equals(entries[i].Key))
{
return true;
}
}
return false;
}
/// <summary>
/// Gets the value if present for the specified key.
/// </summary>
/// <param name="key">The key to look for.</param>
/// <param name="value">The value found, otherwise <see langword="default"/>.</param>
/// <returns>Whether or not the key was present.</returns>
public bool TryGetValue(TKey key, out TValue? value)
{
Entry[] entries = this.entries;
for (int i = this.buckets[key.GetHashCode() & (this.buckets.Length - 1)] - 1;
(uint)i < (uint)entries.Length;
i = entries[i].Next)
{
if (key.Equals(entries[i].Key))
{
value = entries[i].Value!;
return true;
}
}
value = default!;
return false;
}
/// <inheritdoc/>
public bool TryRemove(TKey key, out object? result)
{
Entry[] entries = this.entries;
int bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
int entryIndex = this.buckets[bucketIndex] - 1;
int lastIndex = -1;
while (entryIndex != -1)
{
Entry candidate = entries[entryIndex];
if (candidate.Key.Equals(key))
{
if (lastIndex != -1)
{
entries[lastIndex].Next = candidate.Next;
}
else
{
this.buckets[bucketIndex] = candidate.Next + 1;
}
entries[entryIndex] = default;
entries[entryIndex].Next = -3 - this.freeList;
this.freeList = entryIndex;
this.count--;
result = candidate.Value;
return true;
}
lastIndex = entryIndex;
entryIndex = candidate.Next;
}
result = null;
return false;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Remove(TKey key)
{
return TryRemove(key, out _);
}
/// <summary>
/// Gets the value for the specified key, or, if the key is not present,
/// adds an entry and returns the value by ref. This makes it possible to
/// add or update a value in a single look up operation.
/// </summary>
/// <param name="key">Key to look for</param>
/// <returns>Reference to the new or existing value</returns>
public ref TValue? GetOrAddValueRef(TKey key)
{
Entry[] entries = this.entries;
int bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
for (int i = this.buckets[bucketIndex] - 1;
(uint)i < (uint)entries.Length;
i = entries[i].Next)
{
if (key.Equals(entries[i].Key))
{
return ref entries[i].Value;
}
}
return ref AddKey(key, bucketIndex);
}
/// <summary>
/// Creates a slot for a new value to add for a specified key.
/// </summary>
/// <param name="key">The key to use to add the new value.</param>
/// <param name="bucketIndex">The target bucked index to use.</param>
/// <returns>A reference to the slot for the new value to add.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
private ref TValue? AddKey(TKey key, int bucketIndex)
{
Entry[] entries = this.entries;
int entryIndex;
if (this.freeList != -1)
{
entryIndex = this.freeList;
this.freeList = -3 - entries[this.freeList].Next;
}
else
{
if (this.count == entries.Length || entries.Length == 1)
{
entries = Resize();
bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
}
entryIndex = this.count;
}
entries[entryIndex].Key = key;
entries[entryIndex].Next = this.buckets[bucketIndex] - 1;
this.buckets[bucketIndex] = entryIndex + 1;
this.count++;
return ref entries[entryIndex].Value;
}
/// <summary>
/// Resizes the current dictionary to reduce the number of collisions
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private Entry[] Resize()
{
int count = this.count;
int newSize = this.entries.Length * 2;
if ((uint)newSize > int.MaxValue)
{
ThrowInvalidOperationExceptionForMaxCapacityExceeded();
}
var entries = new Entry[newSize];
Array.Copy(this.entries, 0, entries, 0, count);
var newBuckets = new int[entries.Length];
while (count-- > 0)
{
int bucketIndex = entries[count].Key.GetHashCode() & (newBuckets.Length - 1);
entries[count].Next = newBuckets[bucketIndex] - 1;
newBuckets[bucketIndex] = count + 1;
}
this.buckets = newBuckets;
this.entries = entries;
return entries;
}
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() => new Enumerator(this);
/// <summary>
/// Enumerator for <see cref="DictionarySlim{TKey,TValue}"/>.
/// </summary>
public ref struct Enumerator
{
private readonly Entry[] entries;
private int index;
private int count;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Enumerator(DictionarySlim<TKey, TValue> dictionary)
{
this.entries = dictionary.entries;
this.index = 0;
this.count = dictionary.count;
}
/// <inheritdoc cref="IEnumerator.MoveNext"/>
public bool MoveNext()
{
if (this.count == 0)
{
return false;
}
this.count--;
Entry[] entries = this.entries;
while (entries[this.index].Next < -1)
{
this.index++;
}
// We need to preemptively increment the current index so that we still correctly keep track
// of the current position in the dictionary even if the users doesn't access any of the
// available properties in the enumerator. As this is a possibility, we can't rely on one of
// them to increment the index before MoveNext is invoked again. We ditch the standard enumerator
// API surface here to expose the Key/Value properties directly and minimize the memory copies.
// For the same reason, we also removed the KeyValuePair<TKey, TValue> field here, and instead
// rely on the properties lazily accessing the target instances directly from the current entry
// pointed at by the index property (adjusted backwards to account for the increment here).
this.index++;
return true;
}
/// <summary>
/// Gets the current key.
/// </summary>
public TKey Key
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.entries[this.index - 1].Key;
}
/// <summary>
/// Gets the current value.
/// </summary>
public TValue Value
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.entries[this.index - 1].Value!;
}
}
/// <summary>
/// Throws an <see cref="ArgumentException"/> when trying to load an element with a missing key.
/// </summary>
private static void ThrowArgumentExceptionForKeyNotFound(TKey key)
{
throw new ArgumentException($"The target key {key} was not present in the dictionary");
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when trying to resize over the maximum capacity.
/// </summary>
private static void ThrowInvalidOperationExceptionForMaxCapacityExceeded()
{
throw new InvalidOperationException("Max capacity exceeded");
}
}
}

View File

@ -0,0 +1,17 @@
// 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.
namespace Microsoft.Collections.Extensions
{
/// <summary>
/// A helper class for <see cref="DictionarySlim{TKey,TValue}"/>.
/// </summary>
internal static class HashHelpers
{
/// <summary>
/// An array of type <see cref="int"/> of size 1.
/// </summary>
public static readonly int[] SizeOneIntArray = new int[1];
}
}

View File

@ -0,0 +1,22 @@
// 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.
namespace Microsoft.Collections.Extensions
{
/// <summary>
/// A base interface masking <see cref="DictionarySlim{TKey,TValue}"/> instances and exposing non-generic functionalities.
/// </summary>
internal interface IDictionarySlim
{
/// <summary>
/// Gets the count of entries in the dictionary.
/// </summary>
int Count { get; }
/// <summary>
/// Clears the current dictionary.
/// </summary>
void Clear();
}
}

View File

@ -0,0 +1,27 @@
// 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.
using System;
namespace Microsoft.Collections.Extensions
{
/// <summary>
/// An interface providing key type contravariant and value type covariant access
/// to a <see cref="DictionarySlim{TKey,TValue}"/> instance.
/// </summary>
/// <typeparam name="TKey">The contravariant type of keys in the dictionary.</typeparam>
/// <typeparam name="TValue">The covariant type of values in the dictionary.</typeparam>
internal interface IDictionarySlim<in TKey, out TValue> : IDictionarySlim<TKey>
where TKey : IEquatable<TKey>
where TValue : class
{
/// <summary>
/// Gets the value with the specified key.
/// </summary>
/// <param name="key">The key to look for.</param>
/// <returns>The returned value.</returns>
/// <exception cref="ArgumentException">Thrown if the key wasn't present.</exception>
TValue this[TKey key] { get; }
}
}

View File

@ -0,0 +1,31 @@
// 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.
using System;
namespace Microsoft.Collections.Extensions
{
/// <summary>
/// An interface providing key type contravariant access to a <see cref="DictionarySlim{TKey,TValue}"/> instance.
/// </summary>
/// <typeparam name="TKey">The contravariant type of keys in the dictionary.</typeparam>
internal interface IDictionarySlim<in TKey> : IDictionarySlim
where TKey : IEquatable<TKey>
{
/// <summary>
/// Tries to remove a value with a specified key.
/// </summary>
/// <param name="key">The key of the value to remove.</param>
/// <param name="result">The removed value, if it was present.</param>
/// <returns>.Whether or not the key was present.</returns>
bool TryRemove(TKey key, out object? result);
/// <summary>
/// Removes an item from the dictionary with the specified key, if present.
/// </summary>
/// <param name="key">The key of the item to remove.</param>
/// <returns>Whether or not an item was removed.</returns>
bool Remove(TKey key);
}
}

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
<Title>Windows Community Toolkit Mvvm .NET Standard</Title>
<Description>
This package includes Mvvm .NET Standard code only helpers such as:
- ObservableObject: a base class for objects implementing the INotifyPropertyChanged interface.
- ObservableRecipient: a base class for observable objects with support for the IMessenger service.
- RelayCommand: a simple delegate command implementing the ICommand interface.
- Messenger: a messaging system to exchange messages through different loosely-coupled objects.
- Ioc: a helper class to configure dependency injection service containers.
</Description>
<PackageTags>UWP Toolkit Windows Mvvm observable Ioc dependency injection services extensions helpers</PackageTags>
</PropertyGroup>
<!-- .NET Standard 2.0 doesn't have the Span<T> type -->
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup>
<!-- .NET Standard 2.1 doesn't have the Unsafe type -->
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.7.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,18 @@
// 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.
#if !NETSTANDARD2_1
namespace System.Diagnostics.CodeAnalysis
{
/// <summary>
/// Specifies that an output may be <see langword="null"/> even if the corresponding type disallows it.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
internal sealed class MaybeNullAttribute : Attribute
{
}
}
#endif

View File

@ -122,7 +122,7 @@ public static unsafe void IsBitwiseEqualTo<T>(T value, T target, string name)
return;
}
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
ThrowHelper.ThrowArgumentExceptionForBitwiseEqualTo(value, target, name);
}
else if (sizeof(T) == 2)
{
@ -134,7 +134,7 @@ public static unsafe void IsBitwiseEqualTo<T>(T value, T target, string name)
return;
}
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
ThrowHelper.ThrowArgumentExceptionForBitwiseEqualTo(value, target, name);
}
else if (sizeof(T) == 4)
{
@ -146,7 +146,7 @@ public static unsafe void IsBitwiseEqualTo<T>(T value, T target, string name)
return;
}
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
ThrowHelper.ThrowArgumentExceptionForBitwiseEqualTo(value, target, name);
}
else if (sizeof(T) == 8)
{
@ -158,7 +158,7 @@ public static unsafe void IsBitwiseEqualTo<T>(T value, T target, string name)
return;
}
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
ThrowHelper.ThrowArgumentExceptionForBitwiseEqualTo(value, target, name);
}
else if (sizeof(T) == 16)
{
@ -176,7 +176,7 @@ public static unsafe void IsBitwiseEqualTo<T>(T value, T target, string name)
}
}
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
ThrowHelper.ThrowArgumentExceptionForBitwiseEqualTo(value, target, name);
}
else
{
@ -188,7 +188,7 @@ public static unsafe void IsBitwiseEqualTo<T>(T value, T target, string name)
return;
}
ThrowHelper.ThrowArgumentExceptionForsBitwiseEqualTo(value, target, name);
ThrowHelper.ThrowArgumentExceptionForBitwiseEqualTo(value, target, name);
}
}

View File

@ -141,7 +141,7 @@ public static void ThrowArgumentExceptionForIsNotAssignableToType(object value,
/// <typeparam name="T">The type of input values being compared.</typeparam>
[MethodImpl(MethodImplOptions.NoInlining)]
[DoesNotReturn]
public static void ThrowArgumentExceptionForsBitwiseEqualTo<T>(T value, T target, string name)
public static void ThrowArgumentExceptionForBitwiseEqualTo<T>(T value, T target, string name)
where T : unmanaged
{
ThrowArgumentException(name, $"Parameter {name.ToAssertString()} ({typeof(T).ToTypeString()}) is not a bitwise match, was <{value.ToHexString()}> instead of <{target.ToHexString()}>");

View File

@ -0,0 +1,90 @@
// 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.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
#nullable enable
namespace Microsoft.Toolkit.Extensions
{
/// <summary>
/// Helpers for working with tasks.
/// </summary>
public static class TaskExtensions
{
/// <summary>
/// Gets the result of a <see cref="Task"/> if available, or <see langword="null"/> otherwise.
/// </summary>
/// <param name="task">The input <see cref="Task"/> instance to get the result for.</param>
/// <returns>The result of <paramref name="task"/> if completed successfully, or <see langword="default"/> otherwise.</returns>
/// <remarks>
/// This method does not block if <paramref name="task"/> has not completed yet. Furthermore, it is not generic
/// and uses reflection to access the <see cref="Task{TResult}.Result"/> property and boxes the result if it's
/// a value type, which adds overhead. It should only be used when using generics is not possible.
/// </remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object? GetResultOrDefault(this Task task)
{
// Check if the instance is a completed Task
if (
#if NETSTANDARD2_1
task.IsCompletedSuccessfully
#else
task.Status == TaskStatus.RanToCompletion
#endif
)
{
Type taskType = task.GetType();
// Check if the task is actually some Task<T>
if (
#if NETSTANDARD1_4
taskType.GetTypeInfo().IsGenericType &&
#else
taskType.IsGenericType &&
#endif
taskType.GetGenericTypeDefinition() == typeof(Task<>))
{
// Get the Task<T>.Result property
PropertyInfo propertyInfo =
#if NETSTANDARD1_4
taskType.GetRuntimeProperty(nameof(Task<object>.Result));
#else
taskType.GetProperty(nameof(Task<object>.Result));
#endif
// Finally retrieve the result
return propertyInfo!.GetValue(task);
}
}
return null;
}
/// <summary>
/// Gets the result of a <see cref="Task{TResult}"/> if available, or <see langword="default"/> otherwise.
/// </summary>
/// <typeparam name="T">The type of <see cref="Task{TResult}"/> to get the result for.</typeparam>
/// <param name="task">The input <see cref="Task{TResult}"/> instance to get the result for.</param>
/// <returns>The result of <paramref name="task"/> if completed successfully, or <see langword="default"/> otherwise.</returns>
/// <remarks>This method does not block if <paramref name="task"/> has not completed yet.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[return: MaybeNull]
public static T GetResultOrDefault<T>(this Task<T> task)
{
#if NETSTANDARD2_1
return task.IsCompletedSuccessfully ? task.Result : default;
#else
return task.Status == TaskStatus.RanToCompletion ? task.Result : default;
#endif
}
}
}

View File

@ -12,6 +12,7 @@ namespace Microsoft.Toolkit.Helpers
/// Helper class to wrap around a Task to provide more information usable for UI databinding scenarios. As discussed in MSDN Magazine: https://msdn.microsoft.com/magazine/dn605875.
/// </summary>
/// <typeparam name="TResult">Type of result returned by task.</typeparam>
[Obsolete("This helper will be removed in a future release, use the ObservableObject base class from Microsoft.Toolkit.Mvvm and the SetAndNotifyOnCompletion method")]
public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged
{
/// <summary>
@ -118,7 +119,7 @@ public TResult Result
public bool IsFaulted => Task.IsFaulted;
/// <summary>
/// Gets the exception which occured on the task (if one occurred).
/// Gets the exception which occurred on the task (if one occurred).
/// </summary>
public AggregateException Exception => Task.Exception;

View File

@ -15,7 +15,7 @@ namespace Microsoft.Toolkit.Collections
public interface IIncrementalSource<TSource>
{
/// <summary>
/// This method is invoked everytime the view need to show more items. Retrieves items based on <paramref name="pageIndex"/> and <paramref name="pageSize"/> arguments.
/// This method is invoked every time the view need to show more items. Retrieves items based on <paramref name="pageIndex"/> and <paramref name="pageSize"/> arguments.
/// </summary>
/// <param name="pageIndex">
/// The zero-based index of the page that corresponds to the items to retrieve.

View File

@ -0,0 +1,51 @@
// 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.
using System;
using System.Threading.Tasks;
using Microsoft.Toolkit.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Extensions
{
[TestClass]
public class Test_TaskExtensions
{
[TestCategory("TaskExtensions")]
[TestMethod]
public void Test_TaskExtensions_Nongeneric()
{
#pragma warning disable CS0618 // Type or member is obsolete
Assert.ThrowsException<NotImplementedException>(() => default(Task).ResultOrDefault());
Assert.ThrowsException<NotImplementedException>(() => Task.CompletedTask.ResultOrDefault());
#pragma warning restore CS0618
}
[TestCategory("TaskExtensions")]
[TestMethod]
public void Test_TaskExtensions_Generic_ValueType()
{
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
Assert.AreEqual(0, tcs.Task.ResultOrDefault());
tcs.SetResult(42);
Assert.AreEqual(42, tcs.Task.ResultOrDefault());
}
[TestCategory("TaskExtensions")]
[TestMethod]
public void Test_TaskExtensions_Generic_ReferenceType()
{
TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
Assert.AreEqual(null, tcs.Task.ResultOrDefault());
tcs.SetResult(nameof(Test_TaskExtensions_Generic_ReferenceType));
Assert.AreEqual(nameof(Test_TaskExtensions_Generic_ReferenceType), tcs.Task.ResultOrDefault());
}
}
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
@ -10,6 +11,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" />
</ItemGroup>
<!-- Workaround for the .NET Core 2.1 binary not resolving the Unsafe assembly properly -->
@ -18,6 +20,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.Toolkit.Mvvm\Microsoft.Toolkit.Mvvm.csproj" />
<ProjectReference Include="..\..\Microsoft.Toolkit\Microsoft.Toolkit.csproj" />
</ItemGroup>

View File

@ -0,0 +1,90 @@
// 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.
using System;
using System.Threading.Tasks;
using Microsoft.Toolkit.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Extensions
{
[TestClass]
public class Test_TaskExtensions
{
[TestCategory("TaskExtensions")]
[TestMethod]
public void Test_TaskExtensions_ResultOrDefault()
{
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
Assert.AreEqual(null, ((Task)tcs.Task).GetResultOrDefault());
tcs.SetCanceled();
Assert.AreEqual(null, ((Task)tcs.Task).GetResultOrDefault());
tcs = new TaskCompletionSource<int>();
tcs.SetException(new InvalidOperationException("Test"));
Assert.AreEqual(null, ((Task)tcs.Task).GetResultOrDefault());
tcs = new TaskCompletionSource<int>();
tcs.SetResult(42);
Assert.AreEqual(42, ((Task)tcs.Task).GetResultOrDefault());
}
[TestCategory("TaskExtensions")]
[TestMethod]
public void Test_TaskExtensions_ResultOrDefault_OfT_Int32()
{
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
Assert.AreEqual(0, tcs.Task.GetResultOrDefault());
tcs.SetCanceled();
Assert.AreEqual(0, tcs.Task.GetResultOrDefault());
tcs = new TaskCompletionSource<int>();
tcs.SetException(new InvalidOperationException("Test"));
Assert.AreEqual(0, tcs.Task.GetResultOrDefault());
tcs = new TaskCompletionSource<int>();
tcs.SetResult(42);
Assert.AreEqual(42, tcs.Task.GetResultOrDefault());
}
[TestCategory("TaskExtensions")]
[TestMethod]
public void Test_TaskExtensions_ResultOrDefault_OfT_String()
{
TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
Assert.AreEqual(null, tcs.Task.GetResultOrDefault());
tcs.SetCanceled();
Assert.AreEqual(null, tcs.Task.GetResultOrDefault());
tcs = new TaskCompletionSource<string>();
tcs.SetException(new InvalidOperationException("Test"));
Assert.AreEqual(null, tcs.Task.GetResultOrDefault());
tcs = new TaskCompletionSource<string>();
tcs.SetResult("Hello world");
Assert.AreEqual("Hello world", tcs.Task.GetResultOrDefault());
}
}
}

View File

@ -0,0 +1,112 @@
// 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.
using System;
using System.Threading.Tasks;
using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
[TestClass]
public class Test_AsyncRelayCommand
{
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_AsyncRelayCommand_AlwaysEnabled()
{
int ticks = 0;
var command = new AsyncRelayCommand(async () =>
{
await Task.Delay(1000);
ticks++;
await Task.Delay(1000);
});
Assert.IsTrue(command.CanExecute(null));
Assert.IsTrue(command.CanExecute(new object()));
(object, EventArgs) args = default;
command.CanExecuteChanged += (s, e) => args = (s, e);
command.NotifyCanExecuteChanged();
Assert.AreSame(args.Item1, command);
Assert.AreSame(args.Item2, EventArgs.Empty);
Assert.IsNull(command.ExecutionTask);
Assert.IsFalse(command.IsRunning);
Task task = command.ExecuteAsync(null);
Assert.IsNotNull(command.ExecutionTask);
Assert.AreSame(command.ExecutionTask, task);
Assert.IsTrue(command.IsRunning);
await task;
Assert.IsFalse(command.IsRunning);
Assert.AreEqual(ticks, 1);
command.Execute(new object());
await command.ExecutionTask!;
Assert.AreEqual(ticks, 2);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_AsyncRelayCommand_WithCanExecuteFunctionTrue()
{
int ticks = 0;
var command = new AsyncRelayCommand(
() =>
{
ticks++;
return Task.CompletedTask;
}, () => true);
Assert.IsTrue(command.CanExecute(null));
Assert.IsTrue(command.CanExecute(new object()));
command.Execute(null);
Assert.AreEqual(ticks, 1);
command.Execute(new object());
Assert.AreEqual(ticks, 2);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_AsyncRelayCommand_WithCanExecuteFunctionFalse()
{
int ticks = 0;
var command = new AsyncRelayCommand(
() =>
{
ticks++;
return Task.CompletedTask;
}, () => false);
Assert.IsFalse(command.CanExecute(null));
Assert.IsFalse(command.CanExecute(new object()));
command.Execute(null);
Assert.AreEqual(ticks, 0);
command.Execute(new object());
Assert.AreEqual(ticks, 0);
}
}
}

View File

@ -0,0 +1,154 @@
// 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.
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.DependencyInjection;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
[TestClass]
public class Test_Ioc
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Ioc_ServicesNotConfigured()
{
var ioc = new Ioc();
Assert.ThrowsException<InvalidOperationException>(() => ioc.GetService<IServiceProvider>());
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Ioc_LambdaInitialization()
{
var ioc = new Ioc();
ioc.ConfigureServices(services =>
{
services.AddSingleton<INameService, AliceService>();
});
var service = ioc.GetRequiredService<INameService>();
Assert.IsNotNull(service);
Assert.IsInstanceOfType(service, typeof(AliceService));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Ioc_LambdaInitialization_ConcreteType()
{
var ioc = new Ioc();
ioc.ConfigureServices(services =>
{
services.AddSingleton<AliceService, AliceService>();
});
var service = ioc.GetRequiredService<AliceService>();
Assert.IsNotNull(service);
Assert.IsInstanceOfType(service, typeof(AliceService));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Ioc_LambdaInitialization_ConstructorInjection()
{
var ioc = new Ioc();
var messenger = new Messenger();
ioc.ConfigureServices(services =>
{
services.AddSingleton<INameService, AliceService>();
services.AddSingleton<IMessenger>(messenger);
services.AddTransient<MyRecipient>();
});
var service = ioc.GetRequiredService<MyRecipient>();
Assert.IsNotNull(service);
Assert.IsInstanceOfType(service, typeof(MyRecipient));
Assert.IsNotNull(service.NameService);
Assert.IsInstanceOfType(service.NameService, typeof(AliceService));
Assert.IsNotNull(service.MessengerService);
Assert.AreSame(service.MessengerService, messenger);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Ioc_CollectionInitialization()
{
var ioc = new Ioc();
var services = new ServiceCollection();
services.AddSingleton<INameService, AliceService>();
ioc.ConfigureServices(services);
var service = ioc.GetRequiredService<INameService>();
Assert.IsNotNull(service);
Assert.IsInstanceOfType(service, typeof(AliceService));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Ioc_RepeatedLambdaInitialization()
{
var ioc = new Ioc();
ioc.ConfigureServices(services => { });
Assert.ThrowsException<InvalidOperationException>(() => ioc.ConfigureServices(services => { }));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Ioc_RepeatedCollectionInitialization()
{
var ioc = new Ioc();
var services = new ServiceCollection();
ioc.ConfigureServices(services);
Assert.ThrowsException<InvalidOperationException>(() => ioc.ConfigureServices(services));
}
public interface INameService
{
string GetName();
}
public class BobService : INameService
{
public string GetName() => "Bob";
}
public class AliceService : INameService
{
public string GetName() => "Alice";
}
public class MyRecipient : ObservableRecipient
{
public MyRecipient(INameService nameService, IMessenger messengerService)
: base(messengerService)
{
NameService = nameService;
}
public INameService NameService { get; }
public IMessenger MessengerService => Messenger;
}
}
}

View File

@ -0,0 +1,268 @@
// 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.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Mvvm.Messaging.Messages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
public partial class Test_Messenger
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RequestMessage_Ok()
{
var messenger = new Messenger();
var recipient = new object();
void Receive(NumberRequestMessage m)
{
Assert.IsFalse(m.HasReceivedResponse);
m.Reply(42);
Assert.IsTrue(m.HasReceivedResponse);
}
messenger.Register<NumberRequestMessage>(recipient, Receive);
int result = messenger.Send<NumberRequestMessage>();
Assert.AreEqual(result, 42);
}
[TestCategory("Mvvm")]
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Test_Messenger_RequestMessage_Fail_NoReply()
{
var messenger = new Messenger();
int result = messenger.Send<NumberRequestMessage>();
}
[TestCategory("Mvvm")]
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Test_Messenger_RequestMessage_Fail_MultipleReplies()
{
var messenger = new Messenger();
var recipient = new object();
void Receive(NumberRequestMessage m)
{
m.Reply(42);
m.Reply(42);
}
messenger.Register<NumberRequestMessage>(recipient, Receive);
int result = messenger.Send<NumberRequestMessage>();
}
public class NumberRequestMessage : RequestMessage<int>
{
}
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_Messenger_AsyncRequestMessage_Ok_Sync()
{
var messenger = new Messenger();
var recipient = new object();
void Receive(AsyncNumberRequestMessage m)
{
Assert.IsFalse(m.HasReceivedResponse);
m.Reply(42);
Assert.IsTrue(m.HasReceivedResponse);
}
messenger.Register<AsyncNumberRequestMessage>(recipient, Receive);
int result = await messenger.Send<AsyncNumberRequestMessage>();
Assert.AreEqual(result, 42);
}
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_Messenger_AsyncRequestMessage_Ok_Async()
{
var messenger = new Messenger();
var recipient = new object();
async Task<int> GetNumberAsync()
{
await Task.Delay(100);
return 42;
}
void Receive(AsyncNumberRequestMessage m)
{
Assert.IsFalse(m.HasReceivedResponse);
m.Reply(GetNumberAsync());
Assert.IsTrue(m.HasReceivedResponse);
}
messenger.Register<AsyncNumberRequestMessage>(recipient, Receive);
int result = await messenger.Send<AsyncNumberRequestMessage>();
Assert.AreEqual(result, 42);
}
[TestCategory("Mvvm")]
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public async Task Test_Messenger_AsyncRequestMessage_Fail_NoReply()
{
var messenger = new Messenger();
int result = await messenger.Send<AsyncNumberRequestMessage>();
}
[TestCategory("Mvvm")]
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public async Task Test_Messenger_AsyncRequestMessage_Fail_MultipleReplies()
{
var messenger = new Messenger();
var recipient = new object();
void Receive(AsyncNumberRequestMessage m)
{
m.Reply(42);
m.Reply(42);
}
messenger.Register<AsyncNumberRequestMessage>(recipient, Receive);
int result = await messenger.Send<AsyncNumberRequestMessage>();
}
public class AsyncNumberRequestMessage : AsyncRequestMessage<int>
{
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_CollectionRequestMessage_Ok_NoReplies()
{
var messenger = new Messenger();
var recipient = new object();
void Receive(NumbersCollectionRequestMessage m)
{
}
messenger.Register<NumbersCollectionRequestMessage>(recipient, Receive);
var results = messenger.Send<NumbersCollectionRequestMessage>().Responses;
Assert.AreEqual(results.Count, 0);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_CollectionRequestMessage_Ok_MultipleReplies()
{
var messenger = new Messenger();
object
recipient1 = new object(),
recipient2 = new object(),
recipient3 = new object();
void Receive1(NumbersCollectionRequestMessage m) => m.Reply(1);
void Receive2(NumbersCollectionRequestMessage m) => m.Reply(2);
void Receive3(NumbersCollectionRequestMessage m) => m.Reply(3);
messenger.Register<NumbersCollectionRequestMessage>(recipient1, Receive1);
messenger.Register<NumbersCollectionRequestMessage>(recipient2, Receive2);
messenger.Register<NumbersCollectionRequestMessage>(recipient3, Receive3);
List<int> responses = new List<int>();
foreach (var response in messenger.Send<NumbersCollectionRequestMessage>())
{
responses.Add(response);
}
CollectionAssert.AreEquivalent(responses, new[] { 1, 2, 3 });
}
public class NumbersCollectionRequestMessage : CollectionRequestMessage<int>
{
}
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_Messenger_AsyncCollectionRequestMessage_Ok_NoReplies()
{
var messenger = new Messenger();
var recipient = new object();
void Receive(AsyncNumbersCollectionRequestMessage m)
{
}
messenger.Register<AsyncNumbersCollectionRequestMessage>(recipient, Receive);
var results = await messenger.Send<AsyncNumbersCollectionRequestMessage>().GetResponsesAsync();
Assert.AreEqual(results.Count, 0);
}
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_Messenger_AsyncCollectionRequestMessage_Ok_MultipleReplies()
{
var messenger = new Messenger();
object
recipient1 = new object(),
recipient2 = new object(),
recipient3 = new object(),
recipient4 = new object();
async Task<int> GetNumberAsync()
{
await Task.Delay(100);
return 3;
}
void Receive1(AsyncNumbersCollectionRequestMessage m) => m.Reply(1);
void Receive2(AsyncNumbersCollectionRequestMessage m) => m.Reply(Task.FromResult(2));
void Receive3(AsyncNumbersCollectionRequestMessage m) => m.Reply(GetNumberAsync());
void Receive4(AsyncNumbersCollectionRequestMessage m) => m.Reply(_ => GetNumberAsync());
messenger.Register<AsyncNumbersCollectionRequestMessage>(recipient1, Receive1);
messenger.Register<AsyncNumbersCollectionRequestMessage>(recipient2, Receive2);
messenger.Register<AsyncNumbersCollectionRequestMessage>(recipient3, Receive3);
messenger.Register<AsyncNumbersCollectionRequestMessage>(recipient4, Receive4);
List<int> responses = new List<int>();
await foreach (var response in messenger.Send<AsyncNumbersCollectionRequestMessage>())
{
responses.Add(response);
}
CollectionAssert.AreEquivalent(responses, new[] { 1, 2, 3, 3 });
}
public class AsyncNumbersCollectionRequestMessage : AsyncCollectionRequestMessage<int>
{
}
}
}

View File

@ -0,0 +1,336 @@
// 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.
using System;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
[TestClass]
public partial class Test_Messenger
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_UnregisterRecipientWithMessageType()
{
var messenger = new Messenger();
var recipient = new object();
messenger.Unregister<MessageA>(recipient);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_UnregisterRecipientWithMessageTypeAndToken()
{
var messenger = new Messenger();
var recipient = new object();
messenger.Unregister<MessageA, string>(recipient, nameof(MessageA));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_UnregisterRecipientWithToken()
{
var messenger = new Messenger();
var recipient = new object();
messenger.UnregisterAll(recipient, nameof(MessageA));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_UnregisterRecipientWithRecipient()
{
var messenger = new Messenger();
var recipient = new object();
messenger.UnregisterAll(recipient);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RegisterAndUnregisterRecipientWithMessageType()
{
var messenger = new Messenger();
var recipient = new object();
messenger.Register<MessageA>(recipient, m => { });
messenger.Unregister<MessageA>(recipient);
Assert.IsFalse(messenger.IsRegistered<MessageA>(recipient));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RegisterAndUnregisterRecipientWithMessageTypeAndToken()
{
var messenger = new Messenger();
var recipient = new object();
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
messenger.Unregister<MessageA, string>(recipient, nameof(MessageA));
Assert.IsFalse(messenger.IsRegistered<MessageA, string>(recipient, nameof(MessageA)));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RegisterAndUnregisterRecipientWithToken()
{
var messenger = new Messenger();
var recipient = new object();
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
messenger.UnregisterAll(recipient, nameof(MessageA));
Assert.IsFalse(messenger.IsRegistered<MessageA, string>(recipient, nameof(MessageA)));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RegisterAndUnregisterRecipientWithRecipient()
{
var messenger = new Messenger();
var recipient = new object();
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
messenger.UnregisterAll(recipient);
Assert.IsFalse(messenger.IsRegistered<MessageA, string>(recipient, nameof(MessageA)));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterOfTMessage_WithNoToken()
{
object a = new object();
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
string result = null;
Messenger.Default.Register<MessageA>(a, m => result = m.Text);
Assert.IsTrue(Messenger.Default.IsRegistered<MessageA>(a));
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) });
Assert.AreEqual(result, nameof(MessageA));
Messenger.Default.Unregister<MessageA>(a);
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
result = null;
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) });
Assert.IsNull(result);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterRecipient_WithNoToken()
{
object a = new object();
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
string result = null;
Messenger.Default.Register<MessageA>(a, m => result = m.Text);
Assert.IsTrue(Messenger.Default.IsRegistered<MessageA>(a));
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) });
Assert.AreEqual(result, nameof(MessageA));
Messenger.Default.UnregisterAll(a);
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
result = null;
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) });
Assert.IsNull(result);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterOfTMessage_WithToken()
{
object a = new object();
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
string result = null;
Messenger.Default.Register<MessageA, string>(a, nameof(MessageA), m => result = m.Text);
Assert.IsTrue(Messenger.Default.IsRegistered<MessageA, string>(a, nameof(MessageA)));
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) }, nameof(MessageA));
Assert.AreEqual(result, nameof(MessageA));
Messenger.Default.Unregister<MessageA, string>(a, nameof(MessageA));
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA, string>(a, nameof(MessageA)));
result = null;
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) }, nameof(MessageA));
Assert.IsNull(result);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_DuplicateRegistrationWithMessageType()
{
var messenger = new Messenger();
var recipient = new object();
messenger.Register<MessageA>(recipient, m => { });
Assert.ThrowsException<InvalidOperationException>(() =>
{
messenger.Register<MessageA>(recipient, m => { });
});
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_DuplicateRegistrationWithMessageTypeAndToken()
{
var messenger = new Messenger();
var recipient = new object();
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
Assert.ThrowsException<InvalidOperationException>(() =>
{
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
});
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IRecipient_NoMessages()
{
var messenger = new Messenger();
var recipient = new RecipientWithNoMessages();
messenger.RegisterAll(recipient);
// We just need to verify we got here with no errors, this
// recipient has no declared handlers so there's nothing to do
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IRecipient_SomeMessages_NoToken()
{
var messenger = new Messenger();
var recipient = new RecipientWithSomeMessages();
messenger.RegisterAll(recipient);
Assert.IsTrue(messenger.IsRegistered<MessageA>(recipient));
Assert.IsTrue(messenger.IsRegistered<MessageB>(recipient));
Assert.AreEqual(recipient.As, 0);
Assert.AreEqual(recipient.Bs, 0);
messenger.Send<MessageA>();
Assert.AreEqual(recipient.As, 1);
Assert.AreEqual(recipient.Bs, 0);
messenger.Send<MessageB>();
Assert.AreEqual(recipient.As, 1);
Assert.AreEqual(recipient.Bs, 1);
messenger.UnregisterAll(recipient);
Assert.IsFalse(messenger.IsRegistered<MessageA>(recipient));
Assert.IsFalse(messenger.IsRegistered<MessageB>(recipient));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IRecipient_SomeMessages_WithToken()
{
var messenger = new Messenger();
var recipient = new RecipientWithSomeMessages();
var token = nameof(Test_Messenger_IRecipient_SomeMessages_WithToken);
messenger.RegisterAll(recipient, token);
Assert.IsTrue(messenger.IsRegistered<MessageA, string>(recipient, token));
Assert.IsTrue(messenger.IsRegistered<MessageB, string>(recipient, token));
Assert.IsFalse(messenger.IsRegistered<MessageA>(recipient));
Assert.IsFalse(messenger.IsRegistered<MessageB>(recipient));
Assert.AreEqual(recipient.As, 0);
Assert.AreEqual(recipient.Bs, 0);
messenger.Send<MessageB, string>(token);
Assert.AreEqual(recipient.As, 0);
Assert.AreEqual(recipient.Bs, 1);
messenger.Send<MessageA, string>(token);
Assert.AreEqual(recipient.As, 1);
Assert.AreEqual(recipient.Bs, 1);
messenger.UnregisterAll(recipient, token);
Assert.IsFalse(messenger.IsRegistered<MessageA>(recipient));
Assert.IsFalse(messenger.IsRegistered<MessageB>(recipient));
}
public sealed class RecipientWithNoMessages
{
}
public sealed class RecipientWithSomeMessages
: IRecipient<MessageA>, ICloneable, IRecipient<MessageB>
{
public int As { get; private set; }
public void Receive(MessageA message)
{
As++;
}
public int Bs { get; private set; }
public void Receive(MessageB message)
{
Bs++;
}
// We also add the ICloneable interface to test that the message
// interfaces are all handled correctly even when inteleaved
// by other unrelated interfaces in the type declaration.
public object Clone() => throw new NotImplementedException();
}
public sealed class MessageA
{
public string Text { get; set; }
}
public sealed class MessageB
{
}
}
}

View File

@ -0,0 +1,233 @@
// 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.
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
[TestClass]
public class Test_ObservableObject
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableObject_Events()
{
var model = new SampleModel<int>();
(PropertyChangingEventArgs, int) changing = default;
(PropertyChangedEventArgs, int) changed = default;
model.PropertyChanging += (s, e) =>
{
Assert.IsNull(changing.Item1);
Assert.IsNull(changed.Item1);
Assert.AreSame(model, s);
Assert.IsNotNull(s);
Assert.IsNotNull(e);
changing = (e, model.Data);
};
model.PropertyChanged += (s, e) =>
{
Assert.IsNotNull(changing.Item1);
Assert.IsNull(changed.Item1);
Assert.AreSame(model, s);
Assert.IsNotNull(s);
Assert.IsNotNull(e);
changed = (e, model.Data);
};
model.Data = 42;
Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel<int>.Data));
Assert.AreEqual(changing.Item2, 0);
Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel<int>.Data));
Assert.AreEqual(changed.Item2, 42);
}
public class SampleModel<T> : ObservableObject
{
private T data;
public T Data
{
get => data;
set => SetProperty(ref data, value);
}
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableObject_ProxyCrudWithProperty()
{
var model = new WrappingModelWithProperty(new Person { Name = "Alice" });
(PropertyChangingEventArgs, string) changing = default;
(PropertyChangedEventArgs, string) changed = default;
model.PropertyChanging += (s, e) =>
{
Assert.AreSame(model, s);
changing = (e, model.Name);
};
model.PropertyChanged += (s, e) =>
{
Assert.AreSame(model, s);
changed = (e, model.Name);
};
model.Name = "Bob";
Assert.AreEqual(changing.Item1?.PropertyName, nameof(WrappingModelWithProperty.Name));
Assert.AreEqual(changing.Item2, "Alice");
Assert.AreEqual(changed.Item1?.PropertyName, nameof(WrappingModelWithProperty.Name));
Assert.AreEqual(changed.Item2, "Bob");
Assert.AreEqual(model.PersonProxy.Name, "Bob");
}
public class Person
{
public string Name { get; set; }
}
public class WrappingModelWithProperty : ObservableObject
{
private Person Person { get; }
public WrappingModelWithProperty(Person person)
{
Person = person;
}
public Person PersonProxy => Person;
public string Name
{
get => Person.Name;
set => SetProperty(() => Person.Name, value);
}
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableObject_ProxyCrudWithField()
{
var model = new WrappingModelWithField(new Person { Name = "Alice" });
(PropertyChangingEventArgs, string) changing = default;
(PropertyChangedEventArgs, string) changed = default;
model.PropertyChanging += (s, e) =>
{
Assert.AreSame(model, s);
changing = (e, model.Name);
};
model.PropertyChanged += (s, e) =>
{
Assert.AreSame(model, s);
changed = (e, model.Name);
};
model.Name = "Bob";
Assert.AreEqual(changing.Item1?.PropertyName, nameof(WrappingModelWithField.Name));
Assert.AreEqual(changing.Item2, "Alice");
Assert.AreEqual(changed.Item1?.PropertyName, nameof(WrappingModelWithField.Name));
Assert.AreEqual(changed.Item2, "Bob");
Assert.AreEqual(model.PersonProxy.Name, "Bob");
}
public class WrappingModelWithField : ObservableObject
{
private readonly Person person;
public WrappingModelWithField(Person person)
{
this.person = person;
}
public Person PersonProxy => this.person;
public string Name
{
get => this.person.Name;
set => SetProperty(() => this.person.Name, value);
}
}
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_ObservableObject_NotifyTask()
{
static async Task TestAsync(Action<TaskCompletionSource<int>> callback)
{
var model = new SampleModelWithTask<int>();
var tcs = new TaskCompletionSource<int>();
var task = tcs.Task;
(PropertyChangingEventArgs, Task<int>) changing = default;
(PropertyChangedEventArgs, Task<int>) changed = default;
model.PropertyChanging += (s, e) =>
{
Assert.AreSame(model, s);
changing = (e, model.Data);
};
model.PropertyChanged += (s, e) =>
{
Assert.AreSame(model, s);
changed = (e, model.Data);
};
model.Data = task;
Assert.IsFalse(task.IsCompleted);
Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModelWithTask<int>.Data));
Assert.IsNull(changing.Item2);
Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModelWithTask<int>.Data));
Assert.AreSame(changed.Item2, task);
changed = default;
callback(tcs);
await Task.Delay(100); // Time for the notification to dispatch
Assert.IsTrue(task.IsCompleted);
Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel<int>.Data));
Assert.AreSame(changed.Item2, task);
}
await TestAsync(tcs => tcs.SetResult(42));
await TestAsync(tcs => tcs.SetException(new ArgumentException("Something went wrong")));
await TestAsync(tcs => tcs.SetCanceled());
}
public class SampleModelWithTask<T> : ObservableObject
{
private Task<T> data;
public Task<T> Data
{
get => data;
set => SetPropertyAndNotifyOnCompletion(ref data, () => data, value);
}
}
}
}

View File

@ -0,0 +1,115 @@
// 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.
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Mvvm.Messaging.Messages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
[TestClass]
public class Test_ObservableRecipient
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableRecipient_Activation()
{
var viewmodel = new SomeRecipient<int>();
Assert.IsFalse(viewmodel.IsActivatedCheck);
viewmodel.IsActive = true;
Assert.IsTrue(viewmodel.IsActivatedCheck);
Assert.IsTrue(viewmodel.CurrentMessenger.IsRegistered<SampleMessage>(viewmodel));
viewmodel.IsActive = false;
Assert.IsFalse(viewmodel.IsActivatedCheck);
Assert.IsFalse(viewmodel.CurrentMessenger.IsRegistered<SampleMessage>(viewmodel));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableRecipient_Defaults()
{
var viewmodel = new SomeRecipient<int>();
Assert.AreSame(viewmodel.CurrentMessenger, Messenger.Default);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableRecipient_Injection()
{
var messenger = new Messenger();
var viewmodel = new SomeRecipient<int>(messenger);
Assert.AreSame(viewmodel.CurrentMessenger, messenger);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableRecipient_Broadcast()
{
var messenger = new Messenger();
var viewmodel = new SomeRecipient<int>(messenger);
PropertyChangedMessage<int> message = null;
messenger.Register<PropertyChangedMessage<int>>(messenger, m => message = m);
viewmodel.Data = 42;
Assert.IsNotNull(message);
Assert.AreSame(message.Sender, viewmodel);
Assert.AreEqual(message.OldValue, 0);
Assert.AreEqual(message.NewValue, 42);
Assert.AreEqual(message.PropertyName, nameof(SomeRecipient<int>.Data));
}
public class SomeRecipient<T> : ObservableRecipient
{
public SomeRecipient()
{
}
public SomeRecipient(IMessenger messenger)
: base(messenger)
{
}
public IMessenger CurrentMessenger => Messenger;
private T data;
public T Data
{
get => data;
set => SetProperty(ref data, value, true);
}
public bool IsActivatedCheck { get; private set; }
protected override void OnActivated()
{
IsActivatedCheck = true;
Messenger.Register<SampleMessage>(this, m => { });
}
protected override void OnDeactivated()
{
base.OnDeactivated();
IsActivatedCheck = false;
}
}
public class SampleMessage
{
}
}
}

View File

@ -0,0 +1,83 @@
// 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.
using System;
using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
[TestClass]
public class Test_RelayCommand
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_RelayCommand_AlwaysEnabled()
{
int ticks = 0;
var command = new RelayCommand(() => ticks++);
Assert.IsTrue(command.CanExecute(null));
Assert.IsTrue(command.CanExecute(new object()));
(object, EventArgs) args = default;
command.CanExecuteChanged += (s, e) => args = (s, e);
command.NotifyCanExecuteChanged();
Assert.AreSame(args.Item1, command);
Assert.AreSame(args.Item2, EventArgs.Empty);
command.Execute(null);
Assert.AreEqual(ticks, 1);
command.Execute(new object());
Assert.AreEqual(ticks, 2);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_RelayCommand_WithCanExecuteFunctionTrue()
{
int ticks = 0;
var command = new RelayCommand(() => ticks++, () => true);
Assert.IsTrue(command.CanExecute(null));
Assert.IsTrue(command.CanExecute(new object()));
command.Execute(null);
Assert.AreEqual(ticks, 1);
command.Execute(new object());
Assert.AreEqual(ticks, 2);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_RelayCommand_WithCanExecuteFunctionFalse()
{
int ticks = 0;
var command = new RelayCommand(() => ticks++, () => false);
Assert.IsFalse(command.CanExecute(null));
Assert.IsFalse(command.CanExecute(new object()));
command.Execute(null);
Assert.AreEqual(ticks, 0);
command.Execute(new object());
Assert.AreEqual(ticks, 0);
}
}
}

View File

@ -0,0 +1,69 @@
// 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.
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
[TestClass]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Generic type")]
public class Test_RelayCommandOfT
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_RelayCommandOfT_AlwaysEnabled()
{
string text = string.Empty;
var command = new RelayCommand<string>(s => text = s);
Assert.IsTrue(command.CanExecute("Text"));
Assert.IsTrue(command.CanExecute(null));
Assert.ThrowsException<InvalidCastException>(() => command.CanExecute(new object()));
(object, EventArgs) args = default;
command.CanExecuteChanged += (s, e) => args = (s, e);
command.NotifyCanExecuteChanged();
Assert.AreSame(args.Item1, command);
Assert.AreSame(args.Item2, EventArgs.Empty);
command.Execute("Hello");
Assert.AreEqual(text, "Hello");
command.Execute(null);
Assert.AreEqual(text, null);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_RelayCommand_WithCanExecuteFunction()
{
string text = string.Empty;
var command = new RelayCommand<string>(s => text = s, s => s != null);
Assert.IsTrue(command.CanExecute("Text"));
Assert.IsFalse(command.CanExecute(null));
Assert.ThrowsException<InvalidCastException>(() => command.CanExecute(new object()));
command.Execute("Hello");
Assert.AreEqual(text, "Hello");
command.Execute(null);
Assert.AreEqual(text, "Hello");
}
}
}

View File

@ -18,8 +18,17 @@
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Test_Guard.Array.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Test_Guard.Comparable.Numeric.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Test_Guard.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\Test_TaskExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\Test_ArrayExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\Test_TypeExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\Test_ValueTypeExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_Ioc.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_Messenger.Request.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_Messenger.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_ObservableObject.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_AsyncRelayCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_RelayCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_RelayCommand{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_ObservableRecipient.cs" />
</ItemGroup>
</Project>