[Feature] Unregister all receipents by TToken in IMessenger
Describe the problem
I would like to request a new api method for the IMessenger interface.
The challenge is that the unregister process becomes too tightly coupled in my opinion to the receipent instances, and while it is easy for a receipent to unregister from listening to IMessenger call, it is currently not possible for a source to indicate no further messages will be coming on a specific token.
It would be more decoupled from the UI if you could unregister ALL receipients listening to a token, by using only the token. That way a UI-independent service can clean up.
I have a service which instantiates multiple different senders dynamically in the background of a desktop application, where each instance gets its own token to send messages on. Sometimes these instances can fail due to external factors, and I'd like to un-register every receipent of a given instance's token when that happends.
I'd like the sender to be able to unregister every listener based on a token, so the UI dont have to do it. Still, the sender doesn't know who is listening, it's just saying "We're done here, nothing more coming on this channel(token) from me".
Describe the solution
A new method on the IMessenger interface or an extension method.
public interface IMessenger
{
public void UnregisterAll<TToken>(TToken token)
where TToken : IEquatable<TToken>;
}
Obviously this method will have to be implemented by WeakReferenceManager and StrongReferenceManager.
Optionally this could be a extension method on IMessenger interface.
Alternatives
No response
Additional info
Thoughts?
Help us help you
No.
Hello, 'olejsc! Thanks for submitting a new feature request. I've automatically added a vote 👍 reaction to help get things started. Other community members can vote to help us prioritize this feature in the future!
Would add the mvvm-toolkit label if i knew how to .
So, this is a way for the sender to tell the recipient that it is finished sending messages?
IMessenger's primary role is to decouple the sender/receiver. Why should a sender be able to perform an action that affects the recipient ? Shouldn't it be up to the recipient whether or not it wants to continue listening for messages?
At a minimum - I would suggest changing the proposed method name. Instead of UnregisterAll, maybe UnregisterAllRecipients. Additionally, there could be additional overloads, allowing you to Unregister all recipients for a specific message type, or a message type/token combo. This would bring it in line with the current Unregister and UnregisterAll options.
public interface IMessenger
{
/*** Methods not pertaining to Unregister are omitted. ***/
public void Unregister<TMessage,TToken> (object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>;
public void UnregisterAll (object recipient);
public void UnregisterAll<TToken> (object recipient, TToken token) where TToken : IEquatable<TToken>;
+ public void UnregisterAllRecipients<TToken>(TToken token)
+ where TToken : IEquatable<TToken>;
+ public void UnregisterAllRecipients(Type messageType);
+ public void UnregisterAllRecipients<TToken>(Type messageType, TToken token)
+ where TToken : IEquatable<TToken>;
}
However, I don't feel that this is the best option at all - it promotes the coupling of senders/receivers.
I feel that the optimal course of action would be for you to implement, in your code, a way to notify recipients that "the conversation is complete." The recipients can then choose to (or not choose to) unregister themselves for that message/token.
It could be as simple as a different message sent with that token.
public sealed record MessagesCompleted(Guid TaskId) { }
public sealed record LongTaskStarted(Guid TaskId) { }
public sealed record LongTaskProgress(Guid TaskId, int Completed, int Total) { }
public class Sender
{
public async Task SomeLongTask(int iterations)
{
var taskId = Guid.NewGuid();
WeakReferenceMessenger.Default.Send(new LongTaskStarted(taskId));
var completed = 0;
while(completed < iterations)
{
await Task.Delay(TimeSpan.FromMinutes(1));
++completed;
WeakReferenceMessenger.Default.Send(new LongTaskProgress(taskId, completed, iterations), taskId);
}
WeakReferenceMessenger.Default.Send(new MessagesCompleted(taskId), taskId);
}
}
public class Recipient
{
public Recipient()
{
WeakReferenceMessenger.Default.Register<Recipient, LongTaskStarted>(
this,
OnLongTaskStarted
);
}
private static void OnLongTaskStarted(Recipient recipient, LongTaskStarted message)
{
WeakReferenceMessenger.Default.Register<Recipient, MessagesCompleted, Guid>(
this,
message.TaskId,
OnMessagesCompleted
);
WeakReferenceMessenger.Default.Register<Recipient, LongTaskProgress, Guid>(
this,
message.TaskId,
OnLongTaskProgress
);
}
private static void OnMessagesCompleted(Recipient recipient, MessagesCompleted message)
{
WeakReferenceMessenger.Default.Unregister<MessagesCompleted, Guid>(this, message.TaskId);
WeakReferenceMessenger.Default.Unregister<LongTaskProgress, Guid>(this, message.TaskId);
}
private static void OnLongTaskStarted(Recipient recipient, LongTaskProgress message)
{
// Report progress
}
}
Thanks for a proper reply. Your answer made me rethink my initial suggestion.
So, this is a way for the sender to tell the recipient that it is finished sending messages?
IMessenger's primary role is to decouple the sender/receiver. Why should a sender be able to perform an action that affects the recipient ? Shouldn't it be up to the recipient whether or not it wants to continue listening for messages?
Conceptually I've thought of the messenger more of a publisher/subscriber pattern, because that is more like what my current use case is like.
stateDiagram-v2
Source --> Recepient1
Source --> Recepient2
Source --> Recepient3
Recepient1 --> Source
Recepient2 --> Source
Recepient3 --> Source
However, and slightly oblivious to me, the messenger never registers a "sender" or source. A far more accurate description is an open group, where anyone can say anything to everyone, and even external non-listeners can chime in and communicate to the listeners. A example diagram of that[^1]:
stateDiagram-v2
l1 : Just listener 1
l2 : Just listener 2
state Listeners_and_callers{
Recepient1--> Recepient2
Recepient1--> Recepient3
Recepient1--> Recepient4
Recepient2--> Recepient1
Recepient2--> Recepient3
Recepient2--> Recepient4
Recepient3--> Recepient1
Recepient3--> Recepient2
Recepient3--> Recepient4
Recepient4--> Recepient1
Recepient4--> Recepient2
Recepient4--> Recepient3
}
Listeners_and_callers -->l1
Listeners_and_callers -->l2
ExternalCaller1 --> Listeners_and_callers
ExternalCaller1 --> l2
ExternalCaller1 --> l1
ExternalCaller2 --> Listeners_and_callers
ExternalCaller2 --> l2
ExternalCaller2 --> l1
[^1]It looked way better in the mermaid editor, sorry
At a minimum - I would suggest changing the proposed method name. Instead of
UnregisterAll, maybe >UnregisterAllRecipients. Additionally, there could be additional overloads, allowing you to Unregister all recipients for a specific >message type, or a message type/token combo. This would bring it in line with the currentUnregisterandUnregisterAll>options.
I agree there are some more extension methods that could be useful.
public interface IMessenger { /*** Methods not pertaining to Unregister are omitted. ***/ public void Unregister<TMessage,TToken> (object recipient, TToken token) where TMessage : class where TToken : IEquatable<TToken>; public void UnregisterAll (object recipient); public void UnregisterAll<TToken> (object recipient, TToken token) where TToken : IEquatable<TToken>; + public void UnregisterAllRecipients<TToken>(TToken token) + where TToken : IEquatable<TToken>; + public void UnregisterAllRecipients(Type messageType); + public void UnregisterAllRecipients<TToken>(Type messageType, TToken token) + where TToken : IEquatable<TToken>; }
Looks good to me at first glance.
However, I don't feel that this is the best option at all - it promotes the coupling of senders/receivers.
I feel that the optimal course of action would be for you to implement, in your code, a way to notify recipients that "the >conversation is complete." The recipients can then choose to (or not choose to) unregister themselves for that message/token. It could be as simple as a different message sent with that token.
I feel stupid for not thinking about that myself. It's a obvious solution! 😄
public sealed record MessagesCompleted(Guid TaskId) { } public sealed record LongTaskStarted(Guid TaskId) { } public sealed record LongTaskProgress(Guid TaskId, int Completed, int Total) { } public class Sender { public async Task SomeLongTask(int iterations) { var taskId = Guid.NewGuid(); WeakReferenceMessenger.Default.Send(new LongTaskStarted(taskId)); var completed = 0; while(completed < iterations) { await Task.Delay(TimeSpan.FromMinutes(1)); ++completed; WeakReferenceMessenger.Default.Send(new LongTaskProgress(taskId, completed, iterations), taskId); } WeakReferenceMessenger.Default.Send(new MessagesCompleted(taskId), taskId); } } public class Recipient { public Recipient() { WeakReferenceMessenger.Default.Register<Recipient, LongTaskStarted>( this, OnLongTaskStarted ); } private static void OnLongTaskStarted(Recipient recipient, LongTaskStarted message) { WeakReferenceMessenger.Default.Register<Recipient, MessagesCompleted, Guid>( this, message.TaskId, OnMessagesCompleted ); WeakReferenceMessenger.Default.Register<Recipient, LongTaskProgress, Guid>( this, message.TaskId, OnLongTaskProgress ); } private static void OnMessagesCompleted(Recipient recipient, MessagesCompleted message) { WeakReferenceMessenger.Default.Unregister<MessagesCompleted, Guid>(this, message.TaskId); WeakReferenceMessenger.Default.Unregister<LongTaskProgress, Guid>(this, message.TaskId); } private static void OnLongTaskStarted(Recipient recipient, LongTaskProgress message) { // Report progress } }
A briliant example, thank you.
Closing this as it seems resolved, and yeah it's something that can easily be done externally like shown above. Thank you @mikechristiansenvae to take the time to help out and write that explanation and example! 😄