Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Basic MVVM primitives (.NET Standard) #3230

Closed
Sergio0694 opened this issue Apr 9, 2020 · 48 comments · Fixed by #3229
Closed

[Feature] Basic MVVM primitives (.NET Standard) #3230

Sergio0694 opened this issue Apr 9, 2020 · 48 comments · Fixed by #3229
Labels
Completed 🔥 feature 💡 feature request 📬 A request for new changes to improve functionality .NET Components which are .NET based (non UWP specific) open discussion ☎️
Milestone

Comments

@Sergio0694
Copy link
Member

Sergio0694 commented Apr 9, 2020

Describe the problem this feature would solve

Follow up from the conversation with @michael-hawker on the UWP Community Discord server.

With the MvvmLight library not being supported anymore, and other existing libraries being much bigger in scope that what is often needed for simple apps, I was wondering whether we should add some basics MVVM primitives to the toolkit, possibly in a new Microsoft.Toolkit.Mvvm package so that users not in need of those wouldn't have to reference those too. We're also using this occasion to integrate with the Microsoft.Extensions.DependencyInjection package to provide dependency injection features, so that it'll be both easier for devs to interoperate between this package and existing code, as well as allowing us to offload that part of the code to an official library from Microsoft.

All this would come together in a new Microsoft.Toolkit.Mvvm package.

Preview package

The CI is now running for #3229 , so you should be able to just get the latest package from the CI build artifacts.

Key Tenants

  • Platform and Runtime Independent - .NET Standard 2.0 🚀 (UI Framework Agnostic)
  • Simple to pick-up and use - No strict requirements on Application structure or coding-paradigms (outside of 'MVVM'ness) i.e. flexible usage
  • À la carte - Developer able to choose the components they wish to leverage
  • Reference Implementation - Lean and performant, provides compliments to interfaces and paradigms hinted at in the Base-Class Library, but without provided implementations.

Draft docs here 📖

Feature set 🧰

The idea for this package would be to have a series of common, self-contained, lightweight types:

  • Microsoft.Toolkit.Mvvm.ComponentModel
    • ObservableObject
    • ViewModelBase
  • Microsoft.Toolkit.Mvvm.DependencyInjection
    • Ioc
  • Microsoft.Toolkit.Mvvm.Input
    • RelayCommand
    • RelayCommand<T>
    • AsyncRelayCommand
    • AsyncRelayCommand<T>
    • IRelayCommand
    • IRelayCommand<in T>
    • IAsyncRelayCommand
    • IAsyncRelayCommand<in T>
  • Microsoft.Toolkit.Mvvm.Messaging
    • Messenger
    • IMessenger
  • Microsoft.Toolkit.Mvvm.Messaging.Messages
    • PropertyChangedMessage<T>
    • RequestMessage<T>
    • AsyncRequestMessage<T>
    • CollectionRequestMessage<T>
    • AsyncCollectionRequestMessage<T>
    • ValueChangedMessage<T>

These types alone are usually enough for many users building apps using the MVVM toolkit.
Having these built right into the toolkit would allow them to just reference the new package and bet all set, without having to go dig through other documentation and referencing a series of external packages just to gain access to these basic types. Furthermore, we could keep these smaller in scopes than other big libraries like autofac or Prism, which include a lot of features that are not necessarily useful for many (particularly non-enterprise) devs, while still taking a toll on performance and memory usage. This would also make the learning curve much less steep for developers.

As an example of the benefit of a more modern codebase, which is both more optimized and minimalistic, here's a benchmark showing the performance of the Messenger class from both MvvmLight and Calcium, and the one from the related PR (WCT stands for this new Mvvm package). Note that Calcium only supports a single channel mode:

image

Some summarized results for the Messenger class from #3229:

  • Single channel: ~40x faster than MvvmLight, and ~11% faster than Calcium. At the same time, it uses about 38 million times less memory than MvvmLight, specifically just 20 bytes vs. almost 700MB (!), and about 387,000x less memory than Calcium as well, specifically again just 20 bytes vs. about 7 MB.
  • Multiple channels: about 11x times faster than MvvmLight, and using almost 7 million times less memory, specifically just 416 bytes vs almost 3 GB (!).

The full benchmark code can be found here.

Describe the solution

As mentioned above, and as can be seen in the related PR #3229, this proposal is to add a new Microsoft.Toolkit.Mvvm package, integrating some features from MvvmLight, refactoring them when needed, in order to offer a self-contained alternative. The package would not need a reference to any other Microsoft.Toolkit packages, it would be lightweight and focused on ease of use, performance and memory efficiency.

Since it targets .NET Standard 2.0, this means that it can be used anywhere from UWP apps, to Uno, Xamarin, Unity, ASP.NET, etc. Literally any framework supporting the .NET Standard 2.0 feature set. There are no platform specific dependencies at all. The whole package is entirely platform, runtime, and UI stack agnostic.

Additionally, it would integrate with the existing Microsoft.Extensions.DependencyInjection package, to offer a well known and tested DI platform, with the addition of an easy to use entry point in the toolkit, especially for new users. This would also remove a lot of code from the actual Mvvm package, as there would not be the need to write and maintain yet another DI library or set of APIs.

As mentioned above, the package would target .NET Standard 2.0 to have as much reach as possible, and it wouldn't have any platform-specific dependencies.

Having a set of MVVM-enabled features built-in into the toolkit would align with the general philosophy of the toolkit to help developers getting started in common scenarios (such as with the MVVM pattern), and it would complement all the available APIs from the BCL that just lack a "reference" implementation (such as INotifyPropertyChanged or ICommand).

Furthermore, having a package like this being published by a Microsoft supported project directly would be a big selling points for users not wanting to add "3rd party" references to their projects (this point has been brought up before in the comments below as well by other users).

Describe alternatives you've considered

As mentioned above, common alternatives like autofac, Fody or Prism are much bigger in scope than what many devs typically need and introduce more overhead. MvvmLight on the other hand is also not supported anymore, other than some forks.

@Sergio0694 Sergio0694 added the feature request 📬 A request for new changes to improve functionality label Apr 9, 2020
@ghost
Copy link

ghost commented Apr 9, 2020

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 ghost added the In-PR 🚀 label Apr 9, 2020
@michael-hawker
Copy link
Member

To add to this, I had ported @StephenCleary's MVVM Async helpers from his articles to UWP here. Though I didn't realize he had them on GitHub already here (though not sure if UWP enabled).

Also, Windows Template Studio has an MVVM Basic pattern with some basic helpers, this could be an opportunity to align those in the toolkit instead of part of their template engine. FYI @sibille.

@Sergio0694 I think the best place to start before we dive deeper in code, is to talk more to @lbugnion and @sibille and figure out the best path forward (even if community driven) for longer-term support of a light-weight MVVM framework.

@Sergio0694
Copy link
Member Author

That sounds like a great idea! Looking forward to hearing what they think too! 😄

As for the PR, I've mostly just opened it as a draft to have a reference, and because I wanted to experiment with that Ioc idea I had (which I'll definitely use at least in my apps either way given those benchmarks 🚀). Wasn't planning to work more on it for the time being, as I completely agree with you on gathering more feedback first, and besides I think the fork that @jamesmcroft made of the MvvmLight already contains most of the basic building blocks we'd need, at least for a prototype.

Looking forward to seeing where this goes, glad I could at least spark a discussion on the topic! 😊

@azchohfi
Copy link
Contributor

azchohfi commented Apr 9, 2020

@Sergio0694 how does your implementation compares to the ASP.Net default one? https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/5.0.0-preview.2.20160.3
I'm not even sure it will work on UWP, but it should, as there is a .Net Standard 2.0 version of it.
This might help as well: https://github.com/danielpalme/IocPerformance

@azchohfi
Copy link
Contributor

azchohfi commented Apr 9, 2020

And are these numbers for .Net Native?

@Sergio0694
Copy link
Member Author

Sergio0694 commented Apr 9, 2020

@azchohfi Those are numbers for .NET Core 3.1, since BenchmarkDotNet doesn't support UWP. I did setup a benchmark class to test things on UWP too a while back though, it's not perfectly accurate as BenchmarkDotNet (and it lacks the memory diagnoser) but it's reliable enough, I'll give it a go to see how it runs there! As for the ASP.NET one, not sure if it works on UWP either, but I can definitely at least add that to the .NET Core benchmarks and see how it goes, thank you for the suggestion! 😄

Will update the post as soon as I have more results.

EDIT: updated the post, the ASP.NET solution is faster than MvvmLight but it's still about 18x slower than the Ioc class in this PR. Of course, the ASP.NET version much like the other 2 libraries has more features, though I'll say that the Ioc class in this PR has the advantage of being much easier to setup, of offering static APIs so you don't necessarily need to pass instances of your container around, plus it's all self contained in a single file and with no dependencies. Different use cases, sure 😊
I'll go ahead and test on UWP too!

EDIT 2: done, it's all in the first post! 👍

@Sergio0694
Copy link
Member Author

@azchohfi Not sure whether GitHub sends notifications when mentioning through an edit to an existing comment, posting this just in case and also to add some more details 😄
Since you seemed interested in performance metrics, I run some more benchmarks across all the various implementations, both when retrieving singleton services as well as transient ones.
Here are the updated results (.NET Core 3.1):

image

The situation with singleton services is like before, whereas when retrieving transient ones, this Ioc implementation is about 60x faster than MvvmLight (thanks to compiled expressions) and using less than 1/11th the memory, and it remains over twice as fast as the ASP.NET solution. 🚀

@shaggygi
Copy link

I recall seeing a Visual Studio survey for MVVM feedback on Twitter a few months back. It appeared there was possible work being done to add MVVM basics to the framework to help in this area. Just curious, is this the same work or is it possible there is duplicate work being investigated?

@Sergio0694
Copy link
Member Author

Hey @shaggygi - this is not the same work, it's more of an exploratory issue to put some ideas together and see how best to proceed. We're talking with @jamesmcroft who is maintaining a .NET Standard fork of MvvmLight, and @michael-hawker has contacted both Laurent, the original author of the MvvmLight library, and the folks working on the Windows Template Studio, to see if it could make sense to integrate some of the existing work into the toolkit, or to just link an external repository and update that one, etc.

The draft PR that is linked instead was just me experimenting with some ideas in the meantime 😄

@sibille
Copy link

sibille commented Apr 13, 2020

@Sergio0694 and @michael-hawker I think adding basic MVVM classes to the Toolkit makes a lot of sense.

Among the goals for the MVVMBasic implementation in Windows Template Studio was to provide a solution for people who:

  • can't or don't want to use a 3rd party MVVM Framework
  • want to build their own MVVMFramework

https://github.com/microsoft/WindowsTemplateStudio/blob/dev/docs/UWP/frameworks/mvvmbasic.md

Not sure if this is important to a lot of people, if it is, in WTS we could still offer people both options MVVMBasic (using the WCT) or MVVMBasic (generating the classes in the code).

@Sergio0694
Copy link
Member Author

Hi @sibille - thank you for chiming in!
I'm glad to hear this idea makes sense to you, that's great! 😊

And yes, if the WTS then offered both the option to either reference the MVVM classes from the WCT directly, or having them autogenerated into a project, that'd be ideal, good idea! 😄
Maximum flexibility for users, and offering those who don't mind using an external library the ability to use a compact set of classes that are more up to date, and right from the WCT ecosystem.

@Sergio0694
Copy link
Member Author

Sergio0694 commented Apr 15, 2020

I've added the new Messenger implementation (code here), which completely avoids the use of reflection and is able to have no memory allocations at all when sending messages (and it's also faster than the messenger from MvvmLight). Here's a benchmark of the two Messenger class (gist here), when sending a number of messages between multiple receivers, either through the main channel (no token), or through various separate channels (using a token to distinguish them):

image

You can see that not only is the new Messenger class much faster than the one from MvvmLight (especially when using the default channel), but it also literally does no memory allocations at all, whereas the MvvmLight type ends up allocating almost 500MB (!) in this benchmark 🚀

P.S. the PR should be more or less be feature complete at this point, since the idea was just to have the basic types available. Will go ahead and add more unit tests, as they are just a few for now.

@skendrot
Copy link
Contributor

skendrot commented Apr 16, 2020

Adding "basic" mvvm tooling has been discussed numerous times, both publicly and privately.
Here are a few I found easily:

There are a lot of frameworks already out there.
Just to name a few:

  • MVVM Light
  • Prism
  • Caliburn Micro
  • MVVM Cross

MVVM Light is just one implementation. If it seems that MVVM Light needs a maintainer or work to make it better or "more light" wouldn't it be best to put work into that project? It's already established, it's well known, and it's it's heavily used (as a MVVM framework. Not saying this toolkit isn't used).

MVVM Frameworks always start out as something small. Then more and more features are added making them larger and larger. Creating a framework within the toolkit would just explode to being more and more.

My suggestion is that we should maintain the stance that there are already frameworks out there and that's not what this toolkit is meant for.

@hansmbakker
Copy link
Contributor

hansmbakker commented Apr 16, 2020

About the IoC part: I would really like it if there would be a bit more standardization on dependency injection for UWP, like Asp.Net Core has. Benefits are a clearer approach for everybody (helps to find unified documentation) and aligned efforts rather than scattered efforts. In that sense, personally, I would rather see standardization on Microsoft.Extensions.DependencyInjection. I find the performance you achieved impressive though!

On the MVVM part: I would really like to see a MVVM approach for UWP supported by Microsoft and/or the WCT. The issue with MVVM for UWP currently is that as a developer I do not feel confident at all taking a dependency on any of the established 3rd party frameworks. That is because e.g. they seem to have dropped support for UWP or seem to have stopped being maintained. That is why I am really looking for a clear, reliable, supported way of doing things for UWP. If moving MVVM into the WCT is the way to do that, please go forward.

I know about this XKCD comic about standards. However, I think we got into this situation because, for UWP projects, there was never a clear drive from Microsoft on these two points (MVVM & IoC/DI). Supporting MVVM frameworks for a longer time takes a large effort which I understand, but developers taking a dependency on a MVVM framework do that because they develop a more complex app that needs usually needs support for a longer time. Then it feels scary to take a dependency on a 3rd party MVVM framework that might lose support.

@Sergio0694
Copy link
Member Author

Sergio0694 commented Apr 16, 2020

Hey @hansmbakker - glad you liked the performance improvements in the Ioc class! I'll try to go through all the various points that you and @skendrot raised one at a time.

Regarding the various closed PRs you linked, I feel like some context is important, specifically, the timing. Most of those issues are pretty old (some from 2017), and a lot of things have changed. If you mention the overlap in different frameworks, I'd argue that the Windows Community Toolkit and MvvmLight actually already overlap in quite a few places - for instance, MvvvmLight has a whole helper class that's UWP-specific that handles dispatching to the UI thread, which is exactly the same functionality that the toolkit offers through the DispatcherHelper class.

Having a built-in set of MVVMs types boils down to a few key points:

  • Some of the frameworks you mentioned (eg. MvvmLight) are either not maintained anymore, or are much bigger in scope (eg. supporting platform specific APIs too), or with an outdated codebase, or all of those things combined. Having a single entry point for the basic MVVM types in the toolkit would be a guarantee for new devs for where to go look for well tested and up to date, minimal MVVM APIs to use without 3rd party references.
  • Some of the frameworks (eg. Microsoft.Extensions.DependencyInjection) are either narrow scoped (eg. just dependency injection) or too extensive and less intuitive to use, or both. Having a minimalistic set of MVVM APIs in the toolkit would be an easy way for devs to get started, or an all-in-one solution for many devs just needing the basic types to get going. Eg. personally the classes in this PR would be all I need in my applications, and the same goes for many other devs.
  • As @hansmbakker mentioned, having the APIs being from an official package instead of having to research and add 3rd party packages is a big plus for many devs. They could just add all the Microsoft.Toolkit.* packages they need and they'd be good to go.
  • We can sync up with the devs working on the Windows Template Studio (as @sibille mentioned) so that they could integrate this new package directly into their project as well, also losing the 3rd party reference to MvvmLight in the process, which is also nice.

Regarding what @skendrot said here:

MVVM Frameworks always start out as something small. Then more and more features are added making them larger and larger. Creating a framework within the toolkit would just explode to being more and more.

This is the textbook "slippery slope argument", and I don't think we should base our decision on this. We can define the scope of the package and leave it at that, but not doing anything at all just because it could potentially grow too big in the future is not what I'd suggest. I think all the numerous advantages of this package should also be weighted in, along with the fact that as I mentioned, we could just clearly state the main points of the package and reject possible future proposal that would not go in the same direction. That wouldn't be any different from the toolkit as a whole.

All in all, the main idea here would be to have a minimalistic, easy to use, well maintained, modern, high-performance set of MVVM primitives from a 1st party package, an all-in-one toolkit for MVVM scenarios that would suit a large number of developers needing these basic building blocks.

Many of these are things that literally every developer basically needs (eg. ObservableObject, RelayCommand, etc.), and I think the package as a whole, for how it's structured right now, would be a good fit for what the toolkit represents. Of course I might be biased since I did write the thing, but I hope I raised some valid points here as well 😄

@hansmbakker
Copy link
Contributor

hansmbakker commented Apr 16, 2020

I agree with a lot of things you say. And 👍 for creating this PR! There are a lot of useful classes in it and, if they are maintained in a nuget package, that allows for much easier updating than one-time code generation.

One thing I am wondering about: are there many people who do MVVM without dependency injection? If not, would there be a case for standardization towards DI? Or would too many people complain? What would that change to the ViewModelBase class?

offering static APIs so you don't necessarily need to pass instances of your container around

In what cases would it be needed to pass that container around? I thought that, since both the ViewModels and their dependencies are registered and have their dependencies injected, there is no need to pass your container around?

@Sergio0694
Copy link
Member Author

Sergio0694 commented Apr 16, 2020

@hansmbakker Glad to hear that! 😄

As for that quote, it actually refers to an old version of the PR - both the Ioc and the Messenger class fully support DI now. In fact, I've also added some constructors to ViewModelBase to make it easier for people that use DI to pass those instances. At the same time, both classes also expose a static, thread-safe Default instance, so you're free to use whatever approach you prefer, either DI or static 👍

As for why people would need multiple instances, an example is when running multiple app instances in the same process (eg. with multiple app windows), people would want to have an instance per window. Or, having instances to inject could be useful when writing unit tests.

As for your other question, for instance I use the static approach in some of my apps, as it simplifies the codebase when you don't need the additional flexibility of using DI.

In general, multiple people in the Discord server said they'd want to use the DI approach, so since the required code changes were minimal I added both options 🚀

@Sergio0694
Copy link
Member Author

Sergio0694 commented Apr 19, 2020

Relaying some updates here from the UWP Discord server to keep everyone up to date.

Following an extensive conversation with @Aminator and some prototyping, I went ahead and did a number of changes in #3229, specifically related to the dependency injection area:

  • Completely nuked the old Ioc class/interface.
  • The package now has a dependency on Microsoft.Extensions.DependencyInjection so we can use those types directly (eg. IServiceProvider), which make the package better for interoperability, saves us work/test/maintenance and can also act as an easy to use "entry point" for users to become familiar with the APIs from that package. @azchohfi I think you'll be happy about this since you were the one that suggested that package from the start 😊
  • With the better integration now, the ViewModelBase class now takes the general IServiceProvider interface in its secondary constructors, so users also have the ability to do DI with their own service providers coming from somewhere else.
  • On the topic of better integration, with this change we'd now have both the toolkit and the Windows Template Studio all using the same exact DI APIs, coming from an official 1st party Microsoft package (Microsoft.Extensions.DependencyInjection).
  • The Ioc class now acts as a super easy to use service configurator for those APIs.
  • Tested this already on UWP Debug/Release as well, works just fine.

Sample usage of how to configure services with the new Ioc class:

Ioc.Default.ConfigureServices(services =>
{
    services.AddSingleton<ILogger, Logger>();
    services.AddSingleton<IPrinter, PrinterService>();
    // ...
});

The Ioc class then exposes an IServiceProvider ServiceProvider property which is automatically used by ViewModel base if no other provider is specified, which means that users can just use that code to initialize services, and then every single ViewModel in their app will automatically have the resulting IServiceProvider instance being injected. They can then just use all the extensions available from the Microsoft.Extensions.DependencyInjection package to get service instances from that provider.

This solution has the advantage of being both easy to use, highly customizable, and using the same APIs from Microsoft.Extensions.DependencyInjection while still having the same behavior from the original Ioc class from MvvmLight. Plus we don't have any code to maintain at all in that area.


Other general changes from the PR (and a few more points for @skendrot I forgot to mention the other day). Regarding having basic MVVM building blocks in the toolkit, I realized the toolkit actually already does have a few of them. In particular Singleton<T> and NotifyTaskCompletion<TResult>:

  • The Singleton<T> is scheduled for removal in 7.0 already (see The Singleton<T> class should be removed #3134), but that's just for performance reasons and not lack of usage. In fact, that class was basically a minimalistic inversion of control container. With the new Mvvm package, users relying on that class will be able to easily switch to the new Ioc class without losing any functionality (and gaining the additional features).
  • The NotifyTaskCompletion<TResult> class is being replaced by the SetAndNotifyOnCompletion method from ObservableObject, which exposes the same functionalities (and more) without requiring dedicated types at all, which also makes it more efficient and makes the code easier to read. Here is how the new method can be used in a small sample:
private Task<string> myTask = Task.FromResult<string>(null);

public Task<string> MyTask
{
    get => myTask;
    private set => SetAndNotifyOnCompletion(ref myTask, () => myTask, value);
}
<TextBlock>
    <Run Text="Task status:"/>
    <Run Text="{x:Bind ViewModel.MyTask.Status, Mode=OneWay}"/>
    <LineBreak/>
    <Run Text="Result:"/>
    <Run
        xmlns:ex="using:Microsoft.Toolkit.Extensions"
        Text="{x:Bind ex:TaskExtensions.ResultOrDefault(ViewModel.MyTask), Mode=OneWay}"/>
</TextBlock>

Which results in this with a function that waits two seconds and then returns "Hello world!":

NVXa3N8Wom

Same feature as before (but in a dedicated, specialized package now), and without the additional "proxy" object, so that the property you have in your models is actually of type Task<T> 🎉

@hansmbakker
Copy link
Contributor

Really happy to see the direction you took!

@hansmbakker
Copy link
Contributor

By the way, don’t the view models and views need to be registered with DI, too?

(I thought that is a common practice, but I didn’t pick that up from your description.)

@Aminator
Copy link

@hansmbakker Sergio wants to explicitly not use constructor injection and instead uses the anti-pattern where you access the IServiceProvider directly in your view model. The ViewModelBase accesses a static instance of the IServiceProvider by default, that's why it doesn't need to be registered as well.

This is the opposite to what I do in my apps. I always use constructor injection in my view models and services, which is also why I don't need the Ioc or ViewModelBase classes at all. Singleton and sometimes transient view models are also part of the service provider if you use this pattern. This has the benefit that the view models and services don't know where their dependencies come from and can also be instantiated normally if needed. This is also the recommended pattern in ASP.NET.

Lastly, views are never part of the service provider because those should generally have an empty constructor to be able to instantiate them in XAML, which would interfere with constructor injection. The services a view needs should come from the associated view model instead, which is typically the DataContext property and exposed as a strongly typed ViewModel property to the view for proper support with x:Bind.

@hansmbakker
Copy link
Contributor

explicitly not use constructor injection and instead uses the anti-pattern where you access the IServiceProvider directly in your view model

@Aminator thank you for explaining that, I had not seen that intent / consequence!

@Sergio0694 I agree with @Aminator here, constructor injection with registered view models has the advantages that @Aminator describes, and furthermore it is a common practice (so it makes people "feel at home" quickly), why deviate from that? Is there a reason why you are doing it this way?

If this is going to be the approach that the toolkit will provide, and WTS might use this for the basic MVVM code in the future, please ensure that it promotes best practices.


views are never part of the service provider

@Aminator probably it is less common, I was not used to it too, but I saw it being done in this ReactiveUI/Uno sample (code) last week.

@Sergio0694
Copy link
Member Author

We had an extensive conversation about this too with @Aminator while drafting the new APIs structure, and I'm actually a bit surprised to see my approach be referred to as an "anti-pattern" now, because it really itsn't. Or at least, it might technically be if you're trying to exactly replicate the architecture of an ASP.NET app, but that's not the point of this package. This is not Microsoft.Toolkit.AspDotNet, it's .Mvvm. The MVVM pattern per se doesn't have any single "official" way of handling services, mostly because it doesn't really concern itself with how they are handled. Many people mix MVVM with other patterns like DI, but that is indeed a mix of two separate patterns. MVVM on its own is only about logically separating the view with the business logic of the application and the models, and in particular is meant to facilitate the use of features like data binding. That's really all that MVVM is in a nutshell.

That said, let me clarify a couple points on this and also answer @hansmbakker's question:

  • This has already been answered by @Aminator, views don't need to be registered. Furthermore, you really don't have to register viewmodels either, regardless of what is commonly done in ASP.NET. That's not a requirement for MVVM, though sure you can do that too.
  • All that the current ViewModelBase really does is to basically replicate the functionality that the same class from MvvmLight provided. This is particularly useful for developers that go for a more "traditional" MVVM approach, without also doing DI (like me). Doing this has the advantage of making the code much simpler and easier to maintain, if using this approach is possible in your specific situation. What the ViewModelBase class does in this case is simply this: "are you injecting a service provider? If so, I'll use that. If not, let me ibject the default one for you". You can see how in all cases where devs are in fact only using a single IServiceContainer (or IMessenger) instance for the whole app this incredibly simplifies things, as it lets you completely get rid of manual DI altogether. To make a practical example, here is how I usually declare a view:
<UserControl
    x:Class="MySampleApp.Views.ConsoleView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <UserControl.DataContext>
        <views:ConsoleViewModel x:Name="ViewModel"/>
    </UserControl.DataContext>

    <!--Other stuff in the view here-->
</UserControl>

You can see that I'm spawning the viewmodel right from XAML - I can do this since it doesn't need any kind of DI in its constructors. This also lets me have the ViewModel field directly from there to use x:Bind, without the need of manually adding the strongly typed ViewModel property in code behind at all. In fact, with this approach I often don't need to write any code behind at all. Now, that viewmodel will also automatically receive the default IMessenger and IServiceProvider instance from the ViewModelBase, which means that:

  • Code in the viewmodel can simply use the Messenger and ServiceProvider instances to send messages and resolve services, with no need to use the static instance directly. This also helps the modularization: if one day I wanted to make changes I could just inject a different instance manually without having to change the code at all.
  • The initialization that devs do at startup with Ioc.Default.ConfigureServices(...) is automatically available to all viewmodels in their app by default, without the need to write any other code.

You could say that this is not 100% "proper ASP.NET-like DI patter", and you might be correct, but it's 100% valid MVVM pattern, which is the main point of this package. Furthermore, I should add:

  • This is made possible by a single additional constructor in ViewModelBase, and if devs want to manually inject services they're free to do that too. Both approaches are entirely possible using this package and both would work fine, one is just easier to get started with.
  • As I mentioned, this is exactly the same behavior that MvvmLight was using, and that a lot of devs (especially if migrating from there) will be used to. All I did here was to also enhance that same architecture with better interoperability with the Microsoft.Extensions.DependencyInjection package, so that hopefully everyone is happy 😊

@StephenCleary
Copy link

Regarding my MVVM helpers:

  1. I would recommend taking the NotifyTask from my GitHub; its API has gotten a bit better over the years.
  2. I would not recommend using AsyncCommand. Possibly IAsyncCommand and an AsyncCommandBase, but that would be about it. There's just too much variation in what people expect for a single AsyncCommand to be a useful type. I think it makes more sense for the "async command" concept to be composed of several smaller pieces, and this is the approach I've started down in my GitHub repo, but it's still quite a way from being finished (hence the prerelease on the NuGet package), and I'm not prioritizing it at this time.

@Sergio0694
Copy link
Member Author

Hey @StephenCleary - thank you for chiming in!

  1. I'm not really a fan of the NotifyTask class due to the way it basically duplicates and proxies every single property, whereas we can just bind directly from XAML to all of these, without even needing the second object at all. Have you seen my code example in the second part of this message? That's the solution I came up with so far in the PR, let me know what you think!
  2. Not really sure I understand the argument against not having AsyncCommand 🤔
    We do have the interface, it's just a matter of also providing a reference implementation ready to use - devs who are fine can just use that directly, and devs in need of more control can just reimplement their commands anyway. There's no loss of functionality either way, it's just about also having a concrete type for each interface available out of the box. Additionally, not providing a concrete type would also break the "symmetry" of the available types in the package, where right now we offer a concrete class for each interface we expose, so that devs that are ok with the base functionality don't have to reinvent the wheel on their own every time (eg. Messenger for IMessenger, ObservableObject for INotifyPropertyChanged, RelayCommand for ICommand, etc.).

@michael-hawker
Copy link
Member

"I thought awaiting on .Result was blocking?"

It is, but that .Result there is not the Task<T>.Result property, it's the RequestMessage<T>.Result property! 😄
As in, you're getting the resulting response value, which is a Task<T>, and then awaiting that one.

Should we think about a different property name or something?

@Sergio0694 let's start working on docs and samples, anyone who uses tech other than UWP that wants to help us is more than welcome to contribute as well! I'm thinking we can create a separate repo in the org here specific to MVVM samples and where we can host some of the initial documentation before release, thoughts?

@Sergio0694
Copy link
Member Author

"I thought awaiting on .Result was blocking?"

It is, but that .Result there is not the Task<T>.Result property, it's the RequestMessage<T>.Result property! 😄
As in, you're getting the resulting response value, which is a Task<T>, and then awaiting that one.

Should we think about a different property name or something?

Whoops, forgot to update my reply here! I've refactored those APIs quite a bit in a previous PR, they're way less verbose and more user friendly now when specifically dealing with async request messages, in fact there's a dedicated message type now.

Here's how it works:

// "Async" request message, returns a Task<T>
public sealed class MyAsyncRequestMessage : AsyncRequestMessage<string> { }

// Subscribe to the request
Messenger.Default.Register<MyAsyncRequestMessage>(this, m =>
{
    m.Reply(GetSomeStringAsync());
});

// Request and wait for the response
string response = await Messenger.Default.Send<MyAsyncRequestMessage>();

The trick here is that AsyncRequestMessage<T> exposes a GetAwaiter method, so you can now just use await on it directly, and it'll just relay that to the actual Task<T> response contained within the message for you 😊

This will all be properly documented in the docs of course!

@Sergio0694
Copy link
Member Author

Hey everyone, as @michael-hawker suggested I've linked a (very) draft preview of the docs in the first post 😊

Also including the link here for extra visibility: https://gist.github.com/Sergio0694/987209d0855837ee6835f6e758f5cf40.

@mrlacey
Copy link
Contributor

mrlacey commented Jul 18, 2020

Hey everyone, as @michael-hawker suggested I've linked a (very) draft preview of the docs in the first post 😊

Also including the link here for extra visibility: https://gist.github.com/Sergio0694/987209d0855837ee6835f6e758f5cf40.

Is there a version that makes it easy to give inline feedback on? (say as part of a PR?)

@Sergio0694
Copy link
Member Author

@mrlacey I'm working on a draft in a branch on the sample repo, I'll open a PR there as soon as it's finished so you'll be able to just add review comments directly on each line or section you think needs some work or changes 👍

@mrlacey
Copy link
Contributor

mrlacey commented Jul 18, 2020

I've been looking at how the Calcium framework includes a built-in exception handler for exceptions thrown within messages. Has anything similar been considered/discussed for inclusion here too? I couldn't find anything but this strikes me as a possible nice addition.

N.B. I haven't considered how this might work but just thinking about possibilities.

@LeiYangGH
Copy link

LeiYangGH commented Aug 10, 2020

is this feature ready to use? I tried install Microsoft.Toolkit.mvvm to my .net framework 4.7.2 application but nuget cannot search such package.
Which package in Microsoft.Toolkit is equivalent of mvvmlight?

@mrlacey
Copy link
Contributor

mrlacey commented Aug 10, 2020

is this feature ready to use? I tried install Microsoft.Toolkit.mvvm to my .net framework 4.7.2 application but nuget cannot search such package.
Which package in Microsoft.Toolkit is equivalent of mvvmlight?

This is currently only available in preview (from the MyGet source)
It will be released as part of version 7.0

Documentation on migrating from MvvmLight is currently in progress.

@Sergio0694
Copy link
Member Author

Sergio0694 commented Aug 10, 2020

Hey @LeiYangGH - the feature is ready but it's still in early preview for now, so the package is only available on MyGet. If you'de like to try it out, you need to create a nuget.config file next to your .sln project, with this content:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="ToolkitMyGet" value="https://dotnet.myget.org/F/uwpcommunitytoolkit/api/v3/index.json"/>
  </packageSources>
</configuration>

Then you need to go to the NuGet package manager page for your project and ensure that the ToolkitMyGet feed is selected. From there, you should be able to find the Microsoft.Toolkit.Mvvm package and install it 👍

You can refer to the preview docs that are in the first post for this issue.
We'll also be providing more detailed docs and samples shortly 😄

EDIT: ah, @mrlacey beat me to it by 7 seconds 🤣

@LeiYangGH
Copy link

thanks a lot! your response is the fastest i've ever seen in github. i'll try myget.

@nanney54
Copy link

Hello,
have you plan to add support of cancellation to async command (New class CancelAsyncCommand or add to existing implementation) ?
Adding property like IsBusy/progress or whatever would be great too.

@Sergio0694
Copy link
Member Author

Sergio0694 commented Aug 20, 2020

We've considered it, but it seemed like too much functionality for a base implementation to add to the library at least for now. It should be relatively easy to write a custom command extending the async command interface, to also add cancellation support. Of course, it's certainly doable to also just add support for this to the IAsyncRelayCommand interface, though we'd need more feedbacks on this to decide to go forward with this change.

As a reference, with this change the interface would be something like this:

namespace Microsoft.Toolkit.Mvvm.Input
{
    public interface IAsyncRelayCommand : IRelayCommand, INotifyPropertyChanged
    {
        Task? ExecutionTask { get; }
        bool IsRunning { get; }
        bool IsCanceled { get; }

        void Cancel();
        Task ExecuteAsync(object? parameter);
    }
}

And I'd probably make the constructor take a Func<CancellationToken, Task> instead of just Func<Task>, so you'd be able to either just do _ => { ... } if you don't care about cancellation, or just use it within your code as needed. Might be a bit less handy to use for folks using the C# method group syntax though if they were closing over parameterless async methods. But then again this in turn would enable support for doing the same with ones accepting a token. I guess we can have an entire conversation on this suggestion 😄

Pinging @XamlBrewer @jamesmcroft @mrlacey


>>> Further discussions in #3428 <<<

This is the issue tracking all new changes to the MVVM Toolkit for the next preview of the library.
This issue only referred to the initial preview version and is not tracking the current state of the library.

ghost pushed a commit that referenced this issue Sep 25, 2020
## Follow up for #3230, part of #3428 
<!-- Add the relevant issue number after the "#" mentioned above (for ex: Fixes #1234) which will automatically close the issue once the PR is merged. -->

<!-- Add a brief overview here of the feature/bug & fix. -->

## PR Type
What kind of change does this PR introduce?
<!-- Please uncomment one or more that apply to this PR. -->

- Optimization
<!-- - Bugfix -->
<!-- - Feature -->
<!-- - Code style update (formatting) -->
<!-- - Refactoring (no functional changes, no api changes) -->
<!-- - Build or CI related changes -->
<!-- - Documentation content changes -->
<!-- - Sample app changes -->
<!-- - Other... Please describe: -->

## Overview

This PR includes two main parts: an improvement for the memory pooling buffers, and a refactor of how delegates are registered for message handlers. This last part allows to completely remove local closures for message handlers 🚀

### Memory pooling improvements

We're using memory pooling (by relying on the `ArrayPool<T>` APIs) in the `Messenger` class to achieve an amortized 0-allocation execution of the `Send` method in particular (`UnregisterAll` uses this too). We have code like this:

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/5bf426523cf456fc13db7b6505c56cad380d5f5f/Microsoft.Toolkit.Mvvm/Messaging/Messenger.cs#L250

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/5bf426523cf456fc13db7b6505c56cad380d5f5f/Microsoft.Toolkit.Mvvm/Messaging/Messenger.cs#L401

This works just fine, and we do get the 0-allocation after the initial first invocation where we rent the buffer.
There are two downsides here though:
- In the `Send` method, we rent an `Action<TMessage>` buffer, so we'll rent a different buffer for every message type
- Given that these types are either internal or not likely to ever by used by consumers of the library, all these rented buffers would only ever by used by the `Messenger` class itself, and the consumers would not be able to reuse them on their own.

Neither of these points is a big deal and the current implementation is fine, but I think we can do better 😄

**Idea:** let's leverage the fact that arrays are covariant, and only use a single type to solve both problems, like so:

```csharp
object[] maps = ArrayPool<object>.Shared.Rent(count);

// Do stuff, example:
foreach (object obj in maps)
{
    var myActualItem = (SomeFancyTypePossiblyInternal)obj;

    // Do stuff with the item...
}
```

This both allows us to just use `object[]` arrays, which both reduces the total number of rented arrays (as we can reuse them for different message types), and also makes it so that these rented arrays might potentially also be reused by consumers of the library (should they ever need to pool `object[]` arrays), further reducing allocations 🚀

## Benchmarks

Here are some benchmarks comparing the `Messenger` from this PR with the one in the `Preview1`.

### Sending messages

|   Method |       Categories |         Mean |     Error |    StdDev | Ratio | Gen 0 | Gen 1 | Allocated |
|--------- |----------------- |-------------:|----------:|----------:|------:|------:|------:|----------:|
| Old_Send |   DefaultChannel |     161.7 us |   1.52 us |   1.43 us |  1.00 |     - |     - |         - |
| **New_Send** |   DefaultChannel |     **153.9 us** |   1.62 us |   1.43 us |  0.95 |     - |     - |         - |
|          |                  |              |           |           |       |       |       |           |
| Old_Send | MultipleChannels | 148,668.7 us | 886.65 us | 785.99 us |  1.00 |     - |     - |     336 B |
| **New_Send** | MultipleChannels | **138,825.0 us** | 797.61 us | 746.08 us |  0.93 |     - |     - |     336 B |

Performance when sending message is slightly faster than before. Worst case scenario, it's not any slower.

### Registering messages

|       Method |     Mean |   Error |  StdDev | Ratio |    Gen 0 |   Gen 1 | Allocated |
|------------- |---------:|--------:|--------:|------:|---------:|--------:|----------:|
| Old_Register | 443.6 us | 1.73 us | 1.53 us |  1.00 | 113.7695 | 25.3906 | 581.53 KB |
| **New_Register** | **386.1 us** | 2.47 us | 2.31 us |  0.87 |  82.5195 |  4.3945 | **381.53 KB** |

The new version is **13%** faster when registering messages, and uses **34%** less memory 🚀

### Enabled recipient type-specific handlers

One major annoyance for users working with manually registered handlers was the fact that type information was lost.
As in, recipients were registered as just `object` instances, as it was necessary to cast them in the handler every time.
This PR also changes this by adding support for a type parameter to specify the recipient type.
This enables the following change (which is totally optional anyway, you can still just use `object` if you want):

```csharp
// Before
Messenger.Register<MyMessage>(this, (s, m) => ((MyViewModel)s).SomeMethod(m));

// After
Messenger.Register<MyViewModel, MyMessage>(this, (s, m) => s.SomeMethod(m));
```

### Removed local closures

The original implementation used `Action<T>` for handlers, which caused closures to be constantly created whenever the users wanted to access any local member on the registered recipient. This was because the recipient itself needed to be captured too to be accessed from the handlers. This detail also made it more difficult for other devs to implement `IMessenger` if they wanted to use a weak reference system. I've replaced that handler type with a custom delegate, called `MessageHandler`:

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/32656db1cdd4ddc25e3a88c297ce4062fe64d2ad/Microsoft.Toolkit.Mvvm/Messaging/IMessenger.cs#L10-L22

This delegate also receives the target recipient as input, which allows developers to just use that to access local members, without the need to create closures. The handlers are now essentially static, so the C# compiler can cache the whole delegate too. This is especially useful for the `IRecipient<TMessage>` pattern, as that was previously creating unnecessary closures when registering handlers - that's completely gone now and delegates are cached there as well 🎉

For instance, you can see that here:

https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/32656db1cdd4ddc25e3a88c297ce4062fe64d2ad/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs#L175-L179

We're now using that cached static delegate (will be able to also explicitly mark that as static when C# 9 lands, but it already is) instead of the method group syntax on `recipient.Receive`, which allocated a closure for each invocation.

## PR Checklist

Please check if your PR fulfills the following requirements:

- [X] Tested code with current [supported SDKs](../readme.md#supported)
- [ ] ~~Pull Request has been submitted to the documentation repository [instructions](..\contributing.md#docs). Link: <!-- docs PR link -->~~
- [ ] ~~Sample in sample app has been added / updated (for bug fixes / features)~~
    - [ ] ~~Icon has been created (if new sample) following the [Thumbnail Style Guide and templates](https://github.com/windows-toolkit/WindowsCommunityToolkit-design-assets)~~
- [X] Tests for the changes have been added (for bug fixes / features) (if applicable)
- [X] Header has been added to all new source files (run *build/UpdateHeaders.bat*)
- [X] Contains **NO** breaking changes
@ghost ghost locked as resolved and limited conversation to collaborators Oct 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Completed 🔥 feature 💡 feature request 📬 A request for new changes to improve functionality .NET Components which are .NET based (non UWP specific) open discussion ☎️
Projects
None yet
Development

Successfully merging a pull request may close this issue.