Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Maintenant que nous avons décrit tous les différents composants disponibles par le biais du CommunityToolkit.Mvvm package, nous pouvons examiner un exemple pratique de ces composants pour créer un seul exemple plus grand. Dans ce cas, nous voulons créer un navigateur pour Reddit très simple et minimaliste pour quelques subreddits.
Que voulons-nous créer ?
Commençons par définir exactement ce que nous voulons construire :
- Un navigateur Reddit minimal composé de deux « widgets » : l’un affichant des publications à partir d’un sous-dit, et l’autre affichant le billet actuellement sélectionné. Les deux widgets doivent être autonomes et sans références fortes les unes aux autres.
- Nous voulons que les utilisateurs puissent sélectionner un sous-dit dans une liste d’options disponibles et que nous voulons enregistrer le sous-dit sélectionné en tant que paramètre et le charger la prochaine fois que l’exemple est chargé.
- Nous voulons que le widget de subreddit offre également un bouton de rafraîchissement pour recharger le subreddit en cours.
- Pour les besoins de cet exemple, nous n’avons pas besoin d’être capables de gérer tous les types de publication possibles. Nous allons simplement affecter un exemple de texte à tous les billets chargés et l’afficher directement, pour simplifier les choses.
Configuration des viewmodels
Commençons par le viewmodel qui alimentera le widget subreddit et nous allons parcourir les outils dont nous avons besoin :
-
Commandes : nous avons besoin de la vue pour pouvoir demander au viewmodel de recharger la liste actuelle des publications à partir du sous-dit sélectionné. Nous pouvons utiliser le
AsyncRelayCommandtype pour encapsuler une méthode privée qui extrait les billets de Reddit. Ici, nous exposons la commande via l’interfaceIAsyncRelayCommandpour éviter les références fortes au type de commande exact que nous utilisons. Cela nous permettra également de modifier potentiellement le type de commande à l’avenir sans avoir à vous soucier d’un composant d’interface utilisateur qui s’appuie sur ce type spécifique utilisé. -
Propriétés : nous devons exposer un certain nombre de valeurs à l’interface utilisateur, que nous pouvons faire avec les propriétés observables si elles sont des valeurs que nous prévoyons de remplacer complètement, ou par des propriétés qui sont elles-mêmes observables (par exemple).
ObservableCollection<T>Dans ce cas, nous avons :-
ObservableCollection<object> Posts, qui est la liste observable des billets chargés. Ici, nous utilisons simplementobjectcomme espace réservé, car nous n’avons pas encore créé de modèle pour représenter les publications. Nous pouvons remplacer cela plus tard. -
IReadOnlyList<string> Subreddits, qui est une liste en lecture seule contenant les noms des subreddits parmi lesquels nous permettons aux utilisateurs de choisir. Cette propriété n’est jamais mise à jour. Elle n’a donc pas besoin d’être observable non plus. -
string SelectedSubreddit, qui correspond au subreddit actuellement sélectionné. Cette propriété doit être liée à l’interface utilisateur, car elle sera utilisée pour indiquer le dernier sous-dit sélectionné lors du chargement de l’exemple et pour être manipulée directement à partir de l’interface utilisateur lorsque l’utilisateur modifie la sélection. Ici, nous utilisons laSetPropertyméthode de laObservableObjectclasse. -
object SelectedPost, qui correspond à l’article actuellement sélectionné. Dans ce cas, nous utilisons laSetPropertyméthode de laObservableRecipientclasse pour indiquer que nous voulons également diffuser des notifications lorsque cette propriété change. Cela permet d’informer le widget de publication que la sélection de publication actuelle est modifiée.
-
-
Méthodes : nous avons simplement besoin d’une méthode privée
LoadPostsAsyncqui sera appelée par notre commande asynchrone et qui contiendra la logique permettant de charger les publications du subreddit sélectionné.
Voici le modèle de vue jusqu’ici :
public sealed class SubredditWidgetViewModel : ObservableRecipient
{
/// <summary>
/// Creates a new <see cref="SubredditWidgetViewModel"/> instance.
/// </summary>
public SubredditWidgetViewModel()
{
LoadPostsCommand = new AsyncRelayCommand(LoadPostsAsync);
}
/// <summary>
/// Gets the <see cref="IAsyncRelayCommand"/> instance responsible for loading posts.
/// </summary>
public IAsyncRelayCommand LoadPostsCommand { get; }
/// <summary>
/// Gets the collection of loaded posts.
/// </summary>
public ObservableCollection<object> Posts { get; } = new ObservableCollection<object>();
/// <summary>
/// Gets the collection of available subreddits to pick from.
/// </summary>
public IReadOnlyList<string> Subreddits { get; } = new[]
{
"microsoft",
"windows",
"surface",
"windowsphone",
"dotnet",
"csharp"
};
private string selectedSubreddit;
/// <summary>
/// Gets or sets the currently selected subreddit.
/// </summary>
public string SelectedSubreddit
{
get => selectedSubreddit;
set => SetProperty(ref selectedSubreddit, value);
}
private object selectedPost;
/// <summary>
/// Gets or sets the currently selected subreddit.
/// </summary>
public object SelectedPost
{
get => selectedPost;
set => SetProperty(ref selectedPost, value, true);
}
/// <summary>
/// Loads the posts from a specified subreddit.
/// </summary>
private async Task LoadPostsAsync()
{
// TODO...
}
}
Examinons maintenant ce dont nous avons besoin pour le viewmodel du widget de publication. Il s’agit d’un viewmodel beaucoup plus simple, car il n’a en réalité qu’à exposer une propriété Post contenant la publication actuellement sélectionnée, et à recevoir des messages diffusés par le widget de subreddit pour mettre à jour la propriété Post. Il peut ressembler à ceci :
public sealed class PostWidgetViewModel : ObservableRecipient, IRecipient<PropertyChangedMessage<object>>
{
private object post;
/// <summary>
/// Gets the currently selected post, if any.
/// </summary>
public object Post
{
get => post;
private set => SetProperty(ref post, value);
}
/// <inheritdoc/>
public void Receive(PropertyChangedMessage<object> message)
{
if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) &&
message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost))
{
Post = message.NewValue;
}
}
}
Dans ce cas, nous utilisons l’interface IRecipient<TMessage> pour déclarer les messages que nous voulons que notre viewmodel reçoive. Les gestionnaires des messages déclarés seront ajoutés automatiquement par la classe ObservableRecipient lorsque la propriété IsActive est définie sur true. Notez qu’il n’est pas obligatoire d’utiliser cette approche et que l’inscription manuelle de chaque gestionnaire de messages est également possible, comme suit :
public sealed class PostWidgetViewModel : ObservableRecipient
{
protected override void OnActivated()
{
// We use a method group here, but a lambda expression is also valid
Messenger.Register<PostWidgetViewModel, PropertyChangedMessage<object>>(this, (r, m) => r.Receive(m));
}
/// <inheritdoc/>
public void Receive(PropertyChangedMessage<object> message)
{
if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) &&
message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost))
{
Post = message.NewValue;
}
}
}
Nous avons maintenant un brouillon de nos viewmodels prêts, et nous pouvons commencer à examiner les services dont nous avons besoin.
Création du service de paramètres
Note
L’exemple est généré à l’aide du modèle d’injection de dépendances, qui est l’approche recommandée pour traiter les services dans les viewmodels. Il est également possible d’utiliser d’autres modèles, tels que le modèle de localisateur de services, mais le kit de ressources MVVM n’offre pas d’API intégrées pour l’activer.
Étant donné que nous voulons que certaines de nos propriétés soient enregistrées et persistantes, nous avons besoin d’un moyen pour que les viewmodels puissent interagir avec les paramètres de l’application. Nous ne devrions pas utiliser d'API spécifiques à la plateforme directement dans nos viewmodels, car cela nous empêcherait d'avoir tous nos viewmodels dans un projet portable .NET Standard. Nous pouvons résoudre ce problème en utilisant des services et les API de la bibliothèque Microsoft.Extensions.DependencyInjection pour configurer notre instance IServiceProvider pour l’application. L’idée est d’écrire des interfaces qui représentent toute la surface d’API dont nous avons besoin, puis d’implémenter des types spécifiques à la plateforme qui implémentent cette interface sur toutes nos cibles d’application. Les viewmodels interagissent uniquement avec les interfaces. Ils n’ont donc aucune référence forte à un type spécifique à la plateforme.
Voici une interface simple pour un service de paramètres :
public interface ISettingsService
{
/// <summary>
/// Assigns a value to a settings key.
/// </summary>
/// <typeparam name="T">The type of the object bound to the key.</typeparam>
/// <param name="key">The key to check.</param>
/// <param name="value">The value to assign to the setting key.</param>
void SetValue<T>(string key, T value);
/// <summary>
/// Reads a value from the current <see cref="IServiceProvider"/> instance and returns its casting in the right type.
/// </summary>
/// <typeparam name="T">The type of the object to retrieve.</typeparam>
/// <param name="key">The key associated to the requested object.</param>
[Pure]
T GetValue<T>(string key);
}
Nous pouvons supposer que les types spécifiques à la plateforme qui implémentent cette interface s’occupent de traiter toute la logique nécessaire pour sérialiser réellement les paramètres, les stocker sur le disque, puis les lire. Nous pouvons maintenant utiliser ce service dans notre SubredditWidgetViewModel, afin de rendre la SelectedSubreddit propriété persistante :
/// <summary>
/// Gets the <see cref="ISettingsService"/> instance to use.
/// </summary>
private readonly ISettingsService SettingsService;
/// <summary>
/// Creates a new <see cref="SubredditWidgetViewModel"/> instance.
/// </summary>
public SubredditWidgetViewModel(ISettingsService settingsService)
{
SettingsService = settingsService;
selectedSubreddit = settingsService.GetValue<string>(nameof(SelectedSubreddit)) ?? Subreddits[0];
}
private string selectedSubreddit;
/// <summary>
/// Gets or sets the currently selected subreddit.
/// </summary>
public string SelectedSubreddit
{
get => selectedSubreddit;
set
{
SetProperty(ref selectedSubreddit, value);
SettingsService.SetValue(nameof(SelectedSubreddit), value);
}
}
Ici, nous utilisons l’injection de dépendances et l’injection par constructeur, comme indiqué ci-dessus. Nous avons déclaré un ISettingsService SettingsService champ qui stocke simplement notre service de paramètres (que nous recevons en tant que paramètre dans le constructeur viewmodel), puis nous initialisons la SelectedSubreddit propriété dans le constructeur en utilisant la valeur précédente ou simplement le premier sous-dit disponible. Ensuite, nous avons également modifié le SelectedSubreddit setter afin qu’il utilise également le service de paramètres pour enregistrer la nouvelle valeur sur le disque.
Génial! Maintenant, nous devons simplement écrire une version spécifique de la plateforme de ce service, cette fois directement à l’intérieur de l’un de nos projets d’application. Voici à quoi peut ressembler ce service sur UWP :
public sealed class SettingsService : ISettingsService
{
/// <summary>
/// The <see cref="IPropertySet"/> with the settings targeted by the current instance.
/// </summary>
private readonly IPropertySet SettingsStorage = ApplicationData.Current.LocalSettings.Values;
/// <inheritdoc/>
public void SetValue<T>(string key, T value)
{
if (!SettingsStorage.ContainsKey(key)) SettingsStorage.Add(key, value);
else SettingsStorage[key] = value;
}
/// <inheritdoc/>
public T GetValue<T>(string key)
{
if (SettingsStorage.TryGetValue(key, out object value))
{
return (T)value;
}
return default;
}
}
La dernière partie du puzzle consiste à injecter ce service spécifique à la plateforme dans notre instance de fournisseur de services. Nous pouvons le faire au démarrage, comme suit :
/// <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();
}
Cela enregistrera une instance singleton de notre SettingsService comme type implémentant ISettingsService. Nous enregistrons également le PostWidgetViewModel en tant que service transitoire, ce qui signifie qu’à chaque fois que nous récupérons une instance, il s’agira d’une nouvelle instance (cela peut être utile si vous souhaitez disposer de plusieurs widgets d’article indépendants). Cela signifie que chaque fois que nous résolvons une ISettingsService instance pendant que l’application en cours d’utilisation est UWP, elle recevra une SettingsService instance, qui utilisera les API UWP derrière la scène pour manipuler les paramètres. Parfait.
Création du service Reddit
Le dernier composant du back-end manquant est un service capable d’utiliser les API REST Reddit pour récupérer les publications à partir des sous-titres qui nous intéressent. Pour la créer, nous allons utiliser refit, qui est une bibliothèque permettant de créer facilement des services typés de manière sûre pour communiquer avec des API REST. Comme précédemment, nous devons définir l’interface avec toutes les API que notre service implémentera, comme suit :
public interface IRedditService
{
/// <summary>
/// Get a list of posts from a given subreddit
/// </summary>
/// <param name="subreddit">The subreddit name.</param>
[Get("/r/{subreddit}/.json")]
Task<PostsQueryResponse> GetSubredditPostsAsync(string subreddit);
}
Il s’agit PostsQueryResponse d’un modèle que nous avons écrit qui mappe la réponse JSON pour cette API. La structure exacte de cette classe n’est pas importante : il suffit de dire qu’elle contient une collection d’éléments Post, qui sont des modèles simples représentant nos billets, comme ceci :
public class Post
{
/// <summary>
/// Gets or sets the title of the post.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets the URL to the post thumbnail, if present.
/// </summary>
public string Thumbnail { get; set; }
/// <summary>
/// Gets the text of the post.
/// </summary>
public string SelfText { get; }
}
Une fois que nous avons notre service et nos modèles, nous pouvons les brancher dans nos viewmodels pour terminer notre back-end. Ce faisant, nous pouvons également remplacer ces espaces réservés object par le type Post que nous avons défini :
public sealed class SubredditWidgetViewModel : ObservableRecipient
{
/// <summary>
/// Gets the <see cref="IRedditService"/> instance to use.
/// </summary>
private readonly IRedditService RedditService = Ioc.Default.GetRequiredService<IRedditService>();
/// <summary>
/// Loads the posts from a specified subreddit.
/// </summary>
private async Task LoadPostsAsync()
{
var response = await RedditService.GetSubredditPostsAsync(SelectedSubreddit);
Posts.Clear();
foreach (var item in response.Data.Items)
{
Posts.Add(item.Data);
}
}
}
Nous avons ajouté un nouveau IRedditService champ pour stocker notre service, comme nous l’avons fait pour le service de paramètres, et nous avons implémenté notre LoadPostsAsync méthode, qui était précédemment vide.
Le dernier élément manquant consiste maintenant simplement à injecter le service réel dans notre fournisseur de services. La grande différence dans ce cas est qu’en utilisant refit nous n’avons pas réellement besoin d’implémenter le service du tout ! La bibliothèque crée automatiquement un type implémentant le service pour nous, en arrière-plan. Nous n’avons donc besoin que d’obtenir une IRedditService instance et de l’injecter directement, comme suit :
/// <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();
}
Et c’est tout ce que nous devons faire ! Nous avons maintenant tous nos back-ends prêts à être utilisés, y compris deux services personnalisés que nous avons créés spécifiquement pour cette application ! 🎉
Génération de l’interface utilisateur
Maintenant que tout le back-end est terminé, nous pouvons écrire l’interface utilisateur pour nos widgets. Notez comment utiliser le modèle MVVM nous permet de nous concentrer exclusivement sur la logique métier au début, sans avoir à écrire de code lié à l’interface utilisateur jusqu’à présent. Ici, nous allons supprimer tout le code d’interface utilisateur qui n’interagit pas avec nos viewmodels, par souci de simplicité, et nous allons passer par chaque contrôle différent un par un. Le code source complet se trouve dans l’exemple d’application.
Avant de passer par les différents contrôles, voici comment résoudre les viewmodels pour toutes les différentes vues de notre application (par exemple, :PostWidgetView
public PostWidgetView()
{
this.InitializeComponent();
this.DataContext = App.Current.Services.GetService<PostWidgetViewModel>();
}
public PostWidgetViewModel ViewModel => (PostWidgetViewModel)DataContext;
Nous utilisons notre IServiceProvider instance pour résoudre l’objet PostWidgetViewModel dont nous avons besoin, qui est ensuite affecté à la propriété de contexte de données. Nous créons également une propriété fortement typée ViewModel qui convertit simplement le contexte de données en type viewmodel correct. Cela est nécessaire pour l’activer x:Bind dans le code XAML.
Commençons par le widget subreddit, qui propose une ComboBox sélection d’un sous-dit, un Button pour actualiser le flux, un ListView pour afficher les publications et un ProgressBar pour indiquer quand le flux est chargé. Nous partons du principe que la ViewModel propriété représente une instance du viewmodel que nous avons décrit précédemment. Cela peut être déclaré en XAML ou directement dans le code-behind.
Sélecteur de subreddit :
<ComboBox
ItemsSource="{x:Bind ViewModel.Subreddits}"
SelectedItem="{x:Bind ViewModel.SelectedSubreddit, Mode=TwoWay}">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="SelectionChanged">
<core:InvokeCommandAction Command="{x:Bind ViewModel.LoadPostsCommand}"/>
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</ComboBox>
Ici, nous liez la source à la Subreddits propriété et l’élément sélectionné à la SelectedSubreddit propriété. Notez que la Subreddits propriété n’est liée qu’une seule fois, car la collection elle-même envoie des notifications de modification, tandis que la SelectedSubreddit propriété est liée au TwoWay mode, car nous avons besoin qu’elle puisse charger la valeur que nous récupérons à partir de nos paramètres, ainsi que la mise à jour de la propriété dans le viewmodel lorsque l’utilisateur modifie la sélection. En outre, nous utilisons un comportement XAML pour appeler notre commande chaque fois que la sélection change.
Bouton Actualiser :
<Button Command="{x:Bind ViewModel.LoadPostsCommand}"/>
Ce composant est extrêmement simple, nous allons simplement lier notre commande personnalisée à la Command propriété du bouton, afin que la commande soit appelée chaque fois que l’utilisateur clique dessus.
Liste des publications :
<ListView
ItemsSource="{x:Bind ViewModel.Posts}"
SelectedItem="{x:Bind ViewModel.SelectedPost, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Post">
<Grid>
<TextBlock Text="{x:Bind Title}"/>
<controls:ImageEx Source="{x:Bind Thumbnail}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Ici, nous avons un ListView qui lie la source et la sélection à une propriété de notre ViewModel, ainsi qu’un modèle utilisé pour afficher chaque article disponible. Nous utilisons x:DataType pour permettre x:Bind dans notre gabarit, et nous avons deux contrôles directement liés aux propriétés Title et Thumbnail de notre publication.
Barre de chargement :
<ProgressBar Visibility="{x:Bind ViewModel.LoadPostsCommand.IsRunning, Mode=OneWay}"/>
Ici, nous sommes liés à la IsRunning propriété, qui fait partie de l’interface IAsyncRelayCommand . Le AsyncRelayCommand type s’occupe de déclencher des notifications pour cette propriété chaque fois que l’opération asynchrone démarre ou se termine pour cette commande.
Le dernier élément manquant est l’interface utilisateur du widget de publication. Comme précédemment, nous avons supprimé tout le code lié à l’interface utilisateur qui n’était pas nécessaire pour interagir avec les viewmodels, par souci de simplicité. Le code source complet est disponible dans l’exemple d’application.
<Grid>
<!--Header-->
<Grid>
<TextBlock Text="{x:Bind ViewModel.Post.Title, Mode=OneWay}"/>
<controls:ImageEx Source="{x:Bind ViewModel.Post.Thumbnail, Mode=OneWay}"/>
</Grid>
<!--Content-->
<ScrollViewer>
<TextBlock Text="{x:Bind ViewModel.Post.SelfText, Mode=OneWay}"/>
</ScrollViewer>
</Grid>
Ici, nous avons simplement un en-tête, avec un contrôle TextBlock et un contrôle ImageEx qui lient leurs propriétés Text et Source aux propriétés correspondantes de notre modèle Post, ainsi qu’un simple TextBlock dans un ScrollViewer utilisé pour afficher le contenu d’exemple du billet sélectionné.
Exemple d’application
Exemple d’application disponible ici.
C’est bon ! 🚀
Nous avons maintenant créé tous nos viewmodels, les services nécessaires et l’interface utilisateur de nos widgets - notre navigateur Reddit simple est terminé ! Il s’agissait simplement d’un exemple de création d’une application en suivant le modèle MVVM et en utilisant les API du kit de ressources MVVM.
Comme indiqué ci-dessus, il s’agit uniquement d’une référence, et vous êtes libre de modifier cette structure pour répondre à vos besoins et/ou pour choisir et choisir uniquement un sous-ensemble de composants de la bibliothèque. Quelle que soit l’approche adoptée, le Toolkit MVVM doit fournir une base solide pour démarrer rapidement lors de la création d’une nouvelle application, en vous permettant de vous concentrer sur votre logique métier au lieu d’avoir à mettre en place manuellement toute l’infrastructure nécessaire pour assurer une prise en charge correcte du modèle MVVM.