SolvedWindowsCommunityToolkit [Feature] Basic MVVM primitives (.NET Standard)

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.

38 Answers

βœ”οΈAccepted Answer

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 #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> πŸŽ‰

Other Answers:

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 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).

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

  • #556: Move commands from sample project to Toolkit
  • #831: INotifyPropertyChanged base class
  • #943: Add DelegateCommand/RelayCommand to the Toolkit
  • #2444: Rx Mvvm Framework
  • #2893: Do we need a Messenger system or event bus for the pub-sub pattern?

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

Related Issues:

7
WindowsCommunityToolkit [Feature] Basic MVVM primitives (.NET Standard)
Relaying some updates here from the UWP Discord server to keep everyone up to date Following an exte...
4
WindowsCommunityToolkit [Feature] MVVM Toolkit - Remove 'sealed' from the four Command Classes to allow extension
@laurentkempe @michael-hawker @Sergio0694 @sonnemaf @martinskuta Great arguments and discussion ...
99
omnisharp vscode v1.23.1 fails to load Unity project, worked with v1.23.0
The problem is that the new .NET Core 3.1 SDK isn't supported by any current Mono release ...
69
omnisharp vscode [fail]: OmniSharp.MSBuild.ProjectManager: Attemped to update project that is not loaded.
Thanks a bunch! This solved the issue for me Steps taken Download version 1.15.2 (see link above ...
67
MonoGame templates for visual studio 2019
I could not find templates for VS 2017 anywhere so that I had to install VS 2017 to get them ...
52
mono System.TermInfoReader cannot handle new NCurses 6 TermInfo files
fix is in progress A simple workaround is to set TERM=xterm for example that is some terminfo file t...
50
omnisharp vscode Debug Console window cannot accept Console.ReadLine() input during debugging
From @kieferrm on December 17 2016 14:50 The debug console is output only From @LiangZugeng on Decem...
50
roslyn Unable to compile C# 7.1 code on VSTS build agents
I just checked and <LangVersion>7.1</LangVersion> was in the project file Visual Studio Enterprise 2...
48
graphql code generator [TypeScript Resolvers] Apollo Server resolvers types mismatch
For anyone wondering where to put the useIndexSignature: true my codegen.yml looks like this (see th...
38
csharplang Proposal: Dependency Injection / Service Locator as a Language Feature
DI frameworks tend to have a lot of power and flexibility associated with them DI / Service Locator ...
38
roslyn Language Feature: Extension Everything
Why we still require to name the extension class while it can be self explanatory like: Using class-...
37
docs Please document how to change default console template.
There is no direct way to do this in .NET 6 There are two approaches: We want feedback Voting on thi...
31
omnisharp vscode 'System' not found after update to dotnet core 3.0
I was able to resolve this issue by uninstalling mono via brew and letting omnisharp use the MSBuild...
31
omnisharp vscode Omnisharp can't find .NET SDK when open any C sharp project
Sometimes the Visual Studio warning will still persist even after installing the .NET SDK ...
29
graphql code generator Duplicate identifier error
@dotansimha @danbruegge Faced the same problem Describe the bug Duplicate resolver type is generated...
28
omnisharp vscode Debug multiple ASP.NET Core projects in Visual Studio Code
Try this also Moved from microsoft/vscode#25628 From @mdmoura VSCode Version: 1.11.2 OS Version: Win...
28
omnisharp vscode The SDK 'Microsoft.NET.Sdk.Web' specified could not be found.
Solved this problem by adding omnisharp.json with Was asked to create new issue on #2876 Environment...
27
omnisharp vscode Format code returns: Sorry, but there is no formatter for 'csharp'-files installed.
Ah got it there was line csharp.format.enable: false in my user config after cleared that it started...
26
omnisharp vscode Issues when upgrading to 1.23.3 - Unity assembly definitions not found correctly
A temporary work-around to anyone who finds this for now: Right-click the C# extention from within V...
25
omnisharp vscode Always show "Downloading package 'OmniSharp (.NET 4.6 / x64)' (12310 KB) ."
Sorry for the delay I needed to get 1.6 out the door before taking some time to write up the steps b...
25
csharplang Discussion: try expression without catch for inline use
In PHP @ is used quite often to shorten things. Proposal: try expression without catch for inline us...
24
xamarin macios [Meta] Xcode 12.0 Support
Hey everyone - I know there was a lot of interest in WidgetKit This is a meta issue tracking the sta...
22
omnisharp vscode The SDK 'Microsoft.NET.Sdk.Web' specified could not be found
Latest OmniSharp beta as of now is 3.5.0-beta.2204 which seem to work with the latest .NET Core SDK ...
22
mono Unable to update ca-certificates-mono package
I need to investigate more on how to prevent this from happening but for now please just mv /etc/mon...
21
csharplang Nominal records
Again I see absolutely no reason why this syntax and functionality can't be applied to struct Having...
20
20
csharplang Exploration: Roles, extension interfaces and static interface members
The term view has several definitions in CS Given the enormity of C# code that uses the term in the ...
20
csharplang Proposed changes for Pattern Matching in C# 9.0 - Draft Specification
I do think that: Needs to have exactly the same behaviour (both in terms of any compiler warnings/er...
20
xamarin macios [entityframeworkcore] Query throws Exception with Xamarin iOS when linking
Hey I get it working by create a file in my iOS project : Linkers.xml From @rob2212 on November 2 20...
19
csharplang Open LDM Issues in Pattern-Matching
Some options for the syntax of the match expression All of these assume the introduction of a new ma...
18
csharplang [Proposal][Free function]
Interesting approach liking and +1ing your own suggestion πŸ€”. This proposal is not original at all ...
17
graphql code generator How to specify types for nested Maybes in generated query types?
I'm also having this exact same issue where it's outputting Array<Maybe<Post>> which means it can be...
16
csharplang Proposal: Multi-expression switch statement
Added the following to #1054 switching on a tuple literal In order to switch on a tuple literal ...
16
csharplang Discussion : Local Properties
While the idea of a local identifier that refers to a live calculation is intriguing I don't really ...
16
xamarin macios Build Error - PHImageManager.h: Error GEA45E82A: expected identifier or '{'
I'm the dude that originally reported the issue.. To get around it at the time I patched the SDK Adm...
15
MonoGame [Todo list] Future-proofing MonoGame
Thanks to everyone's effort lately especially harry-cpp and tomspilman MonoGame is getting super clo...
15
cordova plugin inappbrowser iOS - build fails if using "WKWebViewOnly" preference
yes its being sent since August Bug Report Problem What is expected to happen? cordova build ios Bui...
15
graphql code generator Can't return null from nullable filed resolver
@wallzero Adding my solution as the Maybe value fixes this issue. To Reproduce Steps to reproduce th...
15
csharplang Do not use in as a synonym for ref readonly
Since in is supposed to help cut down the ugly verbosity of readonly ref i have an alternative propo...
15
csharplang Adding the pipeline operator to C#
The Pipeline operator is going to add to Javascript ...
15
roslyn Proposal: guard statement
Why not just: ? The 'else' reads super weird to me With the above i can then write: Or ...
15
roslyn Checklist and open issues for C# range (..) operator
Would a type be able to accept a Range as an indexer argument e.g foo[1..10]? Personally ...
14
cordova plugin inappbrowser not work ios13 InAppbrowser select target _blank
This issue is not intended for support Please go to Cordova slack for more help Did you try to add t...
14
csharplang Natural value equality
If value were the keyword I think I'd prefer it as a modifier with class so that it's obvious that y...
14
botframework sdk [Feature Request] Facebook Messenger Feature Requests
We would like support for the Facebook handover protocol We are able to able to pass thread control ...
13
omnisharp vscode remove unused usings
Bump!.. This reasonably basic capability is a big miss on Visual Studio Code Environment data dotnet...
13
graphql code generator Custom Scalars?
Is there an example on how to configure custom scalars mapping them to custom types? How do you spec...
13
graphql code generator Apollo client 3 support
@grancalavera Until it gets fixed With the new apollo client 3 beta release react hooks are now incl...
13
csharplang Champion: readonly object initializers
The builder approach to me is hitting a fly with a giant hammer Really? It seems exceptionally tiny ...
13
csharplang Champion "Nullable type patterns"
I don't think that this is particularly necessary or even makes much sense null has no type ...