リモート UI を使用する理由
VisualStudio.Extensibility モデルのメイン目標の 1 つは、Visual Studio プロセスの外部で拡張機能を実行できるようにすることです。 これにより、ほとんどの UI フレームワークがインプロセスであるため、拡張機能に UI サポートを追加するための障害が発生します。
リモート UI は、アウトプロセス拡張機能で WPF コントロールを定義し、Visual Studio UI の一部として表示できるクラスのセットです。
リモート UI は、XAML とデータ バインディング、コマンド (イベントではなく)、トリガー (分離コードから論理ツリーと対話するのではなく) に依存する Model-View-ViewModel 設計パターンに大きく依存します。
リモート UI はアウトプロセス拡張機能をサポートするために開発されましたが、リモート UI に依存する VisualStudio.Extensibility API では、 ToolWindow
インプロセス拡張機能にもリモート UI が使用されます。
リモート UI と通常の WPF 開発のメインの違いは次のとおりです。
- データ コンテキストへのバインドやコマンドの実行など、ほとんどのリモート UI 操作は非同期です。
- リモート UI データ コンテキストで使用するデータ型を定義する場合は、それらのデータ型を属性と
DataMember
共に修飾するDataContract
必要があります。 - リモート UI では、独自のカスタム コントロールを参照できません。
- リモート ユーザー コントロールは、単一の (ただし複雑で入れ子になった可能性がある) データ コンテキスト オブジェクトを参照する 1 つの XAML ファイルで完全に定義されます。
- リモート UI では、分離コードやイベント ハンドラーはサポートされていません (回避策については、高度なリモート UI の 概念に関する ドキュメントを参照してください)。
- リモート ユーザー コントロールは、拡張機能をホストするプロセスではなく、Visual Studio プロセスでインスタンス化されます。XAML は拡張機能から型とアセンブリを参照することはできませんが、Visual Studio プロセスから型とアセンブリを参照できます。
リモート UI Hello World 拡張機能を作成する
まず、最も基本的なリモート UI 拡張機能を作成します。 最初のアウトプロセス Visual Studio 拡張機能の作成に関するページの手順に従います。
これで、1 つのコマンドで動作する拡張機能が作成されました。次の手順ではToolWindow
RemoteUserControl
、 これは RemoteUserControl
、WPF ユーザー コントロールと同等のリモート UI です。
最終的に、次の 4 つのファイルが作成されます。
.cs
ツール ウィンドウを開くコマンドのファイル.cs
を提供RemoteUserControl
するファイルToolWindow
。.cs
その XAML 定義をRemoteUserControl
参照するファイル- の
.xaml
ファイルですRemoteUserControl
。
その後、MVVM パターンの RemoteUserControl
ViewModel を表すデータ コンテキストを追加します。
コマンドを更新する
次を使用して、ツール ウィンドウを表示するようにコマンドのコードを 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);
}
}
リモート ユーザー コントロールを作成する
次の 3 つのファイルでこのアクションを実行します。
リモート ユーザー コントロール クラス
リモート ユーザー コントロール クラス (名前付き 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>
前述のように、このファイルの名前はリモート ユーザー コントロール クラスと同じである必要があります。 正確には、拡張 RemoteUserControl
するクラスの完全な名前が埋め込みリソースの名前と一致している必要があります。 たとえば、リモート ユーザー コントロール クラスの完全な名前が指定されている場合、埋め込みリソース名MyToolWindowExtension.MyToolWindowContent.xaml
は MyToolWindowExtension.MyToolWindowContent
. 既定では、埋め込みリソースには、プロジェクトのルート名前空間、サブフォルダーのパス、およびそれらのファイル名によって構成される名前が割り当てられます。 これにより、リモート ユーザー コントロール クラスがプロジェクトのルート名前空間とは異なる名前空間を使用している場合や、xaml ファイルがプロジェクトのルート フォルダーにない場合に問題が発生する可能性があります。 必要に応じて、タグを使用して埋め込みリソースの名前を LogicalName
強制できます。
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" LogicalName="MyToolWindowExtension.MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
リモート ユーザー コントロールの XAML 定義は、. DataTemplate
. この XAML は Visual Studio に送信され、ツール ウィンドウの内容を入力するために使用されます。 リモート UI XAML には、特殊な名前空間 (xmlns
属性) を使用します http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml
。
XAML を埋め込みリソースとして設定する
最後に、ファイルを .csproj
開き、XAML ファイルが埋め込みリソースとして扱われることを確認します。
<ItemGroup>
<EmbeddedResource Include="MyToolWindowContent.xaml" />
<Page Remove="MyToolWindowContent.xaml" />
</ItemGroup>
XAML ファイルのオートコンプリートを向上させるために、拡張機能net6.0
net6.0-windows
のターゲット フレームワークを変更することもできます。
拡張機能のテスト
これで、拡張機能をデバッグするために押 F5
すことができるようになります。
テーマのサポートを追加する
Visual Studio をテーマにしてさまざまな色を使用できることを念頭に置いて UI を記述することをお勧めします。
Visual Studio 全体で使用されるスタイルと色を使用するように 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>
<Grid.Resources>
<Style TargetType="Label" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogLabelStyleKey}}" />
</Grid.Resources>
<Label>Hello World</Label>
</Grid>
</DataTemplate>
ラベルは Visual Studio UI の残りの部分と同じテーマを使用し、ユーザーがダーク モードに切り替わると自動的に色が変更されるようになりました。
ここでは、この属性は xmlns
、拡張機能の 依存関係の 1 つではない Microsoft.VisualStudio.Shell.15.0 アセンブリを参照します。 この XAML は Visual Studio プロセスによって使用され、拡張機能自体ではなく Shell.15 に依存しているため、これは問題ありません。
XAML 編集エクスペリエンスを向上させるために、拡張機能プロジェクトに一時的に a をMicrosoft.VisualStudio.Shell.15.0
追加PackageReference
できます。 アウトプロセスの 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
拡張機能ホスト プロセスに存在し、作成された MyToolWindowContent.xaml
WPF コントロールが Visual Studio プロセスに存在するためです。 データ バインディングを機能させるために、リモート UI インフラストラクチャは Visual Studio プロセスでオブジェクトの MyToolWindowData
プロキシを生成します。 属性とDataMember
属性はDataContract
、データ バインディングに関連する型とプロパティを示し、プロキシでレプリケートする必要があります。
リモート ユーザー コントロールのデータ コンテキストは、クラスのRemoteUserControl
コンストラクター パラメーターとして渡されます。RemoteUserControl.DataContext
プロパティは読み取り専用です。 これは、データ コンテキスト全体が不変であることを意味するわけではありませんが、リモート ユーザー コントロールのルート データ コンテキスト オブジェクトを置き換えることはできません。 次のセクションでは MyToolWindowData
、変更可能で監視可能にします。
リモート ユーザー コントロールのライフサイクル
コントロールが WPF コンテナーに ControlLoadedAsync
最初に読み込まれたときに通知を受け取るメソッドをオーバーライドできます。 実装で、データ コンテキストの状態が UI イベントとは別に変化する可能性がある場合、メソッドは、 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 を実装 することで監視可能にすることができます。 または、リモート UI は、 NotifyPropertyChangedObject
定型コードを減らすために拡張できる便利な抽象クラスを提供します。
通常、データ コンテキストには、読み取り専用プロパティと監視可能なプロパティが混在しています。 データ コンテキストは、オブジェクトが and 属性でDataContract
マークされ、必要に応じて INotifyPropertyChanged を実装している限り、オブジェクトの複雑なグラフDataMember
にすることができます。 また、監視可能なコレクションまたは ObservableList<T> を使用することもできます。これは、リモート UI によって提供される拡張 ObservableCollection<T> であり、範囲操作もサポートするため、パフォーマンスが向上します。
また、データ コンテキストにコマンドを追加する必要があります。 リモート UI では、コマンドが実装 IAsyncCommand
されますが、多くの場合、クラスのインスタンスを作成する方が AsyncCommand
簡単です。
IAsyncCommand
は、次の 2 つの点で ICommand
異なります。
Execute
リモート UI のすべてが非同期であるため、このメソッドは置き換えられますExecuteAsync
。- メソッドは
CanExecute(object)
プロパティにCanExecute
置き換えられます。 クラスはAsyncCommand
観察可能にする処理をCanExecute
行います。
リモート UI ではイベント ハンドラーがサポートされていないため、UI から拡張機能へのすべての通知は、データ バインドとコマンドを使用して実装する必要があることに注意してください。
これは、次の結果のコード 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
します。 これはすべて通常の WPF XAML です。 IAsyncCommand
オブジェクトは Visual Studio プロセスで呼び出されたICommand
プロキシを介してアクセスされるため、通常どおりデータバインドできます。
<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>
リモート UI での非同期性について
このツール ウィンドウのリモート UI 通信全体は、次の手順に従います。
データ コンテキストは、元のコンテンツを使用して Visual Studio プロセス内のプロキシを介してアクセスされます。
作成される
MyToolWindowContent.xaml
コントロールは、データ コンテキスト プロキシにバインドされたデータです。ユーザーはテキスト ボックスにテキストを入力します。このテキストは、データ バインドを使用してデータ コンテキスト プロキシのプロパティに割り当てられます
Name
。 新しい値Name
がオブジェクトにMyToolWindowData
反映されます。ユーザーがボタンをクリックすると、効果が連鎖します。
- in
HelloCommand
the in the data context proxy is executed - エクステンダー
AsyncCommand
のコードの非同期実行が開始される - 監視可能なプロパティの値を更新するための
HelloCommand
非同期コールバックText
- の新しい値
Text
がデータ コンテキスト プロキシに伝達される - ツール ウィンドウのテキスト ブロックが、データ バインディング経由の新しい値
Text
に更新されます
- in
競合状態を回避するためのコマンド パラメーターの使用
Visual Studio と拡張機能 (図の青い矢印) の間の通信を含むすべての操作は非同期です。 拡張機能の全体的な設計では、この側面を考慮することが重要です。
このため、整合性が重要な場合は、双方向バインディングではなくコマンド パラメーターを使用して、コマンドの実行時にデータ コンテキストの状態を取得することをお勧めします。
この変更を行うには、ボタンを次の値にName
バインドしますCommandParameter
。
<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
) 場合です。
非同期コマンドは、複数のプロパティのデータを使用します
コマンド パラメーターを使用することは、ユーザーが設定できる複数のプロパティをコマンドで使用する必要がある場合には、オプションではありません。 たとえば、UI に "First Name" と "Last Name" という 2 つのテキスト ボックスがある場合です。
この場合の解決策は、非同期コマンド コールバックで、生成する前にデータ コンテキストからすべてのプロパティの値を取得することです。
コマンド呼び出し時の FirstName
値が使用されていることを確認するために、生成する前に、および LastName
プロパティ値が取得されるサンプルを次に示します。
HelloCommand = new(async (parameter, cancellationToken) =>
{
string firstName = FirstName;
string lastName = LastName;
await Task.Delay(TimeSpan.FromSeconds(1));
Text = $"Hello {firstName} {lastName}!";
});
また、拡張機能が、ユーザーが更新できるプロパティの値を非同期的に更新しないようにすることも重要です。 言い換えると、TwoWay データ バインディングは避けてください。
関連するコンテンツ
ここでの情報は、単純なリモート UI コンポーネントを構築するのに十分なはずです。 より高度なシナリオについては、「高度なリモート UI の概念」を参照してください。