Notes
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.
Dans ce tutoriel, vous allez découvrir les concepts avancés de l’interface utilisateur distante en modifiant de façon incrémentielle une fenêtre d’outil qui affiche une liste de couleurs aléatoires :
Vous en apprendrez davantage sur :
- Comment plusieurs exécutions de commandes asynchrones peuvent s’exécuter en parallèle et comment désactiver les éléments d’interface utilisateur 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’interface utilisateur distante et son proxy.
- Comment utiliser une commande asynchrone en tant que gestionnaire d’événements.
- Comment désactiver un seul bouton 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’interface utilisateur distant.
- Comment utiliser des types WPF, tels que des pinceaux complexes, dans le contexte de données de l’interface utilisateur distante.
- Comment l’interface utilisateur distante gère le threading.
Ce tutoriel est basé sur l’article d’introduction de l’interface utilisateur distante et s’attend à ce que vous disposiez d’une extension VisualStudio.Extensibility opérationnelle, notamment :
- un
.cs
fichier pour la commande qui ouvre la fenêtre outil, - un
MyToolWindow.cs
fichier pour laToolWindow
classe, - un
MyToolWindowContent.cs
fichier pour laRemoteUserControl
classe, - un fichier de ressources incorporé
MyToolWindowContent.xaml
pour la définition XAML deRemoteUserControl
, - un fichier
MyToolWindowData.cs
pour le contexte de données deRemoteUserControl
.
Pour démarrer, mettez à jour MyToolWindowContent.xaml
pour afficher un affichage de 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 MyToolWindowData.cs
de contexte de données :
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 remarquables dans ce code :
MyColor.Color
est unstring
, mais il est utilisé commeBrush
lorsque les données sont liées dans XAML. Il s'agit d'une fonctionnalité fournie par WPF.- Le
AddColorCommand
rappel asynchrone contient un délai de 2 secondes pour simuler une opération de longue durée. - Nous utilisons ObservableList<T>, qui est une ObservableCollection<T> étendue fournie par Remote UI pour prendre également en charge les opérations de plage, ce qui permet de meilleures performances.
MyToolWindowData
etMyColor
n’implémentent pas INotifyPropertyChanged car, à l’heure actuelle, toutes les propriétés sont en lecture seule.
Gérer les commandes asynchrones de longue durée
L’une des différences les plus importantes entre l’interface utilisateur distante et WPF normale est que toutes les opérations qui impliquent la communication entre l’interface utilisateur et l’extension sont asynchrones.
Commandes asynchrones telles que AddColorCommand
rendre cela explicite en fournissant un rappel asynchrone.
Vous pouvez voir l’effet de ceci 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é. Cela peut donner l’impression à l’utilisateur que le bouton Ajouter une couleur ne fonctionne pas.
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 simplement à définir la commande CanExecute
sur "false".
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 présente encore une synchronisation imparfaite car, 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
sur false
, ce qui est ensuite propagé de manière asynchrone au contexte de données proxy dans le processus Visual Studio, entraînant ainsi 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 RunningCommandsCount
propriété 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 de l'interface utilisateur dès que le bouton est cliqué, ce qui permet de désactiver le bouton de manière synchrone en le liant IsEnabled
à RunningCommandsCount.IsZero
.
Étant donné que toutes les commandes d’interface utilisateur 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 MyColor
objet ou disposer d’une seule commande asynchrone et MyToolWindowData
utiliser un paramètre pour identifier la couleur à supprimer. Cette dernière option est une conception plus propre. Nous allons donc implémenter cela.
- Mettez à jour le code 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}}}" />
- Ajoutez le correspondant
AsyncCommand
àMyToolWindowData
:
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
- 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 l'exécution longue d'une commande asynchrone.
Types de référence dans le contexte de données
Dans le code précédent, un MyColor
objet est reçu en tant que paramètre d’une commande asynchrone et utilisé comme paramètre d’un List<T>.Remove
appel, qui utilise l’égalité de référence (étant MyColor
un type de référence qui ne remplace Equals
pas ) pour identifier l’élément à supprimer. Cela est possible car, même si le paramètre est reçu de l’interface utilisateur, l’instance exacte de MyColor
ce qui fait actuellement partie du contexte de données est reçue, et non une copie.
Les processus
- de proxy du contexte de données d’un contrôle utilisateur distant ;
- d’envoi des mises à jour
INotifyPropertyChanged
de l’extension vers Visual Studio ou inversement ; - d’envoi des mises à jour de collection observable de l’extension vers Visual Studio, ou inversement ;
- d’envoi des 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 lors du transfert vers l’extension.
Dans l’image, vous pouvez voir comment chaque objet de type référence dans le contexte de données (les commandes, la collection, chacun MyColor
et même le contexte de données entier) est affecté à un identificateur unique par l’infrastructure de l’interface utilisateur 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’interface utilisateur 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, notez que lorsque l’un des boutons Supprimer est cliqué, tous les boutons Supprimer sont désactivés :
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 la RunningCommandsCount
, car nous avons une seule commande partagée entre tous les boutons.
Nous pouvons atteindre notre objectif en attachant une RunningCommandsCount
propriété à chaque bouton afin que nous disposions d’un compteur distinct pour chaque couleur. Ces fonctionnalités sont fournies par l’espace de noms http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml
, qui permet d’utiliser des types d’interface utilisateur distante depuis 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 vs:ExtensibilityUICommands.EventHandlers
propriété jointe permet d’affecter des commandes asynchrones à n’importe quel événement (par exemple MouseRightButtonUp
) et peut être utile dans des scénarios plus avancés.
vs:EventHandler
peut également avoir un(e) CounterTarget
: un(e) UIElement
auquel(à laquelle) une propriété vs:ExtensibilityUICommands.RunningCommandsCount
doit être attachée, liée à cet événement spécifique, en comptant les exécutions actives. Veillez à 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 :
Dictionnaires utilisateur de ressources XAML
À compter de Visual Studio 17.10, l’interface utilisateur distante prend en charge les dictionnaires de ressources XAML. Cela permet à plusieurs contrôles d’interface utilisateur distant de partager des styles, des modèles et d’autres ressources. Il vous permet également de définir différentes ressources (par exemple, des chaînes) pour différentes langues.
De même qu’un xaml de contrôle d’interface utilisateur distant, 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’interface utilisateur 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 pris en charge du tout par l’interface utilisateur 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, est composé de l’espace de noms racine du projet, de tout chemin d'accès de sous-dossier éventuel, et du nom du fichier. Il est possible de remplacer ce nom en définissant une LogicalName
pour le EmbeddedResource
dans le fichier de 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 du dictionnaire de ressources dans le contrôle d'interface utilisateur à distance en utilisant DynamicResource
.
<Button Content="{DynamicResource removeButtonText}" ...
Localisation des dictionnaires de ressources XAML
Les dictionnaires de ressources de l’interface utilisateur distante peuvent être localisés de la même façon que vous localisiez 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 a été composé de primitives (nombres, chaînes, etc.), de 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, tels que 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 interface utilisateur. 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 -windows
variante).
L’interface utilisateur distante fournit le XamlFragment
type, 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 Color
valeur de propriété est convertie en objet LinearGradientBrush
dans le proxy de contexte de données :
Interface utilisateur distante et threads
Les rappels de commande asynchrone (et les rappels INotifyPropertyChanged
pour les valeurs mises à jour par l’interface utilisateur via la liaison de données) sont déclenchés sur des threads aléatoires du pool de threads. Les rappels sont déclenchés un par un et ne se chevauchent pas tant que le code ne cède pas le contrôle (à l’aide d’une expression await
).
Ce comportement peut être modifié en passant un NonConcurrentSynchronizationContext au RemoteUserControl
constructeur. Dans ce cas, vous pouvez utiliser le contexte de synchronisation fourni pour toutes les commandes asynchrones et INotifyPropertyChanged
rappels liés à ce contrôle.