Поделиться через


Руководство. Расширенный удаленный пользовательский интерфейс

В этом руководстве вы узнаете о расширенных концепциях удаленного пользовательского интерфейса путем добавочного изменения окна инструментов, в который отображается список случайных цветов:

Снимок экрана: окно инструмента случайных цветов.

Вы узнаете:

  • Выполнение нескольких асинхронных команд может выполняться параллельно и как отключить элементы пользовательского интерфейса при выполнении команды.
  • Как привязать несколько кнопок с одной асинхронной командой.
  • Как ссылочные типы обрабатываются в контексте данных удаленного пользовательского интерфейса и его прокси-сервере.
  • Как использовать асинхронную команду в качестве обработчика событий.
  • Отключение одной кнопки при выполнении обратного вызова асинхронной команды, если несколько кнопок привязаны к одной и той же команде.
  • Использование словарей ресурсов XAML из удаленного элемента управления пользовательского интерфейса.
  • Как использовать типы WPF, такие как сложные кисти, в контексте данных удаленного пользовательского интерфейса.
  • Как удаленный пользовательский интерфейс обрабатывает потоки.

Это руководство основано на вводной статье об удаленном пользовательском интерфейсе и ожидает, что у вас есть рабочее расширение VisualStudio.Extensibility, включая:

  1. .cs файл для команды, открывающей окно средства,
  2. MyToolWindow.cs файл для ToolWindow класса,
  3. MyToolWindowContent.cs файл для RemoteUserControl класса,
  4. внедренный MyToolWindowContent.xaml файл ресурсов для RemoteUserControl определения xaml,
  5. MyToolWindowData.cs файл для контекста RemoteUserControlданных .

Чтобы начать, обновите 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"
              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>

Затем обновите класс MyToolWindowData.csконтекста данных:

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

В этом коде есть лишь несколько важных моментов:

  • MyColor.Color — это функция string , которая используется в качестве Brush привязки данных в XAML, это возможность, предоставляемая WPF.
  • Асинхронный AddColorCommand обратный вызов содержит 2-секундную задержку для имитации длительной операции.
  • Мы используем ObservableList<T>, который является расширенным методом ObservableCollection<T> , предоставляемым удаленным пользовательским интерфейсом, для поддержки операций диапазона, что позволяет повысить производительность.
  • MyToolWindowData и MyColor не реализуйте INotifyPropertyChanged , так как на данный момент все свойства прочитаны.

Обработка длительных асинхронных команд

Одним из наиболее важных различий между удаленным пользовательским интерфейсом и обычным WPF является то, что все операции, связанные с взаимодействием между пользовательским интерфейсом и расширением, являются асинхронным.

Асинхронные команды, такие как AddColorCommand сделать это явным, предоставляя асинхронный обратный вызов.

Это можно увидеть при нажатии кнопки "Добавить цвет " несколько раз в короткое время: так как каждое выполнение команды занимает 2 секунды, несколько выполнения выполняются параллельно, а несколько цветов будут отображаться в списке вместе, когда задержка составляет 2 секунды. Это может дать пользователю впечатление о том, что кнопка "Добавить цвет " не работает.

Схема перекрываемого асинхронного выполнения команды.

Чтобы устранить эту проблему, отключите кнопку во время выполнения асинхронной команды . Самый простой способ сделать это — просто задать CanExecute для команды значение 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;
    }
});

Это решение по-прежнему имеет несовершенную синхронизацию, так как, когда пользователь щелкает кнопку, обратный вызов выполняется асинхронно в расширении, CanExecute falseасинхронно распространяется в контекст прокси-данных в процессе Visual Studio, что приводит к отключению кнопки. Пользователь может дважды нажать кнопку в быстром последовательности перед отключением кнопки.

Лучшее решение — использовать RunningCommandsCount свойство асинхронных команд:

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

RunningCommandsCount является счетчиком количества одновременных асинхронных выполнения команды в настоящее время. Этот счетчик увеличивается в потоке пользовательского интерфейса сразу после нажатия кнопки, которая позволяет синхронно отключить кнопку путем привязки IsEnabled RunningCommandsCount.IsZero.

Так как все команды удаленного пользовательского интерфейса выполняются асинхронно, рекомендуется всегда использовать RunningCommandsCount.IsZero для отключения элементов управления, даже если ожидается, что команда будет выполнена быстро.

Асинхронные команды и шаблоны данных

В этом разделе описана кнопка "Удалить ", которая позволяет пользователю удалять запись из списка. Мы можем создать одну асинхронную команду для каждого MyColor объекта или создать одну асинхронную команду MyToolWindowData и использовать параметр, чтобы определить, какой цвет следует удалить. Последний вариант является более чистым дизайном, поэтому давайте реализуем это.

  1. Обновите XAML кнопки в шаблоне данных:
<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. Добавьте соответствующее AsyncCommand MyToolWindowData:
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
  1. Задайте асинхронный обратный вызов команды в конструкторе MyToolWindowData:
RemoveColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
    await Task.Delay(TimeSpan.FromSeconds(2));

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

Этот код используется Task.Delay для имитации длительного выполнения асинхронной команды .

Ссылочные типы в контексте данных

В предыдущем коде MyColor объект получается в качестве параметра асинхронной команды и используется в качестве параметра List<T>.Remove вызова, который использует равенство ссылок (так как MyColor является ссылочным типом, который не переопределяетEquals) для идентификации элемента для удаления. Это возможно, так как, даже если параметр получен из пользовательского интерфейса, то точный экземпляр MyColor этого контекста данных получен, а не копия.

Процессы

  • прокси-сервер контекста данных удаленного элемента управления пользователем;
  • отправка INotifyPropertyChanged обновлений из расширения в Visual Studio или наоборот;
  • отправка наблюдаемых обновлений коллекции из расширения в Visual Studio или наоборот;
  • отправка асинхронных параметров команды

все учитывают удостоверение объектов ссылочного типа. За исключением строк, объекты ссылочного типа никогда не дублируются при передаче обратно в расширение.

Схема ссылочных типов привязки данных удаленного пользовательского интерфейса.

На рисунке показано, как каждый объект ссылочного типа в контексте данных (команды, коллекция, каждый MyColor и даже весь контекст данных) назначается уникальным идентификатором инфраструктуры удаленного пользовательского интерфейса. Когда пользователь нажимает кнопку "Удалить" для объекта цвета прокси-сервера #5, уникальный идентификатор (#5), а не значение объекта, отправляется обратно в расширение. Инфраструктура удаленного пользовательского интерфейса заботится о получении соответствующего MyColor объекта и передаче его в качестве параметра обратному вызову асинхронной команды.

RunningCommandsCount с несколькими привязками и обработкой событий

Если вы протестируете расширение на этом этапе, обратите внимание, что при нажатии одной из кнопок "Удалить " все кнопки "Удалить " отключены:

Схема асинхронной команды с несколькими привязками.

Это может быть требуемое поведение. Но предположим, что вы хотите отключить только текущую кнопку и разрешить пользователю ставить в очередь несколько цветов для удаления: мы не можем использовать свойство асинхронной командыRunningCommandsCount, так как у нас есть одна команда, к которой предоставлен общий доступ между всеми кнопками.

Мы можем достичь нашей цели, подключив RunningCommandsCount свойство к каждой кнопке, чтобы у нас был отдельный счетчик для каждого цвета. Эти функции предоставляются пространством http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml имен, что позволяет использовать типы удаленного пользовательского интерфейса из XAML:

Мы изменим кнопку "Удалить" следующим образом:

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

Присоединенное vs:ExtensibilityUICommands.EventHandlers свойство позволяет назначать асинхронные команды любому событию (например, MouseRightButtonUp) и может оказаться полезным в более сложных сценариях.

vs:EventHandler также может иметь CounterTargetзначение : к UIElement которому vs:ExtensibilityUICommands.RunningCommandsCount следует присоединить свойство, подсчитывая активные выполнения, связанные с этим конкретным событием. Не забудьте использовать скобки (например Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero, при привязке к присоединенному свойству).

В этом случае мы используем vs:EventHandler для подключения к каждой кнопке собственный отдельный счетчик активных выполнений команд. При привязке IsEnabled к присоединенному свойству при удалении соответствующего цвета будет отключена только определенная кнопка:

Схема асинхронной команды с целевым значением RunningCommandsCount.

Словари ресурсов XAML пользователя

Начиная с Visual Studio 17.10 удаленный пользовательский интерфейс поддерживает словари ресурсов XAML. Это позволяет нескольким элементам управления удаленным пользовательским интерфейсом совместно использовать стили, шаблоны и другие ресурсы. Он также позволяет определить различные ресурсы (например, строки) для разных языков.

Аналогично XAML удаленному элементу управления пользовательского интерфейса, файлы ресурсов должны быть настроены как внедренные ресурсы:

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

Удаленный пользовательский интерфейс ссылается на словари ресурсов по-другому, чем WPF: они не добавляются в объединенные словари элемента управления (объединенные словари не поддерживаются вообще удаленным пользовательским интерфейсом), но ссылаются по имени в .cs файле элемента управления:

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

AddEmbeddedResource принимает полное имя внедренного ресурса, который по умолчанию состоит из корневого пространства имен для проекта, любой путь к вложенным папкам, в котором он может находиться, и имя файла. Можно переопределить такое имя, задав LogicalName для него значение EmbeddedResource в файле проекта.

Сам файл ресурсов — это обычный словарь ресурсов 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>

Вы можете ссылаться на ресурс из словаря ресурсов в элементе управления удаленным пользовательским интерфейсом с помощью DynamicResource:

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

Локализация словарей ресурсов XAML

Словари ресурсов удаленного пользовательского интерфейса можно локализовать так же, как и при локализации внедренных ресурсов: вы создаете другие файлы XAML с тем же именем и суффиксом языка, например MyResources.it.xaml для итальянских ресурсов:

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

В файле проекта можно использовать дикие карта для включения всех локализованных словарей XAML в качестве внедренных ресурсов:

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

Использование типов WPF в контексте данных

До сих пор контекст данных удаленного управления пользователем состоит из примитивов (чисел, строк и т. д.), наблюдаемых коллекций и наших собственных классов, помеченных как DataContract. Иногда полезно включить простые типы WPF в контекст данных, такие как сложные кисти.

Так как расширение VisualStudio.Extensibility может даже не выполняться в процессе Visual Studio, оно не может совместно использовать объекты WPF непосредственно с его пользовательским интерфейсом. Расширение может даже не иметь доступа к типам WPF, так как он может целевой netstandard2.0 -windows или net6.0 (не вариант).

Удаленный пользовательский интерфейс предоставляет XamlFragment тип, позволяющий включить определение XAML объекта WPF в контексте данных удаленного пользовательского элемента управления:

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

В приведенном выше Color коде значение свойства преобразуется в LinearGradientBrush объект в прокси-сервер контекста данных: Снимок экрана: типы WPF в контексте данных

Удаленный пользовательский интерфейс и потоки

Обратные вызовы асинхронных командINotifyPropertyChanged обратные вызовы для значений, обновленных пользовательским интерфейсом с помощью предложения данных) создаются на потоках пула случайных потоков. Обратные вызовы создаются по одному за раз и не перекрываются до тех пор, пока код не даст элемент управления (с помощью await выражения).

Это поведение можно изменить, передав конструктору RemoteUserControl nonConcurrentSynchronizationContext. В этом случае можно использовать предоставленный контекст синхронизации для всех асинхронных команд и INotifyPropertyChanged обратных вызовов, связанных с этим элементом управления.