diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
index f1c9b8989b5..cd7fec0634c 100644
--- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
+++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
@@ -27,25 +27,39 @@ public abstract class ObservableObject : INotifyPropertyChanged, INotifyProperty
public event PropertyChangingEventHandler? PropertyChanging;
///
- /// Performs the required configuration when a property has changed, and then
- /// raises the event to notify listeners of the update.
+ /// Raises the event.
+ ///
+ /// The input instance.
+ protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ PropertyChanged?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the event.
+ ///
+ /// The input instance.
+ protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
+ {
+ PropertyChanging?.Invoke(this, e);
+ }
+
+ ///
+ /// Raises the event.
///
/// (optional) The name of the property that changed.
- /// The base implementation only raises the event.
- protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
///
- /// Performs the required configuration when a property is changing, and then
- /// raises the event to notify listeners of the update.
+ /// Raises the event.
///
/// (optional) The name of the property that changed.
- /// The base implementation only raises the event.
- protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null)
+ protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
- PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
+ OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
}
///
diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs
index 2a96ab7b837..e990fcc8b1d 100644
--- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs
+++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs
@@ -19,33 +19,28 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel
///
public abstract class ObservableValidator : ObservableObject, INotifyDataErrorInfo
{
+ ///
+ /// The cached for .
+ ///
+ private static readonly PropertyChangedEventArgs HasErrorsChangedEventArgs = new PropertyChangedEventArgs(nameof(HasErrors));
+
///
/// The instance used to store previous validation results.
///
private readonly Dictionary> errors = new Dictionary>();
+ ///
+ /// Indicates the total number of properties with errors (not total errors).
+ /// This is used to allow to operate in O(1) time, as it can just
+ /// check whether this value is not 0 instead of having to traverse .
+ ///
+ private int totalErrors;
+
///
public event EventHandler? ErrorsChanged;
///
- public bool HasErrors
- {
- get
- {
- // This uses the value enumerator for Dictionary.ValueCollection, so it doesn't
- // allocate. Accessing this property is O(n), but we can stop as soon as we find at least one
- // error in the whole entity, and doing this saves 8 bytes in the object size (no fields needed).
- foreach (var value in this.errors.Values)
- {
- if (value.Count > 0)
- {
- return true;
- }
- }
-
- return false;
- }
- }
+ public bool HasErrors => this.totalErrors > 0;
///
/// Compares the current and new values for a given property. If the value has changed,
@@ -67,12 +62,14 @@ public bool HasErrors
///
protected bool SetProperty(ref T field, T newValue, bool validate, [CallerMemberName] string? propertyName = null)
{
- if (validate)
+ bool propertyChanged = SetProperty(ref field, newValue, propertyName);
+
+ if (propertyChanged && validate)
{
ValidateProperty(newValue, propertyName);
}
- return SetProperty(ref field, newValue, propertyName);
+ return propertyChanged;
}
///
@@ -90,12 +87,14 @@ protected bool SetProperty(ref T field, T newValue, bool validate, [CallerMem
/// if the property was changed, otherwise.
protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, bool validate, [CallerMemberName] string? propertyName = null)
{
- if (validate)
+ bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName);
+
+ if (propertyChanged && validate)
{
ValidateProperty(newValue, propertyName);
}
- return SetProperty(ref field, newValue, comparer, propertyName);
+ return propertyChanged;
}
///
@@ -120,12 +119,14 @@ protected bool SetProperty(ref T field, T newValue, IEqualityComparer comp
///
protected bool SetProperty(T oldValue, T newValue, Action callback, bool validate, [CallerMemberName] string? propertyName = null)
{
- if (validate)
+ bool propertyChanged = SetProperty(oldValue, newValue, callback, propertyName);
+
+ if (propertyChanged && validate)
{
ValidateProperty(newValue, propertyName);
}
- return SetProperty(oldValue, newValue, callback, propertyName);
+ return propertyChanged;
}
///
@@ -144,12 +145,14 @@ protected bool SetProperty(T oldValue, T newValue, Action callback, bool v
/// if the property was changed, otherwise.
protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, bool validate, [CallerMemberName] string? propertyName = null)
{
- if (validate)
+ bool propertyChanged = SetProperty(oldValue, newValue, comparer, callback, propertyName);
+
+ if (propertyChanged && validate)
{
ValidateProperty(newValue, propertyName);
}
- return SetProperty(oldValue, newValue, comparer, callback, propertyName);
+ return propertyChanged;
}
///
@@ -172,12 +175,14 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer compa
protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, bool validate, [CallerMemberName] string? propertyName = null)
where TModel : class
{
- if (validate)
+ bool propertyChanged = SetProperty(oldValue, newValue, model, callback, propertyName);
+
+ if (propertyChanged && validate)
{
ValidateProperty(newValue, propertyName);
}
- return SetProperty(oldValue, newValue, model, callback, propertyName);
+ return propertyChanged;
}
///
@@ -202,12 +207,123 @@ protected bool SetProperty(T oldValue, T newValue, TModel model, Acti
protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, bool validate, [CallerMemberName] string? propertyName = null)
where TModel : class
{
- if (validate)
+ bool propertyChanged = SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
+
+ if (propertyChanged && validate)
{
ValidateProperty(newValue, propertyName);
}
- return SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
+ return propertyChanged;
+ }
+
+ ///
+ /// Tries to validate a new value for a specified property. If the validation is successful,
+ /// is called, otherwise no state change is performed.
+ ///
+ /// The type of the property that changed.
+ /// The field storing the property's value.
+ /// The property's value after the change occurred.
+ /// The resulting validation errors, if any.
+ /// (optional) The name of the property that changed.
+ /// Whether the validation was successful and the property value changed as well.
+ protected bool TrySetProperty(ref T field, T newValue, out IReadOnlyCollection errors, [CallerMemberName] string? propertyName = null)
+ {
+ return TryValidateProperty(newValue, propertyName, out errors) &&
+ SetProperty(ref field, newValue, propertyName);
+ }
+
+ ///
+ /// Tries to validate a new value for a specified property. If the validation is successful,
+ /// is called, otherwise no state change is performed.
+ ///
+ /// The type of the property that changed.
+ /// The field storing the property's value.
+ /// The property's value after the change occurred.
+ /// The instance to use to compare the input values.
+ /// The resulting validation errors, if any.
+ /// (optional) The name of the property that changed.
+ /// Whether the validation was successful and the property value changed as well.
+ protected bool TrySetProperty(ref T field, T newValue, IEqualityComparer comparer, out IReadOnlyCollection errors, [CallerMemberName] string? propertyName = null)
+ {
+ return TryValidateProperty(newValue, propertyName, out errors) &&
+ SetProperty(ref field, newValue, comparer, propertyName);
+ }
+
+ ///
+ /// Tries to validate a new value for a specified property. If the validation is successful,
+ /// is called, otherwise no state change is performed.
+ ///
+ /// The type of the property that changed.
+ /// The current property value.
+ /// The property's value after the change occurred.
+ /// A callback to invoke to update the property value.
+ /// The resulting validation errors, if any.
+ /// (optional) The name of the property that changed.
+ /// Whether the validation was successful and the property value changed as well.
+ protected bool TrySetProperty(T oldValue, T newValue, Action callback, out IReadOnlyCollection errors, [CallerMemberName] string? propertyName = null)
+ {
+ return TryValidateProperty(newValue, propertyName, out errors) &&
+ SetProperty(oldValue, newValue, callback, propertyName);
+ }
+
+ ///
+ /// Tries to validate a new value for a specified property. If the validation is successful,
+ /// is called, otherwise no state change is performed.
+ ///
+ /// The type of the property that changed.
+ /// The current property value.
+ /// The property's value after the change occurred.
+ /// The instance to use to compare the input values.
+ /// A callback to invoke to update the property value.
+ /// The resulting validation errors, if any.
+ /// (optional) The name of the property that changed.
+ /// Whether the validation was successful and the property value changed as well.
+ protected bool TrySetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, out IReadOnlyCollection errors, [CallerMemberName] string? propertyName = null)
+ {
+ return TryValidateProperty(newValue, propertyName, out errors) &&
+ SetProperty(oldValue, newValue, comparer, callback, propertyName);
+ }
+
+ ///
+ /// Tries to validate a new value for a specified property. If the validation is successful,
+ /// is called, otherwise no state change is performed.
+ ///
+ /// The type of model whose property (or field) to set.
+ /// The type of property (or field) to set.
+ /// The current property value.
+ /// The property's value after the change occurred.
+ /// The model
+ /// The callback to invoke to set the target property value, if a change has occurred.
+ /// The resulting validation errors, if any.
+ /// (optional) The name of the property that changed.
+ /// Whether the validation was successful and the property value changed as well.
+ protected bool TrySetProperty(T oldValue, T newValue, TModel model, Action callback, out IReadOnlyCollection errors, [CallerMemberName] string? propertyName = null)
+ where TModel : class
+ {
+ return TryValidateProperty(newValue, propertyName, out errors) &&
+ SetProperty(oldValue, newValue, model, callback, propertyName);
+ }
+
+ ///
+ /// Tries to validate a new value for a specified property. If the validation is successful,
+ /// is called, otherwise no state change is performed.
+ ///
+ /// The type of model whose property (or field) to set.
+ /// The type of property (or field) to set.
+ /// The current property value.
+ /// The property's value after the change occurred.
+ /// The instance to use to compare the input values.
+ /// The model
+ /// The callback to invoke to set the target property value, if a change has occurred.
+ /// The resulting validation errors, if any.
+ /// (optional) The name of the property that changed.
+ /// Whether the validation was successful and the property value changed as well.
+ protected bool TrySetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, out IReadOnlyCollection errors, [CallerMemberName] string? propertyName = null)
+ where TModel : class
+ {
+ return TryValidateProperty(newValue, propertyName, out errors) &&
+ SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
}
///
@@ -285,6 +401,34 @@ private void ValidateProperty(object? value, string? propertyName)
new ValidationContext(this, null, null) { MemberName = propertyName },
propertyErrors);
+ // Update the shared counter for the number of errors, and raise the
+ // property changed event if necessary. We decrement the number of total
+ // errors if the current property is valid but it wasn't so before this
+ // validation, and we increment it if the validation failed after being
+ // correct before. The property changed event is raised whenever the
+ // number of total errors is either decremented to 0, or incremented to 1.
+ if (isValid)
+ {
+ if (errorsChanged)
+ {
+ this.totalErrors--;
+
+ if (this.totalErrors == 0)
+ {
+ OnPropertyChanged(HasErrorsChangedEventArgs);
+ }
+ }
+ }
+ else if (!errorsChanged)
+ {
+ this.totalErrors++;
+
+ if (this.totalErrors == 1)
+ {
+ OnPropertyChanged(HasErrorsChangedEventArgs);
+ }
+ }
+
// Only raise the event once if needed. This happens either when the target property
// had existing errors and is now valid, or if the validation has failed and there are
// new errors to broadcast, regardless of the previous validation state for the property.
@@ -294,6 +438,61 @@ private void ValidateProperty(object? value, string? propertyName)
}
}
+ ///
+ /// Tries to validate a property with a specified name and a given input value, and returns
+ /// the computed errors, if any. If the property is valid, it is assumed that its value is
+ /// about to be set in the current object. Otherwise, no observable local state is modified.
+ ///
+ /// The value to test for the specified property.
+ /// The name of the property to validate.
+ /// The resulting validation errors, if any.
+ /// Thrown when is .
+ private bool TryValidateProperty(object? value, string? propertyName, out IReadOnlyCollection errors)
+ {
+ if (propertyName is null)
+ {
+ ThrowArgumentNullExceptionForNullPropertyName();
+ }
+
+ // Add the cached errors list for later use.
+ if (!this.errors.TryGetValue(propertyName!, out List? propertyErrors))
+ {
+ propertyErrors = new List();
+
+ this.errors.Add(propertyName!, propertyErrors);
+ }
+
+ bool hasErrors = propertyErrors.Count > 0;
+
+ List localErrors = new List();
+
+ // Validate the property, by adding new errors to the local list
+ bool isValid = Validator.TryValidateProperty(
+ value,
+ new ValidationContext(this, null, null) { MemberName = propertyName },
+ localErrors);
+
+ // We only modify the state if the property is valid and it wasn't so before. In this case, we
+ // clear the cached list of errors (which is visible to consumers) and raise the necessary events.
+ if (isValid && hasErrors)
+ {
+ propertyErrors.Clear();
+
+ this.totalErrors--;
+
+ if (this.totalErrors == 0)
+ {
+ OnPropertyChanged(HasErrorsChangedEventArgs);
+ }
+
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
+ }
+
+ errors = localErrors;
+
+ return isValid;
+ }
+
#pragma warning disable SA1204
///
/// Throws an when a property name given as input is .
diff --git a/Microsoft.Toolkit.Mvvm/DependencyInjection/Ioc.cs b/Microsoft.Toolkit.Mvvm/DependencyInjection/Ioc.cs
new file mode 100644
index 00000000000..b44819888ff
--- /dev/null
+++ b/Microsoft.Toolkit.Mvvm/DependencyInjection/Ioc.cs
@@ -0,0 +1,167 @@
+// 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;
+
+#nullable enable
+
+namespace Microsoft.Toolkit.Mvvm.DependencyInjection
+{
+ ///
+ /// A type that facilitates the use of the type.
+ /// The 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:
+ ///
+ /// public interface ILogger
+ /// {
+ /// void Log(string text);
+ /// }
+ ///
+ ///
+ /// public class ConsoleLogger : ILogger
+ /// {
+ /// void Log(string text) => Console.WriteLine(text);
+ /// }
+ ///
+ /// Then the services configuration should then be done at startup, by calling the
+ /// method and passing an instance with the services to use. That instance can
+ /// be from any library offering dependency injection functionality, such as Microsoft.Extensions.DependencyInjection.
+ /// For instance, using that library, can be used as follows in this example:
+ ///
+ /// Ioc.Default.ConfigureServices(
+ /// new ServiceCollection()
+ /// .AddSingleton<ILogger, Logger>()
+ /// .BuildServiceProvider());
+ ///
+ /// Finally, you can use the instance (which implements )
+ /// to retrieve the service instances from anywhere in your application, by doing as follows:
+ ///
+ /// Ioc.Default.GetService<ILogger>().Log("Hello world!");
+ ///
+ ///
+ public sealed class Ioc : IServiceProvider
+ {
+ ///
+ /// Gets the default instance.
+ ///
+ public static Ioc Default { get; } = new Ioc();
+
+ ///
+ /// The instance to use, if initialized.
+ ///
+ private volatile IServiceProvider? serviceProvider;
+
+ ///
+ public object? 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.
+ IServiceProvider? provider = this.serviceProvider;
+
+ if (provider is null)
+ {
+ ThrowInvalidOperationExceptionForMissingInitialization();
+ }
+
+ return provider!.GetService(serviceType);
+ }
+
+ ///
+ /// Tries to resolve an instance of a specified service type.
+ ///
+ /// The type of service to resolve.
+ /// An instance of the specified service, or .
+ /// Throw if the current instance has not been initialized.
+ public T? GetService()
+ where T : class
+ {
+ IServiceProvider? provider = this.serviceProvider;
+
+ if (provider is null)
+ {
+ ThrowInvalidOperationExceptionForMissingInitialization();
+ }
+
+ return (T?)provider!.GetService(typeof(T));
+ }
+
+ ///
+ /// Resolves an instance of a specified service type.
+ ///
+ /// The type of service to resolve.
+ /// An instance of the specified service, or .
+ ///
+ /// Throw if the current instance has not been initialized, or if the
+ /// requested service type was not registered in the service provider currently in use.
+ ///
+ public T GetRequiredService()
+ where T : class
+ {
+ IServiceProvider? provider = this.serviceProvider;
+
+ if (provider is null)
+ {
+ ThrowInvalidOperationExceptionForMissingInitialization();
+ }
+
+ T? service = (T?)provider!.GetService(typeof(T));
+
+ if (service is null)
+ {
+ ThrowInvalidOperationExceptionForUnregisteredType();
+ }
+
+ return service!;
+ }
+
+ ///
+ /// Initializes the shared instance.
+ ///
+ /// The input instance to use.
+ public void ConfigureServices(IServiceProvider serviceProvider)
+ {
+ IServiceProvider? oldServices = Interlocked.CompareExchange(ref this.serviceProvider, serviceProvider, null);
+
+ if (!(oldServices is null))
+ {
+ ThrowInvalidOperationExceptionForRepeatedConfiguration();
+ }
+ }
+
+ ///
+ /// Throws an when the property is used before initialization.
+ ///
+ private static void ThrowInvalidOperationExceptionForMissingInitialization()
+ {
+ throw new InvalidOperationException("The service provider has not been configured yet");
+ }
+
+ ///
+ /// Throws an when the property is missing a type registration.
+ ///
+ private static void ThrowInvalidOperationExceptionForUnregisteredType()
+ {
+ throw new InvalidOperationException("The requested service type was not registered");
+ }
+
+ ///
+ /// Throws an when a configuration is attempted more than once.
+ ///
+ private static void ThrowInvalidOperationExceptionForRepeatedConfiguration()
+ {
+ throw new InvalidOperationException("The default service provider has already been configured");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs b/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs
index d7edbca8e05..e9678946137 100644
--- a/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs
+++ b/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -18,6 +19,21 @@ namespace Microsoft.Toolkit.Mvvm.Input
///
public sealed class AsyncRelayCommand : ObservableObject, IAsyncRelayCommand
{
+ ///
+ /// The cached for .
+ ///
+ internal static readonly PropertyChangedEventArgs CanBeCanceledChangedEventArgs = new PropertyChangedEventArgs(nameof(CanBeCanceled));
+
+ ///
+ /// The cached for .
+ ///
+ internal static readonly PropertyChangedEventArgs IsCancellationRequestedChangedEventArgs = new PropertyChangedEventArgs(nameof(IsCancellationRequested));
+
+ ///
+ /// The cached for .
+ ///
+ internal static readonly PropertyChangedEventArgs IsRunningChangedEventArgs = new PropertyChangedEventArgs(nameof(IsRunning));
+
///
/// The to invoke when is used.
///
@@ -91,15 +107,22 @@ public Task? ExecutionTask
get => this.executionTask;
private set
{
- if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
+ if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ =>
+ {
+ // When the task completes
+ OnPropertyChanged(IsRunningChangedEventArgs);
+ OnPropertyChanged(CanBeCanceledChangedEventArgs);
+ }))
{
- OnPropertyChanged(nameof(IsRunning));
+ // When setting the task
+ OnPropertyChanged(IsRunningChangedEventArgs);
+ OnPropertyChanged(CanBeCanceledChangedEventArgs);
}
}
}
///
- public bool CanBeCanceled => !(this.cancelableExecute is null);
+ public bool CanBeCanceled => !(this.cancelableExecute is null) && IsRunning;
///
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
@@ -142,7 +165,7 @@ public Task ExecuteAsync(object? parameter)
var cancellationTokenSource = this.cancellationTokenSource = new CancellationTokenSource();
- OnPropertyChanged(nameof(IsCancellationRequested));
+ OnPropertyChanged(IsCancellationRequestedChangedEventArgs);
// Invoke the cancelable command delegate with a new linked token
return ExecutionTask = this.cancelableExecute!(cancellationTokenSource.Token);
@@ -156,7 +179,8 @@ public void Cancel()
{
this.cancellationTokenSource?.Cancel();
- OnPropertyChanged(nameof(IsCancellationRequested));
+ OnPropertyChanged(IsCancellationRequestedChangedEventArgs);
+ OnPropertyChanged(CanBeCanceledChangedEventArgs);
}
}
}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs b/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
index 1acf8e72a23..61f1962b1d7 100644
--- a/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
+++ b/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
@@ -91,15 +91,22 @@ public Task? ExecutionTask
get => this.executionTask;
private set
{
- if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
+ if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ =>
{
- OnPropertyChanged(nameof(IsRunning));
+ // When the task completes
+ OnPropertyChanged(AsyncRelayCommand.IsRunningChangedEventArgs);
+ OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
+ }))
+ {
+ // When setting the task
+ OnPropertyChanged(AsyncRelayCommand.IsRunningChangedEventArgs);
+ OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
}
}
}
///
- public bool CanBeCanceled => !(this.cancelableExecute is null);
+ public bool CanBeCanceled => !(this.cancelableExecute is null) && IsRunning;
///
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
@@ -163,7 +170,7 @@ public Task ExecuteAsync(T parameter)
var cancellationTokenSource = this.cancellationTokenSource = new CancellationTokenSource();
- OnPropertyChanged(nameof(IsCancellationRequested));
+ OnPropertyChanged(AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
// Invoke the cancelable command delegate with a new linked token
return ExecutionTask = this.cancelableExecute!(parameter, cancellationTokenSource.Token);
@@ -183,7 +190,8 @@ public void Cancel()
{
this.cancellationTokenSource?.Cancel();
- OnPropertyChanged(nameof(IsCancellationRequested));
+ OnPropertyChanged(AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
+ OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
}
}
}
\ No newline at end of file
diff --git a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs
index 72caa9fb7dd..ef8c7612701 100644
--- a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs
+++ b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs
@@ -27,27 +27,10 @@ public static class IMessengerExtensions
///
private static class MethodInfos
{
- ///
- /// Initializes static members of the class.
- ///
- static MethodInfos()
- {
- RegisterIRecipient = (
- from methodInfo in typeof(IMessengerExtensions).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();
- }
-
///
/// The instance associated with .
///
- public static readonly MethodInfo RegisterIRecipient;
+ public static readonly MethodInfo RegisterIRecipient = new Action, Unit>(Register).Method.GetGenericMethodDefinition();
}
///
diff --git a/Microsoft.Toolkit.Mvvm/Messaging/Internals/Type2.cs b/Microsoft.Toolkit.Mvvm/Messaging/Internals/Type2.cs
index 411b8809ba6..f6f203a37d6 100644
--- a/Microsoft.Toolkit.Mvvm/Messaging/Internals/Type2.cs
+++ b/Microsoft.Toolkit.Mvvm/Messaging/Internals/Type2.cs
@@ -65,19 +65,19 @@ public override bool Equals(object? obj)
[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(TMessage);
+ // To combine the two hashes, we can simply use the fast djb2 hash algorithm. Unfortunately we
+ // can't really skip the callvirt here (eg. by using RuntimeHelpers.GetHashCode like in other
+ // cases), as there are some niche cases mentioned above that might break when doing so.
+ // However since this method is not generally used in a hot path (eg. the message broadcasting
+ // only invokes this a handful of times when initially retrieving the target mapping), this
+ // doesn't actually make a noticeable difference despite the minor overhead of the virtual call.
+ int hash = TMessage.GetHashCode();
- hash = (hash << 5) + hash;
+ hash = (hash << 5) + hash;
- hash += RuntimeHelpers.GetHashCode(TToken);
+ hash += TToken.GetHashCode();
- return hash;
- }
+ return hash;
}
}
}
diff --git a/Microsoft.Toolkit.Mvvm/Messaging/StrongReferenceMessenger.cs b/Microsoft.Toolkit.Mvvm/Messaging/StrongReferenceMessenger.cs
index a8e45b15c2e..dc414ffaeb0 100644
--- a/Microsoft.Toolkit.Mvvm/Messaging/StrongReferenceMessenger.cs
+++ b/Microsoft.Toolkit.Mvvm/Messaging/StrongReferenceMessenger.cs
@@ -371,8 +371,6 @@ public TMessage Send(TMessage message, TToken token)
// that doesn't expose the single standard Current property.
while (mappingEnumerator.MoveNext())
{
- object recipient = mappingEnumerator.Key.Target;
-
// Pick the target handler, if the token is a match for the recipient
if (mappingEnumerator.Value.TryGetValue(token, out object? handler))
{
@@ -382,7 +380,7 @@ public TMessage Send(TMessage message, TToken token)
// We're still using a checked span accesses here though to make sure an out of
// bounds write can never happen even if an error was present in the logic above.
pairs[2 * i] = handler!;
- pairs[(2 * i) + 1] = recipient;
+ pairs[(2 * i) + 1] = mappingEnumerator.Key.Target;
i++;
}
}
diff --git a/UnitTests/UnitTests.Shared/Mvvm/Test_AsyncRelayCommand.cs b/UnitTests/UnitTests.Shared/Mvvm/Test_AsyncRelayCommand.cs
index 09ff1d35a90..85d40de0505 100644
--- a/UnitTests/UnitTests.Shared/Mvvm/Test_AsyncRelayCommand.cs
+++ b/UnitTests/UnitTests.Shared/Mvvm/Test_AsyncRelayCommand.cs
@@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
+using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -124,26 +126,57 @@ public async Task Test_AsyncRelayCommand_WithCancellation()
{
TaskCompletionSource