Updated sample app docs and code samples

This commit is contained in:
Sergio Pedri 2020-11-02 20:43:30 +01:00
Родитель 34bfbdf777
Коммит fbb0afdf80
11 изменённых файлов: 285 добавлений и 80 удалений

Просмотреть файл

@ -9,13 +9,14 @@ dev_langs:
# AsyncRelayCommand and AsyncRelayCommand<T>
The [AsyncRelayCommand](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.AsyncRelayCommand) and [AsyncRelayCommand<T>](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.AsyncRelayCommand-1) are `ICommand` implementations that extend the functionalities offered by `RelayCommand`, with support for asynchronous operations.
The [`AsyncRelayCommand`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.AsyncRelayCommand) and [`AsyncRelayCommand<T>`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.AsyncRelayCommand-1) are `ICommand` implementations that extend the functionalities offered by `RelayCommand`, with support for asynchronous operations.
## How they work
`AsyncRelayCommand` and `AsyncRelayCommand<T>` have the following main features:
- They extend the functionalities of the non-asynchronous commands included in the library, with support for `Task`-returning delegates.
- They extend the functionalities of the synchronous commands included in the library, with support for `Task`-returning delegates.
- They can wrap asynchronous functions with an additional `CancellationToken` parameter to support cancelation, and they expose a `CanBeCanceled` and `IsCancellationRequested` properties, as well as a `Cancel` method.
- They expose an `ExecutionTask` property that can be used to monitor the progress of a pending operation, and an `IsRunning` that can be used to check when an operation completes. This is particularly useful to bind a command to UI elements such as loading indicators.
- They implement the `IAsyncRelayCommand` and `IAsyncRelayCommand<T>` interfaces, which means that viewmodel can easily expose commands using these to reduce the tight coupling between types. For instance, this makes it easier to replace a command with a custom implementation exposing the same public API surface, if needed.

Просмотреть файл

@ -33,6 +33,8 @@ using Microsoft.Toolkit.Mvvm;
Imports Microsoft.Toolkit.Mvvm
```
3. Code samples are available in the other docs pages for the MVVM package, and in the [unit tests](https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/UnitTests/UnitTests.Shared/Mvvm) for the project.
## When should I use this package?
Use this package for access to a collection of standard, self-contained, lightweight types that provide a starting implementation for building modern apps using the MVVM pattern. These types alone are usually enough for many users to build apps without needing additional external references.
@ -65,5 +67,3 @@ The included types are:
- `ValueChangedMessage<T>`
This package aims to offer as much flexibility as possible, so developers are free to choose which components to use. All types are loosely-coupled, so that it's only necessary to include what you use. There is no requirement to go "all-in" with a specific series of all-encompassing APIs, nor is there a set of mandatory patterns that need to be followed when building apps using these helpers. Combine these building blocks in a way that best fits your needs.
Code samples are available in the other docs pages for the MVVM package, and in the [unit tests](https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/UnitTests/UnitTests.Shared/Mvvm) for the project.

Просмотреть файл

@ -1,7 +1,7 @@
---
title: Ioc
author: Sergio0694
description: A type that facilitates the use of the IServiceProvider type
description: An introduction to the use of the IServiceProvider type through the Microsoft.Extensions.DependencyInjection APIs
keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, service, dependency injection, net core, net standard
dev_langs:
- csharp
@ -9,39 +9,74 @@ dev_langs:
# Ioc ([Inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control))
The [Ioc](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.DependencyInjection.Ioc) class is a type that facilitates the use of the `IServiceProvider` type. It's powered by the `Microsoft.Extensions.DependencyInjection` package, which provides a fully featured and powerful DI set of APIs, and acts as an easy to setup and use `IServiceProvider`.
A common pattern that can be used to increase modularity in the codebase of an application using the MVVM pattern is to use some form of inversion of control. One of the most common solution in particular is to use dependency injection, which consists in creating a number of services that are injected into backend classes (ie. passed as parameters to the viewmodel constructors) - this allows code using these services not to rely on implementation details of these services, and it also makes it easy to swap the concrete implementations of these services. This pattern also makes it easy to make platform-specific features available to backend code, by abstracting them through a service which is then injected where needed.
The MVVM Toolkit doesn't provide built-in APIs to facilitate the usage of this pattern, as there already exist dedicated libraries specifically for this such as the `Microsoft.Extensions.DependencyInjection` package, which provides a fully featured and powerful DI set of APIs, and acts as an easy to setup and use `IServiceProvider`. The following guide will refer to this library and provide a series of examples of how to integrate it into applications using the MVVM pattern.
## Configure and resolve services
The main entry point is the `ConfigureServices` method, which can be used like so:
The first step is to declare an `IServiceProvider` instance, and to initialize all the necessary services, usually at startup. For instance, on UWP (but a similar setup can be used on other frameworks too):
```csharp
// Register the services at startup
Ioc.Default.ConfigureServices(services =>
public sealed partial class App : Application
{
services.AddSingleton<IFilesService, FilesService>();
services.AddSingleton<ISettingsService, SettingsService>();
// Other services here...
});
public App()
{
Services = ConfigureServices();
// Retrieve a service instance when needed
IFilesService fileService = Ioc.Default.GetService<IFilesService>();
this.InitializeComponent();
}
/// <summary>
/// Gets the current <see cref="App"/> instance in use
/// </summary>
public new static App Current => (App)Application.Current;
/// <summary>
/// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
/// </summary>
public IServiceProvider Services { get; }
/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<IFilesService, FilesService>();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton<IClipboardService, ClipboardService>();
services.AddSingleton<IShareService, ShareService>();
services.AddSingleton<IEmailService, EmailService>();
return services.BuildServiceProvider();
}
}
```
The `Ioc.Default` property offers a thread-safe `IServiceProvider` instance that can be used anywhere in the application to resolve services. The `ConfigureService` method handles the initialization of that service. It is also possible to create different `Ioc` instances and to initialize each with different services.
Here the `Services` property is initialized at startup, and all the application services and viewmodels are registered. There is also a new `Current` property that can be used to easily access the `Services` property from other views in the application. For instance:
```csharp
IFilesService filesService = App.Current.Services.GetService<IFilesService>();
// Use the files service here...
```
The key aspect here is that each service may very well be using platform-specific APIs, but since those are all abstracted away through the interface our code is using, we don't need to worry about them whenever we're just resolving an instance and using it to perform operations.
## Constructor injection
One powerful feature that is available is "constructor injection", which means that the DI service provider is able to automatically resolve indirect dependencies between registered services when creating instances of the type being requested. Consider the following service:
```csharp
public class ConsoleLogger : ILogger
public class FileLogger : IFileLogger
{
private readonly IFileService FileService;
private readonly IFilesService FileService;
private readonly IConsoleService ConsoleService;
public ConsoleLogger(
IFileService fileService,
public FileLogger(
IFilesService fileService,
IConsoleService consoleService)
{
FileService = fileService;
@ -52,22 +87,61 @@ public class ConsoleLogger : ILogger
}
```
Here we have a `ConsoleLogger` implementing the `ILogger` interface, and requiring `IFileService` and `IConsoleService` instances. Constructor injection means the DI service provider will "automagically" gather all the necessary services, like so:
Here we have a `FileLogger` type implementing the `IFileLogger` interface, and requiring `IFilesService` and `IConsoleService` instances. Constructor injection means the DI service provider will automatically gather all the necessary services, like so:
```csharp
// Register the services at startup
Ioc.Default.ConfigureServices(services =>
/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
services.AddSingleton<IFileService, FileService>();
var services = new ServiceCollection();
services.AddSingleton<IFilesService, FilesService>();
services.AddSingleton<IConsoleService, ConsoleService>();
services.AddSingleton<ILogger, ConsoleLogger>();
});
services.AddSingleton<IFileLogger, FileLogger>();
return services.BuildServiceProvider();
}
// Retrieve a logger service with constructor injection
ILogger consoleLogger = Ioc.Default.GetService<ILogger>();
IFileLogger fileLogger = App.Current.Services.GetService<IFileLogger>();
```
The DI service provider will automatically check whether all the necessary services are registered, then it will retrieve them and invoke the constructor for the registered `ILogger` concrete type, to get the instance to return - all done automatically!
The DI service provider will automatically check whether all the necessary services are registered, then it will retrieve them and invoke the constructor for the registered `IFileLogger` concrete type, to get the instance to return.
## What about viewmodels?
A service provider has "service" in its name, but it can actually be used to resolve instances of any class, including viewmodels! The same concepts explained above still apply, including constructor injection. Imagine we had a `ContactsViewModel` type, using an `IContactsService` and an `IPhoneService` instance through its constructor. We could have a `ConfigureServices` method like this:
```csharp
/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
// Services
services.AddSingleton<IContactsService, ContactsService>();
services.AddSingleton<IPhoneService, PhoneService>();
// Viewmodels
services.AddTransient<ContactsViewModel>();
return services.BuildServiceProvider();
}
```
And then in our `ContactsView`, we would assign the data context as follows:
```csharp
public ContactsView()
{
this.InitializeComponent();
this.DataContext = App.Current.Services.GetService<ContactsViewModel>();
}
```
## More docs

Просмотреть файл

@ -9,15 +9,18 @@ dev_langs:
# Messenger
The [Messenger](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.Messaging.Messenger) class (with the accompanying [IMessenger](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.Messaging.IMessenger) interface) can be used to exchange messages between different objects. This can be useful to decouple different modules of an application without having to keep strong references to types being referenced. It is also possible to send messages to specific channels, uniquely identified by a token, and to have different messengers in different sections of an application.
The [`IMessenger`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.Messaging.IMessenger) interface is a contract for types that can be used to exchange messages between different objects. This can be useful to decouple different modules of an application without having to keep strong references to types being referenced. It is also possible to send messages to specific channels, uniquely identified by a token, and to have different messengers in different sections of an application. The MVVM Toolkit provides two implementations out of the box: [`WeakReferenceMessenger`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.Messaging.WeakReferenceMessenger) and [`StrongReferenceMessenger`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.Messaging.StrongReferenceMessenger): the former uses weak references internally, offering automatic memory management for recipients, while the latter uses strong references and requires developers to manually unsubscribe their recipients when they're no longer needed (more details about how to unregister message handlers can be found below), but in exchange for that offers better performance and far less memory usage.
## How it works
The `Messenger` type is responsible for maintaining links between recipients (receivers of messages) and their registered message types, with relative message handlers. Any object can be registered as a recipient for a given message type using a message handler, which will be invoked whenever the `Messenger` instance is used to send a message of that type. It is also possible to send messages through specific communication channels (each identified by a unique token), so that multiple modules can exchange messages of the same type without causing conflicts. Messages sent without a token use the default shared channel.
Types implementing `IMessenger` are responsible for maintaining links between recipients (receivers of messages) and their registered message types, with relative message handlers. Any object can be registered as a recipient for a given message type using a message handler, which will be invoked whenever the `IMessenger` instance is used to send a message of that type. It is also possible to send messages through specific communication channels (each identified by a unique token), so that multiple modules can exchange messages of the same type without causing conflicts. Messages sent without a token use the default shared channel.
There are two ways to perform message registration: either through the `IRecipient<TMessage>` interface, or using an `Action<TMessage>` delegate acting as message handler. The first lets you register all the handlers with a single call to the `RegisterAll` extension, which automatically registers the recipients of all the declared message handlers, while the latter is useful when you need more flexibility or when you want to use a simple lambda expression as a message handler.
There are two ways to perform message registration: either through the `IRecipient<TMessage>` interface, or using a `MessageHandler<TRecipient, TMessage>` delegate acting as message handler. The first lets you register all the handlers with a single call to the `RegisterAll` extension, which automatically registers the recipients of all the declared message handlers, while the latter is useful when you need more flexibility or when you want to use a simple lambda expression as a message handler.
Similar to the `Ioc` class, `Messenger` exposes a `Default` property that offers a thread-safe implementation built-in into the package. It is also possible to create multiple `Messenger` instances if needed, for instance if a different one is injected with a DI service provider into a different module of the app (for instance, multiple windows running in the same process).
Both `WeakReferenceMessenger` and `StrongReferenceMessenger` also expose a `Default` property that offers a thread-safe implementation built-in into the package. It is also possible to create multiple messenger instances if needed, for instance if a different one is injected with a DI service provider into a different module of the app (for instance, multiple windows running in the same process).
> [!NOTE]
> Since the `WeakReferenceMessenger` type is simpler to use and matches the behavior of the messenger type from the `MvvmLight` library, it is the default type being used by the [`ObservableRecipient`](ObservableRecipient.md) type in the MVVM Toolkit. The `StrongReferenceType` can still be used, by passing an instance to the constructor of that class.
## Sending and receiving messages
@ -33,31 +36,58 @@ public class LoggedInUserChangedMessage : ValueChangedMessage<User>
}
// Register a message in some module
Messenger.Default.Register<LoggedInUserChangedMessage>(this, m =>
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this, (r, m) =>
{
// Handle the message here
// Handle the message here, with r being the recipient and m being the
// input messenger. Using the recipient passed as input makes it so that
// the lambda expression doesn't capture "this", improving performance.
});
// Send a message from some other module
Messenger.Default.Send(new LoggedInUserChangedMessage(user));
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));
```
Let's imagine this message type being used in a simple messaging application, which displays a header with the user name and profile image of the currently logged user, a panel with a list of conversations, and another panel with messages from the current conversation, if one is selected. Let's say these three sections are supported by the `HeaderViewModel`, `ConversationsListViewModel` and `ConversationViewModel` types respectively. In this scenario, the `LoggedInUserChangedMessage` message might be sent by the `HeaderViewModel` after a login operation has completed, and both those other viewmodels might register handlers for it. For instance, `ConversationsListViewModel` will load the list of conversations for the new user, and `ConversationViewModel` will just close the current conversation, if one is present.
The `Messenger` class takes care of delivering messages to all the registered recipients. Note that a recipient can subscribe to messages of a specific type. Note that inherited message types are not registered in the default `Messenger` implementation.
The `IMessenger` instance takes care of delivering messages to all the registered recipients. Note that a recipient can subscribe to messages of a specific type. Note that inherited message types are not registered in the default `IMessenger` implementations provided by the MVVM Toolkit.
When a recipient is not needed anymore, you should unregister it so that it will stop receiving messages. You can unregister either by message type, by registration token, or by recipient:
```csharp
// Unregisters the recipient from a message type
Messenger.Default.Unregister<LoggedInUserChangedMessage>(this);
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage>(this);
// Unregisters the recipient from a message type in a specified channel
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage, int>(this, 42);
// Unregister the recipient from all messages, across all channels
Messenger.Default.UnregisterAll(this);
WeakReferenceMessenger.Default.UnregisterAll(this);
```
> [!WARNING]
> The `Messenger` implementation uses strong references to track the registered recipients. This is done for performance reasons, and it means that each registered recipient should manually be unregistered to avoid memory leaks. That is, as long as a recipient is registered, the `Messenger` instance in use will keep an active reference to it, which will prevent the garbage collector from being able to collect that instance. You can either handle this manually, or you can inherit from [ObservableRecipient](ObservableRecipient.md), which by default automatically takes care of removing all the message registrations for recipient when it is deactivated (see docs on `ObservableRecipient` for more info about this).
> As mentioned before, this is not strictly necessary when using the `WeakReferenceMessenger` type, as it uses weak references to track recipients, meaning that unused recipients will still be eligible for garbage collection even though they still have active message handlers. It is still good practice to unsubscribe them though, to improve performances. On the other hand, the `StrongReferenceMessenger` implementation uses strong references to track the registered recipients. This is done for performance reasons, and it means that each registered recipient should manually be unregistered to avoid memory leaks. That is, as long as a recipient is registered, the `StrongReferenceMessenger` instance in use will keep an active reference to it, which will prevent the garbage collector from being able to collect that instance. You can either handle this manually, or you can inherit from `ObservableRecipient`, which by default automatically takes care of removing all the message registrations for recipient when it is deactivated (see docs on `ObservableRecipient` for more info about this).
It is also possible to use the `IRecipient<TMessage>` interface to register message handlers. In this case, each recipient will need to implement the interface for a given message type, and provide a `Receive(TMessage)` method that will be invoke when receiving messages, like so:
```csharp
// Create a message
public class MyRecipient : IRecipient<LoggedInUserChangedMessage>
{
public void Receive(LoggedInUserChangedMessage message)
{
// Handle the message here...
}
}
// Register that specific message...
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this);
// ...or alternatively, register all declared handlers
WeakReferenceMessenger.Default.RegisterAll(this);
// Send a message from some other module
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));
```
## Using request messages
@ -70,13 +100,16 @@ public class LoggedInUserRequestMessage : RequestMessage<User>
}
// Register the receiver in a module
Messenger.Default.Register<LoggedInUserRequestMessage>(this, m =>
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
m.Reply(CurrentUser); // Assume this is a private member
// Assume that "CurrentUser" is a private member in our viewmodel.
// As before, we're accessing it through the recipient passed as
// input to the handler, to avoid capturing "this" in the delegate.
m.Reply(r.CurrentUser);
});
// Request the value from another module
User user = Messenger.Default.Send<LoggedInUserRequestMessage>();
User user = WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();
```
The `RequestMessage<T>` class includes an implicit converter that makes the conversion from a `LoggedInUserRequestMessage` to its contained `User` object possible. This will also check that a response has been received for the message, and throw an exception if that's not the case. It is also possible to send request messages without this mandatory response guarantee: just store the returned message in a local variable, and then manually check whether a response value is available or not. Doing so will not trigger the automatic exception if a response is not received when the `Send` method returns.
@ -91,13 +124,13 @@ public class LoggedInUserRequestMessage : AsyncRequestMessage<User>
}
// Register the receiver in a module
Messenger.Default.Register<LoggedInUserRequestMessage>(this, m =>
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
m.Reply(GetCurrentUserAsync()); // We're replying with a Task<User>
m.Reply(r.GetCurrentUserAsync()); // We're replying with a Task<User>
});
// Request the value from another module (we can directly await on the request)
User user = await Messenger.Default.Send<LoggedInUserRequestMessage>();
User user = await WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();
```
## Sample Code
@ -113,5 +146,6 @@ There are more examples in the [unit tests](https://github.com/Microsoft/Windows
## API
* [Messenger source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/Messaging/Messenger.cs)
* [IMessenger source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/Messaging/IMessenger.cs)
* [StrongReferenceMessenger source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/Messaging/StrongReferenceMessenger.cs)
* [WeakReferenceMessenger source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/Messaging/WeakReferenceMessenger.cs)

Просмотреть файл

@ -9,7 +9,7 @@ dev_langs:
# ObservableObject
The [ObservableObject](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.componentmodel.ObservableObject) is a base class for objects that are observable by implementing the [INotifyPropertyChanged](https://docs.microsoft.com/dotnet/api/system.componentmodel.inotifypropertychanged) and [INotifyPropertyChanging](https://docs.microsoft.com/dotnet/api/system.componentmodel.inotifypropertychanging) interfaces. It can be used as a starting point for all kinds of objects that need to support property change notifications.
The [`ObservableObject`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.componentmodel.ObservableObject) is a base class for objects that are observable by implementing the [`INotifyPropertyChanged`](https://docs.microsoft.com/dotnet/api/system.componentmodel.inotifypropertychanged) and [`INotifyPropertyChanging`](https://docs.microsoft.com/dotnet/api/system.componentmodel.inotifypropertychanging) interfaces. It can be used as a starting point for all kinds of objects that need to support property change notifications.
## How it works
@ -53,12 +53,24 @@ public class ObservableUser : ObservableObject
public string Name
{
get => user.Name;
set => SetProperty(() => user.Name, value);
set => Set(user.Name, value, user, (u, n) => u.Name = n);
}
}
```
The `SetProperty<T>(Expression<Func<T>>, T, string)` method makes creating these wrapping properties extremely simple, as it takes care of both retrieving and setting the target properties while providing an extremely compact API.
In this case we're using the `SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string)` overload. The signature is slightly more complex than the previous one - this is necessary to let the code still be extremely efficient even if we don't have access to a backing field like in the previous scenario. We can go through each part of this method signature in detail to understand the role of the different components:
- `TModel` is a type argument, indicating the type of the model we're wrapping. In this case, it'll be our `User` class. Note that we don't need to specify this explicitly - the C# compiler will infer this automatically by how we're invoking the `SetProperty` method.
- `T` is the type of the property we want to set. Similarly to `TModel`, this is inferred automatically.
- `T oldValue` is the first parameter, and in this case we're using `user.Name` to pass the current value of that property we're wrapping.
- `T newValue` is the new value to set to the property, and here we're passing `value`, which is the input value within the property setter.
- `TModel model` is the target model we are wrapping, in this case we're passing the instance stored in the `user` field.
- `Action<TModel, T> callback` is a function that will be invoked if the new value of the property is different than the current one, and the property needs to be set. This will be done by this callback function, which receives as input the target model and the new property value to set. In this case we're just assigning the input value (which we called `n`) to the `Name` property (by doing `u.Name = n`). It is important here to avoid capturing values from the current scope and only interact with the ones given as input to the callback, as this allows the C# compiler to cache the callback function and perform a number of performance improvements. It's because of this that we're not just directly accessing the `user` field here or the `value` parameter in the setter, but instead we're only using the input parameters for the lambda expression.
The `SetProperty<TModel, T>(T, T, TModel, Action<TModel, T>, string)` method makes creating these wrapping properties extremely simple, as it takes care of both retrieving and setting the target properties while providing an extremely compact API.
> [!NOTE]
> Compared to the implementation of this method using LINQ expressions, specifically through a parameter of type `Expression<Func<T>>` instead of the state and callback parameters, the performance improvements that can be achieved this way are really significant. In particular, this version is ~200x faster than the one using LINQ expressions, and does not make any memory allocations at all.
## Handling `Task<T>` properties
@ -67,12 +79,12 @@ If a property is a `Task` it's necessary to also raise the notification event on
```csharp
public class MyModel : ObservableObject
{
private Task<int> requestTask;
private TaskNotifier<int>? requestTask;
public Task<int> RequestTask
public Task<int>? RequestTask
{
get => requestTask;
set => SetPropertyAndNotifyOnCompletion(ref requestTask, () => requestTask, value);
set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
}
public void RequestValue()
@ -82,7 +94,10 @@ public class MyModel : ObservableObject
}
```
Here the `SetPropertyAndNotifyOnCompletion<TTask>(ref TTask, Expression<Func<TTask>>, TTask, string)` method will take care of updating the target field, monitoring the new task, if present, and raising the notification event when that task completes. This way, it's possible to just bind to a task property and to be notified when its status changes.
Here the `SetPropertyAndNotifyOnCompletion<T>(ref TaskNotifier<T>, Task<T>, string)` method will take care of updating the target field, monitoring the new task, if present, and raising the notification event when that task completes. This way, it's possible to just bind to a task property and to be notified when its status changes. The `TaskNotifier<T>` is a special type exposed by `ObservableObject` that wraps a target `Task<T>` instance and enables the necessary notification logic for this method. The `TaskNotifier` type is also available to use directly if you have a general `Task` only.
> [!NOTE]
> The `SetPropertyAndNotifyOnCompletion` method is meant to replace the usage of the `NotifyTaskCompletion<T>` type from the `Microsoft.Toolkit` package. If this type was being used, it can be replaced with just the inner `Task` (or `Task<TResult>`) property, and then the `SetPropertyAndNotifyOnCompletion` method can be used to set its value and raise notification changes. All the properties exposed by the `NotifyTaskCompletion<T>` type are available directly on `Task` instances.
## Sample Code

Просмотреть файл

@ -15,7 +15,7 @@ The [`ObservableRecipient`](https://docs.microsoft.com/dotnet/api/microsoft.tool
The `ObservableRecipient` type is meant to be used as a base for viewmodels that also use the `IMessenger` features, as it provides built-in support for it. In particular:
- It has both a parameterless constructor and one that takes an `IMessenger` instance, to be used with dependency injection. It also exposes a `Messenger` property that can be used to send and receive messages in the viewmodel. If the parameterless constructor is used, the `Messenger.Default` instance will be assigned to the `Messenger` property.
- It has both a parameterless constructor and one that takes an `IMessenger` instance, to be used with dependency injection. It also exposes a `Messenger` property that can be used to send and receive messages in the viewmodel. If the parameterless constructor is used, the `WeakReferenceMessenger.Default` instance will be assigned to the `Messenger` property.
- It exposes an `IsActive` property to activate/deactivate the viewmodel. In this context, to "activate" means that a given viewmodel is marked as being in use, such that eg. it will start listening for registered messages, perform other setup operations, etc. There are two related methods, `OnActivated` and `OnDeactivated`, that are invoked when the property changes value. By default, `OnDeactivated` automatically unregisters the current instance from all registered messages. For best results and to avoid memory leaks, it's recommended to use `OnActivated` to register to messages, and to use `OnDeactivated` to do cleanup operations. This pattern allows a viewmodel to be enabled/disabled multiple times, while being safe to collect without the risk of memory leaks every time it's deactivated. By default, `OnActived` will automatically register all the message handlers defined through the `IRecipient<TMessage>` interface.
- It exposes a `Broadcast<T>(T, T, string)` method which sends a `PropertyChangedMessage<T>` message through the `IMessenger` instance available from the `Messenger` property. This can be used to easily broadcast changes in the properties of a viewmodel without having to manually retrieve a `Messenger` instance to use. This method is used by the overload of the various `SetProperty` methods, which have an additional `bool broadcast` property to indicate whether or not to also send a message.
@ -31,7 +31,7 @@ public class MyViewModel : ObservableRecipient, IRecipient<LoggedInUserRequestMe
}
```
In the above, `OnActivated` automatically registers the instance as a recipient for `LoggedInUserRequestMessage` messages, using that method as the action to invoke. Using the `IRecipient<TMessage>` interface is not mandatory, and the registration can also be done manually (even using just an inline lambda expression):
In the example above, `OnActivated` automatically registers the instance as a recipient for `LoggedInUserRequestMessage` messages, using that method as the action to invoke. Using the `IRecipient<TMessage>` interface is not mandatory, and the registration can also be done manually (even using just an inline lambda expression):
```csharp
public class MyViewModel : ObservableRecipient
@ -39,10 +39,10 @@ public class MyViewModel : ObservableRecipient
protected override void OnActivated()
{
// Using a method group...
Messenger.Register<LoggedInUserRequestMessage>(this, Receive);
Messenger.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) => r.Receive(m));
// ...or a lambda expression
Messenger.Register<LoggedInUserRequestMessage>(this, m =>
Messenger.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
// Handle the message here
});

Просмотреть файл

@ -136,7 +136,7 @@ public sealed class PostWidgetViewModel : ObservableRecipient
protected override void OnActivated()
{
// We use a method group here, but a lambda expression is also valid
Messenger.Register<PropertyChangedMessage<object>>(this, Receive);
Messenger.Register<PostWidgetViewModel, PropertyChangedMessage<object>>(this, (r, m) => r.Receive(m));
}
/// <inheritdoc/>
@ -155,9 +155,10 @@ We now have a draft of our viewmodels ready, and we can start looking into the s
## Building the settings service
> **NOTE:** the sample is built using the service locator pattern, but this is not the only possible pattern to use to manage service. The MVVM Toolkit also fully supports the dependency injection pattern, and you can choose the one you prefer depending on the architecture of your application, the available development time or personal preference.
> [!NOTE]
> The sample is built using the dependency injection pattern, which is the recommended approach to deal with services in viewmodels. It is also possible to use other patterns, such as the service locator pattern, but the MVVM Toolkit does not offer built-in APIs to enable that.
Since we want some of our properties to be saved and persisted, we need a way for viewmodels to be able to interact with the application settings. We shouldn't use platform-specific APIs directly in our viewmodels though, as that would prevent us from having all our viewmodels in a portable, .NET Standard project. We can solve this issue by using services, and the `Ioc` class. The idea is to write interfaces that represent all the API surface that we need, and then to implement platform-specific types implementing this interface on all our application targets. The viewmodels will only interact with the interfaces, so they will not have any strong reference to any platform-specific type at all.
Since we want some of our properties to be saved and persisted, we need a way for viewmodels to be able to interact with the application settings. We shouldn't use platform-specific APIs directly in our viewmodels though, as that would prevent us from having all our viewmodels in a portable, .NET Standard project. We can solve this issue by using services, and the APIs in the `Microsoft.Extensions.DependencyInjection` library to setup our `IServiceProvider` instance for the application. The idea is to write interfaces that represent all the API surface that we need, and then to implement platform-specific types implementing this interface on all our application targets. The viewmodels will only interact with the interfaces, so they will not have any strong reference to any platform-specific type at all.
Here's a simple interface for a settings service:
@ -188,14 +189,16 @@ We can assume that platform-specific types implementing this interface will take
/// <summary>
/// Gets the <see cref="ISettingsService"/> instance to use.
/// </summary>
private readonly ISettingsService SettingsService = Ioc.Default.GetRequiredService<ISettingsService>();
private readonly ISettingsService SettingsService;
/// <summary>
/// Creates a new <see cref="SubredditWidgetViewModel"/> instance.
/// </summary>
public SubredditWidgetViewModel()
public SubredditWidgetViewModel(ISettingsService settingsService)
{
selectedSubreddit = SettingsService.GetValue<string>(nameof(SelectedSubreddit)) ?? Subreddits[0];
SettingsService = settingsService;
selectedSubreddit = settingsService.GetValue<string>(nameof(SelectedSubreddit)) ?? Subreddits[0];
}
private string selectedSubreddit;
@ -215,7 +218,7 @@ public string SelectedSubreddit
}
```
Here we're using the service locator pattern, which is one of the supported patterns by the `Ioc` class. We've declared an `ISettingsService SettingsService` field that just stores our settings service (we're retrieving it from the static `Ioc.Default` instance), and then we're initializing the `SelectedSubreddit` property in the constructor, by either using the previous value or just the first available subreddit. Then we also modified the `SelectedSubreddit` setter, so that it will also use the settings service to save the new value to disk.
Here we're using dependency injection and constructor injection, as mentioned above. We've declared an `ISettingsService SettingsService` field that just stores our settings service (which we're receiving as parameter in the viewmodel constructor), and then we're initializing the `SelectedSubreddit` property in the constructor, by either using the previous value or just the first available subreddit. Then we also modified the `SelectedSubreddit` setter, so that it will also use the settings service to save the new value to disk.
Great! Now we just need to write a platform specific version of this service, this time directly inside one of our app projects. Here's what that service might look like on UWP:
@ -247,16 +250,29 @@ public sealed class SettingsService : ISettingsService
}
```
The final piece of the puzzle is to inject this platform-specific service into our service provider instance, which in this case is the `Ioc.Default` instance. We can do this at startup, like so:
The final piece of the puzzle is to inject this platform-specific service into our service provider instance. We can do this at startup, like so:
```csharp
Ioc.Default.ConfigureServices(services =>
/// <summary>
/// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
/// </summary>
public IServiceProvider Services { get; }
/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<ISettingsService, SettingsService>();
});
services.AddTransient<PostWidgetViewModel>();
return services.BuildServiceProvider();
}
```
This will register a singleton instance of our `SettingsService` as a type implementing `ISettingsService`. This means that every time one of our viewmodels uses `Ioc.Default.GetService<ISettingsService>()` while the app in use is the UWP one, it will receive a `SettingsService` instance, which will use the UWP APIs behind the scene to manipulate settings. Perfect!
This will register a singleton instance of our `SettingsService` as a type implementing `ISettingsService`. We are also registering the `PostWidgetViewModel` as a transient service, meaning every time we retrieve an instance, it will be a new one (you can imagine this being useful if wanted to have multiple, independent post widgets). This means that every time we resolve an `ISettingsService` instance while the app in use is the UWP one, it will receive a `SettingsService` instance, which will use the UWP APIs behind the scene to manipulate settings. Perfect!
## Building the Reddit service
@ -328,11 +344,19 @@ We have added a new `IRedditService` field to store our service, just like we di
The last missing piece now is just to inject the actual service into our service provider. The big difference in this case is that by using `refit` we don't actually need to implement the service at all! The library will automatically create a type implementing the service for us, behind the scenes. So we only need to get an `IRedditService` instance and inject it directly, like so:
```csharp
Ioc.Default.ConfigureServices(services =>
/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton(RestService.For<IRedditService>("https://www.reddit.com/"));
});
services.AddTransient<PostWidgetViewModel>();
return services.BuildServiceProvider();
}
```
And that's all we need to do! We now have all our backend ready to use, including two custom services that we created specifically for this app! 🎉
@ -341,9 +365,23 @@ And that's all we need to do! We now have all our backend ready to use, includin
Now that all the backend is completed, we can write the UI for our widgets. Note how using the MVVM pattern let us focus exclusively on the business logic at first, without having to write any UI-related code until now. Here we'll remove all the UI code that's not interacting with our viewmodels, for simplicity, and we'll go through each different control one by one. The full source code can be found in the sample app.
Before going through the various controls, here's how we can resolve viewmodels for all the different views in our application (eg. the `PostWidgetView`):
```csharp
public PostWidgetView()
{
this.InitializeComponent();
this.DataContext = App.Current.Services.GetService<PostWidgetViewModel>();
}
public PostWidgetViewModel ViewModel => (PostWidgetViewModel)DataContext;
```
We're using our `IServiceProvider` instance to resolve the `PostWidgetViewModel` object we need, which is then assigned to the data context property. We're also creating a strongly-typed `ViewModel` property that simply casts the data context to the correct viewmodel type - this is needed to enable `x:Bind` in the XAML code.
Let's start with the subreddit widget, which features a `ComboBox` to select a subreddit, a `Button` to refresh the feed, a `ListView` to display posts and a `ProgressBar` to indicate when the feed is loading. We'll assume that the `ViewModel` property represents an instance of the viewmodel we've described before - this can be declared either in XAML or directly in code behind.
**Subreddit selector:**
### Subreddit selector:
```xml
<ComboBox
@ -359,7 +397,7 @@ Let's start with the subreddit widget, which features a `ComboBox` to select a s
Here we're binding the source to the `Subreddits` property, and the selected item to the `SelectedSubreddit` property. Note how the `Subreddits` property is only bound once, as the collection itself sends change notifications, while the `SelectedSubreddit` property is bound with the `TwoWay` mode, as we need it both to be able to load the value we retrieve from our settings, as well as updating the property in the viewmodel when the user changes the selection. Additionally, we're using a XAML behavior to invoke our command whenever the selection changes.
**Refresh button:**
### Refresh button:
```xml
<Button Command="{x:Bind ViewModel.LoadPostsCommand}"/>
@ -367,7 +405,7 @@ Here we're binding the source to the `Subreddits` property, and the selected ite
This component is extremely simple, we're just binding our custom command to the `Command` property of the button, so that the command will be invoked whenever the user clicks on it.
**Posts list:**
### Posts list:
```xml
<ListView
@ -386,7 +424,7 @@ This component is extremely simple, we're just binding our custom command to the
Here we have a `ListView` binding the source and selection to our viewmodel property, and also a template used to display each post that is available. We're using `x:DataType` to enable `x:Bind` in our template, and we have two controls binding directly to the `Title` and `Thumbnail` properties of our post.
**Loading bar:**
### Loading bar:
```xml
<ProgressBar Visibility="{x:Bind ViewModel.LoadPostsCommand.IsRunning, Mode=OneWay}"/>

Просмотреть файл

@ -9,7 +9,7 @@ dev_langs:
# RelayCommand and RelayCommand&lt;T>
The [RelayCommand](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.RelayCommand) and [RelayCommand<T>](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.RelayCommand-1) are `ICommand` implementations that can expose a method or delegate to the view. These types act as a way to bind commands between the viewmodel and UI elements.
The [`RelayCommand`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.RelayCommand) and [`RelayCommand<T>`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.RelayCommand-1) are `ICommand` implementations that can expose a method or delegate to the view. These types act as a way to bind commands between the viewmodel and UI elements.
## How they work
@ -21,7 +21,50 @@ The [RelayCommand](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.
## Working with `ICommand`
The following sample shows how to set up a simple command using `RelayCommand` to abstract a method in the viewmodel. We also have a property raising change notifications through the `SetProperty` method inherited from `ObservableObject`. The UI has a `Button` control binding to the `ICommand` in the viewmodel, and a `TextBlock` that displays the value of the `Counter` property.
The following shows how to set up a simple command:
```csharp
public class MyViewModel : ObservableObject
{
public MyViewModel()
{
IncrementCounterCommand = new RelayCommand(IncrementCounter);
}
private int counter;
public int Counter
{
get => counter;
private set => SetProperty(ref counter, value);
}
public ICommand IncrementCounterCommand { get; }
private void IncrementCounter() => Counter++;
}
```
And the relative UI could then be (using WinUI XAML):
```xml
<Page
x:Class="MyApp.Views.MyPage"
xmlns:viewModels="using:MyApp.ViewModels">
<Page.DataContext>
<viewModels:MyViewModel x:Name="ViewModel"/>
</Page.DataContext>
<StackPanel Spacing="8">
<TextBlock Text="{x:Bind ViewModel.Counter, Mode=OneWay}"/>
<Button
Content="Click me!"
Command="{x:Bind ViewModel.IncrementCounterCommand}"/>
</StackPanel>
</Page>
```
The `Button` binds to the `ICommand` in the viewmodel, which wraps the private `IncrementCounter` method. The `TextBlock` displays the value of the `Counter` property and is updated every time the property value changes.
## Sample Code

Просмотреть файл

@ -57,7 +57,7 @@ public class UserSenderViewModel : ObservableRecipient
protected override void OnActivated()
{
Messenger.Register&lt;CurrentUsernameRequestMessage>(this, m => m.Reply(Username));
Messenger.Register&lt;UserSenderViewModel, CurrentUsernameRequestMessage>(this, (r, m) => m.Reply(r.Username));
}
}

Просмотреть файл

@ -104,7 +104,7 @@ public class UserReceiverViewModel : ObservableRecipient
protected override void OnActivated()
{
Messenger.Register&lt;UsernameChangedMessage>(this, m => Username = m.Value);
Messenger.Register&lt;UserReceiverViewModel, UsernameChangedMessage>(this, (r, m) => r.Username = m.Value);
}
}

Просмотреть файл

@ -82,7 +82,7 @@ public string Name
&lt;/StackPanel>
</controls:InteractiveSample.XamlCode>
<controls:InteractiveSample.CSharpCode>
private Task myTask;
private TaskNotifier myTask;
/// &lt;summary>
/// Gets or sets the name to display.
@ -90,7 +90,7 @@ private Task myTask;
public Task MyTask
{
get => myTask;
private set => SetPropertyAndNotifyOnCompletion(ref myTask, () => myTask, value);
private set => SetPropertyAndNotifyOnCompletion(ref myTask, value);
}
/// &lt;summary>