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 aDataMember
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:
.cs
soubor pro příkaz, který otevře okno nástroje,.cs
soubor,ToolWindow
který poskytuje saděRemoteUserControl
Visual Studio,.cs
soubor,RemoteUserControl
který odkazuje na jeho definici XAML,.xaml
soubor proRemoteUserControl
.
Později přidáte kontext dat pro RemoteUserControl
model 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/xaml
vzdáleného uživatelského rozhraní používáme speciální obor názvů (xmlns
atribut): .
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.xaml
vlož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
.
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:
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:
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, NotifyPropertyChangedObject
kterou 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émExecuteAsync
uživatelském rozhraní je asynchronní! - Metoda
CanExecute(object)
je nahrazenaCanExecute
vlastností. TřídaAsyncCommand
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>
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:
K kontextu dat se přistupuje prostřednictvím proxy serveru uvnitř procesu sady Visual Studio s původním obsahem.
Ovládací prvek vytvořený z
MyToolWindowContent.xaml
dat je svázaný s proxy kontextem dat,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á hodnotaName
se rozšíří do objektuMyToolWindowData
.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é vlastnostiText
- 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.
- v
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 .
Související obsah
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í.