Sdílet prostřednictvím


Proč vzdálené uživatelské rozhraní

Jedním z hlavních cílů modelu VisualStudio.Extensibility je umožnit spouštění rozšíření mimo proces sady Visual Studio. To představuje překážku pro přidání podpory uživatelského rozhraní do rozšíření, protože většina architektur uživatelského rozhraní je v procesu.

Vzdálené uživatelské rozhraní je sada tříd, které umožňují definovat ovládací prvky WPF v rozšíření mimo proces a zobrazit je jako součást uživatelského rozhraní sady Visual Studio.

Vzdálené uživatelské rozhraní se silně přikloní k vzoru návrhu Modelu-View-ViewModel, který spoléhá na XAML a datovou vazbu, příkazy (místo událostí) a triggery (místo interakce s logickým stromem zezadu).

Zatímco vzdálené uživatelské rozhraní bylo vyvinuto tak, aby podporovalo rozšíření mimo proces, rozhraní API visualStudio.Extensibility, která spoléhají na vzdálené uživatelské rozhraní, například ToolWindow, budou používat vzdálené uživatelské rozhraní i pro rozšíření v procesu.

Mezi hlavní rozdíly mezi vzdáleným uživatelským rozhraním a běžným vývojem WPF patří:

  • Většina vzdálených operací uživatelského rozhraní, včetně vazby na kontext dat a spouštění příkazů, je asynchronní.
  • Při definování datových typů, které se mají použít v kontextech dat vzdáleného uživatelského rozhraní, musí být vyzdobeny DataContract atributy a DataMember jejich typ musí být serializovatelný vzdáleným uživatelským rozhraním (podrobnosti najdete zde ).
  • Vzdálené uživatelské rozhraní neumožňuje odkazování na vlastní ovládací prvky.
  • Vzdálený uživatelský ovládací prvek je plně definován v jednom souboru XAML, který odkazuje na jeden (ale potenciálně složitý a vnořený) objekt kontextu dat.
  • Vzdálené uživatelské rozhraní nepodporuje kód za obslužné rutiny událostí nebo obslužné rutiny událostí (alternativní řešení jsou popsaná v pokročilém dokumentu o konceptech vzdáleného uživatelského rozhraní).
  • Vzdálený uživatelský ovládací prvek se vytvoří v procesu sady Visual Studio, nikoli proces hostující rozšíření: XAML nemůže odkazovat na typy a sestavení z rozšíření, ale může odkazovat na typy a sestavení z procesu sady Visual Studio.

Vytvoření rozšíření Hello World pro vzdálené uživatelské rozhraní

Začněte vytvořením nejzásadnějšího rozšíření vzdáleného uživatelského rozhraní. Postupujte podle pokynů v tématu Vytvoření prvního neprocesového rozšíření sady Visual Studio.

Teď byste měli mít funkční rozšíření s jedním příkazem, dalším krokem je přidání ToolWindow a .RemoteUserControl Jedná se RemoteUserControl o ekvivalent vzdáleného uživatelského rozhraní uživatelského ovládacího prvku WPF.

Nakonec budete mít čtyři soubory:

  1. .cs soubor pro příkaz, který otevře okno nástroje,
  2. .cs soubor, ToolWindow který poskytuje sadě RemoteUserControl Visual Studio,
  3. .cs soubor, RemoteUserControl který odkazuje na jeho definici XAML,
  4. .xaml soubor pro RemoteUserControl.

Později přidáte kontext dat pro RemoteUserControlmodel ViewModel v modelu MVVM.

Aktualizace příkazu

Aktualizujte kód příkazu, aby se zobrazilo okno nástroje pomocí ShowToolWindowAsync:

public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
    return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}

Můžete také zvážit změnu CommandConfiguration a string-resources.json vhodnější zobrazovanou zprávu a umístění:

public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
    Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
  "MyToolWindowCommand.DisplayName": "My Tool Window"
}

Vytvoření okna nástroje

Vytvořte nový MyToolWindow.cs soubor a definujte třídu, která MyToolWindow rozšiřuje ToolWindow.

Metoda GetContentAsync by měla vrátit IRemoteUserControl , kterou definujete v dalším kroku. Vzhledem k tomu, že vzdálené řízení uživatele je uvolnitelné, je potřeba ji uvolnit přepsáním Dispose(bool) metody.

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

Vytvoření vzdáleného uživatelského ovládacího prvku

Proveďte tuto akci napříč třemi soubory:

Třída vzdáleného řízení uživatelů

Třída vzdáleného řízení uživatele s názvem MyToolWindowContent, je jednoduchá:

namespace MyToolWindowExtension;

using Microsoft.VisualStudio.Extensibility.UI;

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: null)
    {
    }
}

Kontext dat ještě nepotřebujete, takže ho teď můžete nastavit na null hodnotu.

Rozšiřující třída RemoteUserControl automaticky používá vložený prostředek XAML se stejným názvem. Pokud chcete toto chování změnit, přepište metodu GetXamlAsync .

Definice XAML

Dále vytvořte soubor s názvem 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>

Definice XAML vzdáleného uživatelského ovládacího prvku je normální WPF XAML popisující DataTemplate. Tento KÓD XAML se odešle do sady Visual Studio a slouží k vyplnění obsahu okna nástroje. Pro XAML http://schemas.microsoft.com/visualstudio/extensibility/2022/xamlvzdáleného uživatelského rozhraní používáme speciální obor názvů (xmlnsatribut): .

Nastavení XAML jako vloženého prostředku

Nakonec soubor otevřete .csproj a ujistěte se, že se soubor XAML považuje za vložený prostředek:

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

Jak jsme popsali dříve, soubor XAML musí mít stejný název jako třída vzdáleného uživatelského řízení . Aby to bylo přesné, úplný název rozšiřující RemoteUserControl třídy musí odpovídat názvu vloženého prostředku. Pokud je například úplný název třídy MyToolWindowExtension.MyToolWindowContent vzdáleného uživatelského řízení , měl by být MyToolWindowExtension.MyToolWindowContent.xamlvložený název prostředku . Vložené prostředky mají ve výchozím nastavení přiřazený název, který se skládá z kořenového oboru názvů projektu, jakékoli podsložky, pod kterou mohou být, a jejich název souboru. To může způsobit problémy, pokud vaše třída vzdáleného řízení uživatelů používá jiný obor názvů než kořenový obor názvů projektu nebo pokud soubor XAML není v kořenové složce projektu. V případě potřeby můžete vynutit název vloženého prostředku pomocí značky LogicalName :

<ItemGroup>
  <EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
  <Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>

Testování rozšíření

Teď byste měli být schopni rozšíření ladit stisknutím klávesy F5 .

Snímek obrazovky s nabídkou a oknem nástrojů

Přidání podpory motivů

Je vhodné napsat uživatelské rozhraní s ohledem na to, že Visual Studio může být motivované, takže se používají různé barvy.

Aktualizujte XAML tak, aby používal styly a barvy používané v sadě 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>

Popisek teď používá stejný motiv jako zbytek uživatelského rozhraní sady Visual Studio a automaticky změní barvu, když uživatel přepne do tmavého režimu:

Snímek obrazovky s oknem nástrojů s motivem

xmlns Tento atribut odkazuje na sestavení Microsoft.VisualStudio.Shell.15.0, které není jednou ze závislostí rozšíření. To je v pořádku, protože tento XAML je používán procesem sady Visual Studio, který má závislost na Shell.15, ne samotným rozšířením.

Pokud chcete získat lepší prostředí pro úpravy XAML, můžete dočasně přidat PackageReference do Microsoft.VisualStudio.Shell.15.0 projektu rozšíření. Nezapomeňte ho později odebrat , protože rozšíření VisualStudio.Extensibility mimo proces by nemělo odkazovat na tento balíček!

Přidání kontextu dat

Přidejte třídu kontextu dat pro vzdálený uživatelský ovládací prvek:

using System.Runtime.Serialization;

namespace MyToolWindowExtension;

[DataContract]
internal class MyToolWindowData
{
    [DataMember]
    public string? LabelText { get; init; }
}

a aktualizujte MyToolWindowContent.cs ho a MyToolWindowContent.xaml používejte ho:

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
    {
    }
<Label Content="{Binding LabelText}" />

Obsah popisku je teď nastavený prostřednictvím vazby dat:

Snímek obrazovky zobrazující okno nástroje s datovou vazbou

Zde je datový kontextový typ označený DataContract atributy a DataMember atributy. Důvodem je to, MyToolWindowData že instance existuje v procesu hostitele rozšíření, zatímco ovládací prvek WPF vytvořený z MyToolWindowContent.xaml existuje v procesu sady Visual Studio. Aby datová vazba fungovala, infrastruktura vzdáleného MyToolWindowData uživatelského rozhraní generuje proxy objektu v procesu sady Visual Studio. DataMember Atributy DataContract označují, které typy a vlastnosti jsou relevantní pro datová vazba a které by se měly replikovat v proxy serveru.

Kontext dat vzdáleného uživatelského ovládacího prvku je předán jako konstruktor parametr RemoteUserControl třídy: RemoteUserControl.DataContext vlastnost je jen pro čtení. To neznamená, že celý kontext dat je neměnný, ale objekt kontextu kořenových dat vzdáleného uživatelského ovládacího prvku nelze nahradit. V další části nastavíme MyToolWindowData proměnlivé a pozorovatelné.

Serializovatelné typy a kontext dat vzdáleného uživatelského rozhraní

Kontext dat vzdáleného uživatelského rozhraní může obsahovat pouze serializovatelné typy nebo přesněji pouze DataMember vlastnosti serializovatelného typu mohou být data příchozí.

Vzdálené uživatelské rozhraní umožňuje serializovat pouze následující typy:

  • primitivní data (většina číselných typů .NET, výčtů, bool, string, DateTime)
  • rozšiřující typy, které jsou označené DataContract atributy ( DataMember a všechny jejich datové členy jsou také serializovatelné)
  • objekty implementované IAsyncCommand
  • XamlFragment a SolidColorBrush – objekty a hodnoty Color
  • Nullable<> hodnoty serializovatelného typu
  • kolekce serializovatelných typů, včetně pozorovatelných kolekcí.

Životní cyklus vzdáleného uživatelského ovládacího prvku

Můžete přepsat metodu ControlLoadedAsync , která má být upozorněna při prvním načtení ovládacího prvku v kontejneru WPF. Pokud ve vaší implementaci se stav kontextu dat může změnit nezávisle na událostech uživatelského rozhraní, ControlLoadedAsync je metoda správným místem pro inicializaci obsahu kontextu dat a zahájení provádění změn.

Můžete také přepsat metodu Dispose , která bude upozorněna, když je ovládací prvek zničen a už nebude použit.

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

Příkazy, pozorovatelnost a obousměrná datová vazba

V dalším kroku nastavíme, aby byl kontext dat pozorovatelný, a přidejte do panelu nástrojů tlačítko.

Kontext dat lze sledovat implementací INotifyPropertyChanged. Případně vzdálené uživatelské rozhraní poskytuje pohodlnou abstraktní třídu, NotifyPropertyChangedObjectkterou můžeme rozšířit, abychom snížili často používaný kód.

Kontext dat obvykle obsahuje kombinaci vlastností jen pro čtení a pozorovatelných vlastností. Kontext dat může být složitým grafem objektů, pokud jsou označené DataContract atributy a DataMember implementují INotifyPropertyChanged podle potřeby. Je také možné mít pozorovatelné kolekce nebo ObservableList<T, což je rozšířený ObservableCollection<T>> poskytovaný vzdáleným uživatelským rozhraním pro podporu operací rozsahu, což umožňuje lepší výkon.

Také potřebujeme přidat příkaz do kontextu dat. Příkazy ve vzdáleném uživatelském rozhraní implementují IAsyncCommand , ale často je jednodušší vytvořit instanci AsyncCommand třídy.

IAsyncCommand se liší od ICommand dvou způsobů:

  • Metoda Execute je nahrazena tím, že vše ve vzdáleném ExecuteAsync uživatelském rozhraní je asynchronní!
  • Metoda CanExecute(object) je nahrazena CanExecute vlastností. Třída AsyncCommand se postará o pozorování CanExecute .

Je důležité si uvědomit, že vzdálené uživatelské rozhraní nepodporuje obslužné rutiny událostí, takže všechna oznámení z uživatelského rozhraní do rozšíření musí být implementována prostřednictvím vazby dat a příkazů.

Toto je výsledný kód pro 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; }
}

Oprava konstruktoru MyToolWindowContent :

public MyToolWindowContent()
    : base(dataContext: new MyToolWindowData())
{
}

Aktualizujte MyToolWindowContent.xaml , aby používaly nové vlastnosti v kontextu dat. To je všechno normální WPF XAML. I k objektu IAsyncCommand se přistupuje prostřednictvím proxy serveru volaného ICommand v procesu sady Visual Studio, takže může být svázaný s daty jako obvykle.

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

Diagram okna nástrojů s obousměrnou vazbou a příkazem

Principy asynchronicity ve vzdáleném uživatelském rozhraní

Celá komunikace s vzdáleným uživatelským rozhraním pro toto okno nástroje se řídí těmito kroky:

  1. K kontextu dat se přistupuje prostřednictvím proxy serveru uvnitř procesu sady Visual Studio s původním obsahem.

  2. Ovládací prvek vytvořený z MyToolWindowContent.xaml dat je svázaný s proxy kontextem dat,

  3. Uživatel zadá do textového pole nějaký text, který je přiřazen vlastnosti Name proxy kontextu dat prostřednictvím vazby dat. Nová hodnota Name se rozšíří do objektu MyToolWindowData .

  4. Uživatel klikne na tlačítko, které způsobuje kaskádu efektů:

    • v HelloCommand proxy kontextu dat se provede.
    • Spustí se asynchronní spuštění kódu extenderu AsyncCommand .
    • asynchronní zpětné volání pro HelloCommand aktualizace hodnoty pozorovatelné vlastnosti Text
    • nová hodnota Text se rozšíří na proxy kontextu dat.
    • textový blok v okně nástroje se aktualizuje na novou hodnotu Text prostřednictvím datové vazby.

Diagram obousměrné vazby okna nástrojů a komunikace příkazů

Použití parametrů příkazů k zabránění podmínek časování

Všechny operace, které zahrnují komunikaci mezi sadou Visual Studio a rozšířením (modré šipky v diagramu), jsou asynchronní. Tento aspekt je důležité vzít v úvahu v celkovém návrhu rozšíření.

Z tohoto důvodu, pokud je konzistence důležitá, je lepší použít parametry příkazů místo obousměrné vazby k načtení stavu kontextu dat v době spuštění příkazu.

Tuto změnu proveďte vazbou tlačítka CommandParameter na Name:

<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />

Potom upravte zpětné volání příkazu tak, aby používal parametr:

HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
    Text = $"Hello {(string)parameter!}!";
    return Task.CompletedTask;
});

Při tomto přístupu se hodnota Name vlastnosti načte synchronně z proxy kontextu dat v době kliknutí na tlačítko a odeslána do rozšíření. Tím se vyhnete jakýmkoli podmínkám časování, zejména pokud HelloCommand se zpětné volání v budoucnu změní na výnos (mají await výrazy).

Asynchronní příkazy využívají data z více vlastností.

Použití parametru příkazu není možnost, pokud příkaz potřebuje spotřebovat více vlastností, které jsou nastaveny uživatelem. Pokud má uživatelské rozhraní například dvě textová pole: Jméno a Příjmení.

Řešením v tomto případě je načtení hodnoty všech vlastností z datového kontextu před načtením v asynchronním zpětném volání příkazu .

Níže vidíte ukázku, ve které FirstName se hodnoty a LastName hodnoty vlastností načtou před tím, abyste měli jistotu, že se hodnota v době vyvolání příkazu používá:

HelloCommand = new(async (parameter, cancellationToken) =>
{
    string firstName = FirstName;
    string lastName = LastName;
    await Task.Delay(TimeSpan.FromSeconds(1));
    Text = $"Hello {firstName} {lastName}!";
});

Je také důležité, aby rozšíření asynchronně aktualizovalo hodnotu vlastností, které může uživatel aktualizovat. Jinými slovy, vyhněte se datové vazbě TwoWay .

Zde uvedené informace by měly stačit k vytvoření jednoduchých komponent vzdáleného uživatelského rozhraní. Pokročilejší scénáře najdete v tématu Pokročilé koncepty vzdáleného uživatelského rozhraní.