Почему удаленный пользовательский интерфейс
Одной из основных целей модели VisualStudio.Extensibility является разрешение расширений выполняться за пределами процесса Visual Studio. Это представляет собой препятствие для добавления поддержки пользовательского интерфейса в расширения, так как большинство платформ пользовательского интерфейса являются внутрипроцессными.
Удаленный пользовательский интерфейс — это набор классов, позволяющих определять элементы управления WPF в расширении вне процесса и отображать их как часть пользовательского интерфейса Visual Studio.
Удаленный пользовательский интерфейс сильно опирается на шаблон конструктора Model-View-ViewModel , основанный на привязке XAML и данных, командах (вместо событий) и триггерах (вместо взаимодействия с логическим деревом из кода позади кода).
Хотя удаленный пользовательский интерфейс был разработан для поддержки расширений вне процесса, API-интерфейсы VisualStudio.Extensibility, использующие удаленный пользовательский интерфейс, например ToolWindow
, будут использовать удаленный пользовательский интерфейс для расширений внутри процесса.
Основными различиями между удаленным пользовательским интерфейсом и обычной разработкой WPF являются:
- Большинство операций удаленного пользовательского интерфейса, включая привязку к контексту данных и выполнению команд, являются асинхронными.
- При определении типов данных, используемых в контекстах данных удаленного пользовательского интерфейса, они должны быть украшены
DataContract
атрибутами иDataMember
их типом должны быть сериализуемыми с помощью удаленного пользовательского интерфейса (см . здесь подробные сведения). - Удаленный пользовательский интерфейс не позволяет ссылаться на собственные пользовательские элементы управления.
- Удаленный пользовательский элемент управления полностью определен в одном XAML-файле, который ссылается на один (но потенциально сложный и вложенный) объект контекста данных.
- Удаленный пользовательский интерфейс не поддерживает код позади или обработчики событий (обходные пути описаны в документе о расширенных концепциях удаленного пользовательского интерфейса).
- Удаленный пользовательский элемент управления создается в процессе Visual Studio, а не в процессе размещения расширения: XAML не может ссылаться на типы и сборки из расширения, но может ссылаться на типы и сборки из процесса Visual Studio.
Создание расширения Hello World для удаленного пользовательского интерфейса
Начните с создания самого базового расширения удаленного пользовательского интерфейса. Следуйте инструкциям по созданию первого внепроцессного расширения Visual Studio.
Теперь у вас должно быть рабочее расширение с одной командой, следующим шагом является добавление ToolWindow
и расширение RemoteUserControl
. Это RemoteUserControl
эквивалент удаленного пользовательского интерфейса пользовательского элемента управления WPF.
В конечном итоге вы получите четыре файла:
.cs
файл для команды, открывающей окно средства,.cs
файл,ToolWindow
предоставляющийRemoteUserControl
Visual Studio,.cs
файл, ссылающийся наRemoteUserControl
определение XAML,.xaml
файл дляRemoteUserControl
.
Далее вы добавите контекст данных для объекта RemoteUserControl
, который представляет ViewModel в шаблоне MVVM.
Обновление команды
Обновите код команды, чтобы отобразить окно инструментов с помощью ShowToolWindowAsync
:
public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}
Вы также можете рассмотреть возможность изменения CommandConfiguration
и string-resources.json
более подходящего отображаемого сообщения и размещения:
public override CommandConfiguration CommandConfiguration => new("%MyToolWindowCommand.DisplayName%")
{
Placements = new[] { CommandPlacement.KnownPlacements.ViewOtherWindowsMenu },
};
{
"MyToolWindowCommand.DisplayName": "My Tool Window"
}
Создание окна средства
Создайте файл MyToolWindow.cs
и определите MyToolWindow
расширение ToolWindow
класса.
Этот GetContentAsync
метод должен вернуть IRemoteUserControl
значение, которое вы определите на следующем шаге. Так как удаленный пользовательский элемент управления является удаленным, заботьтесь о его удалении, переопределяя Dispose(bool)
метод.
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);
}
}
Создание удаленного пользовательского элемента управления
Выполните это действие в трех файлах:
Класс удаленного управления пользователем
Класс удаленного элемента управления пользователем с именем MyToolWindowContent
прост:
namespace MyToolWindowExtension;
using Microsoft.VisualStudio.Extensibility.UI;
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: null)
{
}
}
Контекст данных еще не нужен, поэтому его null
можно задать сейчас.
Класс, расширяющийся RemoteUserControl
автоматически, использует внедренный ресурс XAML с тем же именем. Если вы хотите изменить это поведение, переопределите GetXamlAsync
метод.
Определение XAML
Теперь создайте файл с именем 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>
Определение XAML удаленного пользовательского элемента управления — это обычный КОД WPF XAML, описывающий объект DataTemplate
. Этот XAML отправляется в Visual Studio и используется для заполнения содержимого окна инструментов. Для XAML http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml
удаленного пользовательского интерфейса используется специальное пространство имен (xmlns
атрибут).
Установка XAML в качестве внедренного ресурса
Наконец, откройте .csproj
файл и убедитесь, что XAML-файл рассматривается как внедренный ресурс:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Как описано ранее, XAML-файл должен иметь то же имя, что и класс удаленного пользовательского элемента управления . Чтобы быть точным, полное имя расширения класса RemoteUserControl
должно соответствовать имени внедренного ресурса. Например, если полное имя класса удаленного пользовательского элемента управления имеет значениеMyToolWindowExtension.MyToolWindowContent
, внедренное имя ресурса должно бытьMyToolWindowExtension.MyToolWindowContent.xaml
. По умолчанию внедренные ресурсы назначают имя, состоящее из корневого пространства имен для проекта, любой вложенный путь, в который они могут находиться, и их имя файла. Это может создать проблемы, если класс удаленного пользовательского элемента управления использует пространство имен, отличное от корневого пространства имен проекта, или если файл XAML не находится в корневой папке проекта. При необходимости можно принудительно принудить имя внедренного ресурса с помощью тега LogicalName
:
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
Тестирование расширения
Теперь вы сможете нажать клавишу F5
для отладки расширения.
Добавление поддержки тем
Рекомендуется написать пользовательский интерфейс с учетом того, что Visual Studio может быть темами, в результате чего используются различные цвета.
Обновите XAML, чтобы использовать стили и цвета, используемые в 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>
Теперь метка использует ту же тему, что и остальная часть пользовательского интерфейса Visual Studio, и автоматически изменяет цвет при переходе пользователя в темный режим:
xmlns
Здесь атрибут ссылается на сборку Microsoft.VisualStudio.Shell.15.0, которая не является одной из зависимостей расширения. Это хорошо, так как этот XAML используется процессом Visual Studio, который имеет зависимость от Shell.15, а не самого расширения.
Чтобы улучшить возможности редактирования XAML, можно временно добавить в PackageReference
Microsoft.VisualStudio.Shell.15.0
проект расширения. Не забудьте удалить его позже, так как расширение VisualStudio.Extensibility не должно ссылаться на этот пакет!
Добавление контекста данных
Добавьте класс контекста данных для удаленного пользовательского элемента управления:
using System.Runtime.Serialization;
namespace MyToolWindowExtension;
[DataContract]
internal class MyToolWindowData
{
[DataMember]
public string? LabelText { get; init; }
}
и обновите MyToolWindowContent.cs
и MyToolWindowContent.xaml
используйте его:
internal class MyToolWindowContent : RemoteUserControl
{
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData { LabelText = "Hello Binding!"})
{
}
<Label Content="{Binding LabelText}" />
Содержимое метки теперь устанавливается с помощью привязки данных:
Тип контекста данных здесь отмечен и DataContract
DataMember
атрибутами. Это связано с тем, что MyToolWindowData
экземпляр существует в процессе узла расширения, а элемент управления WPF, созданный из MyToolWindowContent.xaml
существующего в процессе Visual Studio. Чтобы сделать привязку данных работой, инфраструктура удаленного пользовательского MyToolWindowData
интерфейса создает прокси-сервер объекта в процессе Visual Studio. Атрибуты DataContract
указываютDataMember
, какие типы и свойства относятся к привязке данных и должны быть реплика в прокси-сервере.
Контекст данных удаленного пользовательского элемента управления передается в качестве параметра конструктора RemoteUserControl
класса: RemoteUserControl.DataContext
свойство доступно только для чтения. Это не означает, что весь контекст данных неизменяем, но корневой объект контекста данных удаленного пользовательского элемента управления нельзя заменить. В следующем разделе мы сделаем MyToolWindowData
изменяемые и наблюдаемые.
Сериализуемые типы и контекст данных удаленного пользовательского интерфейса
Контекст данных удаленного пользовательского интерфейса может содержать только сериализуемые типы или, чтобы быть более точным, только DataMember
свойства сериализуемого типа могут быть исходящими данными.
Только следующие типы сериализуются удаленным пользовательским интерфейсом:
- примитивные данные (большинство числовых типов .NET, перечисления,
bool
, ,DateTime
string
) - Определяемые расширением типы, помеченные атрибутами
DataContract
иDataMember
атрибутами (и все их члены данных также сериализуются) - объекты, реализующие IAsyncCommand
- Объекты XamlFragment и SolidColorBrush и значения color
Nullable<>
значения для сериализуемого типа- коллекции сериализуемых типов, включая наблюдаемые коллекции.
Жизненный цикл удаленного пользовательского элемента управления
Метод можно переопределить ControlLoadedAsync
, чтобы получать уведомления при первой загрузке элемента управления в контейнер WPF. Если в реализации состояние контекста данных может изменяться независимо от событий пользовательского интерфейса, ControlLoadedAsync
метод является правильным местом для инициализации содержимого контекста данных и начала применения изменений к нему.
Вы также можете переопределить Dispose
метод, который будет уведомляться при уничтожении элемента управления и больше не будет использоваться.
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);
}
}
Команды, наблюдаемость и двусторонняя привязка данных
Затем давайте сделаем контекст данных наблюдаемым и добавим кнопку на панель элементов.
Контекст данных можно сделать наблюдаемым, реализуя INotifyPropertyChanged. Кроме того, удаленный пользовательский интерфейс предоставляет удобный абстрактный класс, NotifyPropertyChangedObject
который можно расширить, чтобы уменьшить стандартный код.
Контекст данных обычно имеет сочетание свойств чтения и наблюдаемых свойств. Контекст данных может быть сложным графом объектов, если они помечены атрибутами DataContract
и DataMember
реализуют INotifyPropertyChanged по мере необходимости. Также можно иметь наблюдаемые коллекции или объект ObservableList<T, который является расширенным методом ObservableCollection<T>>, предоставляемым удаленным пользовательским интерфейсом, для поддержки операций диапазона, что позволяет повысить производительность.
Кроме того, необходимо добавить команду в контекст данных. В удаленном пользовательском интерфейсе команды реализуются IAsyncCommand
, но часто проще создать экземпляр AsyncCommand
класса.
IAsyncCommand
Отличается от ICommand
двух способов:
- Метод
Execute
заменяется,ExecuteAsync
так как все в удаленном пользовательском интерфейсе асинхронно! - Метод
CanExecute(object)
заменяется свойствомCanExecute
. КлассAsyncCommand
заботится о том, чтобы сделатьCanExecute
наблюдаемым.
Важно отметить, что удаленный пользовательский интерфейс не поддерживает обработчики событий, поэтому все уведомления из пользовательского интерфейса в расширение должны быть реализованы с помощью привязки данных и команд.
Это результирующий код для 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
:
public MyToolWindowContent()
: base(dataContext: new MyToolWindowData())
{
}
Обновите MyToolWindowContent.xaml
новые свойства в контексте данных. Это все нормальное XAML WPF. IAsyncCommand
Даже к объекту осуществляется доступ через прокси-сервер, вызываемый ICommand
в процессе 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}}" />
<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>
Общие сведения об асинхронности в удаленном пользовательском интерфейсе
Вся связь с удаленным пользовательским интерфейсом для этого окна инструментов выполняет следующие действия.
Контекст данных осуществляется через прокси-сервер в процессе Visual Studio с исходным содержимым.
Элемент управления, созданный из
MyToolWindowContent.xaml
данных, привязан к прокси-серверу контекста данных,Пользователь вводит некоторый текст в текстовом поле, который назначается
Name
свойству прокси-сервера контекста данных через привязку данных. Новое значениеName
распространяется наMyToolWindowData
объект.Пользователь нажимает кнопку, вызывающую каскад эффектов:
HelloCommand
Выполняется прокси-сервер контекста данных- запускается асинхронное выполнение кода расширителя
AsyncCommand
- асинхронный обратный вызов для
HelloCommand
обновления значения наблюдаемого свойстваText
- новое значение
Text
распространяется на прокси-сервер контекста данных - Текстовый блок в окне инструмента обновляется до нового значения привязки
Text
данных.
Использование параметров команды для предотвращения условий гонки
Все операции, связанные с взаимодействием между Visual Studio и расширением (синие стрелки на схеме), являются асинхронными. Важно учитывать этот аспект в общей структуре расширения.
По этой причине, если согласованность важна, лучше использовать параметры команд вместо двусторонней привязки, чтобы получить состояние контекста данных во время выполнения команды.
Внесите это изменение, привязав кнопку CommandParameter
к Name
:
<Button Content="Say Hello" Command="{Binding HelloCommand}" CommandParameter="{Binding Name}" Grid.Column="2" />
Затем измените обратный вызов команды, чтобы использовать параметр:
HelloCommand = new AsyncCommand((parameter, cancellationToken) =>
{
Text = $"Hello {(string)parameter!}!";
return Task.CompletedTask;
});
При таком подходе значение Name
свойства извлекается синхронно из прокси-сервера контекста данных во время нажатия кнопки и отправляется в расширение. Это позволяет избежать каких-либо условий гонки, особенно если HelloCommand
обратный вызов изменяется в будущем, чтобы получить (имеют await
выражения).
Асинхронные команды используют данные из нескольких свойств
Использование параметра команды не является параметром, если команда должна использовать несколько свойств, которые задаются пользователем. Например, если в пользовательском интерфейсе есть два текстовых поля: "Имя" и "Фамилия".
Решением в этом случае является получение в асинхронном вызове команды значение всех свойств из контекста данных перед получением.
Ниже приведен пример, в котором FirstName
извлекаются значения свойств и LastName
значения свойств перед получением, чтобы убедиться, что значение во время вызова команды используется:
HelloCommand = new(async (parameter, cancellationToken) =>
{
string firstName = FirstName;
string lastName = LastName;
await Task.Delay(TimeSpan.FromSeconds(1));
Text = $"Hello {firstName} {lastName}!";
});
Также важно избежать асинхронного обновления расширения значения свойств, которые также могут быть обновлены пользователем. Другими словами, избегайте привязки данных TwoWay .
Связанный контент
Сведения здесь должны быть достаточно для создания простых компонентов удаленного пользовательского интерфейса. Дополнительные сценарии см. в разделе "Дополнительные понятия удаленного пользовательского интерфейса".