Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fdb0f17
Added new ObservableObject.OnProperty... overloads
Sergio0694 Oct 6, 2020
30ce321
Added notification to AsyncRelayCommand.CanBeCanceled
Sergio0694 Oct 6, 2020
634e243
Added cached args to async relay commands
Sergio0694 Oct 6, 2020
599c76e
Fixed unit test, added missing notification
Sergio0694 Oct 7, 2020
db2742a
Fixed another missing property change notification
Sergio0694 Oct 7, 2020
7464bd0
Enabled notification for ObservableValidator.HasErrors
Sergio0694 Oct 7, 2020
2f71694
Fixed ObservableValidator.HasErrors unit test
Sergio0694 Oct 7, 2020
78991fe
Tweaks to the OnPropertyChanging overloads
Sergio0694 Oct 9, 2020
2ceb110
Improved OnPropertyChanging overloads
Sergio0694 Oct 9, 2020
66825f9
Reintroduced simplified Ioc class
Sergio0694 Oct 12, 2020
fb17f61
Added generic Ioc.GetService<T> method
Sergio0694 Oct 12, 2020
727e828
Minor code refactoring and performance tweak
Sergio0694 Oct 20, 2020
7e0ac4b
Merge branch 'master' into feature/mvvm-toolkit-preview4
michael-hawker Oct 22, 2020
a1ccc5a
Merge branch 'master' into feature/mvvm-toolkit-preview4
Sergio0694 Oct 26, 2020
bbe8dfd
Added comments to unit tests
Sergio0694 Oct 30, 2020
ee1d768
Fixed a small bug in Type2.GetHashCode
Sergio0694 Nov 8, 2020
07f1a27
Minor code tweak
Sergio0694 Nov 8, 2020
bab6d9a
Minor tweaks to ObservableValidator
Sergio0694 Nov 9, 2020
2faf3ea
Fixed unit tests
Sergio0694 Nov 9, 2020
eb42db0
Merge branch 'master' into feature/mvvm-toolkit-preview4
michael-hawker Nov 9, 2020
e4116ca
Added Ioc.GetRequiredService<T> method
Sergio0694 Nov 10, 2020
ffecfc4
Added ObservableValidator.TrySetProperty methods
Sergio0694 Nov 11, 2020
9d1b4a7
Added some TrySetProperty tests
Sergio0694 Nov 11, 2020
fc82bbe
Merge branch 'master' into feature/mvvm-toolkit-preview4
michael-hawker Nov 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,39 @@ public abstract class ObservableObject : INotifyPropertyChanged, INotifyProperty
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.
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>
/// <param name="e">The input <see cref="PropertyChangedEventArgs"/> instance.</param>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged?.Invoke(this, e);
}

/// <summary>
/// Raises the <see cref="PropertyChanging"/> event.
/// </summary>
/// <param name="e">The input <see cref="PropertyChangingEventArgs"/> instance.</param>
protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
{
PropertyChanging?.Invoke(this, e);
}

/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// </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)
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
OnPropertyChanged(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.
/// Raises the <see cref="PropertyChanging"/> event.
/// </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)
protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
}

/// <summary>
Expand Down
259 changes: 229 additions & 30 deletions Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs

Large diffs are not rendered by default.

167 changes: 167 additions & 0 deletions Microsoft.Toolkit.Mvvm/DependencyInjection/Ioc.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <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 the <see cref="ConfigureServices"/>
/// method and passing an <see cref="IServiceProvider"/> 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, <see cref="ConfigureServices"/> can be used as follows in this example:
/// <code>
/// Ioc.Default.ConfigureServices(
/// new ServiceCollection()
/// .AddSingleton&lt;ILogger, Logger&gt;()
/// .BuildServiceProvider());
/// </code>
/// Finally, you can use the <see cref="Ioc"/> instance (which implements <see cref="IServiceProvider"/>)
/// to retrieve the service instances from anywhere in your application, by doing as follows:
/// <code>
/// Ioc.Default.GetService&lt;ILogger&gt;().Log("Hello world!");
/// </code>
/// </summary>
public sealed class Ioc : IServiceProvider
{
/// <summary>
/// Gets the default <see cref="Ioc"/> instance.
/// </summary>
public static Ioc Default { get; } = new Ioc();

/// <summary>
/// The <see cref="IServiceProvider"/> instance to use, if initialized.
/// </summary>
private volatile IServiceProvider? serviceProvider;

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

/// <summary>
/// Tries to resolve an instance of a specified service type.
/// </summary>
/// <typeparam name="T">The type of service to resolve.</typeparam>
/// <returns>An instance of the specified service, or <see langword="null"/>.</returns>
/// <exception cref="InvalidOperationException">Throw if the current <see cref="Ioc"/> instance has not been initialized.</exception>
public T? GetService<T>()
where T : class
{
IServiceProvider? provider = this.serviceProvider;

if (provider is null)
{
ThrowInvalidOperationExceptionForMissingInitialization();
}

return (T?)provider!.GetService(typeof(T));
}

/// <summary>
/// Resolves an instance of a specified service type.
/// </summary>
/// <typeparam name="T">The type of service to resolve.</typeparam>
/// <returns>An instance of the specified service, or <see langword="null"/>.</returns>
/// <exception cref="InvalidOperationException">
/// Throw if the current <see cref="Ioc"/> instance has not been initialized, or if the
/// requested service type was not registered in the service provider currently in use.
/// </exception>
public T GetRequiredService<T>()
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!;
}

/// <summary>
/// Initializes the shared <see cref="IServiceProvider"/> instance.
/// </summary>
/// <param name="serviceProvider">The input <see cref="IServiceProvider"/> instance to use.</param>
public void ConfigureServices(IServiceProvider serviceProvider)
{
IServiceProvider? oldServices = Interlocked.CompareExchange(ref this.serviceProvider, serviceProvider, null);

if (!(oldServices is null))
{
ThrowInvalidOperationExceptionForRepeatedConfiguration();
}
}

/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when the <see cref="IServiceProvider"/> 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 the <see cref="IServiceProvider"/> property is missing a type registration.
/// </summary>
private static void ThrowInvalidOperationExceptionForUnregisteredType()
{
throw new InvalidOperationException("The requested service type was not registered");
}

/// <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");
}
}
}
34 changes: 29 additions & 5 deletions Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +19,21 @@ namespace Microsoft.Toolkit.Mvvm.Input
/// </summary>
public sealed class AsyncRelayCommand : ObservableObject, IAsyncRelayCommand
{
/// <summary>
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="CanBeCanceled"/>.
/// </summary>
internal static readonly PropertyChangedEventArgs CanBeCanceledChangedEventArgs = new PropertyChangedEventArgs(nameof(CanBeCanceled));

/// <summary>
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IsCancellationRequested"/>.
/// </summary>
internal static readonly PropertyChangedEventArgs IsCancellationRequestedChangedEventArgs = new PropertyChangedEventArgs(nameof(IsCancellationRequested));

/// <summary>
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IsRunning"/>.
/// </summary>
internal static readonly PropertyChangedEventArgs IsRunningChangedEventArgs = new PropertyChangedEventArgs(nameof(IsRunning));

/// <summary>
/// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute"/> is used.
/// </summary>
Expand Down Expand Up @@ -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);
}
}
}

/// <inheritdoc/>
public bool CanBeCanceled => !(this.cancelableExecute is null);
public bool CanBeCanceled => !(this.cancelableExecute is null) && IsRunning;

/// <inheritdoc/>
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
Expand Down Expand Up @@ -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);
Expand All @@ -156,7 +179,8 @@ public void Cancel()
{
this.cancellationTokenSource?.Cancel();

OnPropertyChanged(nameof(IsCancellationRequested));
OnPropertyChanged(IsCancellationRequestedChangedEventArgs);
OnPropertyChanged(CanBeCanceledChangedEventArgs);
}
}
}
18 changes: 13 additions & 5 deletions Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

/// <inheritdoc/>
public bool CanBeCanceled => !(this.cancelableExecute is null);
public bool CanBeCanceled => !(this.cancelableExecute is null) && IsRunning;

/// <inheritdoc/>
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
Expand Down Expand Up @@ -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);
Expand All @@ -183,7 +190,8 @@ public void Cancel()
{
this.cancellationTokenSource?.Cancel();

OnPropertyChanged(nameof(IsCancellationRequested));
OnPropertyChanged(AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
}
}
}
19 changes: 1 addition & 18 deletions Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,10 @@ public static class IMessengerExtensions
/// </summary>
private static class MethodInfos
{
/// <summary>
/// Initializes static members of the <see cref="MethodInfos"/> class.
/// </summary>
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();
}

/// <summary>
/// The <see cref="MethodInfo"/> instance associated with <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/>.
/// </summary>
public static readonly MethodInfo RegisterIRecipient;
public static readonly MethodInfo RegisterIRecipient = new Action<IMessenger, IRecipient<object>, Unit>(Register).Method.GetGenericMethodDefinition();
}

/// <summary>
Expand Down
20 changes: 10 additions & 10 deletions Microsoft.Toolkit.Mvvm/Messaging/Internals/Type2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Loading