Gründe für eine Remotebenutzeroberfläche
Eines der Standard Ziele des VisualStudio.Extensibility-Modells besteht darin, Erweiterungen außerhalb des Visual Studio-Prozesses auszuführen. Dies führt zu einem Hindernis für das Hinzufügen von UI-Unterstützung zu Erweiterungen, da die meisten Benutzeroberflächenframeworks in Bearbeitung sind.
Remote-UI ist eine Reihe von Klassen, mit denen Sie WPF-Steuerelemente in einer Out-of-Process-Erweiterung definieren und als Teil der Visual Studio-Benutzeroberfläche anzeigen können.
Die Remotebenutzeroberfläche lehnt sich stark dem Entwurfsmuster "Model-View-ViewModel" zu, das sich auf XAML und Datenbindung, Befehle (anstelle von Ereignissen) und Trigger stützt (anstatt mit der logischen Struktur aus CodeBehind zu interagieren).
Während Remote-UI entwickelt wurde, um Out-of-Process-Erweiterungen zu unterstützen, verwenden VisualStudio.Extensibility-APIs, die auf Remote-UI basieren, z ToolWindow
. B. Remote-UI für In-Process-Erweiterungen.
Die Standard Unterschiede zwischen Remote-UI und normaler WPF-Entwicklung sind:
- Die meisten Remote-UI-Vorgänge, einschließlich der Bindung an den Datenkontext und die Ausführung von Befehlen, sind asynchron.
- Beim Definieren von Datentypen, die in Remote-UI-Datenkontexten verwendet werden sollen, müssen sie mit den
DataContract
Und-AttributenDataMember
versehen werden. - Die Remote-UI lässt keinen Verweis auf ihre eigenen benutzerdefinierten Steuerelemente zu.
- Ein Remotebenutzersteuerelement ist in einer einzelnen XAML-Datei vollständig definiert, die auf ein einzelnes (aber potenziell komplexes und geschachteltes) Datenkontextobjekt verweist.
- Die Remotebenutzeroberfläche unterstützt codeBehind oder Ereignishandler nicht (Problemumgehungen werden im Dokument für erweiterte Remote-UI-Konzepte beschrieben).
- Ein Remotebenutzersteuerelement wird im Visual Studio-Prozess instanziiert, nicht der Prozess, der die Erweiterung hostet: Der XAML-Code kann nicht auf Typen und Assemblys aus der Erweiterung verweisen, sondern auf Typen und Assemblys aus dem Visual Studio-Prozess verweisen.
Erstellen einer Remote-UI-Hallo Welt-Erweiterung
Erstellen Sie zunächst die einfachste Remote-UI-Erweiterung. Befolgen Sie die Anweisungen zum Erstellen Ihrer ersten out-of-process Visual Studio-Erweiterung.
Sie sollten nun über eine funktionierende Erweiterung mit einem einzigen Befehl verfügen, der nächste Schritt besteht darin, ein ToolWindow
und eins RemoteUserControl
hinzuzufügen. Dies RemoteUserControl
ist die Entsprechung der Remote-Benutzeroberfläche eines WPF-Benutzersteuerelements.
Sie werden mit vier Dateien enden:
- eine
.cs
Datei für den Befehl, der das Toolfenster öffnet, - eine
.cs
Datei für die, dieToolWindow
RemoteUserControl
Visual Studio bereitstellt, - eine
.cs
Datei für die XAML-Definition, dieRemoteUserControl
auf die XAML-Definition verweist, - eine
.xaml
Datei für dieRemoteUserControl
.
Später fügen Sie einen Datenkontext für den RemoteUserControl
, der das ViewModel im MVVM-Muster darstellt.
Aktualisieren des Befehls
Aktualisieren Sie den Code des Befehls, um das Toolfenster mithilfe ShowToolWindowAsync
von :
public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}
Sie können auch eine Änderung CommandConfiguration
und eine geeignetere Anzeigemeldung und string-resources.json
Platzierung in Betracht ziehen:
public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
"MyToolWindowCommand.DisplayName": "My Tool Window"
}
Erstellen des Toolfensters
Erstellen Sie eine neue MyToolWindow.cs
Datei, und definieren Sie eine MyToolWindow
Klassenerweiterung ToolWindow
.
Die GetContentAsync
Methode soll einen IRemoteUserControl
Wert zurückgeben, den Sie im nächsten Schritt definieren werden. Da die Fernbedienung einweg ist, kümmern Sie sich darum, sie zu entsorgen, indem Sie die Dispose(bool)
Methode überschreiben.
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);
}
}
Erstellen der Remotebenutzersteuerung
Führen Sie diese Aktion in drei Dateien aus:
Remotebenutzersteuerungsklasse
Die Remotebenutzersteuerungsklasse namens MyToolWindowContent
", ist einfach:
namespace MyToolWindowExtension;
using Microsoft.VisualStudio.Extensibility.UI;
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: null)
{
}
}
Sie benötigen noch keinen Datenkontext, damit Sie ihn null
für den jetzigen Zeitkontext festlegen können.
Eine Klasse, die erweitert wird RemoteUserControl
, verwendet automatisch die eingebettete XAML-Ressource mit demselben Namen. Wenn Sie dieses Verhalten ändern möchten, setzen Sie die GetXamlAsync
Methode außer Kraft.
XAML-Definition
Erstellen Sie als Nächstes eine Datei mit dem Namen 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>
Wie zuvor beschrieben, muss diese Datei denselben Namen wie die Remotebenutzersteuerungsklasse haben. Um genau zu sein, muss der vollständige Name der Klassenerweiterung RemoteUserControl
mit dem Namen der eingebetteten Ressource übereinstimmen. Wenn beispielsweise der vollständige Name der Remotebenutzersteuerungsklasse lautetMyToolWindowExtension.MyToolWindowContent
, sollte der name der eingebetteten Ressource seinMyToolWindowExtension.MyToolWindowContent.xaml
. Standardmäßig werden eingebettete Ressourcen einem Namen zugewiesen, der aus dem Stammnamespace für das Projekt besteht, alle Unterordnerpfade, unter denen sie stehen können, und deren Dateiname. Dies kann Probleme verursachen, wenn Ihre Remotebenutzersteuerungsklasse einen Namespace verwendet, der sich vom Stammnamespace des Projekts unterscheidet oder wenn sich die XAML-Datei nicht im Stammordner des Projekts befindet. Bei Bedarf können Sie mithilfe des LogicalName
Tags einen Namen für die eingebettete Ressource erzwingen:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Die XAML-Definition der Remotebenutzersteuerung ist ein normaler WPF-XAML-Code, der eine DataTemplate
Beschreibung beschreibt. Dieser XAML-Code wird an Visual Studio gesendet und zum Ausfüllen des Toolfensterinhalts verwendet. Wir verwenden einen speziellen Namespace (xmlns
Attribut) für Remote-UI-XAML: http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml
.
Festlegen des XAML-Codes als eingebettete Ressource
Öffnen Sie schließlich die .csproj
Datei, und stellen Sie sicher, dass die XAML-Datei als eingebettete Ressource behandelt wird:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Sie können auch das Zielframework für Ihre Erweiterung von net6.0
zu net6.0-windows
" ändern, um eine bessere AutoVervollständigen in der XAML-Datei zu erhalten.
Testen der Erweiterung
Sie sollten jetzt in F5
der Lage sein, die Erweiterung zu debuggen.
Hinzufügen von Unterstützung für Designs
Es empfiehlt sich, die Benutzeroberfläche zu schreiben, wobei Sie berücksichtigen, dass Visual Studio designiert werden kann, sodass unterschiedliche Farben verwendet werden.
Aktualisieren Sie den XAML-Code, um die formatvorlagen und Farben zu verwenden, die in Visual Studio verwendet werden:
<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>
Die Beschriftung verwendet jetzt das gleiche Design wie die restliche Visual Studio-Benutzeroberfläche und ändert die Farbe automatisch, wenn der Benutzer in den dunklen Modus wechselt:
Hier verweist das xmlns
Attribut auf die Microsoft.VisualStudio.Shell.15.0-Assembly , die keine der Erweiterungsabhängigkeiten ist. Dies ist in Ordnung, da dieser XAML-Code vom Visual Studio-Prozess verwendet wird, der eine Abhängigkeit von Shell.15 hat, nicht von der Erweiterung selbst.
Um eine bessere XAML-Bearbeitung zu erzielen, können Sie dem Erweiterungsprojekt vorübergehend einen Microsoft.VisualStudio.Shell.15.0
PackageReference
hinzufügen. Vergessen Sie nicht, es später zu entfernen, da eine out-of-process VisualStudio.Extensibility-Erweiterung nicht auf dieses Paket verweisen sollte!
Hinzufügen eines Datenkontexts
Fügen Sie eine Datenkontextklasse für die Remotebenutzersteuerung hinzu:
using System.Runtime.Serialization;
namespace MyToolWindowExtension;
[DataContract]
internal class MyToolWindowData
{
[DataMember]
public string? LabelText { get; init; }
}
und aktualisieren MyToolWindowContent.cs
und MyToolWindowContent.xaml
verwenden Sie es:
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
{
}
<Label Content="{Binding LabelText}" />
Der Inhalt der Bezeichnung wird jetzt über die Datenbindung festgelegt:
Der hier aufgeführte Datentyp ist mit DataContract
und DataMember
Attributen gekennzeichnet. Dies liegt daran, dass die MyToolWindowData
Instanz im Erweiterungshostprozess vorhanden ist, während das aus dem Visual Studio-Prozess erstellte MyToolWindowContent.xaml
WPF-Steuerelement vorhanden ist. Damit die Datenbindung funktioniert, generiert die Remote-UI-Infrastruktur einen Proxy des MyToolWindowData
Objekts im Visual Studio-Prozess. Die DataContract
Attribute DataMember
geben an, welche Typen und Eigenschaften für die Datenbindung relevant sind und im Proxy repliziert werden sollen.
Der Datenkontext der Remotebenutzersteuerung wird als Konstruktorparameter der RemoteUserControl
Klasse übergeben: Die RemoteUserControl.DataContext
Eigenschaft ist schreibgeschützt. Dies bedeutet nicht, dass der gesamte Datenkontext unveränderlich ist, aber das Stammdatenkontextobjekt einer Remotebenutzersteuerung kann nicht ersetzt werden. Im nächsten Abschnitt werden MyToolWindowData
wir änderbar und feststellbar machen.
Lebenszyklus einer Remotebenutzersteuerung
Sie können die ControlLoadedAsync
Methode außer Kraft setzen, die benachrichtigt werden soll, wenn das Steuerelement zum ersten Mal in einem WPF-Container geladen wird. Wenn sich der Status des Datenkontexts in Ihrer Implementierung unabhängig von UI-Ereignissen ändern kann, ist die ControlLoadedAsync
Methode der richtige Ort, um den Inhalt des Datenkontexts zu initialisieren und änderungen darauf anzuwenden.
Sie können die Dispose
Methode auch überschreiben, die benachrichtigt werden soll, wenn das Steuerelement zerstört wird und nicht mehr verwendet wird.
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);
}
}
Befehle, Observability und bidirektionale Datenbindung
Als Nächstes machen wir den Datenkontext feststellbar und fügen der Toolbox eine Schaltfläche hinzu.
Der Datenkontext kann durch die Implementierung von INotifyPropertyChanged beobachtet werden. Alternativ bietet remote UI eine bequeme abstrakte Klasse, NotifyPropertyChangedObject
die wir erweitern können, um Codebausteine zu reduzieren.
Ein Datenkontext verfügt in der Regel über eine Mischung aus readonly-Eigenschaften und feststellbaren Eigenschaften. Der Datenkontext kann ein komplexes Diagramm von Objekten sein, solange sie mit den DataContract
Und-Attributen DataMember
gekennzeichnet sind und INotifyPropertyChanged bei Bedarf implementieren. Es ist auch möglich, feststellbare Sammlungen oder ein ObservableList<T> zu haben, bei dem es sich um ein erweitertes ObservableCollection<T> handelt, das von remote UI bereitgestellt wird, um auch Bereichsvorgänge zu unterstützen und eine bessere Leistung zu ermöglichen.
Außerdem müssen wir dem Datenkontext einen Befehl hinzufügen. In remote UI implementieren IAsyncCommand
Befehle, aber häufig ist es einfacher, eine Instanz der AsyncCommand
Klasse zu erstellen.
IAsyncCommand
unterscheidet sich von ICommand
zwei Arten:
- Die
Execute
Methode wird ersetzt,ExecuteAsync
da alles in der Remote-UI asynchron ist! - Die
CanExecute(object)
Methode wird durch eineCanExecute
Eigenschaft ersetzt. DieAsyncCommand
Klasse kümmert sich darum, observierbar zu machenCanExecute
.
Es ist wichtig zu beachten, dass die Remote-Benutzeroberfläche keine Ereignishandler unterstützt. Daher müssen alle Benachrichtigungen von der Benutzeroberfläche an die Erweiterung über Datenbindung und Befehle implementiert werden.
Dies ist der resultierende Code für 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; }
}
Korrigieren Sie den MyToolWindowContent
Konstruktor:
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
}
Aktualisieren, MyToolWindowContent.xaml
um die neuen Eigenschaften im Datenkontext zu verwenden. Dies ist alles normale WPF-XAML. Auch auf das IAsyncCommand
Objekt wird über einen Proxy zugegriffen, der im Visual Studio-Prozess aufgerufen ICommand
wird, sodass es wie gewohnt datengebunden werden kann.
<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>
Grundlegendes zur Asynchronität in der Remote-UI
Die gesamte Remote-UI-Kommunikation für dieses Toolfenster führt die folgenden Schritte aus:
Auf den Datenkontext wird über einen Proxy innerhalb des Visual Studio-Prozesses mit seinem ursprünglichen Inhalt zugegriffen,
Das erstellte
MyToolWindowContent.xaml
Steuerelement ist Daten, die an den Datenkontextproxy gebunden sind,Der Benutzer gibt Text in das Textfeld ein, der der
Name
Eigenschaft des Datenkontextproxys über die Datenbindung zugewiesen ist. Der neue Wert wirdName
an dasMyToolWindowData
Objekt weitergegeben.Der Benutzer klickt auf die Schaltfläche, was zu einer Weitergabe von Effekten führt:
- der
HelloCommand
Im Datenkontextproxy ausgeführt wird - Die asynchrone Ausführung des Erweiterungscodes
AsyncCommand
wird gestartet. - der asynchrone Rückruf zum
HelloCommand
Aktualisieren des Werts der observable-EigenschaftText
- der neue Wert, der
Text
an den Datenkontextproxy weitergegeben wird - Der Textblock im Toolfenster wird auf den neuen Wert der
Text
Datenbindung aktualisiert.
- der
Verwenden von Befehlsparametern, um Rennbedingungen zu vermeiden
Alle Vorgänge, die die Kommunikation zwischen Visual Studio und der Erweiterung (blaue Pfeile im Diagramm) umfassen, sind asynchron. Es ist wichtig, diesen Aspekt im Gesamtentwurf der Erweiterung zu berücksichtigen.
Aus diesem Grund ist es besser, Befehlsparameter anstelle von bidirektionalem Binden zu verwenden, um den Datenkontextstatus zum Zeitpunkt der Ausführung eines Befehls abzurufen, wenn die Konsistenz wichtig ist.
Nehmen Sie diese Änderung vor, indem Sie die Schaltfläche CommandParameter
an Name
folgendes binden:
<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />
Ändern Sie dann den Rückruf des Befehls so, dass er den Parameter verwendet:
HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
Text = $"Hello {(string)parameter!}!";
return Task.CompletedTask;
});
Bei diesem Ansatz wird der Wert der Name
Eigenschaft synchron vom Datenkontextproxy zum Zeitpunkt des Klickens auf die Schaltfläche abgerufen und an die Erweiterung gesendet. Dies vermeidet rennbezogene Bedingungen, insbesondere, wenn der HelloCommand
Rückruf in Zukunft geändert wird, um Ausbeute zu erzielen (ausdrücke haben await
).
Asynchrone Befehle nutzen Daten aus mehreren Eigenschaften
Die Verwendung eines Befehlsparameters ist keine Option, wenn der Befehl mehrere Eigenschaften nutzen muss, die vom Benutzer festgelegt werden können. Wenn beispielsweise auf der Benutzeroberfläche zwei Textfelder vorhanden sind: "Vorname" und "Nachname".
Die Lösung in diesem Fall besteht darin, im asynchronen Befehlsrückruf den Wert aller Eigenschaften aus dem Datenkontext abzurufen, bevor sie zurückgegeben werden.
Unten sehen Sie ein Beispiel, in dem die FirstName
Werte und LastName
Eigenschaften abgerufen werden, bevor Sie sicherstellen, dass der Wert zum Zeitpunkt des Befehlsaufrufs verwendet wird:
HelloCommand = new(async (parameter, cancellationToken) =>
{
string firstName = FirstName;
string lastName = LastName;
await Task.Delay(TimeSpan.FromSeconds(1));
Text = $"Hello {firstName} {lastName}!";
});
Außerdem ist es wichtig, dass die Erweiterung den Wert von Eigenschaften asynchron aktualisiert, die auch vom Benutzer aktualisiert werden können. Mit anderen Worten: Vermeiden Sie die TwoWay-Datenbindung .
Zugehöriger Inhalt
Die hier aufgeführten Informationen sollten ausreichen, um einfache Remote-UI-Komponenten zu erstellen. Weitere erweiterte Szenarien finden Sie unter Advanced Remote UI concepts.