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
etDataMember
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 :
- un fichier
.cs
pour la commande qui ouvre la fenêtre outil, - un fichier
.cs
pour leToolWindow
qui fournit leRemoteUserControl
à Visual Studio, - un fichier
.cs
pour leRemoteUserControl
qui fait référence à sa définition XAML, - un fichier
.xaml
pour leRemoteUserControl
.
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.
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 :
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 :
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
etDataMember
(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 parExecuteAsync
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é deCanExecute
. La classeAsyncCommand
se charge de rendreCanExecute
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>
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 :
Le contexte de données est accessible à l’aide d’un proxy à l’intérieur du processus Visual Studio avec son contenu d’origine,
Le contrôle créé à partir de
MyToolWindowContent.xaml
données est lié au proxy de contexte de données,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 deName
est propagée à l’objetMyToolWindowData
.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 deText
- 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
- le
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.
Contenu connexe
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.