Dlaczego zdalny interfejs użytkownika
Jednym z głównych celów modelu rozszerzenia VisualStudio.Extensibility jest umożliwienie uruchamiania rozszerzeń poza procesem programu Visual Studio. Stanowi to przeszkodę dla dodawania obsługi interfejsu użytkownika do rozszerzeń, ponieważ większość struktur interfejsu użytkownika jest w toku.
Zdalny interfejs użytkownika to zestaw klas, które umożliwiają definiowanie kontrolek WPF w rozszerzeniu poza procesem i wyświetlanie ich w ramach interfejsu użytkownika programu Visual Studio.
Zdalny interfejs użytkownika opiera się mocno na wzorcu projektowania Model-View-ViewModel opartym na języku XAML i powiązaniu danych, poleceniach (zamiast zdarzeń) i wyzwalaczach (zamiast wchodzić w interakcje z drzewem logicznym z kodu za pomocą kodu ).
Chociaż interfejs użytkownika zdalnego został opracowany w celu obsługi rozszerzeń poza procesem, interfejsy API programu VisualStudio.Extensibility, które opierają się na zdalnym interfejsie użytkownika, na przykład ToolWindow
, będą używać zdalnego interfejsu użytkownika dla rozszerzeń procesów.
Główne różnice między zdalnym interfejsem użytkownika i normalnym programowaniem WPF są następujące:
- Większość zdalnych operacji interfejsu użytkownika, w tym powiązania z kontekstem danych i wykonywaniem poleceń, jest asynchroniczna.
- Podczas definiowania typów danych, które mają być używane w kontekstach danych interfejsu użytkownika zdalnego, muszą być one ozdobione atrybutami
DataContract
i,DataMember
a ich typ musi być serializowalny przez zdalny interfejs użytkownika (zobacz tutaj, aby uzyskać szczegółowe informacje). - Zdalny interfejs użytkownika nie zezwala na odwoływanie się do własnych kontrolek niestandardowych.
- Zdalne sterowanie użytkownikami jest w pełni zdefiniowane w jednym pliku XAML odwołującym się do pojedynczego obiektu kontekstu danych (ale potencjalnie złożonego i zagnieżdżonego).
- Zdalny interfejs użytkownika nie obsługuje kodu za pomocą programów obsługi zdarzeń ani obsługi zdarzeń (obejścia są opisane w zaawansowanym dokumencie Pojęcia dotyczące zdalnego interfejsu użytkownika).
- Zdalne sterowanie użytkownikami jest tworzone w procesie programu Visual Studio, a nie w procesie hostowania rozszerzenia: język XAML nie może odwoływać się do typów i zestawów z rozszerzenia, ale może odwoływać się do typów i zestawów z procesu programu Visual Studio.
Tworzenie rozszerzenia Hello World zdalnego interfejsu użytkownika
Zacznij od utworzenia najbardziej podstawowego rozszerzenia zdalnego interfejsu użytkownika. Postępuj zgodnie z instrukcjami w temacie Tworzenie pierwszego rozszerzenia programu Visual Studio poza procesem.
Teraz powinno istnieć rozszerzenie robocze z jednym poleceniem, następnym krokiem jest dodanie elementu ToolWindow
i .RemoteUserControl
Jest RemoteUserControl
to zdalny odpowiednik interfejsu użytkownika kontrolki użytkownika WPF.
Zostaną wyświetlone cztery pliki:
.cs
plik polecenia, który otwiera okno narzędzia,.cs
plik programuToolWindow
, który udostępniaRemoteUserControl
programOwi Visual Studio,.cs
plik,RemoteUserControl
który odwołuje się do jego definicji XAML,- plik
.xaml
dla plikuRemoteUserControl
.
Później dodasz kontekst danych dla RemoteUserControl
elementu , który reprezentuje model ViewModel we wzorcu MVVM.
Aktualizowanie polecenia
Zaktualizuj kod polecenia, aby wyświetlić okno narzędzia przy użyciu polecenia ShowToolWindowAsync
:
public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}
Możesz również rozważyć zmianę CommandConfiguration
i string-resources.json
w celu uzyskania bardziej odpowiedniego komunikatu wyświetlania i umieszczania:
public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
"MyToolWindowCommand.DisplayName": "My Tool Window"
}
Tworzenie okna narzędzi
Utwórz nowy MyToolWindow.cs
plik i zdefiniuj klasę rozszerzającą MyToolWindow
ToolWindow
element .
Metoda GetContentAsync
ma zwrócić element IRemoteUserControl
, który zostanie zdefiniowany w następnym kroku. Ponieważ zdalne sterowanie użytkownikami jest jednorazowe, należy zadbać o jego usunięcie przez zastąpienie 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);
}
}
Tworzenie zdalnego sterowania użytkownikami
Wykonaj tę akcję w trzech plikach:
Zdalna klasa sterowania użytkownikami
Zdalna klasa sterowania użytkownika o nazwie MyToolWindowContent
, jest prosta:
namespace MyToolWindowExtension;
using Microsoft.VisualStudio.Extensibility.UI;
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: null)
{
}
}
Nie potrzebujesz jeszcze kontekstu danych, więc możesz ustawić go null
na teraz.
Klasa rozszerzająca RemoteUserControl
automatycznie używa osadzonego zasobu XAML o tej samej nazwie. Jeśli chcesz zmienić to zachowanie, zastąpij metodę GetXamlAsync
.
Definicja XAML
Następnie utwórz plik o nazwie 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>
Definicja XAML zdalnego sterowania użytkownika jest normalną definicją WPF XAML opisującą element DataTemplate
. Ten kod XAML jest wysyłany do programu Visual Studio i używany do wypełniania zawartości okna narzędzi. Używamy specjalnej przestrzeni nazw (xmlns
atrybutu) dla Zdalnego interfejsu użytkownika XAML: http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml
.
Ustawianie kodu XAML jako zasobu osadzonego
Na koniec otwórz .csproj
plik i upewnij się, że plik XAML jest traktowany jako zasób osadzony:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Jak opisano wcześniej, plik XAML musi mieć taką samą nazwę jak klasa zdalnego sterowania użytkownikami. Aby być precyzyjnym, pełna nazwa rozszerzonej RemoteUserControl
klasy musi być zgodna z nazwą zasobu osadzonego. Jeśli na przykład pełna nazwa klasy zdalnego sterowania użytkownika to MyToolWindowExtension.MyToolWindowContent
, osadzona nazwa zasobu powinna mieć wartość MyToolWindowExtension.MyToolWindowContent.xaml
. Domyślnie zasoby osadzone mają przypisaną nazwę składającą się z głównej przestrzeni nazw projektu, dowolnej ścieżki podfolderu, w której znajdują się, oraz nazwy pliku. Może to spowodować problemy, jeśli klasa zdalnego sterowania użytkownika korzysta z przestrzeni nazw innej niż główna przestrzeń nazw projektu lub jeśli plik xaml nie znajduje się w folderze głównym projektu. W razie potrzeby możesz wymusić nazwę zasobu osadzonego przy użyciu tagu LogicalName
:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Testowanie rozszerzenia
Teraz powinno być możliwe naciśnięcie klawisza F5
, aby debugować rozszerzenie.
Dodawanie obsługi motywów
Dobrym pomysłem jest napisanie interfejsu użytkownika, mając na uwadze, że program Visual Studio może mieć motywy, co powoduje, że używane są różne kolory.
Zaktualizuj język XAML, aby używać stylów i kolorów używanych w programie 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>
Etykieta używa teraz tego samego motywu co reszta interfejsu użytkownika programu Visual Studio i automatycznie zmienia kolor, gdy użytkownik przełączy się do trybu ciemnego:
xmlns
W tym miejscu atrybut odwołuje się do zestawu Microsoft.VisualStudio.Shell.15.0, który nie jest jedną z zależności rozszerzeń. Jest to w porządku, ponieważ ten kod XAML jest używany przez proces programu Visual Studio, który ma zależność od powłoki.15, a nie przez samo rozszerzenie.
Aby uzyskać lepsze środowisko edycji XAML, możesz tymczasowo dodać element PackageReference
do Microsoft.VisualStudio.Shell.15.0
projektu rozszerzenia. Nie zapomnij usunąć go później, ponieważ rozszerzenie VisualStudio.Extensibility poza procesem nie powinno odwoływać się do tego pakietu!
Dodawanie kontekstu danych
Dodaj klasę kontekstu danych dla zdalnego sterowania użytkownikami:
using System.Runtime.Serialization;
namespace MyToolWindowExtension;
[DataContract]
internal class MyToolWindowData
{
[DataMember]
public string? LabelText { get; init; }
}
i zaktualizuj MyToolWindowContent.cs
i MyToolWindowContent.xaml
użyj go:
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
{
}
<Label Content="{Binding LabelText}" />
Zawartość etykiety jest teraz ustawiana za pomocą powiązania danych:
Tutaj typ kontekstu danych jest oznaczony atrybutami DataContract
i DataMember
. Jest to spowodowane tym, że MyToolWindowData
wystąpienie istnieje w procesie hosta rozszerzenia, podczas gdy kontrolka WPF utworzona na podstawie MyToolWindowContent.xaml
istnieje w procesie programu Visual Studio. Aby powiązanie danych działało, infrastruktura zdalnego interfejsu użytkownika generuje serwer proxy MyToolWindowData
obiektu w procesie programu Visual Studio. Atrybuty DataContract
i DataMember
wskazują, które typy i właściwości są istotne dla powiązania danych i powinny być replikowane na serwerze proxy.
Kontekst danych zdalnego sterowania użytkownika jest przekazywany jako parametr RemoteUserControl
konstruktora klasy: RemoteUserControl.DataContext
właściwość jest tylko do odczytu. Nie oznacza to, że cały kontekst danych jest niezmienny, ale nie można zamienić obiektu kontekstu danych głównych zdalnego sterowania użytkownikami. W następnej sekcji będziemy modyfikować MyToolWindowData
i obserwowalny.
Typy z możliwością serializacji i kontekst danych zdalnego interfejsu użytkownika
Kontekst danych interfejsu użytkownika zdalnego może zawierać tylko typy z możliwością serializacji lub, aby było bardziej precyzyjne, tylko DataMember
właściwości typu z możliwością serializacji mogą być dane przychodzące do.
Tylko następujące typy są serializowalne przez zdalny interfejs użytkownika:
- dane pierwotne (większość typów liczbowych platformy .NET, wyliczenia,
bool
,string
,DateTime
) - typy zdefiniowane przez rozszerzenie, które są oznaczone atrybutami
DataContract
iDataMember
(a wszystkie ich składowe danych są również serializowalne) - obiekty implementowania polecenia IAsyncCommand
- Obiekty XamlFragment i SolidColorBrush oraz wartości Color
Nullable<>
wartości dla typu możliwego do serializacji- kolekcje typów możliwych do serializacji, w tym widoczne kolekcje.
Cykl życia zdalnego sterowania użytkownikami
Możesz zastąpić metodę ControlLoadedAsync
, aby otrzymywać powiadomienia, gdy kontrolka zostanie po raz pierwszy załadowana w kontenerze WPF. Jeśli w implementacji stan kontekstu danych może ulec zmianie niezależnie od zdarzeń interfejsu użytkownika, ControlLoadedAsync
metoda jest właściwym miejscem do zainicjowania zawartości kontekstu danych i rozpoczęcia stosowania do niego zmian.
Możesz również zastąpić metodę Dispose
, aby otrzymywać powiadomienia, gdy kontrolka zostanie zniszczona i nie będzie już używana.
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);
}
}
Polecenia, możliwość obserwowania i dwukierunkowe powiązanie danych
Następnie przyjrzyjmy się kontekstowi danych i dodajmy przycisk do przybornika.
Kontekst danych można zaobserwować, implementując element INotifyPropertyChanged. Alternatywnie interfejs użytkownika zdalnego zapewnia wygodną klasę abstrakcyjną , NotifyPropertyChangedObject
którą możemy rozszerzyć w celu zmniejszenia liczby kodów standardowych.
Kontekst danych zwykle ma kombinację właściwości tylko do odczytu i obserwowalnych właściwości. Kontekst danych może być złożonym grafem obiektów, o ile są one oznaczone atrybutami DataContract
i DataMember
i implementują element INotifyPropertyChanged w razie potrzeby. Istnieje również możliwość obserwowalnej kolekcji lub funkcji ObservableList<T>, która jest rozszerzoną funkcją ObservableCollection<T> dostarczaną przez zdalny interfejs użytkownika do obsługi operacji zakresu, co pozwala na lepszą wydajność.
Musimy również dodać polecenie do kontekstu danych. W zdalnym interfejsie użytkownika polecenia implementują IAsyncCommand
, ale często łatwiej jest utworzyć wystąpienie AsyncCommand
klasy.
IAsyncCommand
różni się od ICommand
dwóch sposobów:
- Metoda
Execute
jest zastępowanaExecuteAsync
, ponieważ wszystko w zdalnym interfejsie użytkownika jest asynchroniczne! - Metoda
CanExecute(object)
jest zastępowanaCanExecute
przez właściwość . KlasaAsyncCommand
dba o obserwowanieCanExecute
.
Należy pamiętać, że zdalny interfejs użytkownika nie obsługuje procedur obsługi zdarzeń, dlatego wszystkie powiadomienia z interfejsu użytkownika do rozszerzenia muszą być implementowane za pomocą powiązania danych i poleceń.
Jest to wynikowy kod dla elementu 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; }
}
MyToolWindowContent
Napraw konstruktor:
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
}
Zaktualizuj MyToolWindowContent.xaml
, aby używać nowych właściwości w kontekście danych. To wszystko jest normalne WPF XAML. IAsyncCommand
Nawet obiekt jest uzyskiwany za pośrednictwem serwera proxy wywoływanego ICommand
w procesie programu Visual Studio, dzięki czemu może być powiązany ze zwykłymi danymi.
<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>
Opis asynchroniczności w zdalnym interfejsie użytkownika
Cała zdalna komunikacja interfejsu użytkownika dla tego okna narzędzi jest następująca:
Dostęp do kontekstu danych jest uzyskiwany za pośrednictwem serwera proxy w procesie programu Visual Studio z oryginalną zawartością,
Kontrolka utworzona na podstawie
MyToolWindowContent.xaml
to dane powiązane z serwerem proxy kontekstu danych,Użytkownik wpisze jakiś tekst w polu tekstowym, który jest przypisany do
Name
właściwości serwera proxy kontekstu danych za pośrednictwem powiązania danych. Nowa wartośćName
elementu jest propagowana doMyToolWindowData
obiektu.Użytkownik klika przycisk powodujący kaskadę efektów:
- na
HelloCommand
serwerze proxy kontekstu danych jest wykonywany - asynchroniczne wykonywanie kodu rozszerzenia
AsyncCommand
jest uruchamiane - asynchroniczne wywołanie zwrotne dla
HelloCommand
aktualizacji wartości obserwowalnej właściwościText
- nowa wartość
Text
jest propagowana do serwera proxy kontekstu danych - blok tekstowy w oknie narzędzia jest aktualizowany do nowej wartości
Text
za pomocą powiązania danych
- na
Używanie parametrów poleceń w celu uniknięcia warunków wyścigu
Wszystkie operacje obejmujące komunikację między programem Visual Studio i rozszerzeniem (niebieskie strzałki na diagramie) są asynchroniczne. Ważne jest, aby wziąć pod uwagę ten aspekt w ogólnym projekcie rozszerzenia.
Z tego powodu, jeśli spójność jest ważna, lepiej użyć parametrów polecenia, zamiast powiązania dwukierunkowego, aby pobrać stan kontekstu danych w czasie wykonywania polecenia.
Wprowadź tę zmianę, tworząc powiązanie przycisku CommandParameter
z :Name
<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />
Następnie zmodyfikuj wywołanie zwrotne polecenia, aby użyć parametru :
HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
Text = $"Hello {(string)parameter!}!";
return Task.CompletedTask;
});
Dzięki temu podejściu wartość Name
właściwości jest pobierana synchronicznie z serwera proxy kontekstu danych w momencie kliknięcia przycisku i wysłana do rozszerzenia. Pozwala to uniknąć wszelkich warunków wyścigu, zwłaszcza jeśli HelloCommand
wywołanie zwrotne zostanie zmienione w przyszłości w celu uzyskania (mają await
wyrażenia).
Polecenia asynchroniczne używają danych z wielu właściwości
Użycie parametru polecenia nie jest opcją, jeśli polecenie musi korzystać z wielu właściwości, które można ustawić przez użytkownika. Jeśli na przykład interfejs użytkownika miał dwa pola tekstowe: "Imię" i "Nazwisko".
Rozwiązaniem w tym przypadku jest pobranie wartości wszystkich właściwości z kontekstu danych przed uzyskaniem wartości wywołania zwrotnego polecenia asynchronicznego.
Poniżej przedstawiono przykład, w którym FirstName
są pobierane wartości właściwości i LastName
przed uzyskaniem, aby upewnić się, że wartość w momencie wywołania polecenia jest używana:
HelloCommand = new(async (parameter, cancellationToken) =>
{
string firstName = FirstName;
string lastName = LastName;
await Task.Delay(TimeSpan.FromSeconds(1));
Text = $"Hello {firstName} {lastName}!";
});
Ważne jest również, aby uniknąć asynchronicznego aktualizowania wartości właściwości, które mogą być również aktualizowane przez użytkownika. Innymi słowy, unikaj powiązania danych TwoWay .
Powiązana zawartość
Informacje w tym miejscu powinny wystarczyć do skompilowania prostych składników interfejsu użytkownika zdalnego. Aby uzyskać bardziej zaawansowane scenariusze, zobacz Advanced Remote UI concepts (Zaawansowane pojęcia dotyczące zdalnego interfejsu użytkownika).