Samouczek: zaawansowany zdalny interfejs użytkownika
Z tego samouczka dowiesz się więcej o zaawansowanych pojęciach zdalnego interfejsu użytkownika przez przyrostowe modyfikowanie okna narzędzia zawierającego listę losowych kolorów:
Dowiesz się więcej o:
- Jak wiele poleceń asynchronicznych może być uruchamianych równolegle i jak wyłączyć elementy interfejsu użytkownika, gdy polecenie jest uruchomione.
- Jak powiązać wiele przycisków z tym samym poleceniem asynchronicznym.
- Sposób obsługi typów odwołań w kontekście danych interfejsu użytkownika zdalnego i jego serwera proxy.
- Jak używać polecenia asynchronicznego jako procedury obsługi zdarzeń.
- Jak wyłączyć pojedynczy przycisk, gdy wywołanie zwrotne polecenia asynchronicznego jest wykonywane, jeśli wiele przycisków jest powiązanych z tym samym poleceniem.
- Jak używać słowników zasobów XAML z poziomu zdalnego sterowania interfejsem użytkownika.
- Jak używać typów WPF, takich jak złożone pędzle, w kontekście danych zdalnego interfejsu użytkownika.
- Jak zdalny interfejs użytkownika obsługuje wątki.
Ten samouczek jest oparty na artykule wprowadzającym zdalny interfejs użytkownika i oczekuje, że masz działające rozszerzenie VisualStudio.Extensibility, w tym:
.cs
plik polecenia, który otwiera okno narzędzia,MyToolWindow.cs
plik dlaToolWindow
klasy,MyToolWindowContent.cs
plik dlaRemoteUserControl
klasy,MyToolWindowContent.xaml
osadzony plik zasobu dlaRemoteUserControl
definicji xaml,MyToolWindowData.cs
plik dla kontekstu danych .RemoteUserControl
Aby rozpocząć, zaktualizuj MyToolWindowContent.xaml
widok listy i przycisk":
<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>
Następnie zaktualizuj klasę MyToolWindowData.cs
kontekstu danych :
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; }
}
}
W tym kodzie znajduje się kilka godnych uwagi elementów:
MyColor.Color
jest elementemstring
, ale jest używany jakoBrush
element, gdy dane powiązane w języku XAML, jest to funkcja zapewniana przez WPF.- Wywołanie
AddColorCommand
zwrotne asynchroniczne zawiera 2-sekundowe opóźnienie w celu symulowania długotrwałej operacji. - Używamy funkcji ObservableList<T>, która jest rozszerzoną funkcją ObservableCollection<T> dostarczaną przez zdalny interfejs użytkownika, aby obsługiwać również operacje zakresu, co zapewnia lepszą wydajność.
MyToolWindowData
iMyColor
nie implementuj elementu INotifyPropertyChanged , ponieważ w tej chwili wszystkie właściwości są tylko do odczytu.
Obsługa długotrwałych poleceń asynchronicznych
Jedną z najważniejszych różnic między zdalnym interfejsem użytkownika a normalnym WPF jest to, że wszystkie operacje obejmujące komunikację między interfejsem użytkownika a rozszerzeniem są asynchroniczne.
Polecenia asynchroniczne , takie jak AddColorCommand
to jawne, udostępniając asynchroniczne wywołanie zwrotne.
Efekt tego można zobaczyć, jeśli klikniesz przycisk Dodaj kolor wiele razy w krótkim czasie: ponieważ każde wykonanie polecenia trwa 2 sekundy, wiele wykonań równolegle, a wiele kolorów pojawi się na liście razem, gdy opóźnienie 2 sekundy się skończyło. Może to dać użytkownikowi wrażenie, że przycisk Dodaj kolor nie działa.
Aby rozwiązać ten problem, wyłącz przycisk podczas wykonywania polecenia asynchronicznego. Najprostszym sposobem wykonania tej czynności jest po prostu ustawienie CanExecute
polecenia na wartość false:
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;
}
});
To rozwiązanie nadal ma niedoskonałą synchronizację, ponieważ gdy użytkownik kliknie przycisk, wywołanie zwrotne polecenia jest wykonywane asynchronicznie w rozszerzeniu, wywołanie zwrotne ustawia CanExecute
wartość false
, która jest następnie propagowana asynchronicznie do kontekstu danych serwera proxy w procesie programu Visual Studio, co powoduje wyłączenie przycisku. Użytkownik może dwukrotnie kliknąć przycisk w krótkim odstępie czasu, zanim przycisk zostanie wyłączony.
Lepszym rozwiązaniem jest użycie RunningCommandsCount
właściwości poleceń asynchronicznych:
<Button Content="Add color" Command="{Binding AddColorCommand}" IsEnabled="{Binding AddColorCommand.RunningCommandsCount.IsZero}" Grid.Row="1" />
RunningCommandsCount
to licznik liczby współbieżnych wykonań asynchronicznych polecenia, które są obecnie w toku. Ten licznik jest zwiększany w wątku interfejsu użytkownika po kliknięciu przycisku, co pozwala synchronicznie wyłączyć przycisk przez powiązanie IsEnabled
z RunningCommandsCount.IsZero
.
Ponieważ wszystkie polecenia zdalnego interfejsu użytkownika są wykonywane asynchronicznie, najlepszym rozwiązaniem jest zawsze używanie RunningCommandsCount.IsZero
do wyłączania kontrolek w razie potrzeby, nawet jeśli polecenie ma zostać wykonane szybko.
Polecenia asynchroniczne i szablony danych
W tej sekcji zaimplementujesz przycisk Usuń , który umożliwia użytkownikowi usunięcie wpisu z listy. Możemy utworzyć jedno polecenie asynchroniczne dla każdego MyColor
obiektu lub możemy mieć jedno polecenie asynchroniczne w MyToolWindowData
programie i użyć parametru do określenia, który kolor należy usunąć. Ta ostatnia opcja jest czystszą konstrukcją, więc zaimplementujmy to.
- Zaktualizuj przycisk XAML w szablonie danych:
<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}}}" />
- Dodaj odpowiadający element
AsyncCommand
:MyToolWindowData
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
- Ustaw asynchroniczne wywołanie zwrotne polecenia w konstruktorze
MyToolWindowData
polecenia :
RemoveColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
await Task.Delay(TimeSpan.FromSeconds(2));
Colors.Remove((MyColor)parameter!);
});
Ten kod używa klasy a Task.Delay
do symulowania długotrwałego wykonywania polecenia asynchronicznego.
Typy odwołań w kontekście danych
W poprzednim kodzie MyColor
obiekt jest odbierany jako parametr polecenia asynchronicznego i używany jako parametr List<T>.Remove
wywołania, który stosuje równość odwołań (ponieważ MyColor
jest typem odwołania, który nie zastępuje Equals
) w celu zidentyfikowania elementu do usunięcia. Jest to możliwe, ponieważ nawet jeśli parametr jest odbierany z interfejsu użytkownika, dokładne wystąpienie MyColor
, które jest obecnie częścią kontekstu danych, jest odbierane, a nie kopia.
Procesy
- serwer proxy kontekstu danych zdalnego sterowania użytkownikami;
- wysyłanie
INotifyPropertyChanged
aktualizacji z rozszerzenia do programu Visual Studio lub na odwrót; - wysyłanie obserwowanych aktualizacji kolekcji z rozszerzenia do programu Visual Studio lub na odwrót;
- wysyłanie parametrów polecenia asynchronicznego
wszystkie honoruje tożsamość obiektów typu odwołania. Z wyjątkiem ciągów obiekty typu odwołania nigdy nie są duplikowane po przeniesieniu z powrotem do rozszerzenia.
Na obrazie można zobaczyć, jak każdy obiekt typu odwołania w kontekście danych (polecenia, kolekcja, każdy MyColor
, a nawet cały kontekst danych) jest przypisany unikatowy identyfikator przez infrastrukturę zdalnego interfejsu użytkownika. Gdy użytkownik kliknie przycisk Usuń dla obiektu koloru serwera proxy #5, unikatowy identyfikator (#5), a nie wartość obiektu, zostanie odesłany do rozszerzenia. Infrastruktura zdalnego interfejsu użytkownika zajmuje się pobieraniem odpowiedniego MyColor
obiektu i przekazywaniem go jako parametru do wywołania zwrotnego polecenia asynchronicznego.
RunningCommandsCount z wieloma powiązaniami i obsługą zdarzeń
Jeśli w tym momencie przetestujesz rozszerzenie, zwróć uwagę, że po kliknięciu jednego z przycisków Usuń wszystkie przyciski Usuń są wyłączone:
Może to być pożądane zachowanie. Załóżmy jednak, że chcesz wyłączyć tylko bieżący przycisk i zezwolić użytkownikowi na kolejkowanie wielu kolorów do usunięcia: nie możemy użyć właściwości poleceniaRunningCommandsCount
asynchronicznego, ponieważ mamy jedno polecenie współużytkowane między wszystkimi przyciskami.
Możemy osiągnąć nasz cel, dołączając RunningCommandsCount
właściwość do każdego przycisku, aby mieć oddzielny licznik dla każdego koloru. Te funkcje są udostępniane przez przestrzeń nazw, która umożliwia korzystanie z typów zdalnego interfejsu http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml
użytkownika z języka XAML:
Zmienimy przycisk Usuń na następujące:
<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>
Dołączona vs:ExtensibilityUICommands.EventHandlers
właściwość umożliwia przypisywanie poleceń asynchronicznych do dowolnego zdarzenia (na przykład MouseRightButtonUp
) i może być przydatne w bardziej zaawansowanych scenariuszach.
vs:EventHandler
może również mieć wartość CounterTarget
: , UIElement
do której vs:ExtensibilityUICommands.RunningCommandsCount
należy dołączyć właściwość, zliczając aktywne wykonania związane z tym konkretnym zdarzeniem. Pamiętaj, aby użyć nawiasów (na przykład Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero
) podczas tworzenia powiązania z dołączoną właściwością.
W tym przypadku użyjemy polecenia vs:EventHandler
, aby dołączyć do każdego przycisku własny oddzielny licznik aktywnych wykonań poleceń. IsEnabled
Powiązanie z dołączoną właściwością spowoduje wyłączenie tylko tego konkretnego przycisku po usunięciu odpowiedniego koloru:
Słowniki zasobów XAML użytkownika
Począwszy od programu Visual Studio 17.10, interfejs użytkownika zdalnego obsługuje słowniki zasobów XAML. Dzięki temu wiele zdalnych kontrolek interfejsu użytkownika może udostępniać style, szablony i inne zasoby. Umożliwia również definiowanie różnych zasobów (np. ciągów) dla różnych języków.
Podobnie jak w przypadku Zdalnego sterowania interfejsem użytkownika XAML pliki zasobów muszą być skonfigurowane jako zasoby osadzone:
<ItemGroup>
<EmbeddedResource Include="MyResources.xaml" />
<Page Remove="MyResources.xaml" />
</ItemGroup>
Zdalny interfejs użytkownika odwołuje się do słowników zasobów w inny sposób niż WPF: nie są one dodawane do scalonych słowników kontrolki (scalone słowniki nie są w ogóle obsługiwane przez zdalny interfejs użytkownika), ale przywoływane przez nazwę w pliku .cs kontrolki:
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
this.ResourceDictionaries.AddEmbeddedResource(
"MyToolWindowExtension.MyResources.xaml");
}
...
AddEmbeddedResource
przyjmuje pełną nazwę osadzonego zasobu, który domyślnie składa się z głównej przestrzeni nazw projektu, dowolnej ścieżki podfolderu, w której może znajdować się, oraz nazwy pliku. Można zastąpić taką nazwę, ustawiając wartość dla LogicalName
EmbeddedResource
elementu w pliku projektu.
Sam plik zasobu jest normalnym słownikiem zasobów WPF:
<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>
Zasób można odwołać ze słownika zasobów w kontrolce zdalnego interfejsu użytkownika przy użyciu polecenia DynamicResource
:
<Button Content="{DynamicResource removeButtonText}" ...
Lokalizowanie słowników zasobów XAML
Słowniki zasobów zdalnego interfejsu użytkownika mogą być zlokalizowane w taki sam sposób, jak w przypadku lokalizowania zasobów osadzonych: tworzysz inne pliki XAML o tej samej nazwie i sufiksie języka, na przykład MyResources.it.xaml
dla włoskich zasobów:
<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>
Możesz użyć symboli wieloznacznych w pliku projektu, aby uwzględnić wszystkie zlokalizowane słowniki XAML jako zasoby osadzone:
<ItemGroup>
<EmbeddedResource Include="MyResources.*xaml" />
<Page Remove="MyResources.*xaml" />
</ItemGroup>
Używanie typów WPF w kontekście danych
Do tej pory kontekst danych zdalnego sterowania użytkownika składał się z elementów pierwotnych (liczb, ciągów itp.), obserwowalnych kolekcji i naszych własnych klas oznaczonych jako DataContract
. Czasami przydatne jest uwzględnienie prostych typów WPF w kontekście danych, takich jak złożone pędzle.
Ponieważ rozszerzenie VisualStudio.Extensibility może nawet nie być uruchamiane w procesie programu Visual Studio, nie może udostępniać obiektów WPF bezpośrednio za pomocą interfejsu użytkownika. Rozszerzenie może nawet nie mieć dostępu do typów WPF, ponieważ może być przeznaczone netstandard2.0
lub net6.0
(a nie wariant).-windows
Zdalny interfejs użytkownika udostępnia XamlFragment
typ, który umożliwia uwzględnienie definicji XAML obiektu WPF w kontekście danych zdalnego sterowania użytkownikami:
[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; }
}
Po powyższym Color
kodzie wartość właściwości jest konwertowana na LinearGradientBrush
obiekt na serwerze proxy kontekstu danych:
Zdalny interfejs użytkownika i wątki
Asynchroniczne wywołania zwrotne poleceń (i INotifyPropertyChanged
wywołania zwrotne dla wartości zaktualizowanych przez interfejs użytkownika za pośrednictwem oferty danych) są wywoływane w wątkach puli wątków losowych. Wywołania zwrotne są wywoływane pojedynczo i nie nakładają się, dopóki kod nie zwróci kontroli (przy użyciu await
wyrażenia).
To zachowanie można zmienić, przekazując element NonConcurrentSynchronizationContext do konstruktora RemoteUserControl
. W takim przypadku można użyć podanego kontekstu synchronizacji dla wszystkich poleceń asynchronicznych i INotifyPropertyChanged
wywołań zwrotnych związanych z tym formantem.
Powiązana zawartość
- Składniki rozszerzenia VisualStudio.Extensibility.