Conversation
Sergio0694
left a comment
There was a problem hiding this comment.
Love where this is going, thank you for putting together this proposal! 🚀
I've left some quick low hanging improvements that could be added 😄
| _navAnimationInProgress = value; | ||
| if (value) | ||
| MessagingCenter.Send(this, CloseContextActionsSignalName); | ||
| WeakReferenceMessenger.Default.Send(new CloseContextActionsMessage()); |
There was a problem hiding this comment.
Nit: this could be simplied through the extension:
| WeakReferenceMessenger.Default.Send(new CloseContextActionsMessage()); | |
| WeakReferenceMessenger.Default.Send<CloseContextActionsMessage>(); |
| WeakReferenceMessenger.Default.Register<Activity, PageBusyMessage>(Activity, (r, m) => OnPageBusy(m.Page, m.IsBusy)); | ||
| WeakReferenceMessenger.Default.Register<Activity, PageAlertMessage>(Activity, (r, m) => OnAlertRequested(m.Page, m.Arguments)); | ||
| WeakReferenceMessenger.Default.Register<Activity, PromptMessage>(Activity, (r, m) => OnPromptRequested(m.Page, m.Arguments)); | ||
| WeakReferenceMessenger.Default.Register<Activity, ActionSheetMessage>(Activity, (r, m) => OnActionSheetRequested(m.Page, m.Arguments)); |
There was a problem hiding this comment.
Nit: if we don't care about the strongly-typed recipient (it's not used in the handler), this can be simplified.
Additionally (though up to style preference), the recipient can be a discard to make it more explicit:
| WeakReferenceMessenger.Default.Register<Activity, PageBusyMessage>(Activity, (r, m) => OnPageBusy(m.Page, m.IsBusy)); | |
| WeakReferenceMessenger.Default.Register<Activity, PageAlertMessage>(Activity, (r, m) => OnAlertRequested(m.Page, m.Arguments)); | |
| WeakReferenceMessenger.Default.Register<Activity, PromptMessage>(Activity, (r, m) => OnPromptRequested(m.Page, m.Arguments)); | |
| WeakReferenceMessenger.Default.Register<Activity, ActionSheetMessage>(Activity, (r, m) => OnActionSheetRequested(m.Page, m.Arguments)); | |
| WeakReferenceMessenger.Default.Register<PageBusyMessage>(Activity, (_, m) => OnPageBusy(m.Page, m.IsBusy)); | |
| WeakReferenceMessenger.Default.Register<PageAlertMessage>(Activity, (_, m) => OnAlertRequested(m.Page, m.Arguments)); | |
| WeakReferenceMessenger.Default.Register<PromptMessage>(Activity, (_, m) => OnPromptRequested(m.Page, m.Arguments)); | |
| WeakReferenceMessenger.Default.Register<ActionSheetMessage>(Activity, (_, m) => OnActionSheetRequested(m.Page, m.Arguments)); |
| realListView.OnItemLongClickListener = this; | ||
|
|
||
| MessagingCenter.Subscribe<ListViewAdapter>(this, Platform.CloseContextActionsSignalName, lva => CloseContextActions()); | ||
| WeakReferenceMessenger.Default.Register<CloseContextActionsMessage>(this, (recipient, msg) => CloseContextActions()); |
There was a problem hiding this comment.
This is introducing a closure, we can skip the allocation by invoking directly on the recipient:
| WeakReferenceMessenger.Default.Register<CloseContextActionsMessage>(this, (recipient, msg) => CloseContextActions()); | |
| WeakReferenceMessenger.Default.Register<ListViewAdapter, CloseContextActionsMessage>(this, (r, _) => r.CloseContextActions()); |
| _container.SizeChanged += OnRendererSizeChanged; | ||
|
|
||
| MessagingCenter.Subscribe(this, Page.BusySetSignalName, (Page sender, bool enabled) => | ||
| WeakReferenceMessenger.Default.Register<UI.Xaml.Window, PageBusyMessage>(page, (window, message) => |
There was a problem hiding this comment.
The recipient is not used, we can drop the first type argument:
| WeakReferenceMessenger.Default.Register<UI.Xaml.Window, PageBusyMessage>(page, (window, message) => | |
| WeakReferenceMessenger.Default.Register<PageBusyMessage>(page, (_, message) => |
| var busyCount = 0; | ||
| MessagingCenter.Subscribe(this, Page.BusySetSignalName, (Page sender, bool enabled) => | ||
|
|
||
| WeakReferenceMessenger.Default.Register<Platform, PageBusyMessage>(this, (platform, message) => |
There was a problem hiding this comment.
This is introducing a closure to capture this in the handler, as it's calling that PageIsChildOfPlatform. Why not invoke it from the recipient directly instead to avoid allocations? Same thing for the other 3 handlers defined below as well.
| } | ||
|
|
||
| MessagingCenter.Subscribe<Map, MapSpan>(this, MoveMessageName, (s, a) => MoveToRegion(a), mapModel); | ||
| WeakReferenceMessenger.Default.Register<MapRenderer, MapSpan>(this, (renderer, args) => MoveToRegion(args)); |
There was a problem hiding this comment.
Same here with the closure:
| WeakReferenceMessenger.Default.Register<MapRenderer, MapSpan>(this, (renderer, args) => MoveToRegion(args)); | |
| WeakReferenceMessenger.Default.Register<MapRenderer, MapSpan>(this, (r, args) => r.MoveToRegion(args)); |
Additionally, all these handles could be static, for clarity and to avoid mistakes.
| _navAnimationInProgress = value; | ||
| if (value) | ||
| MessagingCenter.Send(this, CloseContextActionsSignalName); | ||
| WeakReferenceMessenger.Default.Send(new CloseContextActionsMessage()); |
There was a problem hiding this comment.
Nit: could be simplified:
| WeakReferenceMessenger.Default.Send(new CloseContextActionsMessage()); | |
| WeakReferenceMessenger.Default.Send<CloseContextActionsMessage>(); |
There was a problem hiding this comment.
Cool, I didn't know that it could automatically create the message for me.
Question, though - are there performance implications here? Is this using Activator.CreateInstance to create the message object?
| HandleRadioButtonGroupNameChanged); | ||
| MessagingCenter.Subscribe<RadioButton, RadioButtonValueChanged>(this, RadioButton.ValueChangedMessage, | ||
| HandleRadioButtonValueChanged); | ||
| WeakReferenceMessenger.Default.Register<RadioButtonGroupController, RadioButtonGroupSelectionChanged>(this, HandleRadioButtonGroupSelectionChanged); |
There was a problem hiding this comment.
These registrations are introducing closures, and also the controller parameter in the handlers doesn't actually seem to be used at all? Those could drop that parameter, and then the handlers here could be (r, m) => r.Handle___(m), which would just cache a single delegate and avoid allocations.
There was a problem hiding this comment.
The controller parameter isn't being used yet; I was hoping to convert the HandleX methods to static later (rather than having them be instance methods on RBGController).
I guess that's a question, though - which is better for performance? Instance methods (and don't pass in the controller parameter), or static methods? My assumption was that the static option was better.
There was a problem hiding this comment.
The most efficient solution is one that allows the registration to be allocation free, which needs to (1) not be creating a closure, so not capturing anything, and (2) allow Roslyn to cache and reuse the delegate instance. The way the compiler works right now, the method group syntax will always result in a new delegate instance being allocated. So the optimal way to register handlers (without using IRecipient<TMessage>), would be to use an instance method being invoked directly on the input recipient. This will alow the delegate to be stateless and reusable, and there will also be no allocations due to closures 😊
There was a problem hiding this comment.
Cool, I'll fix that (and change the RadioButton suff back to instance methods, too).
optimal way to register handlers (without using IRecipient)
I'd much rather use IRecipient for some of these (one RegisterAll call vs 4 for things like the AlertManager) - what are the performance implications if I go that direction?
There was a problem hiding this comment.
So, there are a few considerations with using IRecipient<TMessage>, I'll try to go through all of them:
- Once a handler is registered, they're mostly equivalent. Invoking a handler that was registered through
IRecipient<TMessage>is actually ever so slightly faster, as there's a hot path for it in the broadcast code that allows the messenger to skip the callvirt to invoke the delegate. I wouldn't say it's a major difference though. - If you register handlers individually (that is, using
IMessenger.Register<TMessage>(IRecipient<TMessage>)), the registration is free and super fast, basically the same as what you're doing now manually. - If you want to use the
RegisterAllextension, that comes with some caveats. It either needs the source-generated registration methods (generated by the generator that ships with the MVVM Toolkit) to be available (meaning you either have to disable trimming, or manually add annotations to keep that generated method), or it will fallback to a path using a compiled LINQ Expressions, which will have a fair bit of overhead for the first invocation (the generated code will then be cached and reused for further invocations). - Another aspect with
IRecipient<TMessage>is that it adds a public interface to the recipient type. If that type is public, that's an obvious implementation detail leak.
Due to the last 3 points I would generally recommend sticking with manual registration in this scenario. Hope this helps, let me know if you have any other questions on this! 😄
There was a problem hiding this comment.
This is great, thanks for the thorough answer!
For our purposes, I think manual registration is the right answer.
| WeakReferenceMessenger.Default.Register<RadioButtonGroupController, RadioButtonGroupNameChanged>(this, HandleRadioButtonGroupNameChanged); | ||
| WeakReferenceMessenger.Default.Register<RadioButtonGroupController, RadioButtonValueChanged>(this, HandleRadioButtonValueChanged); | ||
| WeakReferenceMessenger.Default.Register<RadioButtonGroupController, RadioButtonGroupSelectionChanged>(this, (r,m) => r.HandleRadioButtonGroupSelectionChanged(m)); | ||
| WeakReferenceMessenger.Default.Register<RadioButtonGroupController, RadioButtonGroupNameChanged>(this, (r,m) => HandleRadioButtonGroupNameChanged(m)); |
There was a problem hiding this comment.
Missed r. here? 🤔
I also would recommend making the lambdas static to make the code more explicit and less error prone 🙂
| WeakReferenceMessenger.Default.Register<RadioButtonGroupController, RadioButtonGroupNameChanged>(this, (r,m) => HandleRadioButtonGroupNameChanged(m)); | |
| WeakReferenceMessenger.Default.Register<RadioButtonGroupController, RadioButtonGroupNameChanged>(this, static (r,m) => r.HandleRadioButtonGroupNameChanged(m)); |
There was a problem hiding this comment.
I also would recommend making the lambdas static
Ah, added in C# 9. TIL.
|
Maybe this is a philosophical issue, I'm not sure I understand having a dependency on Windows Community Toolkit makes sense inside of MAUI itself. Is the goal here to integrate the Messenger codebase into MAUI and replace the existing stack wholesale? |
Well, that's why I opened this up - so we could have the discussion. This is in the same vein as moving MAUI to use Microsoft.Extensions.DependencyInjection rather than relying on the minimal DependencyService we had in Forms. While it's nice that Forms shipped with MessagingCenter built-in, MC has problems. The API is awkward, the performance is sub-par, and it's likely got some memory leak issues. Also, the maintenance burden falls on the MAUI team, which is kind of a comparative advantage fail. Most folks would not recommend anyone use MC outside of a Forms/MAUI.Controls project, and even within a project using those SDKs, the recommendation would be to use something else if you're doing a lot of messaging. So I think it's worth considering the option of obsoleting MessagingCenter and using something else with a better API and performance characteristics within MAUI.Controls. And freeing up maintenance energy for things which are more core to MAUI. I don't have a particular problem with the dependency. (I don't love the MVVM part of the namespace, but whatever.) But if the dependency is a worry, maybe there's a case to be made for moving Messenger somewhere else? Also, I should point out that this dependency is entirely within MAUI.Controls - this is not a dependency in MAUI.Core. |
|
I think the difference between This toolkit has other features beyond just the messaging stuff, and that's also being brought into anyone who consumes MAUI Controls, whether they want it or not. It would be like automatically including parts of the MAUI Community Toolkit into MAUI itself because it has helper functions that would make MAUI development easier. IMO that's not ideal. But moving the stuff that would help into a shared place is the best of both worlds. I do think changing it out is a good idea, and standardizing on one approach is 💯. Maybe if this specific part of the toolkit was refactored into its own library, then both could use it separately? |
|
Another alternative is to promote CommunityToolkit.MVVM’s Messenger into .NET MAUI. This is one of the primary goals of the Community Toolkits, to promote their features into the .NET Libraries. Similarly, we recently promoted CommunityToolkit.Maui’s WeakEventManager into .NET MAUI. |
@drasticactions forgive me for this small nitpick, but the MVVM Toolkit is from the .NET Community Toolkit, not the Windows Community Toolkit. We split all the .NET libraries that were originally in the Windows Community Toolkit off into this new toolkit to make it clearer that it's completely decoupled from anything related to Windows 😊
@brminnick The issue I have with this though is that this would mean that it would then be locked to MAUI, whereas one of the core principles of the MVVM Toolkit (or really, the .NET Community Toolkit as a whole) is that it's not dependent on any specific runtime or framework. There are many users of the MVVM Toolkit that eg. use WPF, UWP, Uno, even Unity, or more. We can't really remove the feature from there and only make it available through MAUI. This means that if we did want to promote this to be inbox into MAUI, we'd have to explore into eg. sharing the code there, under a different namespace. Still perfectl doable, just pointing out that simply moving the APIs to MAUI and removing it from the MVVM Toolkit isn't ideal 😅 |
Totally. I'd recommend adding it to .NET MAUI and keeping it in |
It's not the name of it that matters here, it's that it's bringing in a community toolkit (that has a bunch of features) into the core Controls platform, to specifically handle one feature. IMO if we feel this messenger code is good enough for everyone's apps out of the box, we should either bake it directly into MAUI by promoting the code to live inside this repo, or move this messenger code into its own separate library and treat it like the DependencyInjection libraries. |
|
A bit of an aside... If dotnet community toolkit became a dependency, would it be worth seeing what else from it could be used? For example replacing |
What are the obstacles to doing something like this? Any dependencies or other issues that would prevent Messenger from becoming a standalone library usable by the Toolkit, WPF, UWP, MAUI, etc.? |
No, the MVVM Toolkit (or really the .NET Community Toolkit as a whole) has no dependencies on any UI frameworks, it only multi-targets .NET Standard 2.0, 2.1, and .NET 6. It was one of the core reasons why we split stuff into the new Toolkit 🙂 |
|
After a stack of discussion here and offline I think we reached a place where we can't depend on the community toolkit since part of them depend on us :) I did have a spike a few different ways over here: #12910 I think with the way things are now, the page knows which window it is in and all windows have the alert manager. So in the end, the page can just do That PR is a bit stalled as I need to focus on the upcoming net8 preview, but once I figure out the test failures it is a pretty minimal set of chnages. |
Hey Matt! Can you elaborate on this? The .NET MAUI Community Toolkit does have a dependency on .NET MAUI (obviously). But the Messaging Center APIs live in the MVVM Community Toolkit which does not have a dependency on .NET MAUI. And the .NET MAUI Community Toolkit does not have a dependency on the MVVM Community Toolkit. tl;dr - I'm confused where a dependency conflict exists |
|
Ah this is not directly a reference from mvvm to maui, but rather the concept of the community toolkits depending the core frameworks. Should have been more clear on this. More talking conceptually. But either way, using a messaging system to talk from child to parent was a tiny bit overkill. |
This is related to #3797 (and also to #95).
This is a draft of what it would look like to utilize the Community Toolkit Messenger for the messaging internals in Controls.
This is mostly just a quick-and-dirty direct replacement. There are some places where the features of Messenger could be better utilized for more performance (e.g., in several places we could update the message handlers to use static methods).
This is intended as a discussion starting point / proof-of-concept.