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:
commit
5907963f71
Microsoft.Toolkit.HighPerformance
Box{T}.cs
Extensions
ArrayExtensions.2D.csMemoryExtensions.csObjectExtensions.csReadOnlyMemoryExtensions.csReadOnlySpanExtensions.cs
Microsoft.Toolkit.HighPerformance.csprojReadOnlyRef{T}.csRef{T}.csMicrosoft.Toolkit.Mvvm
ComponentModel
DependencyInjection
Input
Messaging
IMessenger.csIRecipient{TMessage}.cs
Microsoft.Toolkit.Mvvm.csprojMessages
AsyncCollectionRequestMessage{T}.csAsyncRequestMessage{T}.csCollectionRequestMessage{T}.csPropertyChangedMessage{T}.csRequestMessage{T}.csValueChangedMessage{T}.cs
Messenger.csMessengerExtensions.Unit.csMessengerExtensions.csMicrosoft.Collections.Extensions
Microsoft.Toolkit
Attributes
Diagnostics
Extensions
Helpers
IncrementalLoadingCollection
UnitTests
Extensions
UnitTests.NetCore
UnitTests.Shared
@ -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]
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
{
|
||||
|
421
Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
Normal file
421
Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
234
Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs
Normal file
234
Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
145
Microsoft.Toolkit.Mvvm/DependencyInjection/Ioc.cs
Normal file
145
Microsoft.Toolkit.Mvvm/DependencyInjection/Ioc.cs
Normal 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<ILogger, Logger>();
|
||||
/// });
|
||||
/// </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<ILogger>().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");
|
||||
}
|
||||
}
|
||||
}
|
101
Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs
Normal file
101
Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
128
Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
Normal file
128
Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
Normal 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!);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
20
Microsoft.Toolkit.Mvvm/Input/Interfaces/IRelayCommand.cs
Normal file
20
Microsoft.Toolkit.Mvvm/Input/Interfaces/IRelayCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
30
Microsoft.Toolkit.Mvvm/Input/Interfaces/IRelayCommand{T}.cs
Normal file
30
Microsoft.Toolkit.Mvvm/Input/Interfaces/IRelayCommand{T}.cs
Normal 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);
|
||||
}
|
||||
}
|
78
Microsoft.Toolkit.Mvvm/Input/RelayCommand.cs
Normal file
78
Microsoft.Toolkit.Mvvm/Input/RelayCommand.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
105
Microsoft.Toolkit.Mvvm/Input/RelayCommand{T}.cs
Normal file
105
Microsoft.Toolkit.Mvvm/Input/RelayCommand{T}.cs
Normal 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!);
|
||||
}
|
||||
}
|
||||
}
|
91
Microsoft.Toolkit.Mvvm/Messaging/IMessenger.cs
Normal file
91
Microsoft.Toolkit.Mvvm/Messaging/IMessenger.cs
Normal 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();
|
||||
}
|
||||
}
|
20
Microsoft.Toolkit.Mvvm/Messaging/IRecipient{TMessage}.cs
Normal file
20
Microsoft.Toolkit.Mvvm/Messaging/IRecipient{TMessage}.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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<XmlDocument> { }
|
||||
/// </code>
|
||||
/// We can then request and enumerate the results like so:
|
||||
/// <code>
|
||||
/// await foreach (var document in Messenger.Default.Send<OpenDocumentsRequestMessage>())
|
||||
/// {
|
||||
/// // 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<OpenDocumentsRequestMessage>().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
680
Microsoft.Toolkit.Mvvm/Messaging/Messenger.cs
Normal file
680
Microsoft.Toolkit.Mvvm/Messaging/Messenger.cs
Normal 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<LoginCompletedMessage>(this, m =>
|
||||
/// {
|
||||
/// // Handle the message here...
|
||||
/// });
|
||||
/// </code>
|
||||
/// Finally, send a message when needed, like so:
|
||||
/// <code>
|
||||
/// Messenger.Default.Send<LoginCompletedMessage>();
|
||||
/// </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<LoginCompletedMessage>(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");
|
||||
}
|
||||
}
|
||||
}
|
41
Microsoft.Toolkit.Mvvm/Messaging/MessengerExtensions.Unit.cs
Normal file
41
Microsoft.Toolkit.Mvvm/Messaging/MessengerExtensions.Unit.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
266
Microsoft.Toolkit.Mvvm/Messaging/MessengerExtensions.cs
Normal file
266
Microsoft.Toolkit.Mvvm/Messaging/MessengerExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
33
Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj
Normal file
33
Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj
Normal 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>
|
18
Microsoft.Toolkit/Attributes/MaybeNullAttribute.cs
Normal file
18
Microsoft.Toolkit/Attributes/MaybeNullAttribute.cs
Normal 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
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()}>");
|
||||
|
90
Microsoft.Toolkit/Extensions/TaskExtensions.cs
Normal file
90
Microsoft.Toolkit/Extensions/TaskExtensions.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
51
UnitTests/Extensions/Test_TaskExtensions.cs
Normal file
51
UnitTests/Extensions/Test_TaskExtensions.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
|
90
UnitTests/UnitTests.Shared/Extensions/Test_TaskExtensions.cs
Normal file
90
UnitTests/UnitTests.Shared/Extensions/Test_TaskExtensions.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
112
UnitTests/UnitTests.Shared/Mvvm/Test_AsyncRelayCommand.cs
Normal file
112
UnitTests/UnitTests.Shared/Mvvm/Test_AsyncRelayCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
154
UnitTests/UnitTests.Shared/Mvvm/Test_Ioc.cs
Normal file
154
UnitTests/UnitTests.Shared/Mvvm/Test_Ioc.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
268
UnitTests/UnitTests.Shared/Mvvm/Test_Messenger.Request.cs
Normal file
268
UnitTests/UnitTests.Shared/Mvvm/Test_Messenger.Request.cs
Normal 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>
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
336
UnitTests/UnitTests.Shared/Mvvm/Test_Messenger.cs
Normal file
336
UnitTests/UnitTests.Shared/Mvvm/Test_Messenger.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
233
UnitTests/UnitTests.Shared/Mvvm/Test_ObservableObject.cs
Normal file
233
UnitTests/UnitTests.Shared/Mvvm/Test_ObservableObject.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
115
UnitTests/UnitTests.Shared/Mvvm/Test_ObservableRecipient.cs
Normal file
115
UnitTests/UnitTests.Shared/Mvvm/Test_ObservableRecipient.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
83
UnitTests/UnitTests.Shared/Mvvm/Test_RelayCommand.cs
Normal file
83
UnitTests/UnitTests.Shared/Mvvm/Test_RelayCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
69
UnitTests/UnitTests.Shared/Mvvm/Test_RelayCommand{T}.cs
Normal file
69
UnitTests/UnitTests.Shared/Mvvm/Test_RelayCommand{T}.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
Loading…
Reference in New Issue
Block a user