Skip to content

Commit 846afb2

Browse files
author
msftbot[bot]
authored
[Feature] Microsoft.Toolkit.Mvvm package (Preview 4) (#3527)
## Follow up to #3428 <!-- Add the relevant issue number after the "#" mentioned above (for ex: Fixes #1234) which will automatically close the issue once the PR is merged. --> This PR is for tracking all changes/fixes/improvements to the `Microsoft.Toolkit.Mvvm` package following the Preview 3. <!-- Add a brief overview here of the feature/bug & fix. --> ## PR Type What kind of change does this PR introduce? <!-- Please uncomment one or more that apply to this PR. --> - Feature - Improvements <!-- - Code style update (formatting) --> <!-- - Refactoring (no functional changes, no api changes) --> <!-- - Build or CI related changes --> <!-- - Documentation content changes --> <!-- - Sample app changes --> <!-- - Other... Please describe: --> ## Overview This PR is used to track and implement new features and tweaks for the `Microsoft.Toolkit.Mvvm` package. See the linked issue for more info, and for a full list of changes included in this PR. ## PR Checklist Please check if your PR fulfills the following requirements: - [X] Tested code with current [supported SDKs](../readme.md#supported) - [ ] ~~Pull Request has been submitted to the documentation repository [instructions](..\contributing.md#docs). Link: <!-- docs PR link -->~~ - [ ] ~~Sample in sample app has been added / updated (for bug fixes / features)~~ - [ ] ~~Icon has been created (if new sample) following the [Thumbnail Style Guide and templates](https://github.com/windows-toolkit/WindowsCommunityToolkit-design-assets)~~ - [X] Tests for the changes have been added (for bug fixes / features) (if applicable) - [X] Header has been added to all new source files (run *build/UpdateHeaders.bat*) - [X] Contains **NO** breaking changes
2 parents 9d90b62 + fc82bbe commit 846afb2

File tree

10 files changed

+579
-83
lines changed

10 files changed

+579
-83
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,39 @@ public abstract class ObservableObject : INotifyPropertyChanged, INotifyProperty
2727
public event PropertyChangingEventHandler? PropertyChanging;
2828

2929
/// <summary>
30-
/// Performs the required configuration when a property has changed, and then
31-
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
30+
/// Raises the <see cref="PropertyChanged"/> event.
31+
/// </summary>
32+
/// <param name="e">The input <see cref="PropertyChangedEventArgs"/> instance.</param>
33+
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
34+
{
35+
PropertyChanged?.Invoke(this, e);
36+
}
37+
38+
/// <summary>
39+
/// Raises the <see cref="PropertyChanging"/> event.
40+
/// </summary>
41+
/// <param name="e">The input <see cref="PropertyChangingEventArgs"/> instance.</param>
42+
protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
43+
{
44+
PropertyChanging?.Invoke(this, e);
45+
}
46+
47+
/// <summary>
48+
/// Raises the <see cref="PropertyChanged"/> event.
3249
/// </summary>
3350
/// <param name="propertyName">(optional) The name of the property that changed.</param>
34-
/// <remarks>The base implementation only raises the <see cref="PropertyChanged"/> event.</remarks>
35-
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
51+
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
3652
{
37-
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
53+
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
3854
}
3955

4056
/// <summary>
41-
/// Performs the required configuration when a property is changing, and then
42-
/// raises the <see cref="PropertyChanged"/> event to notify listeners of the update.
57+
/// Raises the <see cref="PropertyChanging"/> event.
4358
/// </summary>
4459
/// <param name="propertyName">(optional) The name of the property that changed.</param>
45-
/// <remarks>The base implementation only raises the <see cref="PropertyChanging"/> event.</remarks>
46-
protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null)
60+
protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
4761
{
48-
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
62+
OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
4963
}
5064

5165
/// <summary>

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs

Lines changed: 229 additions & 30 deletions
Large diffs are not rendered by default.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Threading;
7+
8+
#nullable enable
9+
10+
namespace Microsoft.Toolkit.Mvvm.DependencyInjection
11+
{
12+
/// <summary>
13+
/// A type that facilitates the use of the <see cref="IServiceProvider"/> type.
14+
/// The <see cref="Ioc"/> provides the ability to configure services in a singleton, thread-safe
15+
/// service provider instance, which can then be used to resolve service instances.
16+
/// The first step to use this feature is to declare some services, for instance:
17+
/// <code>
18+
/// public interface ILogger
19+
/// {
20+
/// void Log(string text);
21+
/// }
22+
/// </code>
23+
/// <code>
24+
/// public class ConsoleLogger : ILogger
25+
/// {
26+
/// void Log(string text) => Console.WriteLine(text);
27+
/// }
28+
/// </code>
29+
/// Then the services configuration should then be done at startup, by calling the <see cref="ConfigureServices"/>
30+
/// method and passing an <see cref="IServiceProvider"/> instance with the services to use. That instance can
31+
/// be from any library offering dependency injection functionality, such as Microsoft.Extensions.DependencyInjection.
32+
/// For instance, using that library, <see cref="ConfigureServices"/> can be used as follows in this example:
33+
/// <code>
34+
/// Ioc.Default.ConfigureServices(
35+
/// new ServiceCollection()
36+
/// .AddSingleton&lt;ILogger, Logger&gt;()
37+
/// .BuildServiceProvider());
38+
/// </code>
39+
/// Finally, you can use the <see cref="Ioc"/> instance (which implements <see cref="IServiceProvider"/>)
40+
/// to retrieve the service instances from anywhere in your application, by doing as follows:
41+
/// <code>
42+
/// Ioc.Default.GetService&lt;ILogger&gt;().Log("Hello world!");
43+
/// </code>
44+
/// </summary>
45+
public sealed class Ioc : IServiceProvider
46+
{
47+
/// <summary>
48+
/// Gets the default <see cref="Ioc"/> instance.
49+
/// </summary>
50+
public static Ioc Default { get; } = new Ioc();
51+
52+
/// <summary>
53+
/// The <see cref="IServiceProvider"/> instance to use, if initialized.
54+
/// </summary>
55+
private volatile IServiceProvider? serviceProvider;
56+
57+
/// <inheritdoc/>
58+
public object? GetService(Type serviceType)
59+
{
60+
// As per section I.12.6.6 of the official CLI ECMA-335 spec:
61+
// "[...] read and write access to properly aligned memory locations no larger than the native
62+
// word size is atomic when all the write accesses to a location are the same size. Atomic writes
63+
// shall alter no bits other than those written. Unless explicit layout control is used [...],
64+
// data elements no larger than the natural word size [...] shall be properly aligned.
65+
// Object references shall be treated as though they are stored in the native word size."
66+
// The field being accessed here is of native int size (reference type), and is only ever accessed
67+
// directly and atomically by a compare exchange instruction (see below), or here. We can therefore
68+
// assume this read is thread safe with respect to accesses to this property or to invocations to one
69+
// of the available configuration methods. So we can just read the field directly and make the necessary
70+
// check with our local copy, without the need of paying the locking overhead from this get accessor.
71+
IServiceProvider? provider = this.serviceProvider;
72+
73+
if (provider is null)
74+
{
75+
ThrowInvalidOperationExceptionForMissingInitialization();
76+
}
77+
78+
return provider!.GetService(serviceType);
79+
}
80+
81+
/// <summary>
82+
/// Tries to resolve an instance of a specified service type.
83+
/// </summary>
84+
/// <typeparam name="T">The type of service to resolve.</typeparam>
85+
/// <returns>An instance of the specified service, or <see langword="null"/>.</returns>
86+
/// <exception cref="InvalidOperationException">Throw if the current <see cref="Ioc"/> instance has not been initialized.</exception>
87+
public T? GetService<T>()
88+
where T : class
89+
{
90+
IServiceProvider? provider = this.serviceProvider;
91+
92+
if (provider is null)
93+
{
94+
ThrowInvalidOperationExceptionForMissingInitialization();
95+
}
96+
97+
return (T?)provider!.GetService(typeof(T));
98+
}
99+
100+
/// <summary>
101+
/// Resolves an instance of a specified service type.
102+
/// </summary>
103+
/// <typeparam name="T">The type of service to resolve.</typeparam>
104+
/// <returns>An instance of the specified service, or <see langword="null"/>.</returns>
105+
/// <exception cref="InvalidOperationException">
106+
/// Throw if the current <see cref="Ioc"/> instance has not been initialized, or if the
107+
/// requested service type was not registered in the service provider currently in use.
108+
/// </exception>
109+
public T GetRequiredService<T>()
110+
where T : class
111+
{
112+
IServiceProvider? provider = this.serviceProvider;
113+
114+
if (provider is null)
115+
{
116+
ThrowInvalidOperationExceptionForMissingInitialization();
117+
}
118+
119+
T? service = (T?)provider!.GetService(typeof(T));
120+
121+
if (service is null)
122+
{
123+
ThrowInvalidOperationExceptionForUnregisteredType();
124+
}
125+
126+
return service!;
127+
}
128+
129+
/// <summary>
130+
/// Initializes the shared <see cref="IServiceProvider"/> instance.
131+
/// </summary>
132+
/// <param name="serviceProvider">The input <see cref="IServiceProvider"/> instance to use.</param>
133+
public void ConfigureServices(IServiceProvider serviceProvider)
134+
{
135+
IServiceProvider? oldServices = Interlocked.CompareExchange(ref this.serviceProvider, serviceProvider, null);
136+
137+
if (!(oldServices is null))
138+
{
139+
ThrowInvalidOperationExceptionForRepeatedConfiguration();
140+
}
141+
}
142+
143+
/// <summary>
144+
/// Throws an <see cref="InvalidOperationException"/> when the <see cref="IServiceProvider"/> property is used before initialization.
145+
/// </summary>
146+
private static void ThrowInvalidOperationExceptionForMissingInitialization()
147+
{
148+
throw new InvalidOperationException("The service provider has not been configured yet");
149+
}
150+
151+
/// <summary>
152+
/// Throws an <see cref="InvalidOperationException"/> when the <see cref="IServiceProvider"/> property is missing a type registration.
153+
/// </summary>
154+
private static void ThrowInvalidOperationExceptionForUnregisteredType()
155+
{
156+
throw new InvalidOperationException("The requested service type was not registered");
157+
}
158+
159+
/// <summary>
160+
/// Throws an <see cref="InvalidOperationException"/> when a configuration is attempted more than once.
161+
/// </summary>
162+
private static void ThrowInvalidOperationExceptionForRepeatedConfiguration()
163+
{
164+
throw new InvalidOperationException("The default service provider has already been configured");
165+
}
166+
}
167+
}

Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.ComponentModel;
67
using System.Runtime.CompilerServices;
78
using System.Threading;
89
using System.Threading.Tasks;
@@ -18,6 +19,21 @@ namespace Microsoft.Toolkit.Mvvm.Input
1819
/// </summary>
1920
public sealed class AsyncRelayCommand : ObservableObject, IAsyncRelayCommand
2021
{
22+
/// <summary>
23+
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="CanBeCanceled"/>.
24+
/// </summary>
25+
internal static readonly PropertyChangedEventArgs CanBeCanceledChangedEventArgs = new PropertyChangedEventArgs(nameof(CanBeCanceled));
26+
27+
/// <summary>
28+
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IsCancellationRequested"/>.
29+
/// </summary>
30+
internal static readonly PropertyChangedEventArgs IsCancellationRequestedChangedEventArgs = new PropertyChangedEventArgs(nameof(IsCancellationRequested));
31+
32+
/// <summary>
33+
/// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IsRunning"/>.
34+
/// </summary>
35+
internal static readonly PropertyChangedEventArgs IsRunningChangedEventArgs = new PropertyChangedEventArgs(nameof(IsRunning));
36+
2137
/// <summary>
2238
/// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute"/> is used.
2339
/// </summary>
@@ -91,15 +107,22 @@ public Task? ExecutionTask
91107
get => this.executionTask;
92108
private set
93109
{
94-
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
110+
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ =>
111+
{
112+
// When the task completes
113+
OnPropertyChanged(IsRunningChangedEventArgs);
114+
OnPropertyChanged(CanBeCanceledChangedEventArgs);
115+
}))
95116
{
96-
OnPropertyChanged(nameof(IsRunning));
117+
// When setting the task
118+
OnPropertyChanged(IsRunningChangedEventArgs);
119+
OnPropertyChanged(CanBeCanceledChangedEventArgs);
97120
}
98121
}
99122
}
100123

101124
/// <inheritdoc/>
102-
public bool CanBeCanceled => !(this.cancelableExecute is null);
125+
public bool CanBeCanceled => !(this.cancelableExecute is null) && IsRunning;
103126

104127
/// <inheritdoc/>
105128
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
@@ -142,7 +165,7 @@ public Task ExecuteAsync(object? parameter)
142165

143166
var cancellationTokenSource = this.cancellationTokenSource = new CancellationTokenSource();
144167

145-
OnPropertyChanged(nameof(IsCancellationRequested));
168+
OnPropertyChanged(IsCancellationRequestedChangedEventArgs);
146169

147170
// Invoke the cancelable command delegate with a new linked token
148171
return ExecutionTask = this.cancelableExecute!(cancellationTokenSource.Token);
@@ -156,7 +179,8 @@ public void Cancel()
156179
{
157180
this.cancellationTokenSource?.Cancel();
158181

159-
OnPropertyChanged(nameof(IsCancellationRequested));
182+
OnPropertyChanged(IsCancellationRequestedChangedEventArgs);
183+
OnPropertyChanged(CanBeCanceledChangedEventArgs);
160184
}
161185
}
162186
}

Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,22 @@ public Task? ExecutionTask
9191
get => this.executionTask;
9292
private set
9393
{
94-
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ => OnPropertyChanged(nameof(IsRunning))))
94+
if (SetPropertyAndNotifyOnCompletion(ref this.executionTask, value, _ =>
9595
{
96-
OnPropertyChanged(nameof(IsRunning));
96+
// When the task completes
97+
OnPropertyChanged(AsyncRelayCommand.IsRunningChangedEventArgs);
98+
OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
99+
}))
100+
{
101+
// When setting the task
102+
OnPropertyChanged(AsyncRelayCommand.IsRunningChangedEventArgs);
103+
OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
97104
}
98105
}
99106
}
100107

101108
/// <inheritdoc/>
102-
public bool CanBeCanceled => !(this.cancelableExecute is null);
109+
public bool CanBeCanceled => !(this.cancelableExecute is null) && IsRunning;
103110

104111
/// <inheritdoc/>
105112
public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true;
@@ -163,7 +170,7 @@ public Task ExecuteAsync(T parameter)
163170

164171
var cancellationTokenSource = this.cancellationTokenSource = new CancellationTokenSource();
165172

166-
OnPropertyChanged(nameof(IsCancellationRequested));
173+
OnPropertyChanged(AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
167174

168175
// Invoke the cancelable command delegate with a new linked token
169176
return ExecutionTask = this.cancelableExecute!(parameter, cancellationTokenSource.Token);
@@ -183,7 +190,8 @@ public void Cancel()
183190
{
184191
this.cancellationTokenSource?.Cancel();
185192

186-
OnPropertyChanged(nameof(IsCancellationRequested));
193+
OnPropertyChanged(AsyncRelayCommand.IsCancellationRequestedChangedEventArgs);
194+
OnPropertyChanged(AsyncRelayCommand.CanBeCanceledChangedEventArgs);
187195
}
188196
}
189197
}

Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,10 @@ public static class IMessengerExtensions
2727
/// </summary>
2828
private static class MethodInfos
2929
{
30-
/// <summary>
31-
/// Initializes static members of the <see cref="MethodInfos"/> class.
32-
/// </summary>
33-
static MethodInfos()
34-
{
35-
RegisterIRecipient = (
36-
from methodInfo in typeof(IMessengerExtensions).GetMethods()
37-
where methodInfo.Name == nameof(Register) &&
38-
methodInfo.IsGenericMethod &&
39-
methodInfo.GetGenericArguments().Length == 2
40-
let parameters = methodInfo.GetParameters()
41-
where parameters.Length == 3 &&
42-
parameters[1].ParameterType.IsGenericType &&
43-
parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(IRecipient<>)
44-
select methodInfo).First();
45-
}
46-
4730
/// <summary>
4831
/// The <see cref="MethodInfo"/> instance associated with <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/>.
4932
/// </summary>
50-
public static readonly MethodInfo RegisterIRecipient;
33+
public static readonly MethodInfo RegisterIRecipient = new Action<IMessenger, IRecipient<object>, Unit>(Register).Method.GetGenericMethodDefinition();
5134
}
5235

5336
/// <summary>

Microsoft.Toolkit.Mvvm/Messaging/Internals/Type2.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,19 @@ public override bool Equals(object? obj)
6565
[MethodImpl(MethodImplOptions.AggressiveInlining)]
6666
public override int GetHashCode()
6767
{
68-
unchecked
69-
{
70-
// To combine the two hashes, we can simply use the fast djb2 hash algorithm.
71-
// This is not a problem in this case since we already know that the base
72-
// RuntimeHelpers.GetHashCode method is providing hashes with a good enough distribution.
73-
int hash = RuntimeHelpers.GetHashCode(TMessage);
68+
// To combine the two hashes, we can simply use the fast djb2 hash algorithm. Unfortunately we
69+
// can't really skip the callvirt here (eg. by using RuntimeHelpers.GetHashCode like in other
70+
// cases), as there are some niche cases mentioned above that might break when doing so.
71+
// However since this method is not generally used in a hot path (eg. the message broadcasting
72+
// only invokes this a handful of times when initially retrieving the target mapping), this
73+
// doesn't actually make a noticeable difference despite the minor overhead of the virtual call.
74+
int hash = TMessage.GetHashCode();
7475

75-
hash = (hash << 5) + hash;
76+
hash = (hash << 5) + hash;
7677

77-
hash += RuntimeHelpers.GetHashCode(TToken);
78+
hash += TToken.GetHashCode();
7879

79-
return hash;
80-
}
80+
return hash;
8181
}
8282
}
8383
}

0 commit comments

Comments
 (0)