Sdílet prostřednictvím


Kurz: Pokročilé vzdálené uživatelské rozhraní

V tomto kurzu se dozvíte o pokročilých konceptech vzdáleného uživatelského rozhraní postupným úpravou okna nástroje, který zobrazuje seznam náhodných barev:

Snímek obrazovky s oknem nástroje pro náhodné barvy

Dozvíte se o:

  • Jak se může paralelně spouštět více asynchronních příkazů a jak zakázat prvky uživatelského rozhraní při spuštění příkazu.
  • Postup vytvoření vazby více tlačítek ke stejnému asynchronnímu příkazu
  • Způsob zpracování referenčních typů v kontextu dat vzdáleného uživatelského rozhraní a jeho proxy serveru
  • Jak použít asynchronní příkaz jako obslužnou rutinu události.
  • Jak zakázat jedno tlačítko, když je volání asynchronního příkazu spuštěno, pokud je více tlačítek vázáno na stejný příkaz.
  • Použití slovníků prostředků XAML ze vzdáleného ovládacího prvku uživatelského rozhraní
  • Jak používat typy WPF, jako jsou složité štětce, v kontextu dat vzdáleného uživatelského rozhraní.
  • Jak vzdálené uživatelské rozhraní zpracovává vlákna.

Tento kurz je založený na úvodním článku o vzdáleném uživatelském rozhraní a očekává, že máte funkční rozšíření VisualStudio.Extensibility, včetně:

  1. .cs soubor pro příkaz, který otevře okno nástroje,
  2. MyToolWindow.cs soubor pro ToolWindow třídu,
  3. MyToolWindowContent.cs soubor pro RemoteUserControl třídu,
  4. vložený MyToolWindowContent.xaml soubor prostředků pro definici RemoteUserControl xaml,
  5. MyToolWindowData.cs a file for the data context of the RemoteUserControl.

Začněte tak, že aktualizujete MyToolWindowContent.xaml zobrazení seznamu a tlačítko":

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

Potom aktualizujte třídu MyToolWindowData.cskontextu dat:

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

V tomto kódu je jen pár zajímavých věcí:

  • MyColor.Color je, string ale používá se jako Brush při vazbě dat v JAZYCE XAML, jedná se o funkci, kterou poskytuje WPF.
  • Asynchronní AddColorCommand zpětné volání obsahuje dvousekundové zpoždění pro simulaci dlouhotrvající operace.
  • Používáme 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.
  • MyToolWindowData a MyColor neimplementujte INotifyPropertyChanged , protože v tuto chvíli jsou všechny vlastnosti jen pro čtení.

Zpracování dlouhotrvajících asynchronních příkazů

Jedním z nejdůležitějších rozdílů mezi vzdáleným uživatelským rozhraním a normálním WPF je, že všechny operace, které zahrnují komunikaci mezi uživatelským rozhraním a rozšířením, jsou asynchronní.

Asynchronní příkazy , jako je AddColorCommand tato explicitní, poskytují asynchronní zpětné volání.

Pokud kliknete na tlačítko Přidat barvu několikrát za krátkou dobu, uvidíte efekt tohoto příkazu: vzhledem k tomu, že každé spuštění příkazu trvá 2 sekundy, dojde k více spuštěním paralelně a v seznamu se zobrazí více barev, když je zpoždění dvousekundové. To může uživateli dát dojem, že tlačítko Přidat barvu nefunguje.

Diagram provádění překrývajících se asynchronních příkazů

Pokud to chcete vyřešit, zakažte tlačítko, zatímco se provádí asynchronní příkaz . Nejjednodušším způsobem, jak to udělat, je jednoduše nastavit CanExecute příkaz na 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;
    }
});

Toto řešení má stále nedostupnou synchronizaci, protože když uživatel klikne na tlačítko, spustí se zpětné volání příkazu asynchronně v rozšíření, zpětné volání se nastaví CanExecute na false, který se pak asynchronně rozšíří do kontextu dat proxy v procesu sady Visual Studio, což vede k zakázání tlačítka. Uživatel může kliknout dvakrát po sobě, než bude tlačítko zakázané.

Lepším řešením je použít RunningCommandsCount vlastnost asynchronních příkazů:

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

RunningCommandsCount je čítač toho, kolik souběžných asynchronních spuštění příkazu právě probíhá. Tento čítač se na vlákně uživatelského rozhraní zvýší, jakmile na tlačítko kliknete, což umožňuje synchronně zakázat tlačítko vazbou IsEnabled na RunningCommandsCount.IsZero.

Vzhledem k tomu, že všechny příkazy vzdáleného uživatelského rozhraní se spouštějí asynchronně, osvědčeným postupem je vždy RunningCommandsCount.IsZero zakázat ovládací prvky, pokud je to vhodné, i když se očekává, že se příkaz dokončí rychle.

Asynchronní příkazy a šablony dat

V této části implementujete tlačítko Odebrat , které uživateli umožní odstranit položku ze seznamu. Můžeme buď vytvořit jeden asynchronní příkaz pro každý MyColor objekt, nebo můžeme mít jeden asynchronní příkaz MyToolWindowData a použít parametr k identifikaci barvy, která se má odebrat. Druhá možnost je čistější návrh, takže to implementujme.

  1. Aktualizujte kód XAML tlačítka v šabloně dat:
<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. Přidejte odpovídajícíAsyncCommand:MyToolWindowData
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
  1. Nastavte asynchronní zpětné volání příkazu v konstruktoru MyToolWindowData:
RemoveColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
    await Task.Delay(TimeSpan.FromSeconds(2));

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

Tento kód používá Task.Delay k simulaci dlouhotrvajícího asynchronního spuštění příkazu .

Odkazové typy v kontextu dat

V předchozím kódu MyColor se objekt přijme jako parametr asynchronního příkazu a použije se jako parametr List<T>.Remove volání, který využívá rovnost odkazů (protože MyColor jde o typ odkazu, který nepřepíše Equals) k identifikaci prvku, který se má odebrat. Je to možné, protože i když je parametr přijat z uživatelského rozhraní, přesná instance MyColor , která je aktuálně součástí kontextu dat, se přijímá, nikoli kopie.

Procesy

  • proxy kontextu dat vzdáleného uživatelského ovládacího prvku;
  • odesílání INotifyPropertyChanged aktualizací z rozšíření do sady Visual Studio nebo naopak;
  • odesílání pozorovatelných aktualizací kolekce z rozšíření do sady Visual Studio nebo naopak;
  • odesílání parametrů asynchronního příkazu

všechny respektují identitu objektů typu odkazu. S výjimkou řetězců se objekty typu odkazu při přenosu zpět do rozšíření nikdy duplikují.

Diagram referenčních typů datových vazeb vzdáleného uživatelského rozhraní

Na obrázku vidíte, jak má každý objekt typu odkazu v kontextu dat (příkazy, kolekci, každý MyColor i celý kontext dat) přiřazen jedinečný identifikátor infrastruktury vzdáleného uživatelského rozhraní. Když uživatel klikne na tlačítko Odebrat pro objekt barvy proxy serveru #5, jedinečný identifikátor (#5), nikoli hodnota objektu, se odešle zpět do rozšíření. Infrastruktura vzdáleného uživatelského rozhraní se postará o načtení odpovídajícího MyColor objektu a jeho předání jako parametr zpětnému volání asynchronního příkazu.

RunningCommandsCount s více vazbami a zpracováním událostí

Pokud rozšíření otestujete v tomto okamžiku, všimněte si, že když kliknete na jedno z tlačítek Odebrat , jsou všechna tlačítka Odebrat zakázaná:

Diagram asynchronního příkazu s více vazbami

Toto může být požadované chování. Předpokládejme ale, že chcete zakázat pouze aktuální tlačítko a uživateli povolíte, aby zařadil několik barev do fronty pro odebrání: nemůžeme použít vlastnost asynchronního RunningCommandsCount příkazu, protože mezi všemi tlačítky sdílíme jeden příkaz.

Náš cíl můžeme dosáhnout tak, že ke každému tlačítku RunningCommandsCount připojíme vlastnost, abychom pro každou barvu měli samostatný čítač. Obor názvů poskytuje tyto funkce, které umožňují využívat typy vzdáleného http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml uživatelského rozhraní z XAML:

Tlačítko Odebrat změníme na následující:

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

Připojená vs:ExtensibilityUICommands.EventHandlers vlastnost umožňuje přiřadit asynchronní příkazy k jakékoli události (například MouseRightButtonUp) a může být užitečné v pokročilejších scénářích.

vs:EventHandler může mít CounterTargettaké : a, UIElement ke které vs:ExtensibilityUICommands.RunningCommandsCount by měla být vlastnost připojena, počítá aktivní spuštění související s danou konkrétní událostí. Při vazbě k připojené vlastnosti nezapomeňte použít závorky (například Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero).

V tomto případě se k jednotlivým tlačítkům připojíme vs:EventHandler k vlastnímu samostatnému čítači aktivních spuštění příkazů. IsEnabled Vazbou na připojenou vlastnost je při odebrání odpovídající barvy zakázáno pouze toto konkrétní tlačítko:

Diagram asynchronního příkazu s cílovým parametrem RunningCommandsCount

Slovníky prostředků XAML uživatele

Od sady Visual Studio 17.10 podporuje vzdálené uživatelské rozhraní slovníky prostředků XAML. To umožňuje více ovládacím prvkům vzdáleného uživatelského rozhraní sdílet styly, šablony a další prostředky. Umožňuje také definovat různé prostředky (např. řetězce) pro různé jazyky.

Podobně jako soubor XAML vzdáleného ovládacího prvku uživatelského rozhraní musí být soubory prostředků nakonfigurované jako vložené prostředky:

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

Vzdálené uživatelské rozhraní odkazuje na slovníky prostředků jiným způsobem než WPF: nejsou přidány do sloučených slovníků ovládacího prvku (sloučené slovníky nejsou podporovány vůbec vzdáleným uživatelským rozhraním), ale odkazují na název v souboru .cs ovládacího prvku:

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

AddEmbeddedResource přebírá úplný název vloženého prostředku, který se ve výchozím nastavení skládá z kořenového oboru názvů projektu, jakékoli podsložky, pod kterou může být, a název souboru. Tento název je možné přepsat nastavením LogicalName pro EmbeddedResource soubor projektu.

Samotný soubor prostředků je normální slovník prostředků 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>

Prostředek ze slovníku prostředků můžete odkazovat v ovládacím prvku vzdáleného uživatelského rozhraní pomocí DynamicResource:

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

Lokalizace slovníků prostředků XAML

Slovníky prostředků vzdáleného uživatelského rozhraní lze lokalizovat stejným způsobem, jako byste lokalizovali vložené prostředky: vytvoříte další soubory XAML se stejným názvem a příponou jazyka, například MyResources.it.xaml pro italské prostředky:

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

Pomocí zástupných znaků v souboru projektu můžete zahrnout všechny lokalizované slovníky XAML jako vložené prostředky:

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

Použití typů WPF v kontextu dat

Doteď se datový kontext našeho vzdáleného uživatelského ovládacího prvku skládají z primitiv (čísla, řetězce atd.), pozorovatelných kolekcí a vlastních tříd označených DataContract. Někdy je užitečné zahrnout do kontextu dat jednoduché typy WPF, jako jsou složité štětce.

Vzhledem k tomu, že rozšíření VisualStudio.Extensibility nemusí ani běžet v procesu sady Visual Studio, nemůže sdílet objekty WPF přímo s jeho uživatelským rozhraním. Rozšíření nemusí mít ani přístup k typům WPF, protože může cílit netstandard2.0 nebo net6.0 (ne varianta -windows ).

Vzdálené uživatelské rozhraní poskytuje XamlFragment typ, který umožňuje zahrnutí definice XAML objektu WPF v datovém kontextu vzdáleného uživatelského ovládacího prvku:

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

Při výše uvedeném Color kódu se hodnota vlastnosti převede na LinearGradientBrush objekt v proxy kontextu dat: Snímek obrazovky znázorňující typy WPF v kontextu dat

Vzdálené uživatelské rozhraní a vlákna

Zpětná volání asynchronních příkazů (a INotifyPropertyChanged zpětná volání pro hodnoty aktualizované uživatelským rozhraním prostřednictvím nabídky dat) jsou vyvolána v náhodných vláknech fondu vláken. Zpětná volání jsou vyvolána po jednom a nepřekrývají se, dokud kód nevrátí ovládací prvek (pomocí výrazu await ).

Toto chování lze změnit předáním NonConcurrentSynchronizationContext konstruktoru RemoteUserControl . V takovém případě můžete použít zadaný kontext synchronizace pro všechny asynchronní příkazy a INotifyPropertyChanged zpětná volání související s tímto ovládacím prvek.