Skip to content

Commit d86f794

Browse files
committed
Added MessageHandler<in TRecipient> "covariance"
1 parent e514869 commit d86f794

File tree

4 files changed

+94
-26
lines changed

4 files changed

+94
-26
lines changed

Microsoft.Toolkit.Mvvm/Messaging/IMessenger.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
1313
/// closures: if an instance method on a recipient needs to be invoked it is possible to just
1414
/// cast the recipient to the right type and then access the local method from that instance.
1515
/// </summary>
16+
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
1617
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
1718
/// <param name="recipient">The recipient that is receiving the message.</param>
1819
/// <param name="message">The message being received.</param>
19-
public delegate void MessageHandler<in TMessage>(object recipient, TMessage message);
20+
public delegate void MessageHandler<in TRecipient, in TMessage>(TRecipient recipient, TMessage message)
21+
where TRecipient : class
22+
where TMessage : class;
2023

2124
/// <summary>
2225
/// An interface for a type providing the ability to exchange messages between different objects.
@@ -39,13 +42,15 @@ bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
3942
/// <summary>
4043
/// Registers a recipient for a given type of message.
4144
/// </summary>
45+
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
4246
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
4347
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
4448
/// <param name="recipient">The recipient that will receive the messages.</param>
4549
/// <param name="token">A token used to determine the receiving channel to use.</param>
46-
/// <param name="handler">The <see cref="MessageHandler{T}"/> to invoke when a message is received.</param>
50+
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
4751
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
48-
void Register<TMessage, TToken>(object recipient, TToken token, MessageHandler<TMessage> handler)
52+
void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
53+
where TRecipient : class
4954
where TMessage : class
5055
where TToken : IEquatable<TToken>;
5156

Microsoft.Toolkit.Mvvm/Messaging/Messenger.cs

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
5050
/// <code>
5151
/// Messenger.Default.Register&lt;LoginCompletedMessage&gt;(this, Receive);
5252
/// </code>
53-
/// The C# compiler will automatically convert that expression to a <see cref="MessageHandler{TMessage}"/> instance
54-
/// compatible with the <see cref="MessengerExtensions.Register{T}(IMessenger,object,MessageHandler{T})"/> method.
53+
/// The C# compiler will automatically convert that expression to a <see cref="MessageHandler{TRecipient,TMessage}"/> instance
54+
/// compatible with <see cref="MessengerExtensions.Register{TRecipient,TMessage}(IMessenger,TRecipient,MessageHandler{TRecipient,TMessage})"/>.
5555
/// This will also work if multiple overloads of that method are available, each handling a different
5656
/// message type: the C# compiler will automatically pick the right one for the current message type.
5757
/// For info on the other available features, check the <see cref="IMessenger"/> interface.
@@ -66,18 +66,23 @@ public sealed class Messenger : IMessenger
6666
// | ________(recipients registrations)___________\________/ / __/
6767
// | / _______(channel registrations)_____\___________________/ /
6868
// | / / \ /
69-
// DictionarySlim<Recipient, DictionarySlim<TToken, MessageHandler<TMessage>>> mapping = Mapping<TMessage, TToken>
70-
// / / \ / /
71-
// ___(Type2.tToken)____/ / \______/___________________/
69+
// DictionarySlim<Recipient, DictionarySlim<TToken, MessageHandler<object, TMessage>>> mapping = Mapping<TMessage, TToken>
70+
// / / \ / /
71+
// ___(Type2.tToken)____/ / \______/_________________________/
7272
// /________________(Type2.tMessage)____/ /
7373
// / ________________________________________/
7474
// / /
7575
// DictionarySlim<Type2, IMapping> typesMap;
7676
// --------------------------------------------------------------------------------------------------------
77-
// Each combination of <TMessage, TToken> results in a concrete Mapping<TMessage, TToken> type, which holds
78-
// the references from registered recipients to handlers. The handlers are stored in a <TToken, Action<TMessage>>
79-
// dictionary, so that each recipient can have up to one registered handler for a given token, for each
80-
// message type. Each mapping is stored in the types map, which associates each pair of concrete types to its
77+
// Each combination of <TMessage, TToken> results in a concrete Mapping<TMessage, TToken> type, which holds the references
78+
// from registered recipients to handlers. The handlers are stored in a <TToken, MessageHandler<object, TMessage>>
79+
// dictionary, so that each recipient can have up to one registered handler for a given token, for each message type.
80+
// Note that the registered handlers are unsafe-cast to a MessageHandler<object, TMessage> instance even if they were
81+
// actually of type MessageHandler<TRecipient, TMessage>. This allows the messenger to track and invoke type-specific
82+
// handlers without using reflection and without having to capture the input handler in a proxy delegate, causing one
83+
// extra memory allocations and adding overhead. The type conversion is guaranteed to be respected due to how the
84+
// messenger type itself works - as registered handlers are always invoked on their respective recipients.
85+
// Each mapping is stored in the types map, which associates each pair of concrete types to its
8186
// mapping instance. Mapping instances are exposed as IMapping items, as each will be a closed type over
8287
// a different combination of TMessage and TToken generic type parameters. Each existing recipient is also stored in
8388
// the main recipients map, along with a set of all the existing dictionaries of handlers for that recipient (for all
@@ -137,7 +142,8 @@ public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
137142
}
138143

139144
/// <inheritdoc/>
140-
public void Register<TMessage, TToken>(object recipient, TToken token, MessageHandler<TMessage> handler)
145+
public void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
146+
where TRecipient : class
141147
where TMessage : class
142148
where TToken : IEquatable<TToken>
143149
{
@@ -146,19 +152,20 @@ public void Register<TMessage, TToken>(object recipient, TToken token, MessageHa
146152
// Get the <TMessage, TToken> registration list for this recipient
147153
Mapping<TMessage, TToken> mapping = GetOrAddMapping<TMessage, TToken>();
148154
var key = new Recipient(recipient);
149-
ref DictionarySlim<TToken, MessageHandler<TMessage>>? map = ref mapping.GetOrAddValueRef(key);
155+
ref DictionarySlim<TToken, MessageHandler<object, TMessage>>? map = ref mapping.GetOrAddValueRef(key);
150156

151-
map ??= new DictionarySlim<TToken, MessageHandler<TMessage>>();
157+
map ??= new DictionarySlim<TToken, MessageHandler<object, TMessage>>();
152158

153159
// Add the new registration entry
154-
ref MessageHandler<TMessage>? registeredHandler = ref map.GetOrAddValueRef(token);
160+
ref MessageHandler<object, TMessage>? registeredHandler = ref map.GetOrAddValueRef(token);
155161

156162
if (!(registeredHandler is null))
157163
{
158164
ThrowInvalidOperationExceptionForDuplicateRegistration();
159165
}
160166

161-
registeredHandler = handler;
167+
// Treat the input delegate as if it was covariant (see comments below in the Send method)
168+
registeredHandler = Unsafe.As<MessageHandler<object, TMessage>>(handler);
162169

163170
// Update the total counter for handlers for the current type parameters
164171
mapping.TotalHandlersCount++;
@@ -356,7 +363,7 @@ public void Unregister<TMessage, TToken>(object recipient, TToken token)
356363

357364
var key = new Recipient(recipient);
358365

359-
if (!mapping!.TryGetValue(key, out DictionarySlim<TToken, MessageHandler<TMessage>>? dictionary))
366+
if (!mapping!.TryGetValue(key, out DictionarySlim<TToken, MessageHandler<object, TMessage>>? dictionary))
360367
{
361368
return;
362369
}
@@ -461,9 +468,14 @@ public unsafe TMessage Send<TMessage, TToken>(TMessage message, TToken token)
461468
{
462469
// We're doing an unsafe cast to skip the type checks again.
463470
// See the comments in the UnregisterAll method for more info.
464-
MessageHandler<TMessage> handler = Unsafe.As<MessageHandler<TMessage>>(Unsafe.Add(ref handlersRef, (IntPtr)(void*)(uint)j));
465-
466-
handler(Unsafe.Add(ref recipientsRef, (IntPtr)(void*)(uint)j), message);
471+
object handler = Unsafe.Add(ref handlersRef, (IntPtr)(void*)(uint)j);
472+
object recipient = Unsafe.Add(ref recipientsRef, (IntPtr)(void*)(uint)j);
473+
474+
// Here we perform an unsafe cast to enable covariance for delegate types.
475+
// We know that the input recipient will always respect the type constraints
476+
// of each original input delegate, and doing so allows us to still invoke
477+
// them all from here without worrying about specific generic type arguments.
478+
Unsafe.As<MessageHandler<object, TMessage>>(handler)(recipient, message);
467479
}
468480
}
469481
finally
@@ -559,7 +571,7 @@ private Mapping<TMessage, TToken> GetOrAddMapping<TMessage, TToken>()
559571
/// This type is defined for simplicity and as a workaround for the lack of support for using type aliases
560572
/// over open generic types in C# (using type aliases can only be used for concrete, closed types).
561573
/// </remarks>
562-
private sealed class Mapping<TMessage, TToken> : DictionarySlim<Recipient, DictionarySlim<TToken, MessageHandler<TMessage>>>, IMapping
574+
private sealed class Mapping<TMessage, TToken> : DictionarySlim<Recipient, DictionarySlim<TToken, MessageHandler<object, TMessage>>>, IMapping
563575
where TMessage : class
564576
where TToken : IEquatable<TToken>
565577
{

Microsoft.Toolkit.Mvvm/Messaging/MessengerExtensions.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ static Action<IMessenger, object, TToken> GetRegistrationAction(Type type, Metho
174174
public static void Register<TMessage>(this IMessenger messenger, IRecipient<TMessage> recipient)
175175
where TMessage : class
176176
{
177-
messenger.Register<TMessage, Unit>(recipient, default, (r, m) => Unsafe.As<IRecipient<TMessage>>(r).Receive(m));
177+
messenger.Register<IRecipient<TMessage>, TMessage, Unit>(recipient, default, (r, m) => r.Receive(m));
178178
}
179179

180180
/// <summary>
@@ -191,7 +191,7 @@ public static void Register<TMessage, TToken>(this IMessenger messenger, IRecipi
191191
where TMessage : class
192192
where TToken : IEquatable<TToken>
193193
{
194-
messenger.Register<TMessage, TToken>(recipient, token, (r, m) => Unsafe.As<IRecipient<TMessage>>(r).Receive(m));
194+
messenger.Register<IRecipient<TMessage>, TMessage, TToken>(recipient, token, (r, m) => r.Receive(m));
195195
}
196196

197197
/// <summary>
@@ -200,15 +200,49 @@ public static void Register<TMessage, TToken>(this IMessenger messenger, IRecipi
200200
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
201201
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
202202
/// <param name="recipient">The recipient that will receive the messages.</param>
203-
/// <param name="handler">The <see cref="MessageHandler{T}"/> to invoke when a message is received.</param>
203+
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
204204
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
205205
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
206-
public static void Register<TMessage>(this IMessenger messenger, object recipient, MessageHandler<TMessage> handler)
206+
public static void Register<TMessage>(this IMessenger messenger, object recipient, MessageHandler<object, TMessage> handler)
207207
where TMessage : class
208208
{
209209
messenger.Register(recipient, default(Unit), handler);
210210
}
211211

212+
/// <summary>
213+
/// Registers a recipient for a given type of message.
214+
/// </summary>
215+
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
216+
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
217+
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
218+
/// <param name="recipient">The recipient that will receive the messages.</param>
219+
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
220+
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
221+
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
222+
public static void Register<TRecipient, TMessage>(this IMessenger messenger, TRecipient recipient, MessageHandler<TRecipient, TMessage> handler)
223+
where TRecipient : class
224+
where TMessage : class
225+
{
226+
messenger.Register(recipient, default(Unit), handler);
227+
}
228+
229+
/// <summary>
230+
/// Registers a recipient for a given type of message.
231+
/// </summary>
232+
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
233+
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
234+
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
235+
/// <param name="recipient">The recipient that will receive the messages.</param>
236+
/// <param name="token">A token used to determine the receiving channel to use.</param>
237+
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
238+
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
239+
public static void Register<TMessage, TToken>(this IMessenger messenger, object recipient, TToken token, MessageHandler<object, TMessage> handler)
240+
where TMessage : class
241+
where TToken : IEquatable<TToken>
242+
{
243+
messenger.Register(recipient, token, handler);
244+
}
245+
212246
/// <summary>
213247
/// Unregisters a recipient from messages of a given type.
214248
/// </summary>

UnitTests/UnitTests.Shared/Mvvm/Test_Messenger.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,25 @@ public void Test_Messenger_IRecipient_SomeMessages_WithToken()
297297
Assert.IsFalse(messenger.IsRegistered<MessageB>(recipient));
298298
}
299299

300+
[TestCategory("Mvvm")]
301+
[TestMethod]
302+
public void Test_Messenger_RegisterWithTypeParameter()
303+
{
304+
var messenger = new Messenger();
305+
var recipient = new RecipientWithNoMessages { Number = 42 };
306+
307+
int number = 0;
308+
309+
messenger.Register<RecipientWithNoMessages, MessageA>(recipient, (r, m) => number = r.Number);
310+
311+
messenger.Send<MessageA>();
312+
313+
Assert.AreEqual(number, 42);
314+
}
315+
300316
public sealed class RecipientWithNoMessages
301317
{
318+
public int Number { get; set; }
302319
}
303320

304321
public sealed class RecipientWithSomeMessages

0 commit comments

Comments
 (0)