forked from CommunityToolkit/WindowsCommunityToolkit
-
Notifications
You must be signed in to change notification settings - Fork 3
/
MessengerExtensions.cs
277 lines (258 loc) · 16 KB
/
MessengerExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// 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>
/// A class that acts as a container to load the <see cref="MethodInfo"/> instance linked to
/// the <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/> method.
/// This class is needed to avoid forcing the initialization code in the static constructor to run as soon as
/// the <see cref="MessengerExtensions"/> type is referenced, even if that is done just to use methods
/// that do not actually require this <see cref="MethodInfo"/> instance to be available.
/// We're effectively using this type to leverage the lazy loading of static constructors done by the runtime.
/// </summary>
private static class MethodInfos
{
/// <summary>
/// Initializes static members of the <see cref="MethodInfos"/> class.
/// </summary>
static MethodInfos()
{
RegisterIRecipient = (
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>
/// The <see cref="MethodInfo"/> instance associated with <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/>.
/// </summary>
public static readonly MethodInfo RegisterIRecipient;
}
/// <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>
/// 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 = MethodInfos.RegisterIRecipient.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);
}
}
}