Partager via


Présentation de l’IU distante

L’un des principaux objectifs du modèle VisualStudio.Extensibility est de permettre aux extensions de s’exécuter hors du processus Visual Studio. Cela présente un obstacle à l’ajout de la prise en charge de l’IU aux extensions, car la plupart des infrastructures d’IU sont en cours de traitement.

L’IU distante est un ensemble de classes qui vous permettent de définir des contrôles WPF dans une extension hors processus et de les afficher dans le cadre de l’IU de Visual Studio.

L’IU distante s’appuie fortement sur le modèle de conception Model-View-ViewModel en s’appuyant sur la liaison XAML et les données, les commandes (au lieu d’événements) et les déclencheurs (au lieu d’interagir avec l’arborescence logique à partir du code-behind).

Bien que l’IU distante ait été développée pour prendre en charge les extensions hors processus, les API VisualStudio.Extensibility qui s’appuient sur l’IU distante, comme ToolWindow, utiliseront également l’IU distante pour les extensions in-process.

Les principales différences entre l’IU distante et le développement WPF normal sont les suivantes :

  • La plupart des opérations d’IU distantes, y compris la liaison au contexte de données et à l’exécution de commandes, sont asynchrones.
  • Lors de la définition des types de données à utiliser dans les contextes de données de l’IU distante, ils doivent être décorés avec les attributs de DataContract et DataMember leur type doit être sérialisable par l’IU distante (voir ici pour plus d’informations).
  • L’IU distante n’autorise pas le référencement de vos propres contrôles personnalisés.
  • Un contrôle utilisateur distant est entièrement défini dans un fichier XAML unique qui fait référence à un objet de contexte de données unique (mais potentiellement complexe et imbriqué).
  • L’IU distante ne prend pas en charge le code behind ou les gestionnaires d’événements (les solutions de contournement sont décrites dans le document de concepts avancés de l’IU distante).
  • Un contrôle utilisateur distant est instancié dans le processus Visual Studio, et non dans le processus hébergeant l’extension : le code XAML ne peut pas référencer les types et les assemblys de l’extension, mais peut référencer des types et des assemblys à partir du processus Visual Studio.

Créer une extension Hello World de l’IU distante

Commencez par créer l’extension d’IU distante la plus simple. Suivez les instructions de création de votre première extension Visual Studio hors processus.

Vous devez à présent disposer d’une extension de travail avec une seule commande, l’étape suivante consiste à ajouter un ToolWindow et un RemoteUserControl. Le RemoteUserControl est l’équivalent de l’IU distante d’un contrôle utilisateur WPF.

Vous obtiendrez quatre fichiers :

  1. un fichier .cs pour la commande qui ouvre la fenêtre outil,
  2. un fichier .cs pour le ToolWindow qui fournit le RemoteUserControl à Visual Studio,
  3. un fichier .cs pour le RemoteUserControl qui fait référence à sa définition XAML,
  4. un fichier .xaml pour le RemoteUserControl.

Plus tard, vous ajoutez un contexte de données pour le RemoteUserControl, qui représente le ViewModel dans le modèle MVVM.

Mettre à jour la commande

Mettez à jour le code de la commande pour afficher la fenêtre outil à l’aide de ShowToolWindowAsync :

public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
    return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}

Vous pouvez également envisager de modifier CommandConfiguration et string-resources.json d’afficher un message d’affichage et une position plus appropriés :

public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
    Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
  "MyToolWindowCommand.DisplayName": "My Tool Window"
}

Créer la fenêtre outil

Créez un fichier MyToolWindow.cs et définissez une classe MyToolWindow qui étend ToolWindow.

La méthode GetContentAsync est censée retourner un IRemoteUserControl que vous allez définir à la prochaine étape. Étant donné que le contrôle utilisateur distant est jetable, veillez à le supprimer en remplaçant la méthode de Dispose(bool).

namespace MyToolWindowExtension;

using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.ToolWindows;
using Microsoft.VisualStudio.RpcContracts.RemoteUI;

[VisualStudioContribution]
internal class MyToolWindow : ToolWindow
{
    private readonly MyToolWindowContent content = new();

    public MyToolWindow(VisualStudioExtensibility extensibility)
        : base(extensibility)
    {
        Title = "My Tool Window";
    }

    public override ToolWindowConfiguration ToolWindowConfiguration => new()
    {
        Placement = ToolWindowPlacement.DocumentWell,
    };

    public override async Task<IRemoteUserControl> GetContentAsync(CancellationToken cancellationToken)
        => content;

    public override Task InitializeAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;

    protected override void Dispose(bool disposing)
    {
        if (disposing)
            content.Dispose();

        base.Dispose(disposing);
    }
}

Créez le contrôle utilisateur distant

Effectuez cette action sur trois fichiers :

Classe de contrôle utilisateur distant

La classe de contrôle utilisateur distant, nommée MyToolWindowContent, est simple :

namespace MyToolWindowExtension;

using Microsoft.VisualStudio.Extensibility.UI;

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: null)
    {
    }
}

Vous n’avez pas encore besoin d’un contexte de données. Par conséquent vous pouvez le définir à null pour l’instant.

Une classe qui étend RemoteUserControl utilise automatiquement la ressource incorporée XAML avec le même nom. Si vous souhaitez modifier ce comportement, remplacez la méthode de GetXamlAsync.

Définition XAML

Créez ensuite un fichier nommé MyToolWindowContent.xaml :

<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">
    <Label>Hello World</Label>
</DataTemplate>

La définition XAML du contrôle utilisateur distant est un XAML WPF normal qui décrit un DataTemplate. Ce code XAML est envoyé à Visual Studio et utilisé afin de remplir le contenu de la fenêtre outil. Nous utilisons un espace de noms spécial (attribut de xmlns) pour XAML de l’IU distante : http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml.

Définition du code XAML comme ressource incorporée

Enfin, ouvrez le fichier .csproj et vérifiez que le fichier XAML est traité comme une ressource incorporée :

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

Tel que décrit précédemment, le fichier XAML doit avoir le même nom que la classe de contrôle utilisateur distant. Pour être précis, le nom complet de la classe étendant RemoteUserControl doit correspondre au nom de la ressource incorporée. Par exemple, si le nom complet de la classe de contrôle utilisateur distant est MyToolWindowExtension.MyToolWindowContent, le nom de la ressource incorporée doit être MyToolWindowExtension.MyToolWindowContent.xaml. Par défaut, les ressources incorporées reçoivent un nom composé par l’espace de noms racine du projet, tout chemin d’accès de sous-dossier sous lequel ils peuvent être placés et leur nom de fichier. Cela peut créer des problèmes si votre classe de contrôle utilisateur à distance utilise un espace de noms autre que l’espace de noms racine du projet ou si le fichier xaml ne se trouve pas dans le dossier racine du projet. Au besoin, vous pouvez forcer un nom pour la ressource incorporée à l’aide de la balise LogicalName :

<ItemGroup>
  <EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
  <Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>

Test de l’extension

Vous devez maintenant pouvoir appuyer F5 pour le débogage de l’extension.

Capture d’écran montrant le menu et la fenêtre Outil.

Ajouter la prise en charge pour les thèmes

Il est judicieux d’écrire l’IU en gardant à l’esprit que Visual Studio peut être thème, ce qui entraîne l’utilisation de différentes couleurs.

Mettez à jour le code XAML pour utiliser les styles et les couleurs utilisés à travers Visual Studio :

<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>
        <Grid.Resources>
            <Style TargetType="Label" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogLabelStyleKey}}" />
        </Grid.Resources>
        <Label>Hello World</Label>
    </Grid>
</DataTemplate>

L’étiquette utilise désormais le même thème que le reste de l’IU de Visual Studio et change automatiquement de couleur lorsque l’utilisateur passe en mode sombre :

Capture d’écran montrant la fenêtre Outil en thème.

Ici, l’attribut xmlns fait référence à l’assembly Microsoft.VisualStudio.Shell.15.0, qui n’est pas l’une des dépendances d’extension. Ceci est correct, car ce code XAML est utilisé par le processus Visual Studio, qui a une dépendance sur Shell.15, et non par l’extension elle-même.

Pour bénéficier d’une meilleure expérience d’édition XAML, vous pouvez provisoirement ajouter un PackageReference à Microsoft.VisualStudio.Shell.15.0 au projet d’extension. N’oubliez pas de le supprimer plus tard, étant donné qu’une extension VisualStudio.Extensibility hors processus ne doit pas référencer ce package !

Ajouter un contexte de données

Ajoutez une classe de contexte de données pour le contrôle utilisateur distant :

using System.Runtime.Serialization;

namespace MyToolWindowExtension;

[DataContract]
internal class MyToolWindowData
{
    [DataMember]
    public string? LabelText { get; init; }
}

et mettez à jour MyToolWindowContent.cs et MyToolWindowContent.xaml l’utiliser :

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
    {
    }
<Label Content="{Binding LabelText}" />

Le contenu de l’étiquette est maintenant défini à travers la liaison de données :

Capture d’écran montrant la fenêtre outil avec la liaison de données.

Le type de contexte de données ici est marqué avec les attributs de DataContract et DataMember. Ceci est dû au fait que l’instance MyToolWindowData existe dans le processus hôte d’extension alors que le contrôle WPF créé à partir de MyToolWindowContent.xaml existe dans le processus Visual Studio. Pour que la liaison de données fonctionne, l’infrastructure d’IU distante génère un proxy de l’objet MyToolWindowData dans le processus Visual Studio. Les attributs DataContract et DataMember indiquent quels types et propriétés sont pertinents pour la liaison de données et doivent être répliqués dans le proxy.

Le contexte de données du contrôle utilisateur distant est passé en tant que paramètre de constructeur de la classe RemoteUserControl : la propriété RemoteUserControl.DataContext est en lecture seule. Ceci n’implique pas que le contexte de données entier est immuable, mais l’objet de contexte de données racine d’un contrôle utilisateur distant ne peut pas être remplacé. Dans la prochaine section, nous allons rendre MyToolWindowData mutable et observable.

Types sérialisables et contexte de données de l’IU distante

Un contexte de données de l’IU distante ne peut contenir que des types sérialisables ou, pour être plus précis, seules les propriétés de DataMember d’un type sérialisable peuvent être liées aux données.

Seuls les types suivants sont sérialisables par l’IU distante :

  • données primitives (la plupart des types numériques .NET, enums, bool, string, DateTime)
  • types étendus définis par l’extension marqués avec les attributs de DataContract et DataMember (et tous leurs membres de données sont également sérialisables)
  • objets qui implémentent IAsyncCommand
  • Objets XamlFragment, SolidColorBrush et valeurs Color
  • valeurs de Nullable<> d’un type sérialisable
  • collections de types sérialisables, y compris les collections observables.

Cycle de vie d’un contrôle utilisateur distant

Vous pouvez remplacer la méthode de ControlLoadedAsync à notifier lorsque le contrôle est chargé pour la première fois dans un conteneur WPF. Si dans votre implémentation, l’état du contexte de données peut changer indépendamment des événements d’IU, la méthode ControlLoadedAsync est le bon endroit pour initialiser le contenu du contexte de données et commencer à y appliquer des modifications.

Vous pouvez également remplacer la méthode de Dispose pour être averti lorsque le contrôle est détruit et ne sera plus utilisé.

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: new MyToolWindowData())
    {
    }

    public override async Task ControlLoadedAsync(CancellationToken cancellationToken)
    {
        await base.ControlLoadedAsync(cancellationToken);
        // Your code here
    }

    protected override void Dispose(bool disposing)
    {
        // Your code here
        base.Dispose(disposing);
    }
}

Commandes, observabilité et liaison de données bidirectionnelle

Puis nous allons rendre le contexte de données observable et ajouter un bouton à la boîte à outils.

Le contexte de données peut être observable en implémentant INotifyPropertyChanged. L’IU distante fournit également une classe abstraite pratique, NotifyPropertyChangedObject, que nous pouvons étendre pour réduire le code réutilisable.

Un contexte de données comprend généralement une combinaison de propriétés en lecture seule et de propriétés observables. Le contexte de données peut être un graphique complexe d’objets tant qu’ils sont marqués avec les attributs DataContract et DataMember implémentent INotifyPropertyChanged au besoin. Il est également possible d’avoir des collections observables ou un 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.

Nous devons aussi ajouter une commande au contexte de données. Dans l’IU distante, les commandes implémentent IAsyncCommand, mais il est souvent plus facile de créer une instance de la classe AsyncCommand.

IAsyncCommand diffère de deux ICommand façons :

  • La méthode Execute est remplacée par ExecuteAsync parce que tout ce qui se trouve dans l’IU distante est asynchrone !
  • La méthode CanExecute(object) est remplacée par une propriété de CanExecute. La classe AsyncCommand se charge de rendre CanExecute observable.

Il est important de noter que l’IU distante ne prend pas en charge les gestionnaires d’événements. Par conséquent, toutes les notifications de l’IU vers l’extension doivent être implémentées via la liaison de données et les commandes.

Il s’agit du code résultant pour MyToolWindowData :

[DataContract]
internal class MyToolWindowData : NotifyPropertyChangedObject
{
    public MyToolWindowData()
    {
        HelloCommand = new((parameter, cancellationToken) =>
        {
            Text = $"Hello {Name}!";
            return Task.CompletedTask;
        });
    }

    private string _name = string.Empty;
    [DataMember]
    public string Name
    {
        get => _name;
        set => SetProperty(ref this._name, value);
    }

    private string _text = string.Empty;
    [DataMember]
    public string Text
    {
        get => _text;
        set => SetProperty(ref this._text, value);
    }

    [DataMember]
    public AsyncCommand HelloCommand { get; }
}

Corrigez le constructeur de MyToolWindowContent :

public MyToolWindowContent()
    : base(dataContext: new MyToolWindowData())
{
}

Mettre à jour MyToolWindowContent.xaml pour utiliser les nouvelles propriétés dans le contexte de données. Il s’agit de tous les XAML WPF normaux. Même l’objet IAsyncCommand est accessible via un proxy nommé ICommand dans le processus Visual Studio afin qu’il puisse être lié aux données comme d’habitude.

<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>
        <Grid.Resources>
            <Style TargetType="Label" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogLabelStyleKey}}" />
            <Style TargetType="TextBox" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.TextBoxStyleKey}}" />
            <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.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Label Content="Name:" />
        <TextBox Text="{Binding Name}" Grid.Column="1" />
        <Button Content="Say Hello" Command="{Binding HelloCommand}" Grid.Column="2" />
        <TextBlock Text="{Binding Text}" Grid.ColumnSpan="2" Grid.Row="1" />
    </Grid>
</DataTemplate>

Diagramme de la fenêtre outil ayant une liaison bidirectionnelle et une commande.

Présentation de l’asynchronité dans l’IU distante

L’ensemble de la communication de l’IU distante pour cette fenêtre d’outil suit les étapes ci-après :

  1. Le contexte de données est accessible à l’aide d’un proxy à l’intérieur du processus Visual Studio avec son contenu d’origine,

  2. Le contrôle créé à partir de MyToolWindowContent.xaml données est lié au proxy de contexte de données,

  3. L’utilisateur saisit du texte dans la zone de texte, qui est affecté à la propriété de Name du proxy de contexte de données par le biais de la liaison de données. La nouvelle valeur de Name est propagée à l’objet MyToolWindowData .

  4. L’utilisateur clique sur le bouton qui provoque une cascade d’effets :

    • le HelloCommand dans le proxy de contexte de données est exécuté
    • l’exécution asynchrone du code AsyncCommand de l’extendeur est démarrée
    • le rappel asynchrone de HelloCommand met à jour la valeur de la propriété observable de Text
    • la nouvelle valeur de Text est propagée au proxy de contexte de données
    • le bloc de texte dans la fenêtre outil est mis à jour vers la nouvelle valeur de Text à travers la liaison de données

Diagramme de la fenêtre outil ayant une liaison bidirectionnelle et une communication des commandes.

Utilisation des paramètres de commande afin d’éviter les conditions de concurrence

Toutes les opérations impliquant la communication entre Visual Studio et l’extension (flèches bleues dans le diagramme) sont asynchrones. Il est important de considérer cet aspect dans la conception générale de l’extension.

Pour cela, si la cohérence est importante, il est préférable d’utiliser des paramètres de commande, au lieu de liaison bidirectionnelle, pour récupérer l’état du contexte de données au moment de l’exécution d’une commande.

Apportez cette modification en liant le bouton CommandParameter à Name :

<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />

Ensuite, modifiez le rappel de la commande pour utiliser le paramètre :

HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
    Text = $"Hello {(string)parameter!}!";
    return Task.CompletedTask;
});

Avec cette approche, la valeur de la propriété de Name est récupérée de façon synchrone à partir du proxy de contexte de données au moment du clic du bouton et envoyée à l’extension. Ceci évite toute condition de concurrence, en particulier si le rappel de HelloCommand est modifié à l’avenir pour production (avoir des expressions de await).

Les commandes asynchrones consomment des données à partir de plusieurs propriétés

L’utilisation d’un paramètre de commande n’est pas une option si la commande doit consommer plusieurs propriétés définies par l’utilisateur. Par exemple, si l’IU avait deux zones de texte : « Prénom » et « Nom ».

Dans ce cas, la solution consiste à récupérer, dans le rappel de commande asynchrone, la valeur de toutes les propriétés du contexte de données avant la production.

Vous pouvez voir ci-dessous un exemple où les valeurs de propriété de FirstName et LastName sont récupérées avant production pour vous assurer que la valeur au moment de l’appel de commande est utilisée :

HelloCommand = new(async (parameter, cancellationToken) =>
{
    string firstName = FirstName;
    string lastName = LastName;
    await Task.Delay(TimeSpan.FromSeconds(1));
    Text = $"Hello {firstName} {lastName}!";
});

Il est également important d’éviter la mise à jour asynchrone de l’extension de la valeur des propriétés qui peuvent aussi être mises à jour par l’utilisateur. En d’autres termes, évitez la liaison de données TwoWay.

Les informations fournies ici doivent suffire pour créer des composants d’IU distante simples. Pour des scénarios plus avancés, consultez les concepts avancés de l’IU distante.