dotnet icon indicating copy to clipboard operation
dotnet copied to clipboard

[Feature] Unregister all receipents by TToken in IMessenger

Open olejsc opened this issue 3 years ago • 4 comments

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.

olejsc avatar Sep 12 '22 15:09 olejsc

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!

ghost avatar Sep 12 '22 15:09 ghost

Would add the mvvm-toolkit label if i knew how to .

olejsc avatar Sep 12 '22 15:09 olejsc

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
    }
}

mikechristiansenvae avatar Sep 19 '22 17:09 mikechristiansenvae

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 current Unregister and UnregisterAll >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.

olejsc avatar Sep 23 '22 23:09 olejsc

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! 😄

Sergio0694 avatar Jan 02 '23 10:01 Sergio0694