Skip to content

[Feature] Behaviors for WeakReferenceMessenger and StrongReferenceMessengerΒ #238

Closed

Description

@Arlodotexe

Problem

The MVVM Toolkit has a StrongReferenceMessenger and a WeakReferenceMessenger, used for relaying messages via the messenger pattern.

These APIs are extremely handy for passing messages around to decoupled parts of the app, but they require code-behind to send and receive messages.

In scenarios where the user may prefer or be forced to write only XAML (such as retemplating a control), it becomes cumbersome to send and receive messages with the default Messenger.

Solution

It would useful to developers if they had the ability to send and receive messages for WeakReferenceMessenger and StrongReferenceMessenger via Behavior actions and triggers in XAML.

tl;dr

XAML has Behaviors.
Behaviors are like bacon.
Messenger needs bacon.

Examples

Sending a message

The user specifies a SendMessageAction, changes the messenger to Weak or Strong as needed, and provides the model to send.

<ItemsControl x:Name="UsersList" ItemsSource="{x:Bind AvailableUsers}" SelectionMode="Single">
    <Interactivity:Interaction.Behaviors>
        <Interactions:EventTriggerBehavior EventName="SelectionChanged">
            <tk:SendMessageAction Messenger="Weak">
                 <tk:SendMessageAction.Message>
                     <messages:UserChangedMessage User="{Binding SelectedItem, ElementName=UsersList}" />
                 <tk:SendMessageAction.Message>
            </tk:SendMessageAction>
        </Interactions:EventTriggerBehavior>
    </Interactivity:Interaction.Behaviors>
</ItemsControl>

Receiving a message

Unconditional triggering

It would be easy to just invoke an action when a message of a certain type is emitted.

<UserControl x:Name="Container">
    <Interactivity:Interaction.Behaviors>
        <tk:ReceiveMessageTrigger Messenger="Weak" Type="messages:UserChangedMessage">            
            <Interactions:ChangePropertyAction TargetObject="{Binding ElementName=Container}" PropertyName="Visibility" Value="Collapsed"/>
        </tk:ReceiveMessageTrigger>
    </Interactivity:Interaction.Behaviors>
</UserControl>

Conditional triggering

However, unconditional triggering doesn't fit most user scenarios. We need the ability to conditionally execute the behavior based on data in the model, since the action performed often depends on what data is in the message.

There's a few options here. Which approach is best is open to discussion. (I'm personally a fan of the filters approach.)

Using filters declared in XAML

This is similar to DataTriggerBehavior, with a few key differences

  • A filter is always compared against received messages of the same type.
  • Multiple filters can be declared.
    • Multiple filters are treated as implicitly && (and).
    • Can be changed to || (or) by declaring Optional="True".
<UserControl x:Name="Container">
    <Interactivity:Interaction.Behaviors>
        <tk:ReceiveMessageTrigger Messenger="Weak" Type="messages:UserChangedMessage">
            <tk:ReceiveMessageTrigger.Filters>
                <tk:ReceivedMessageFilter Property="IsLoggedIn" ComparisonCondition="Equal" Value="False" Optional="True"  /> 
                <tk:ReceivedMessageFilter Property="CanLogIn" ComparisonCondition="Equal" Value="True"  /> 
            </tk:ReceiveMessageTrigger.Filters>
            <Interactions:ChangePropertyAction TargetObject="{Binding ElementName=Container}" PropertyName="Visibility" Value="Collapsed"/>
        </tk:ReceiveMessageTrigger>
    </Interactivity:Interaction.Behaviors>
</UserControl>

Using a converter

This option has the most flexibility but requires C# code and a questionable usage of converters.

public sealed class CustomUserChangedMessageFilterConverter : IValueConverter
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool Convert(UserChangedMessage value)
    {
        return !value.IsLoggedIn;
    }

    public object Convert(object value, Type targetType, object parameter, string language)
    {
        return value is UserChangedMessage message && Convert(message);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}
<UserControl x:Name="Container">
    <Interactivity:Interaction.Behaviors>
        <tk:ReceiveMessageTrigger Messenger="Weak" Type="messages:UserChangedMessage" MessageFilterConverter="{StaticResource CustomUserChangedMessageFilterConverter}">            
            <Interactions:ChangePropertyAction TargetObject="{Binding ElementName=Container}" PropertyName="Visibility" Value="Collapsed"/>
        </tk:ReceiveMessageTrigger>
    </Interactivity:Interaction.Behaviors>
</UserControl>

Additional info

Addressing one big potential concern ahead of time:

"But it's an anti-pattern!"

From what I've read, it's standard practice to use the Messenger pattern in ViewModels almost exclusively. You'd be hard pressed to find an example of the messenger pattern used outside of a ViewModel.

At some point it became the default way to use the Messenger pattern, and after using it in combination with pure trickle-down MVVM for a few years, I'm really not sure why.

"Who decided that anyway?"

In my ventures developing apps, I've found this statement to be flawed for a few reasons.

About 90% of the time, controls do not / should not exclusively use a ViewModel for internal implementation.
Instead, almost all well designed Controls use DependencyProperties to consume and bind data.

Controls may consume an existing "backend/data" ViewModel via DependencyProperty.
Controls can add more DependencyProperties, event handlers, and more for custom behavior.
If needed, the control can wrap a given "backend/data" VM with a control-specific ViewModel if the data structure doesn't quite meet its needs.

And so on.

What is it, then?

The messenger pattern itself closely resembles a pub-sub pattern, and so all decoupling benefits of the pub-sub pattern come with it, no matter the context, and even outside of ViewModels.

For example

The messenger pattern is fantastic for general app navigation. But if a well-crafted control, which uses DependencyProperties to consume and bind data, doesn't need a ViewModel, then how do you send a navigation message?

You do it in the Control code-behind. Or, with this new Behavior, you could do it directly in XAML.

Help us help you

Yes, I'd like to be assigned to work on this item.

Activity

  1. Sergio0694 commented on Jan 24, 2022

    @Sergio0694
    Member

    This is an interesting proposal. As I mentioned on Discord, I think before discussing this in detail, we should reason about where would this go. Given this requires whatever package contains it to have a reference to the MVVM Toolkit, this automatically means it's a complete non-starter for any of the existing packages in the Windows Community Toolkit. As such, we'd need to create a new package to contain this code, such as CommunityToolkit.Uwp.Mvvm or similar. Few questions:

    • Is there actually interest in creating a new package specifically to support the MVVM Toolkit on UWP?
    • Would there be other APIs that are needed that would go there? By that I mean, creating a whole new package just for this messenger behavior is a tough sell, and I don't really see it happening unless we reach some critical mass in proposed APIs.
  2. michael-hawker commented on Jan 24, 2022

    @michael-hawker
    Member

    Yeah, we wouldn't want the Behaviors package itself to also pull in the MVVM library, I think. I mean it does pull in the Animations package, and soon WinUI itself, but MVVM is generally isolated.

    I really wish we could have dependencies only be pulled in if certain code is used... would make things so much easier.

    I mean in the new CommunityToolkit root space, we do have the ability to ship new packages more easily. So it's not that big a deal to make a new package. Main thing is if we'd want to open the door to have more specific helpers for MVVM stuff. We'd also probably just want to call it CommunityToolkit.WinUI.Mvvm and have it work for both UWP and WinUI 3 vs. having two package roots...

    I do like this idea though. Definitely seems like a good thing to at least experiment with in πŸ§ͺ Labs... πŸ˜‰

  3. Arlodotexe commented on Mar 22, 2022

    @Arlodotexe
    MemberAuthor

    It may be worth considering scrapping the "Filters" idea and creating a separate, generic conditional trigger wrapper.

  4. michael-hawker commented on Mar 22, 2022

    @michael-hawker
    Member

    FYI @Sergio0694 this may also play into what you've been playing with lately too as a separate or coordinated lab idea.

  5. locked and limited conversation to collaborators on Jul 28, 2022
  6. converted this issue into a discussion #239 on Jul 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions