Руководство. Расширенный удаленный пользовательский интерфейс
В этом руководстве вы узнаете о расширенных концепциях удаленного пользовательского интерфейса путем добавочного изменения окна инструментов, в который отображается список случайных цветов:
Вы узнаете:
- Выполнение нескольких асинхронных команд может выполняться параллельно и как отключить элементы пользовательского интерфейса при выполнении команды.
- Как привязать несколько кнопок с одной асинхронной командой.
- Как ссылочные типы обрабатываются в контексте данных удаленного пользовательского интерфейса и его прокси-сервере.
- Как использовать асинхронную команду в качестве обработчика событий.
- Отключение одной кнопки при выполнении обратного вызова асинхронной команды, если несколько кнопок привязаны к одной и той же команде.
- Использование словарей ресурсов XAML из удаленного элемента управления пользовательского интерфейса.
- Как использовать типы WPF, такие как сложные кисти, в контексте данных удаленного пользовательского интерфейса.
- Как удаленный пользовательский интерфейс обрабатывает потоки.
Это руководство основано на вводной статье об удаленном пользовательском интерфейсе и ожидает, что у вас есть рабочее расширение VisualStudio.Extensibility, включая:
.cs
файл для команды, открывающей окно средства,MyToolWindow.cs
файл дляToolWindow
класса,MyToolWindowContent.cs
файл дляRemoteUserControl
класса,- внедренный
MyToolWindowContent.xaml
файл ресурсов дляRemoteUserControl
определения xaml, 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
и использовать параметр, чтобы определить, какой цвет следует удалить. Последний вариант является более чистым дизайном, поэтому давайте реализуем это.
- Обновите 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}}}" />
- Добавьте соответствующее
AsyncCommand
MyToolWindowData
:
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
- Задайте асинхронный обратный вызов команды в конструкторе
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
к присоединенному свойству при удалении соответствующего цвета будет отключена только определенная кнопка:
Словари ресурсов 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
объект в прокси-сервер контекста данных:
Удаленный пользовательский интерфейс и потоки
Обратные вызовы асинхронных команд (и INotifyPropertyChanged
обратные вызовы для значений, обновленных пользовательским интерфейсом с помощью предложения данных) создаются на потоках пула случайных потоков. Обратные вызовы создаются по одному за раз и не перекрываются до тех пор, пока код не даст элемент управления (с помощью await
выражения).
Это поведение можно изменить, передав конструктору RemoteUserControl
nonConcurrentSynchronizationContext. В этом случае можно использовать предоставленный контекст синхронизации для всех асинхронных команд и INotifyPropertyChanged
обратных вызовов, связанных с этим элементом управления.
Связанный контент
- Компоненты расширения VisualStudio.Extensibility.