Perché l'interfaccia utente remota
Uno degli obiettivi principali del modello VisualStudio.Extensibility è consentire l'esecuzione delle estensioni all'esterno del processo di Visual Studio. Questo introduce un ostacolo per l'aggiunta del supporto dell'interfaccia utente alle estensioni perché la maggior parte dei framework dell'interfaccia utente è in-process.
L'interfaccia utente remota è un set di classi che consentono di definire controlli WPF in un'estensione out-of-process e visualizzarli come parte dell'interfaccia utente di Visual Studio.
L'interfaccia utente remota si basa molto sul modello di progettazione Model-View-ViewModel che si basa su XAML e sul data binding, sui comandi (anziché sugli eventi) e sui trigger (anziché interagire con l'alberologico da code-behind).
Anche se l'interfaccia utente remota è stata sviluppata per supportare le estensioni out-of-process, le API VisualStudio.Extensibility che si basano sull'interfaccia utente remota, ad esempio ToolWindow
, useranno anche l'interfaccia utente remota per le estensioni in-process.
Le principali differenze tra l'interfaccia utente remota e lo sviluppo normale di WPF sono:
- La maggior parte delle operazioni remote dell'interfaccia utente, tra cui l'associazione al contesto dati e l'esecuzione dei comandi, sono asincrone.
- Quando si definiscono i tipi di dati da usare nei contesti di dati dell'interfaccia utente remota, devono essere decorati con gli
DataContract
attributi eDataMember
e il relativo tipo deve essere serializzabile dall'interfaccia utente remota (vedere qui per informazioni dettagliate). - L'interfaccia utente remota non consente di fare riferimento ai propri controlli personalizzati.
- Un controllo utente remoto è completamente definito in un singolo file XAML che fa riferimento a un singolo oggetto di contesto dati (ma potenzialmente complesso e annidato).
- L'interfaccia utente remota non supporta code behind o gestori eventi (le soluzioni alternative sono descritte nel documento concetti avanzati dell'interfaccia utente remota).
- Viene creata un'istanza di un controllo utente remoto nel processo di Visual Studio, non nel processo che ospita l'estensione: xaml non può fare riferimento a tipi e assembly dall'estensione, ma può fare riferimento a tipi e assembly dal processo di Visual Studio.
Creare un'estensione Hello World dell'interfaccia utente remota
Per iniziare, creare l'estensione dell'interfaccia utente remota più semplice. Seguire le istruzioni in Creazione della prima estensione out-of-process di Visual Studio.
A questo punto dovrebbe essere disponibile un'estensione funzionante con un singolo comando, il passaggio successivo consiste nell'aggiungere un ToolWindow
e un .RemoteUserControl
RemoteUserControl
è l'equivalente dell'interfaccia utente remota di un controllo utente WPF.
Si finirà con quattro file:
- un
.cs
file per il comando che apre la finestra degli strumenti, - un
.cs
file per cheToolWindow
fornisce aRemoteUserControl
Visual Studio, - un
.cs
file per l'oggettoRemoteUserControl
che fa riferimento alla relativa definizione XAML, - un
.xaml
file per l'oggettoRemoteUserControl
.
Successivamente, si aggiunge un contesto dati per , RemoteUserControl
che rappresenta viewModel nel modello MVVM.
Aggiornare il comando
Aggiornare il codice del comando per visualizzare la finestra degli strumenti usando ShowToolWindowAsync
:
public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}
È anche possibile prendere in considerazione la modifica CommandConfiguration
e string-resources.json
per un messaggio di visualizzazione e una posizione più appropriati:
public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
"MyToolWindowCommand.DisplayName": "My Tool Window"
}
Creare la finestra degli strumenti
Creare un nuovo MyToolWindow.cs
file e definire una MyToolWindow
classe che ToolWindow
estende .
Il GetContentAsync
metodo dovrebbe restituire un oggetto IRemoteUserControl
che verrà definito nel passaggio successivo. Poiché il controllo utente remoto è eliminabile, è necessario eliminarlo eseguendo l'override del Dispose(bool)
metodo .
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);
}
}
Creare il controllo utente remoto
Eseguire questa azione in tre file:
Classe di controllo utente remoto
La classe di controllo utente remoto, denominata MyToolWindowContent
, è semplice:
namespace MyToolWindowExtension;
using Microsoft.VisualStudio.Extensibility.UI;
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: null)
{
}
}
Non è ancora necessario un contesto dati, quindi è possibile impostarlo su null
per il momento.
Una classe che RemoteUserControl
estende automaticamente usa la risorsa incorporata XAML con lo stesso nome. Se si desidera modificare questo comportamento, eseguire l'override del GetXamlAsync
metodo .
Definizione XAML
Creare quindi un file denominato 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 definizione XAML del controllo utente remoto è un codice XAML WPF normale che descrive un oggetto DataTemplate
. Questo codice XAML viene inviato a Visual Studio e usato per riempire il contenuto della finestra degli strumenti. Usiamo uno spazio dei nomi speciale (xmlns
attributo) per XAML dell'interfaccia utente remota: http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml
.
Impostazione del codice XAML come risorsa incorporata
Infine, aprire il .csproj
file e assicurarsi che il file XAML venga considerato come una risorsa incorporata:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Come descritto in precedenza, il file XAML deve avere lo stesso nome della classe di controllo utente remoto. Per essere precisi, il nome completo della classe che estende RemoteUserControl
deve corrispondere al nome della risorsa incorporata. Ad esempio, se il nome completo della classe di controllo utente remoto è MyToolWindowExtension.MyToolWindowContent
, il nome della risorsa incorporata deve essere MyToolWindowExtension.MyToolWindowContent.xaml
. Per impostazione predefinita, alle risorse incorporate viene assegnato un nome composto dallo spazio dei nomi radice per il progetto, da qualsiasi percorso della sottocartella in cui possono trovarsi e dal nome del file. Questo può causare problemi se la classe di controllo utente remoto usa uno spazio dei nomi diverso dallo spazio dei nomi radice del progetto o se il file xaml non si trova nella cartella radice del progetto. Se necessario, è possibile forzare un nome per la risorsa incorporata usando il LogicalName
tag :
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Test dell'estensione
A questo momento dovrebbe essere possibile premere F5
per eseguire il debug dell'estensione.
Aggiungere il supporto per i temi
È consigliabile scrivere l'interfaccia utente tenendo presente che Visual Studio può essere sottoposto a tema con colori diversi.
Aggiornare il codice XAML per usare gli stili e i colori usati in 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'etichetta ora usa lo stesso tema del resto dell'interfaccia utente di Visual Studio e cambia automaticamente il colore quando l'utente passa alla modalità scura:
In questo caso, l'attributo xmlns
fa riferimento all'assembly Microsoft.VisualStudio.Shell.15.0 , che non è una delle dipendenze dell'estensione. Ciò è corretto perché questo codice XAML viene usato dal processo di Visual Studio, che ha una dipendenza da Shell.15, non dall'estensione stessa.
Per ottenere una migliore esperienza di modifica XAML, puoi aggiungere temporaneamente un oggetto PackageReference
al Microsoft.VisualStudio.Shell.15.0
progetto di estensione. Non dimenticare di rimuoverlo in un secondo momento perché un'estensione VisualStudio.Extensibility out-of-process non deve fare riferimento a questo pacchetto.
Aggiungere un contesto dei dati
Aggiungere una classe di contesto dati per il controllo utente remoto:
using System.Runtime.Serialization;
namespace MyToolWindowExtension;
[DataContract]
internal class MyToolWindowData
{
[DataMember]
public string? LabelText { get; init; }
}
e aggiornarlo MyToolWindowContent.cs
e MyToolWindowContent.xaml
usarlo:
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
{
}
<Label Content="{Binding LabelText}" />
Il contenuto dell'etichetta viene ora impostato tramite databinding:
Il tipo di contesto dati qui è contrassegnato con DataContract
gli attributi e DataMember
. Ciò è dovuto al fatto che l'istanza MyToolWindowData
esiste nel processo host dell'estensione mentre il controllo WPF creato da MyToolWindowContent.xaml
esiste nel processo di Visual Studio. Per consentire il funzionamento del data binding, l'infrastruttura dell'interfaccia utente remota genera un proxy dell'oggetto MyToolWindowData
nel processo di Visual Studio. Gli DataContract
attributi e DataMember
indicano quali tipi e proprietà sono rilevanti per il data binding e devono essere replicati nel proxy.
Il contesto dati del controllo utente remoto viene passato come parametro del costruttore della RemoteUserControl
classe : la RemoteUserControl.DataContext
proprietà è di sola lettura. Ciò non implica che l'intero contesto dei dati non sia modificabile, ma l'oggetto contesto dati radice di un controllo utente remoto non può essere sostituito. Nella sezione successiva si renderanno MyToolWindowData
modificabili e osservabili.
Tipi serializzabili e contesto dati dell'interfaccia utente remota
Un contesto dati dell'interfaccia utente remota può contenere solo tipi serializzabili o, per essere più precisi, solo DataMember
le proprietà di un tipo serializzabile possono essere in ingresso dati.
Solo i tipi seguenti sono serializzabili dall'interfaccia utente remota:
- dati primitivi (la maggior parte dei tipi numerici .NET, enums,
bool
,string
,DateTime
) - Tipi definiti da extender contrassegnati con
DataContract
gli attributi eDataMember
(e tutti i relativi membri dati sono serializzabili) - oggetti che implementano IAsyncCommand
- Oggetti XamlFragment e SolidColorBrush e valori Color
Nullable<>
valori per un tipo serializzabile- raccolte di tipi serializzabili, incluse le raccolte osservabili.
Ciclo di vita di un controllo utente remoto
È possibile eseguire l'override del ControlLoadedAsync
metodo per ricevere una notifica quando il controllo viene caricato per la prima volta in un contenitore WPF. Se nell'implementazione, lo stato del contesto dati può cambiare indipendentemente dagli eventi dell'interfaccia utente, il ControlLoadedAsync
metodo è il posto giusto per inizializzare il contenuto del contesto dati e iniziare ad applicare le modifiche.
È anche possibile eseguire l'override del Dispose
metodo per ricevere una notifica quando il controllo viene eliminato definitivamente e non verrà più usato.
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);
}
}
Comandi, osservabilità e data binding bidirezionale
Successivamente, rendere osservabile il contesto dei dati e aggiungere un pulsante alla casella degli strumenti.
Il contesto dei dati può essere reso osservabile implementando INotifyPropertyChanged. In alternativa, l'interfaccia utente remota fornisce una comoda classe astratta, NotifyPropertyChangedObject
, che è possibile estendere per ridurre il codice boilerplate.
Un contesto dati include in genere una combinazione di proprietà di sola lettura e proprietà osservabili. Il contesto dei dati può essere un grafico complesso di oggetti, purché siano contrassegnati con gli DataContract
attributi e DataMember
e implementi INotifyPropertyChanged in base alle esigenze. È anche possibile avere raccolte osservabili, o ObservableList <T>, che è un oggetto ObservableCollection<T> esteso fornito dall'interfaccia utente remota per supportare anche operazioni di intervallo, consentendo prestazioni migliori.
È anche necessario aggiungere un comando al contesto dati. Nell'interfaccia utente remota i comandi implementano IAsyncCommand
, ma spesso è più semplice creare un'istanza della AsyncCommand
classe .
IAsyncCommand
differisce da ICommand
in due modi:
- Il
Execute
metodo viene sostituito conExecuteAsync
perché tutto nell'interfaccia utente remota è asincrono. - Il
CanExecute(object)
metodo viene sostituito da unaCanExecute
proprietà . LaAsyncCommand
classe si occupa di rendereCanExecute
osservabile.
È importante notare che l'interfaccia utente remota non supporta i gestori eventi, quindi tutte le notifiche dall'interfaccia utente all'estensione devono essere implementate tramite databinding e comandi.
Questo è il codice risultante per 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; }
}
Correggere il MyToolWindowContent
costruttore:
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
}
Aggiornare MyToolWindowContent.xaml
per usare le nuove proprietà nel contesto dati. Questo è tutto il normale XAML WPF. Anche l'oggetto IAsyncCommand
è accessibile tramite un proxy chiamato ICommand
nel processo di Visual Studio in modo che possa essere associato a dati come di consueto.
<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>
Informazioni sull'asincronità nell'interfaccia utente remota
L'intera comunicazione dell'interfaccia utente remota per questa finestra degli strumenti segue questa procedura:
L'accesso al contesto dei dati viene eseguito tramite un proxy all'interno del processo di Visual Studio con il relativo contenuto originale.
Il controllo creato da è costituito da
MyToolWindowContent.xaml
dati associati al proxy del contesto dati,L'utente digita un testo nella casella di testo, assegnato alla
Name
proprietà del proxy del contesto dati tramite l'associazione dati. Il nuovo valore diName
viene propagato all'oggettoMyToolWindowData
.L'utente fa clic sul pulsante causando una cascata di effetti:
- nel
HelloCommand
proxy del contesto dati viene eseguito - viene avviata l'esecuzione asincrona del codice dell'extender
AsyncCommand
- callback asincrono per
HelloCommand
aggiornare il valore della proprietà osservabileText
- il nuovo valore di
Text
viene propagato al proxy del contesto dati - il blocco di testo nella finestra degli strumenti viene aggiornato al nuovo valore di
Text
tramite data binding
- nel
Uso dei parametri di comando per evitare race condition
Tutte le operazioni che coinvolgono la comunicazione tra Visual Studio e l'estensione (frecce blu nel diagramma) sono asincrone. È importante considerare questo aspetto nella progettazione complessiva dell'estensione.
Per questo motivo, se la coerenza è importante, è preferibile usare i parametri dei comandi, anziché l'associazione bidirezionale, per recuperare lo stato del contesto dati al momento dell'esecuzione di un comando.
Apportare questa modifica associando il pulsante CommandParameter
a Name
:
<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />
Modificare quindi il callback del comando per usare il parametro :
HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
Text = $"Hello {(string)parameter!}!";
return Task.CompletedTask;
});
Con questo approccio, il valore della Name
proprietà viene recuperato in modo sincrono dal proxy del contesto dati al momento del clic del pulsante e inviato all'estensione. In questo modo si evitano le race condition, soprattutto se il HelloCommand
callback viene modificato in futuro in modo da restituire (con await
espressioni).
I comandi asincroni usano i dati da più proprietà
L'uso di un parametro di comando non è un'opzione se il comando deve utilizzare più proprietà che sono impostabili dall'utente. Ad esempio, se l'interfaccia utente ha due caselle di testo: "Nome" e "Cognome".
In questo caso, la soluzione consiste nel recuperare, nel callback del comando asincrono, il valore di tutte le proprietà del contesto dati prima di restituire.
Di seguito è possibile visualizzare un esempio in cui i valori delle FirstName
proprietà e LastName
vengono recuperati prima di restituire per assicurarsi che il valore al momento della chiamata al comando venga usato:
HelloCommand = new(async (parameter, cancellationToken) =>
{
string firstName = FirstName;
string lastName = LastName;
await Task.Delay(TimeSpan.FromSeconds(1));
Text = $"Hello {firstName} {lastName}!";
});
È anche importante evitare che l'estensione aggiorni in modo asincrono il valore delle proprietà che possono essere aggiornate anche dall'utente. In altre parole, evitare il data binding TwoWay .
Contenuto correlato
Le informazioni qui dovrebbero essere sufficienti per compilare semplici componenti dell'interfaccia utente remota. Per scenari più avanzati, vedere Concetti avanzati relativi all'interfaccia utente remota.