Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Eines der Hauptziele des VisualStudio.Extensibility-Modells besteht darin, Erweiterungen außerhalb des Visual Studio-Prozesses auszuführen. Diese Entscheidung führt zu einem Hindernis für das Hinzufügen der 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 (Windows Presentation Foundation) 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 (Extensible Application Markup Language) und Datenbindung, Befehle (anstelle von Ereignissen) und Triggern 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, wie ToolWindow, Remote-UI auch für In-Process-Erweiterungen.
Die wichtigsten 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
DataContractAttributen undDataMemberAttributen versehen werden, und ihr Typ muss von der Remote-UI serialisierbar sein (weitere Informationen finden Sie hier ). - Die Remote-UI erlaubt keinen Verweis auf Ihre eigenen benutzerdefinierten Steuerelemente.
- 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 Hello World-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 jetzt über eine funktionierende Erweiterung mit einem einzigen Befehl verfügen. Der nächste Schritt besteht darin, ein ToolWindow sowie ein RemoteUserControl hinzuzufügen. Die RemoteUserControl ist das Remote UI-Äquivalent eines WPF-Benutzersteuerelements.
Sie enden mit vier Dateien:
- eine
.csDatei für den Befehl, der das Toolfenster öffnet, - eine
.csDatei für dasToolWindow, dasRemoteUserControlfür Visual Studio bereitstellt, - eine
.csDatei für dasRemoteUserControl, das auf seine XAML-Definition verweist, - eine
.xamlDatei für dieRemoteUserControl.
Später fügen Sie einen Datenkontext für das RemoteUserControl hinzu, das das ViewModel im MVVM-Muster (Model-View-ViewModel) darstellt.
Aktualisieren des Befehls
Aktualisieren Sie den Code des Befehls, um das Toolfenster mithilfe von ShowToolWindowAsync anzuzeigen.
public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}
Sie können auch die Änderung von CommandConfiguration und eine geeignetere Anzeigemeldung und Platzierung von string-resources.json 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 Klasse, die ToolWindow erweitert.
Die GetContentAsync Methode sollte einen IRemoteUserControl Wert zurückgeben, den Sie im nächsten Schritt definieren werden. Da die Fernsteuerung entsorgt werden muss, entsorgen Sie sie, 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:
Klasse für Fernsteuerung von Benutzern
Die Remote-Benutzersteuerungsklasse 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, sodass Sie ihn vorerst auf null festlegen können.
Eine Klasse, die RemoteUserControl erweitert, 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>
Die XAML-Definition der Remote-Benutzersteuerung ist ein normaler WPF-XAML-Code, der ein DataTemplate darstellt. 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 von XAML 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>
Wie zuvor beschrieben, muss die XAML-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 lautet MyToolWindowExtension.MyToolWindowContent, sollte der name der eingebetteten Ressource sein MyToolWindowExtension.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>
Testen der Erweiterung
Sie sollten jetzt F5 drücken können, um die Erweiterung zu debuggen.
Hinzufügen von Unterstützung für Designs
Es ist ratsam, die Benutzeroberfläche so zu gestalten, dass Visual Studio thematisiert werden kann, wodurch 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 ein besseres XAML-Bearbeitungserlebnis zu erzielen, können Sie dem Erweiterungsprojekt vorübergehend ein PackageReference zu Microsoft.VisualStudio.Shell.15.0 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; }
}
Aktualisieren Sie MyToolWindowContent.cs und MyToolWindowContent.xaml, um sie zu verwenden:
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 Attribute DataContract und 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.
Serialisierbare Typen und Remote-UI-Datenkontext
Ein Remote-UI-Datenkontext kann nur serialisierbare Typen enthalten, oder, um präziser zu sein, können nur DataMember Eigenschaften eines serialisierbaren Typs datengebunden sein.
Nur die folgenden Typen können von der Remote-UI serialisiert werden:
- Primitivdatentypen (die meisten numerischen .NET-Typen, Enumerationen,
bool,string,DateTime) - erweiterungsdefinierte Typen, die mit
DataContractundDataMemberAttributen gekennzeichnet sind, und deren alle Datenmitglieder ebenfalls serialisierbar sind - Objekte, die IAsyncCommand implementieren
- XamlFragment- und SolidColorBrush-Objekte und Farbwerte
-
Nullable<>Werte für einen serialisierbaren Typ - Auflistungen serialisierbarer Typen, einschließlich beobachtbarer Auflistungen.
Lebenszyklus einer Remotebenutzersteuerung
Sie können die ControlLoadedAsync-Methode überschreiben, um benachrichtigt zu werden, 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, um benachrichtigt zu werden, wenn die Steuerung 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, Beobachtbarkeit 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, NotifyPropertyChangedObjectdie 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 komplexer Graph von Objekten sein, solange sie mit den DataContract und DataMember Attributen gekennzeichnet sind und INotifyPropertyChanged wie erforderlich implementieren. Es ist auch möglich, beobachtbare Sammlungen oder eine ObservableList<T> zu haben, das eine Erweiterung von ObservableCollection<T> ist, die von Remote UI bereitgestellt wird, um auch Bereichsoperationen zu unterstützen und eine bessere Leistung zu ermöglichen.
Außerdem müssen wir dem Datenkontext einen Befehl hinzufügen. In der Remote-Benutzeroberfläche implementiert IAsyncCommand Befehle, aber häufig ist es einfacher, eine Instanz der AsyncCommand Klasse zu erstellen.
IAsyncCommand unterscheidet sich von ICommand in zwei Punkten:
- Die
ExecuteMethode wird durchExecuteAsyncersetzt, da alles in der Remote-UI asynchron ist! - Die
CanExecute(object)Methode wird durch eineCanExecuteEigenschaft ersetzt. DieAsyncCommand-Klasse kümmert sich darum,CanExecuteobservierbar zu machen.
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, der im Visual Studio-Prozess ICommand genannt wird, zugegriffen, 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
MyToolWindowContent.xamlerstellte Steuerelement ist an den Datenkontextproxy gebunden.Der Benutzer gibt Text in das Textfeld ein, der der
NameEigenschaft des Datenkontextproxys über die Datenbindung zugewiesen ist. Der neue Wert vonNamewird auf dasMyToolWindowDataObjekt übertragen.Der Benutzer klickt auf die Schaltfläche, was eine Kaskade von Effekten auslöst.
- Der
HelloCommandwird im Datenkontext-Proxy ausgeführt. - Die asynchrone Ausführung des Erweiterungscodes
AsyncCommandwird gestartet. - Der asynchrone Rückruf für
HelloCommandaktualisiert den Wert der beobachtbaren EigenschaftText. - der neue Wert von
Textwird an den Datenkontext-Proxy übermittelt - Der Textblock im Toolfenster wird durch die Datenbindung auf den neuen Wert von
Textaktualisiert.
- 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 Namefolgendes 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 Werte der Eigenschaften FirstName und LastName abgerufen werden, bevor der Befehl ausgeführt wird, um sicherzustellen, 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}!";
});
Es ist auch wichtig zu vermeiden, dass die Erweiterung den Wert der Eigenschaften asynchron aktualisiert, die Benutzer ebenfalls aktualisieren können. Mit anderen Worten: Vermeiden Sie die TwoWay-Datenbindung .
Verwandte Inhalte
Die hier aufgeführten Informationen sollten ausreichen, um einfache Remote-UI-Komponenten zu erstellen. Weitere Themen zum Arbeiten mit dem Remote-UI-Modell finden Sie unter "Weitere Konzepte der Remote-UI". Weitere erweiterte Szenarien finden Sie unter Advanced Remote UI concepts.