다음을 통해 공유


원격 UI를 사용하는 이유

VisualStudio.Extensibility 모델의 기본 목표 중 하나는 Visual Studio 프로세스 외부에서 확장을 실행할 수 있도록 하는 것입니다. 대부분의 UI 프레임워크가 진행 중이므로 확장에 UI 지원을 추가하는 데 장애가 발생합니다.

원격 UI는 Out-of-process 확장에서 WPF 컨트롤을 정의하고 Visual Studio UI의 일부로 표시할 수 있는 클래스 집합입니다.

원격 UI는 XAML 및 데이터 바인딩, 명령(이벤트 대신) 및 트리거(코드 숨김의 논리 트리와 상호 작용하는 대신)를 사용하는 Model-View-ViewModel 디자인 패턴에 크게 의존합니다.

원격 UI는 Out-of-process 확장을 지원하기 위해 개발되었지만, 원격 UI를 사용하는 VisualStudio.Extensibility API는 프로세스 ToolWindow내 확장에도 원격 UI를 사용합니다.

원격 UI와 일반 WPF 개발 간의 기본 차이점은 다음과 같습니다.

  • 데이터 컨텍스트에 바인딩 및 명령 실행을 비롯한 대부분의 원격 UI 작업은 비동기적입니다.
  • 원격 UI 데이터 컨텍스트에서 사용할 데이터 형식을 정의할 때 DataContractDataMember 속성으로 데코레이트해야 하고 해당 유형은 원격 UI에서 직렬화할 수 있어야 합니다(자세한 내용은 여기 참조).
  • 원격 UI는 사용자 고유의 사용자 지정 컨트롤을 참조하는 것을 허용하지 않습니다.
  • 원격 사용자 컨트롤은 단일(하지만 잠재적으로 복잡하고 중첩된) 데이터 컨텍스트 개체를 참조하는 단일 XAML 파일에서 완전히 정의됩니다.
  • 원격 UI는 코드 숨김 또는 이벤트 처리기를 지원하지 않습니다(해결 방법은 고급 원격 UI 개념 문서에 설명 되어 있음 ).
  • 원격 사용자 컨트롤은 확장을 호스팅하는 프로세스가 아니라 Visual Studio 프로세스에서 인스턴스화됩니다. XAML은 확장에서 형식 및 어셈블리를 참조할 수 없지만 Visual Studio 프로세스에서 형식 및 어셈블리를 참조할 수 있습니다.

원격 UI 헬로 월드 확장 만들기

가장 기본적인 원격 UI 확장을 만들어 시작합니다. 첫 번째 Out-of-process Visual Studio 확장 만들기의 지침을 따릅니다.

이제 단일 명령으로 작업 확장이 있어야 합니다. 다음 단계는 ToolWindow 그리고 RemoteUserControl를 추가하는 것입니다. 이 RemoteUserControl 는 WPF 사용자 정의 컨트롤에 해당하는 원격 UI입니다.

그러면 4개의 파일이 표시됩니다.

  1. 도구 창을 여는 명령에 대한 .cs 파일입니다.
  2. .cs Visual Studio에 ToolWindow 제공하는 RemoteUserControl 파일
  3. RemoteUserControl XAML 정의를 참조하는 .cs 파일
  4. RemoteUserControl 에 대한 .xaml 파일입니다 .

나중에 MVVM 패턴에서 ViewModel을RemoteUserControl나타내는 데이터 컨텍스트를 추가합니다.

명령 업데이트

다음을 ShowToolWindowAsync사용하여 도구 창을 표시하도록 명령 코드를 업데이트합니다:

public override Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
    return Extensibility.Shell().ShowToolWindowAsync<MyToolWindow>(activate: true, cancellationToken);
}

또한 변경 CommandConfigurationstring-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 정의는 DataTemplate를 설명하는 일반적인 WPF XAML입니다 . 이 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 파일의 이름은 원격 사용자 제어 클래스와 동일해야 합니다. 정확하게 말하면 확장되는 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를 테마로 지정하여 다른 색을 사용할 수 있다는 점을 염두에 두고 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 특성은 확장 종속성 중 하나가 아닌 Microsoft.VisualStudio.Shell.15.0 어셈블리를 참조합니다. 이 XAML은 확장 자체가 아니라 Shell.15에 종속된 Visual Studio 프로세스에서 사용되기 때문에 괜찮습니다.

XAML 편집 환경을 향상하기 위해 확장 프로젝트에 임시로PackageReferenceMicrosoft.VisualStudio.Shell.15.0 에 추가할 수 있습니다. Out-of-process 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}" />

이제 레이블의 콘텐츠가 데이터 바인딩을 통해 설정됩니다.

데이터 바인딩이 있는 도구 창을 보여주는 스크린샷입니다.

여기에 있는 데이터 컨텍스트 형식은 DataMember 특성 그리고 DataContract 과 함께 표시됩니다. 이는 인스턴스가 MyToolWindowData 확장 호스트 프로세스에 있는 반면에서 MyToolWindowContent.xaml 만든 WPF 컨트롤은 Visual Studio 프로세스에 있기 때문입니다. 데이터 바인딩이 작동하도록 원격 UI 인프라는 Visual Studio 프로세스에서 개체의 MyToolWindowData 프록시를 생성합니다. 이 DataContractDataMember 특성은 데이터 바인딩과 관련된 형식 및 속성을 나타내며 프록시에 복제본(replica) 합니다.

원격 사용자 제어의 데이터 컨텍스트는 RemoteUserControl 클래스의 생성자 매개 변수로 전달됩니다.이 RemoteUserControl.DataContext 속성은 읽기 전용입니다. 이는 전체 데이터 컨텍스트를 변경할 수 없음을 의미하지는 않지만 원격 사용자 제어의 루트 데이터 컨텍스트 개체를 바꿀 수는 없습니다. 다음 섹션에서는 MyToolWindowData 변경 가능하고 관찰할 수 있도록 합니다.

직렬화 가능한 형식 및 원격 UI 데이터 컨텍스트

원격 UI 데이터 컨텍스트는 직렬화할 수 있는 형식만 포함할 수 있으며, 더 정확하게 말하면 직렬화할 수 있는 형식의 DataMember 속성만 데이터로 바인딩할 수 있습니다.

원격 UI에서 직렬화할 수 있는 형식은 다음과 같습니다.

  • 기본 데이터(대부분의 .NET 숫자 형식, 열거형, bool, string, DateTime)
  • DataContractDataMember 속성으로 표시된 확장자 정의 형식(그리고 모든 데이터 멤버도 직렬화 가능)
  • IAsyncCommand를 구현하는 개체
  • XamlFragmentSolidColorBrush 개체 및 색상
  • 직렬화 가능한 형식의 Nullable<>
  • 관찰 가능한 컬렉션을 포함하여 직렬화 가능한 형식의 컬렉션입니다.

원격 사용자 컨트롤의 수명 주기

컨트롤이 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를 제공합니다.

데이터 컨텍스트에는 일반적으로 읽기 전용 속성과 관찰 가능한 속성이 혼합되어 있습니다. 데이터 컨텍스트는 개체 및 DataContract 그리고 DataMember 특성으로 표시되고 필요에 따라 INotifyPropertyChanged를 구현하는 한 개체의 복잡한 그래프가 될 수 있습니다. 또한 범위 작업을 지원하기 위해 원격 UI에서 제공하는 확장된 ObservableCollection<T인> 관찰 가능한 컬렉션 또는 ObservableList<T를> 포함할 수 있으므로 성능이 향상됩니다.

또한 데이터 컨텍스트에 명령을 추가해야 합니다. 원격 UI에서 명령은 구현 IAsyncCommand 하지만 이 AsyncCommand 클래스의 인스턴스를 만드는 것이 더 쉬운 경우가 많습니다.

IAsyncCommand 은 다음과 같은 ICommand 두 가지 방법으로 다릅니다.

  • 왜냐하면, ExecuteAsync 원격 UI의 모든 항목이 비동기이므로 이 Execute 메서드가 대체됩니다.
  • 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 통신은 다음 단계를 따릅니다.

  1. 데이터 컨텍스트는 원래 콘텐츠를 사용하여 Visual Studio 프로세스 내의 프록시를 통해 액세스됩니다.

  2. MyToolWindowContent.xaml 에서 생성된 컨트롤은 데이터 컨텍스트 프록시에 바인딩된 데이터입니다.

  3. 사용자는 데이터 바인딩을 통해 데이터 컨텍스트 프록시의 이 Name 속성에 할당되는 텍스트 상자에 일부 텍스트를 입력합니다. 새 값 NameMyToolWindowData 개체에 전파됩니다.

  4. 사용자가 단추를 클릭하면 연속적인 효과가 발생합니다.

    • 데이터 컨텍스트 프록시의 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 표현 포함) 경합 조건을 방지할 수 있습니다.

비동기 명령은 여러 속성의 데이터를 사용합니다.

명령 매개 변수를 사용하는 것은 명령이 사용자가 설정할 수 있는 여러 속성을 사용해야 하는 경우 옵션이 아닙니다. 예를 들어 UI에 "이름" 및 "성"이라는 두 개의 텍스트 상자가 있는 경우입니다.

이 경우 해결 방법은 비동기 명령 콜백에서 생성하기 전에 데이터 컨텍스트의 모든 속성 값을 검색하는 것입니다.

아래에서 명령 호출 시 값이 사용되는지 확인하기 위해 생성하기 전에 LastName 속성 값과 FirstName 속성 값이 검색되는 샘플을 볼 수 있습니다.

HelloCommand = new(async (parameter, cancellationToken) =>
{
    string firstName = FirstName;
    string lastName = LastName;
    await Task.Delay(TimeSpan.FromSeconds(1));
    Text = $"Hello {firstName} {lastName}!";
});

또한 사용자가 업데이트할 수 있는 속성 값을 비동기적으로 업데이트하지 않도록 하는 것이 중요합니다. 즉, TwoWay 데이터 바인딩을 사용하지 마세요.

여기에 있는 정보는 간단한 원격 UI 구성 요소를 빌드하기에 충분해야 합니다. 고급 시나리오는 고급 원격 UI 개념을 참조 하세요.