Partager via


Tutoriel : IU distante avancée

Dans ce tutoriel, vous allez découvrir les concepts avancés de l’IU distante en modifiant de façon incrémentielle une fenêtre Outil qui affiche une liste de couleurs aléatoires :

Capture d’écran montrant la fenêtre Outil des couleurs aléatoires.

Voici ce que vous allez apprendre :

  • Comment plusieurs exécutions de commandes asynchrones peuvent s’exécuter en parallèle et comment désactiver les éléments d’IU lorsqu’une commande est en cours d’exécution.
  • Comment lier plusieurs boutons à la même commande asynchrone.
  • Comment les types de référence sont gérés dans le contexte de données de l’IU distante et son proxy.
  • Comment utiliser une commande asynchrone en tant que gestionnaire d’événements.
  • Comment désactiver un bouton unique lorsque le rappel de sa commande asynchrone s’exécute si plusieurs boutons sont liés à la même commande.
  • Comment utiliser des dictionnaires de ressources XAML à partir d’un contrôle d’IU distante.
  • Comment utiliser des types WPF, tels que des pinceaux complexes, dans le contexte de données de l’IU distante.
  • Comment l’IU distante gère le threading.

Ce tutoriel est basé sur l’article d’introduction de l’IU distante et s’attend à ce que vous disposiez d’une extension VisualStudio.Extensibility opérationnelle, notamment :

  1. un fichier .cs pour la commande qui ouvre la fenêtre outil,
  2. un fichier MyToolWindow.cs pour la classe de ToolWindow,
  3. un fichier MyToolWindowContent.cs pour la classe de RemoteUserControl,
  4. un fichier MyToolWindowContent.xaml de ressources incorporé pour la définition xaml de RemoteUserControl,
  5. un fichier MyToolWindowData.cs pour le contexte de données du RemoteUserControl.

Pour démarrer, mettez à jour MyToolWindowContent.xaml pour afficher un mode Liste et un bouton :

<DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:vs="http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml"
              xmlns:styles="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
              xmlns:colors="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0">
    <Grid x:Name="RootGrid">
        <Grid.Resources>
            <Style TargetType="ListView" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogListViewStyleKey}}" />
            <Style TargetType="Button" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ButtonStyleKey}}" />
            <Style TargetType="TextBlock">
                <Setter Property="Foreground" Value="{DynamicResource {x:Static styles:VsBrushes.WindowTextKey}}" />
            </Style>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListView ItemsSource="{Binding Colors}" HorizontalContentAlignment="Stretch">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="{Binding ColorText}" />
                        <Rectangle Fill="{Binding Color}" Width="50px" Grid.Column="1" />
                        <Button Content="Remove" Grid.Column="2" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Button Content="Add color" Command="{Binding AddColorCommand}" Grid.Row="1" />
    </Grid>
</DataTemplate>

Ensuite mettez à jour la classe du contexte de données MyToolWindowData.cs :

using Microsoft.VisualStudio.Extensibility.UI;
using System.Collections.ObjectModel;
using System.Runtime.Serialization;
using System.Text;
using System.Windows.Media;

namespace MyToolWindowExtension;

[DataContract]
internal class MyToolWindowData
{
    private Random random = new();

    public MyToolWindowData()
    {
        AddColorCommand = new AsyncCommand(async (parameter, cancellationToken) =>
        {
            await Task.Delay(TimeSpan.FromSeconds(2));

            var color = new byte[3];
            random.NextBytes(color);
            Colors.Add(new MyColor(color[0], color[1], color[2]));
        });
    }

    [DataMember]
    public ObservableList<MyColor> Colors { get; } = new();

    [DataMember]
    public AsyncCommand AddColorCommand { get; }

    [DataContract]
    public class MyColor
    {
        public MyColor(byte r, byte g, byte b)
        {
            ColorText = Color = $"#{r:X2}{g:X2}{b:X2}";
        }

        [DataMember]
        public string ColorText { get; }

        [DataMember]
        public string Color { get; }
    }
}

Il n’y a que quelques éléments notables dans ce code :

  • MyColor.Color est un string, mais il est utilisé comme un Brush lorsque les données sont liées dans XAML, il s’agit d’une fonctionnalité fournie par WPF.
  • Le rappel asynchrone de AddColorCommand contient un délai de 2 secondes pour simuler une opération de longue durée.
  • Nous utilisons ObservableList<T>, qui est un ObservableCollection<T> étendu fourni par l’IU distante pour prendre également en charge les opérations de plage, ce qui favorise de meilleures performances.
  • MyToolWindowData et MyColor n’implémentent pas INotifyPropertyChanged car, à l’heure actuelle, toutes les propriétés sont readonly.

Gérer les commandes asynchrones durables

L’une des différences les plus importantes entre l’IU distante et WPF normale est que toutes les opérations qui impliquent la communication entre l’IU et l’extension sont asynchrones.

Les commandes asynchrones telles que AddColorCommand rendent cela explicite en fournissant un rappel asynchrone.

Vous pouvez voir son effet si vous cliquez sur le bouton Ajouter une couleur plusieurs fois dans un court délai : étant donné que chaque exécution de commande prend 2 secondes, plusieurs exécutions se produisent en parallèle et plusieurs couleurs apparaissent dans la liste ensemble lorsque le délai de 2 secondes est terminé. Ceci peut donner l’impression à l’utilisateur que le bouton Ajouter une couleur ne fonctionne pas.

Diagramme de l’exécution des commandes asynchrones superposées.

Pour résoudre ce problème, désactivez le bouton pendant l’exécution de la commande asynchrone. La façon la plus simple d’effectuer cette opération consiste à définir CanExecute pour la commande sur faux :

AddColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
    AddColorCommand!.CanExecute = false;
    try
    {
        await Task.Delay(TimeSpan.FromSeconds(2));
        var color = new byte[3];
        random.NextBytes(color);
        Colors.Add(new MyColor(color[0], color[1], color[2]));
    }
    finally
    {
        AddColorCommand.CanExecute = true;
    }
});

Cette solution a toujours une synchronisation incorrecte depuis que, lorsque l’utilisateur clique sur le bouton, le rappel de commande est exécuté de manière asynchrone dans l’extension, le rappel définit CanExecute à false, qui est ensuite propagé de manière asynchrone au contexte de données proxy dans le processus Visual Studio, ce qui entraîne la désactivation du bouton. L’utilisateur peut cliquer deux fois sur le bouton en succession rapide avant la désactivation du bouton.

Une meilleure solution consiste à utiliser la propriété de RunningCommandsCount des commandes asynchrones :

<Button Content="Add color" Command="{Binding AddColorCommand}" IsEnabled="{Binding AddColorCommand.RunningCommandsCount.IsZero}" Grid.Row="1" />

RunningCommandsCount est un compteur du nombre d’exécutions asynchrones simultanées de la commande en cours. Ce compteur est incrémenté sur le thread d'interface utilisateur dès qu’on clique sur le bouton, ce qui permet de désactiver de manière synchrone le bouton en liant IsEnabled à RunningCommandsCount.IsZero.

Étant donné que toutes les commandes d’IU distantes s’exécutent de manière asynchrone, la meilleure pratique consiste à toujours utiliser RunningCommandsCount.IsZero pour désactiver les contrôles le cas échéant, même si la commande est censée s’exécuter rapidement.

Commandes asynchrones et modèles de données

Dans cette section, vous implémentez le bouton Supprimer, qui permet à l’utilisateur de supprimer une entrée de la liste. Nous pouvons créer une commande asynchrone pour chaque objet de MyColor ou disposer d’une seule commande asynchrone dans MyToolWindowData et utiliser un paramètre pour identifier la couleur à supprimer. Cette dernière option est une conception plus dégagée. Nous allons donc l’implémenter.

  1. Mettez à jour le XAML du bouton dans le modèle de données :
<Button Content="Remove" Grid.Column="2"
        Command="{Binding DataContext.RemoveColorCommand,
            RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}}"
        CommandParameter="{Binding}"
        IsEnabled="{Binding DataContext.RemoveColorCommand.RunningCommandsCount.IsZero,
            RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListView}}}" />
  1. Ajouter le AsyncCommand correspondant à MyToolWindowData :
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
  1. Définissez le rappel asynchrone de la commande dans le constructeur de MyToolWindowData :
RemoveColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
    await Task.Delay(TimeSpan.FromSeconds(2));

    Colors.Remove((MyColor)parameter!);
});

Ce code utilise un Task.Delay pour simuler une exécution durable de la commande asynchrone.

Types de référence dans le contexte de données

Dans le code précédent, un objet MyColor est reçu comme paramètre d’une commande asynchrone et utilisé comme paramètre d’un appel de List<T>.Remove, qui utilise l’égalité de référence (étant donné que MyColor est un type de référence qui ne remplace pas Equals) pour identifier l’élément à supprimer. Ceci est possible car, même si le paramètre est reçu de l’IU, l’instance exacte de MyColor ce qui fait actuellement partie du contexte de données est reçue, et non une copie.

Processus de

  • proxy du contexte de données d’un contrôle utilisateur distant‭‬ ;
  • envoi des mises à jour de INotifyPropertyChanged de l’extension vers Visual Studio ou inversement ;
  • envoi des mises à jour des collections observables de l’extension vers Visual Studio ou inversement ;
  • envoi de paramètres de commande asynchrone

tous respectent l’identité des objets de type référence. À l’exception des chaînes, les objets de type référence ne sont jamais dupliqués au moment du transfert vers l’extension.

Diagramme des types de référence de liaison de données de l’IU distante.

Dans l’image, vous pouvez voir comment chaque objet de type référence dans le contexte de données (les commandes, la collection, chaque MyColor et même le contexte de données entier) est affecté à un identificateur unique par l’infrastructure de l’IU distante. Lorsque l’utilisateur clique sur le bouton Supprimer de l’objet de couleur proxy #5, l’identificateur unique (#5), et non la valeur de l’objet, est renvoyé à l’extension. L’infrastructure de l’IU distante s’occupe de récupérer l’objet correspondant MyColor et de le transmettre en tant que paramètre au rappel de la commande asynchrone.

RunningCommandsCount avec plusieurs liaisons et gestion des événements

Si vous testez l’extension à ce stade, vous remarquerez que lorsqu’on clique sur l’un des boutons Supprimer, tous les boutons Supprimer se désactivent :

Diagramme de commande asynchrone avec plusieurs liaisons.

Il peut s’agir du comportement souhaité. Toutefois, supposons que vous souhaitez que seul le bouton actif soit désactivé et que vous autorisez l’utilisateur à mettre en file d’attente plusieurs couleurs pour la suppression : nous ne pouvons pas utiliser la propriété de RunningCommandsCount de la commande asynchrone, car nous avons une seule commande partagée entre tous les boutons.

Nous pouvons atteindre notre objectif en attachant une propriété de RunningCommandsCount à chaque bouton afin de disposer d’un compteur distinct pour chaque couleur. Ces fonctionnalités sont fournies par l’espace de noms de http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml, ce qui vous permet d’utiliser des types d’IU distante à partir de XAML :

Nous modifions le bouton Supprimer en procédant comme suit :

<Button Content="Remove" Grid.Column="2"
        IsEnabled="{Binding Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero, RelativeSource={RelativeSource Self}}">
    <vs:ExtensibilityUICommands.EventHandlers>
        <vs:EventHandlerCollection>
            <vs:EventHandler Event="Click"
                             Command="{Binding DataContext.RemoveColorCommand, ElementName=RootGrid}"
                             CommandParameter="{Binding}"
                             CounterTarget="{Binding RelativeSource={RelativeSource Self}}" />
        </vs:EventHandlerCollection>
    </vs:ExtensibilityUICommands.EventHandlers>
</Button>

La propriété jointe de vs:ExtensibilityUICommands.EventHandlers permet d’affecter des commandes asynchrones à n’importe quel événement (par exempleMouseRightButtonUp) et peut être utile dans des scénarios plus avancés.

vs:EventHandler peut également avoir un CounterTarget : un UIElement auquel une propriété de vs:ExtensibilityUICommands.RunningCommandsCount doit être attachée, en comptant les exécutions actives liées à cet événement spécifique. Assurez-vous d’utiliser des parenthèses (par exemple Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero) lors de la liaison à une propriété jointe.

Dans ce cas, nous utilisons vs:EventHandler pour attacher à chaque bouton son propre compteur distinct d’exécutions de commandes actives. En liant IsEnabled à la propriété jointe, seul ce bouton spécifique est désactivé lorsque la couleur correspondante est supprimée :

Diagramme de commande asynchrone avec RunningCommandsCount ciblé.

Dictionnaires de ressources XAML de l’utilisateur

À compter de Visual Studio 17.10, l’IU distante prend en charge les dictionnaires de ressources XAML. Ceci permet à plusieurs contrôles d’IU distante de partager des styles, des modèles et d’autres ressources. Vous permet également définir différentes ressources (par exemple, des chaînes) pour différentes langues.

De même qu’un xaml de contrôle d’IU distante, les fichiers de ressources doivent être configurés en tant que ressources incorporées :

<ItemGroup>
  <EmbeddedResource Include="MyResources.xaml" />
  <Page Remove="MyResources.xaml" />
</ItemGroup>

L’IU distante référence les dictionnaires de ressources d’une manière différente de WPF : elles ne sont pas ajoutées aux dictionnaires fusionnés du contrôle (les dictionnaires fusionnés ne sont pas du tout pris en charge par l’IU distante), mais référencées par nom dans le fichier .cs du contrôle :

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: new MyToolWindowData())
    {
        this.ResourceDictionaries.AddEmbeddedResource(
            "MyToolWindowExtension.MyResources.xaml");
    }
...

AddEmbeddedResource prend le nom complet de la ressource incorporée qui, par défaut, comprend l’espace de noms racine pour le projet, tout chemin d’accès de sous-dossier sous lequel ils peuvent être placés et leur nom de fichier. Il est possible de remplacer ce nom en définissant une valeur de LogicalName pour le EmbeddedResource dans le fichier projet.

Le fichier de ressources lui-même est un dictionnaire de ressources WPF normal :

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:system="clr-namespace:System;assembly=mscorlib">
  <system:String x:Key="removeButtonText">Remove</system:String>
  <system:String x:Key="addButtonText">Add color</system:String>
</ResourceDictionary>

Vous pouvez référencer une ressource à partir du dictionnaire de ressources dans le contrôle d’IU distante à l’aide de DynamicResource :

<Button Content="{DynamicResource removeButtonText}" ...

Localisation des dictionnaires de ressources XAML

Les dictionnaires de ressources de l’IU distante peuvent être localisés de la même façon que vous localisez les ressources incorporées : vous créez d’autres fichiers XAML portant le même nom et un suffixe de langue, par exemple MyResources.it.xaml pour les ressources italiennes :

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:system="clr-namespace:System;assembly=mscorlib">
  <system:String x:Key="removeButtonText">Rimuovi</system:String>
  <system:String x:Key="addButtonText">Aggiungi colore</system:String>
</ResourceDictionary>

Vous pouvez utiliser des caractères génériques dans le fichier projet pour inclure tous les dictionnaires XAML localisés en tant que ressources incorporées :

<ItemGroup>
  <EmbeddedResource Include="MyResources.*xaml" />
  <Page Remove="MyResources.*xaml" />
</ItemGroup>

Utiliser des types WPF dans le contexte de données

Jusqu’à présent, le contexte de données de notre contrôle utilisateur distant comprend des primitives (nombres, chaînes, etc.), des collections observables et de nos propres classes marquées par DataContract. Il est parfois utile d’inclure des types WPF simples dans le contexte de données, comme des pinceaux complexes.

Étant donné qu’une extension VisualStudio.Extensibility ne peut même pas s’exécuter dans le processus Visual Studio, elle ne peut pas partager directement des objets WPF avec son IU. L’extension n’a peut-être même pas accès aux types WPF, car elle peut cibler netstandard2.0 ou net6.0 (pas la variante de -windows).

L’IU distante fournit le type XamlFragment, qui permet d’inclure une définition XAML d’un objet WPF dans le contexte de données d’un contrôle utilisateur distant :

[DataContract]
public class MyColor
{
    public MyColor(byte r, byte g, byte b)
    {
        ColorText = $"#{r:X2}{g:X2}{b:X2}";
        Color = new(@$"<LinearGradientBrush xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                               StartPoint=""0,0"" EndPoint=""1,1"">
                           <GradientStop Color=""Black"" Offset=""0.0"" />
                           <GradientStop Color=""{ColorText}"" Offset=""0.7"" />
                       </LinearGradientBrush>");
    }

    [DataMember]
    public string ColorText { get; }

    [DataMember]
    public XamlFragment Color { get; }
}

Avec le code ci-dessus, la valeur de propriété de Color est convertie en objet de LinearGradientBrush dans le proxy de contexte de données : Capture d’écran montrant les types WPF dans le contexte de données

IU distante et conversations

Les rappels de commandes asynchrones (et les rappels de INotifyPropertyChanged pour les valeurs mises à jour par l’IU via l’offre de données) sont déclenchés sur des conversations de poule de conversations aléatoires. Les rappels sont déclenchés chacun à son tour et ne se chevauchent pas tant que le code ne génère pas de contrôle (à l’aide d’une expression de await).

Ce comportement peut être modifié en passant un NonConcurrentSynchronizationContext au constructeur de RemoteUserControl. Dans ce cas, vous pouvez utiliser le contexte de synchronisation fourni pour toutes les commandes asynchrones et les rappels de INotifyPropertyChangedliés à ce contrôle.