リモート 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 つのコマンドで動作する拡張機能が作成されました。次の手順ではToolWindowRemoteUserControl、 これは RemoteUserControl 、WPF ユーザー コントロールと同等のリモート UI です。

最終的に、次の 4 つのファイルが作成されます。

  1. .csツール ウィンドウを開くコマンドのファイル
  2. .csを提供RemoteUserControlするファイルToolWindow
  3. .csその XAML 定義をRemoteUserControl参照するファイル
  4. .xaml ファイルです RemoteUserControl

その後、MVVM パターンの RemoteUserControlViewModel を表すデータ コンテキストを追加します。

コマンドを更新する

次を使用して、ツール ウィンドウを表示するようにコマンドのコードを 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.xamlMyToolWindowExtension.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.0net6.0-windowsのターゲット フレームワークを変更することもできます。

拡張機能のテスト

これで、拡張機能をデバッグするために押 F5 すことができるようになります。

Screenshot showing menu and tool window.

テーマのサポートを追加する

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 の残りの部分と同じテーマを使用し、ユーザーがダーク モードに切り替わると自動的に色が変更されるようになりました。

Screenshot showing themed tool window.

ここでは、この属性は 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}" />

ラベルの内容は、データ バインドによって設定されるようになりました。

Screenshot showing tool window with data binding.

ここでのデータ コンテキストの種類は、属性でDataContractDataMemberマークされています。 これは、インスタンスが 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>

Diagram of tool window with two-way binding and a command.

リモート UI での非同期性について

このツール ウィンドウのリモート UI 通信全体は、次の手順に従います。

  1. データ コンテキストは、元のコンテンツを使用して Visual Studio プロセス内のプロキシを介してアクセスされます。

  2. 作成される MyToolWindowContent.xaml コントロールは、データ コンテキスト プロキシにバインドされたデータです。

  3. ユーザーはテキスト ボックスにテキストを入力します。このテキストは、データ バインドを使用してデータ コンテキスト プロキシのプロパティに割り当てられます Name 。 新しい値 Name がオブジェクトに MyToolWindowData 反映されます。

  4. ユーザーがボタンをクリックすると、効果が連鎖します。

    • in HelloCommand the in the data context proxy is executed
    • エクステンダー AsyncCommand のコードの非同期実行が開始される
    • 監視可能なプロパティの値を更新するための HelloCommand 非同期コールバック Text
    • の新しい値 Text がデータ コンテキスト プロキシに伝達される
    • ツール ウィンドウのテキスト ブロックが、データ バインディング経由の新しい値 Text に更新されます

Diagram of tool window two-way binding and commands communication.

競合状態を回避するためのコマンド パラメーターの使用

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 の概念」を参照してください