Freigeben über


Tutorial: Erweiterte Remotebenutzeroberfläche

In diesem Lernprogramm erfahren Sie mehr über erweiterte Remote-Benutzeroberflächenkonzepte, indem Sie ein Toolfenster inkrementell ändern, in dem eine Liste zufälliger Farben angezeigt wird:

Screenshot des Toolfensters „Zufällige Farben“.

Behandelte Themen:

  • Wie mehrere asynchrone Befehle parallel ausgeführt werden können und wie Benutzeroberflächenelemente deaktiviert werden, wenn ein Befehl ausgeführt wird.
  • So binden Sie mehrere Schaltflächen an denselben asynchronen Befehl.
  • Wie Referenztypen im Datenkontext einer Remote-Benutzeroberfläche und dem zugehörigen Proxy behandelt werden.
  • Verwenden eines asynchronen Befehls als Ereignishandler
  • So deaktivieren Sie eine einzelne Schaltfläche, wenn der Rückruf des asynchronen Befehls ausgeführt wird, wenn mehrere Schaltflächen an denselben Befehl gebunden sind.
  • Verwenden von XAML-Ressourcenverzeichnissen aus einem Steuerelement einer Remote-Benutzeroberfläche.
  • Verwenden von WPF-Typen, z. B. komplexe Pinsel, im Datenkontext einer Remote-Benutzeroberfläche.
  • So handhabt eine Remote-Benutzeroberfläche Threading.

Dieses Lernprogramm basiert auf dem einführenden Artikel Remote-Benutzeroberfläche und setzt voraus, dass Sie über eine funktionierende VisualStudio.Extensibility-Erweiterung verfügen, einschließlich:

  1. eine .cs-Datei für den Befehl, der das Toolfenster öffnet,
  2. eine MyToolWindow.cs-Datei für die ToolWindow-Klasse,
  3. eine MyToolWindowContent.cs-Datei für die RemoteUserControl-Klasse,
  4. einer MyToolWindowContent.xaml-eingebettete Ressourcendatei für die RemoteUserControl XAML-Definition,
  5. einer MyToolWindowData.cs-Datei für den Datenkontext von RemoteUserControl.

Aktualisieren Sie zum Starten MyToolWindowContent.xaml, um eine Listenansicht und eine Schaltfläche anzuzeigen“:

<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>

Aktualisieren Sie dann die Datenbankkontextklasse MyToolWindowData.cs:

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; }
    }
}

In diesem Code gibt es nur ein paar beachtenswerte Dinge:

  • MyColor.Color ist ein string, aber es wird verwendet, wenn Brush-Daten in XAML gebunden sind. Dies ist eine Funktion, die von WPF bereitgestellt wird.
  • Der AddColorCommand-asynchrone Rückruf enthält eine 2-Sekunden-Verzögerung, um eine zeitintensive Operation zu simulieren.
  • Wir verwenden ObservableList<T>, eine erweiterte ObservableCollection<T>, die von einer Remote-Benutzeroberfläche bereitgestellt wird, um auch Bereichsoperationen zu unterstützen und eine bessere Leistung zu ermöglichen.
  • MyToolWindowData und MyColor implementieren INotifyPropertyChanged nicht, da derzeit alle Eigenschaften schreibgeschützt sind.

Handhabung von zeitintensiven asynchronen Befehlen

Einer der wichtigsten Unterschiede zwischen einer Remote-Benutzeroberfläche und normalem WPF besteht darin, dass alle Vorgänge, die die Kommunikation zwischen der Benutzeroberfläche und der Erweiterung umfassen, asynchron sind.

Asynchrone Befehle wie AddColorCommand machen dies deutlich, indem sie einen asynchronen Rückruf bereitstellen.

Sie können die Auswirkung dieser Operation sehen, wenn Sie mehrmals auf die Schaltfläche Farbe hinzufügen klicken: Da jede Befehlsausführung 2 Sekunden dauert, treten mehrere Ausführungen parallel auf, und mehrere Farben werden in der Liste zusammen angezeigt, wenn die 2-Sekunden-Verzögerung abgelaufen ist. Dies kann dem Benutzer den Eindruck vermitteln, dass die Schaltfläche Farbe hinzufügen nicht funktioniert.

Diagramm der überlappenden asynchronen Befehlsausführung.

Um dies zu beheben, deaktivieren Sie die Schaltfläche, während der asynchrone Befehl ausgeführt wird. Die einfachste Möglichkeit hierfür besteht darin, CanExecute für den Befehl einfach auf „false“ festzulegen:

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;
    }
});

Diese Lösung weist weiterhin unvollkommene Synchronisierung auf, da der Befehlsrückruf asynchron in der Erweiterung ausgeführt wird, und der Rückruf CanExecute auf false setzt, was dann asynchron an den Proxy-Datenkontext im Visual Studio-Prozess weitergegeben wird, was dazu führt, dass die Schaltfläche deaktiviert wird. Der Benutzer könnte zweimal in schneller Folge auf die Schaltfläche klicken, bevor die Schaltfläche deaktiviert ist.

Eine bessere Lösung besteht darin, die RunningCommandsCount-Eigenschaft asynchroner Befehle zu verwenden:

<Button Content="Add color" Command="{Binding AddColorCommand}" IsEnabled="{Binding AddColorCommand.RunningCommandsCount.IsZero}" Grid.Row="1" />

RunningCommandsCount ist ein Indikator für die Anzahl gleichzeitiger asynchroner Ausführungen des Befehls, die derzeit ausgeführt werden. Dieser Zähler wird im UI-Thread erhöht, sobald auf die Schaltfläche geklickt wird, wodurch die Schaltfläche synchron durch Bindung von IsEnabled an RunningCommandsCount.IsZerodeaktiviert werden kann.

Da alle Befehle der Remote-Benutzeroberfläche asynchron ausgeführt werden, empfiehlt es sich, bei Bedarf immer RunningCommandsCount.IsZero zum Deaktivieren von Steuerelementen zu verwenden, auch wenn der Befehl voraussichtlich schnell abgeschlossen wird.

Asynchrone Befehle und Datenvorlagen

In diesem Abschnitt implementieren Sie die Schaltfläche Entfernen, mit der der Benutzer einen Eintrag aus der Liste löschen kann. Wir können entweder einen asynchronen Befehl für jedes MyColor-Objekt erstellen oder über einen einzigen asynchronen Befehl in MyToolWindowData verfügen und einen Parameter verwenden, um zu ermitteln, welche Farbe entfernt werden soll. Letztere Option ist ein saubereres Design, also lassen Sie uns dies implementieren.

  1. Aktualisieren Sie den XAML-Code der Schaltfläche in der Datenvorlage:
<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}}}" />
  1. Fügen Sie das entsprechende AsyncCommand zu MyToolWindowData hinzu:
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
  1. Legen Sie den asynchronen Rückruf des Befehls im Konstruktor von MyToolWindowData fest:
RemoveColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
    await Task.Delay(TimeSpan.FromSeconds(2));

    Colors.Remove((MyColor)parameter!);
});

Dieser Code verwendet einen Task.Delay, um eine zeitintensive asynchrone Befehlsausführung zu simulieren.

Referenztypen im Datenkontext

Im vorherigen Code wird ein MyColor-Objekt als Parameter eines asynchronen Befehls empfangen und als Parameter eines List<T>.Remove-Aufrufs verwendet, der die Referenzgleichheit verwendet (da es sich bei MyColor um einen Verweistyp handelt, der Equals nicht außer Kraft setzt), um das zu entfernende Element zu identifizieren. Dies ist möglich, da selbst wenn der Parameter von der Benutzeroberfläche empfangen wird, die genaue Instanz davon MyColor, die derzeit Teil des Datenkontexts ist, empfangen wird, und nicht eine Kopie.

Die Prozesse von

  • Proxy des Datenkontexts eines Remote-Benutzersteuerelements;
  • Senden von INotifyPropertyChanged-Updates von der Erweiterung an Visual Studio oder umgekehrt;
  • Senden beobachtbarer Sammlungsupdates von der Erweiterung an Visual Studio oder umgekehrt;
  • Senden von Parametern asynchroner Befehle

alle berücksichtigen die Identität von Verweistypobjekten. Mit Ausnahme von Zeichenkette werden Verweistypobjekte nie dupliziert, wenn sie zurück zur Erweiterung übertragen werden.

Diagramm der Verweistypen von Datenbindungen einer Remote-Benutzeroberfläche.

In der Grafik können Sie sehen, wie jedem Verweistypobjekt im Datenkontext (Befehle, Sammlung, jeder MyColor und sogar der gesamte Datenkontext) ein eindeutiger Bezeichner durch die Infrastruktur der Remote-Benutzeroberfläche zugewiesen wird. Wenn der Benutzer auf die Schaltfläche Entfernen für das Proxyfarbobjekt #5 klickt, wird der eindeutige Bezeichner (#5) und nicht der Wert des Objekts an die Erweiterung zurückgesendet. Die Infrastruktur der Remote-Benutzeroberfläche übernimmt das Abrufen des entsprechenden MyColor-Objekts und übergibt es als Parameter an den Rückruf des asynchronen Befehls.

RunningCommandsCount mit mehreren Bindungen und Ereignishandhabung

Wenn Sie die Erweiterung an diesem Punkt testen, beachten Sie, dass beim Klicken auf eine der Schaltflächen Entfernen alle Schaltflächen Entfernen deaktiviert sind:

Diagramm des asynchronen Befehls mit mehreren Bindungen.

Dies kann das gewünschte Verhalten sein. Angenommen, Sie möchten, dass nur die aktuelle Schaltfläche deaktiviert wird, und Sie lassen zu, dass der Benutzer mehrere Farben zum Entfernen in die Warteschlange stellt: Wir können die RunningCommandsCount-Eigenschaft des asynchronen Befehls nicht verwenden, da ein einzelner Befehl für alle Schaltflächen freigegeben ist.

Wir können unser Ziel erreichen, indem wir eine RunningCommandsCount-Eigenschaft an jede Schaltfläche anfügen, sodass wir für jede Farbe einen separaten Zähler haben. Diese Features werden vom http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml-Namespace bereitgestellt, mit dem Sie Typen der Remote-Benutzeroberfläche aus XAML nutzen können:

Wir ändern die Schaltfläche Entfernen wie folgt:

<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>

Die angefügte vs:ExtensibilityUICommands.EventHandlers-Eigenschaft ermöglicht das Zuweisen asynchroner Befehle zu jedem Ereignis (z. B. MouseRightButtonUp) und sie kann in komplexeren Szenarien nützlich sein.

vs:EventHandler kann auch ein CounterTarget aufweisen: ein UIElement, an das eine vs:ExtensibilityUICommands.RunningCommandsCount-Eigenschaft angefügt werden soll, wobei die aktiven Ausführungen im Zusammenhang mit diesem bestimmten Ereignis gezählt werden. Stellen Sie sicher, dass Sie beim Binden an eine angefügte Eigenschaft Klammern (z. B. Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero) verwenden.

In diesem Fall verwenden wir vs:EventHandler, um jeder Schaltfläche einen eigenen separaten Zähler für aktive Befehlsausführungen anzufügen. Durch Binden von IsEnabled an die angefügte Eigenschaft wird nur diese bestimmte Schaltfläche deaktiviert, wenn die entsprechende Farbe entfernt wird:

Diagramm des asynchronen Befehls mit zielorientiertem RunningCommandsCount.

Benutzer-XAML-Ressourcenverzeichnisse

Ab Visual Studio 17.10 unterstützt die Remote-Benutzeroberfläche XAML-Ressourcenverzeichnisse. Auf diese Weise können mehrere Steuerelemente einer Remote-Benutzeroberfläche Stile, Vorlagen und andere Ressourcen freigeben. Außerdem können Sie unterschiedliche Ressourcen (z. B.Zeichenketten) für verschiedene Sprachen definieren.

Ähnlich wie bei der Steuerung einer XAML-Remote-Benutzeroberfläche müssen Ressourcendateien als eingebettete Ressourcen konfiguriert werden:

<ItemGroup>
  <EmbeddedResource Include="MyResources.xaml" />
  <Page Remove="MyResources.xaml" />
</ItemGroup>

Eine Remote-Benutzeroberfläche verweist auf Ressourcenverzeichnisse anders als WPF: Sie werden nicht zu den zusammengeführten Verzeichnissen des Steuerelements hinzugefügt (zusammengeführte Verzeichnisse werden überhaupt nicht von Remote-Benutzeroberflächen unterstützt), sondern anhand des Namens in der Datei .cs des Steuerelements referenziert:

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: new MyToolWindowData())
    {
        this.ResourceDictionaries.AddEmbeddedResource(
            "MyToolWindowExtension.MyResources.xaml");
    }
...

AddEmbeddedResource verwendet den vollständigen Namen der eingebetteten Ressource, die standardmäßig aus dem Stammnamespace für das Projekt besteht, einem beliebigen Unterordnerpfad, unter dem er sich befinden kann, und dem Dateinamen. Es ist möglich, diesen Namen außer Kraft zu setzen, indem LogicalName für EmbeddedResource in der Projektdatei festgelegt wird.

Die Ressourcendatei selbst ist ein normales WPF-Ressourcenverzeichnis:

<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>

Sie können über das Ressourcenverzeichnis im Steuerelement der Remote-Benutzeroberfläche mit DynamicResource auf eine Ressource verweisen:

<Button Content="{DynamicResource removeButtonText}" ...

Lokalisieren von XAML-Ressourcenverzeichnissen

Ressourcenverzeichnisse von Remote-Benutzeroberflächen können auf die gleiche Weise lokalisiert werden wie eingebettete Ressourcen: Sie erstellen andere XAML-Dateien mit demselben Namen und einem Sprachsuffix, z. B. MyResources.it.xaml für italienische Ressourcen:

<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>

Sie können Platzhalter in der Projektdatei verwenden, um alle lokalisierten XAML-Schlüsselverzeichnisse als eingebettete Ressourcen einzuschließen:

<ItemGroup>
  <EmbeddedResource Include="MyResources.*xaml" />
  <Page Remove="MyResources.*xaml" />
</ItemGroup>

Verwenden von WPF-Typen im Datenkontext

Bisher besteht der Datenkontext unserer Remotebenutzersteuerung aus Grundtypen (Zahlen, Zeichenketten usw.), beobachtbaren Sammlungen und unseren eigenen Klassen, die mit DataContract markiert sind. Es ist manchmal hilfreich, einfache WPF-Typen in den Datenkontext einzuschließen, z. B. komplexe Pinsel.

Da eine VisualStudio.Extensibility-Erweiterung möglicherweise nicht einmal im Visual Studio-Prozess ausgeführt wird, kann sie WPF-Objekte nicht direkt mit der Benutzeroberfläche teilen. Die Erweiterung verfügt möglicherweise nicht einmal über Zugriff auf WPF-Typen, da sie aauf netstandard2.0 oder net6.0 (nicht die Variante) -windows) abzielen kann.

Eine Remote-Benutzeroberfläche stellt den XamlFragment-Typ bereit, der das Einschließen einer XAML-Definition eines WPF-Objekts im Datenkontext eines Remote-Benutzersteuerelements ermöglicht:

[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; }
}

Mit dem obigen Code wird der Color-Eigenschaftswert in ein LinearGradientBrush-Objekt im Datenkontextproxy konvertiert: Screenshot: WPF-Typen im Datenkontext.

Remote-Benutzeroberfläche und Threads

Asynchrone Befehl-Rückrufe (und INotifyPropertyChanged-Rückrufe für Werte, die von der Benutzeroberfläche durch Datenbindung aktualisiert werden) werden in zufälligen Thread-Pool-Threads ausgelöst. Rückrufe werden einzeln ausgelöst und überlappen sich erst, wenn der Code ein Steuerelement (mithilfe eines await-Ausdrucks) zurückgibt.

Dieses Verhalten kann geändert werden, indem ein NonConcurrentSynchronizationContext an den Konstruktor übergeben wird RemoteUserControl . In diesem Fall können Sie den bereitgestellten Synchronisierungskontext für alle asynchronen Befehle und INotifyPropertyChanged-Rückrufe im Zusammenhang mit diesem Steuerelement verwenden.