다음을 통해 공유


자습서: 고급 원격 UI

이 자습서에서는 임의 색 목록을 표시하는 도구 창을 증분 방식으로 수정하여 고급 원격 UI 개념에 대해 알아봅니다.

임의 색상 도구 창을 보여주는 스크린샷입니다.

학습 내용은 다음과 같습니다.

  • 여러 비동기 명령 실행을 병렬로 실행할 수 있는 방법과 명령 이 실행 중일 때 UI 요소를 사용하지 않도록 설정하는 방법입니다.
  • 여러 단추를 동일한 비동기 명령에 바인딩하는 방법입니다.
  • 원격 UI 데이터 컨텍스트 및 해당 프록시에서 참조 형식이 처리되는 방식
  • 비동기 명령을 이벤트 처리기로 사용하는 방법입니다.
  • 여러 단추가 동일한 명령에 바인딩된 경우 비동기 명령의 콜백이 실행 중일 때 단일 단추를 사용하지 않도록 설정하는 방법입니다.
  • 원격 UI 컨트롤에서 XAML 리소스 사전을 사용하는 방법입니다.
  • 원격 UI 데이터 컨텍스트에서 복잡한 브러시와 같은 WPF 형식을 사용하는 방법입니다.
  • 원격 UI에서 스레딩을 처리하는 방법입니다.

이 자습서는 소개 원격 UI 문서를 기반으로 하며 다음과 같은 VisualStudio.Extensibility 확장이 작동합니다.

  1. 도구 창을 여는 명령에 대한 .cs 파일입니다.
  2. MyToolWindow.cs 클래스에 대한 ToolWindow 파일,
  3. MyToolWindowContent.cs 클래스에 대한 RemoteUserControl 파일,
  4. RemoteUserControl xaml 정의에 대한 MyToolWindowContent.xaml 포함된 리소스 파일
  5. 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.Colorstring XAML에 바인딩된 데이터로 Brush 사용되지만 WPF에서 제공하는 기능입니다.
  • 비동기 콜백에는 AddColorCommand 장기 실행 작업을 시뮬레이션하는 데 2초의 지연이 포함됩니다.
  • 원격 UI에서 제공하는 확장된 ObservableCollection<T인> ObservableList<T>를 사용하여 범위 작업도 지원하므로 성능이 향상됩니다.
  • 현재 모든 속성이 읽기 전용이므로 INotifyPropertyChanged를 MyToolWindowData 그리고 MyColor 를 구현하지 마세요.

장기 실행 비동기 명령 처리

원격 UI와 일반 WPF 간의 가장 중요한 차이점 중 하나는 UI와 확장 간의 통신을 포함하는 모든 작업이 비동기라는 것입니다.

AddColorCommand 와 같은 비동기 명령은 비동기 콜백을 제공하여 이를 명시적으로 만듭니다.

짧은 시간에 색 추가 단추를 여러 번 클릭하면 이 효과를 볼 수 있습니다. 각 명령 실행은 2초가 걸리므로 여러 실행이 병렬로 수행되고 2초 지연이 끝나면 목록에 여러 색이 함께 표시됩니다. 이렇게 하면 사용자에게 색 추가 단추가 작동하지 않는다는 인상을 줄 수 있습니다.

겹치는 비동기 명령 실행 다이어그램

이 문제를 해결하려면 비동기 명령이 실행되는 동안 단추를 사용하지 않도록 설정합니다. 이 작업을 수행하는 가장 간단한 방법은 명령을 false로 CanExecute 설정하는 것입니다.

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 는 현재 진행 중인 명령의 동시 비동기 실행 수에 대한 카운터입니다. 이 카운터는 단추를 클릭하는 즉시 UI 스레드에서 증가하므로 바인딩 IsEnabled RunningCommandsCount.IsZero하여 단추를 동기적으로 사용하지 않도록 설정할 수 있습니다.

모든 원격 UI 명령은 비동기적으로 실행되므로 명령이 신속하게 완료되어야 하는 경우에도 항상 적절한 경우 컨트롤을 사용하지 않도록 RunningCommandsCount.IsZero 설정하는 것이 가장 좋습니다.

비동기 명령 및 데이터 템플릿

이 섹션에서는 사용자가 목록에서 항목을 삭제할 수 있도록 하는 제거 단추를 구현합니다. 각 MyColor 개체에 대해 하나의 비동기 명령을 만들거나 단일 MyToolWindowData 에서 비동기 명령을 사용하고 매개 변수를 사용하여 제거해야 하는 색을 식별할 수 있습니다. 후자의 옵션은 클리너 디자인이므로 구현해 보겠습니다.

  1. 데이터 템플릿에서 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}}}" />
  1. AsyncCommand 에서 MyToolWindowData에 해당하는 값을 추가합니다.
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
  1. 다음 생성자 MyToolWindowData에서 명령의 비동기 콜백을 설정합니다.
RemoveColorCommand = new AsyncCommand(async (parameter, ancellationToken) =>
{
    await Task.Delay(TimeSpan.FromSeconds(2));

    Colors.Remove((MyColor)parameter!);
});

이 코드는 Task.Delay 를 사용하여 장기 실행 비동기 명령 실행을 시뮬레이션합니다.

데이터 컨텍스트의 참조 형식

이전 코드에서 MyColor 개체는 비동기 명령의 매개 변수로 수신되고 참조 같음( MyColor 는 재정의 되지 않는 Equals참조 형식이므로)을 사용하여 제거할 요소를 식별하는 List<T>.Remove 호출의 매개 변수로 사용됩니다. 이는 매개 변수가 UI에서 수신되더라도 현재 데이터 컨텍스트의 MyColor 일부인 정확한 인스턴스가 복사본이 아니라 수신되기 때문일 수 있습니다.

의 프로세스

  • 원격 사용자 제어의 데이터 컨텍스트 프록시;
  • 확장에서 Visual Studio로 또는 그 반대로 INotifyPropertyChanged 업데이트 보내기
  • 내선에서 Visual Studio 또는 그 반대로 관찰 가능한 컬렉션 업데이트 전송;
  • 비동기 명령 매개 변수 보내기

모두 참조 형식 개체의 ID를 적용합니다. 문자열을 제외하고 참조 형식 개체는 확장으로 다시 전송될 때 중복되지 않습니다.

원격 UI 데이터 바인딩 참조 형식의 다이어그램

그림에서 데이터 컨텍스트의 모든 참조 형식 개체(명령, 컬렉션, 각 MyColor 데이터 컨텍스트, 심지어 전체 데이터 컨텍스트)가 원격 UI 인프라에 의해 고유 식별자를 할당하는 방법을 확인할 수 있습니다. 사용자가 프록시 색 개체 #5에 대한 제거 단추를 클릭하면 개체 값이 아닌 고유 식별자(#5)가 확장으로 다시 전송됩니다. 원격 UI 인프라는 해당 MyColor 개체를 검색하고 비동기 명령의 콜백에 매개 변수로 전달하는 작업을 처리합니다.

여러 바인딩 및 이벤트 처리가 있는 RunningCommandsCount

이 시점에서 확장을 테스트하는 경우 제거 단추 중 하나를 클릭하면 모든 제거 단추가 비활성화됩니다.

여러 바인딩이 있는 비동기 명령의 다이어그램

이는 원하는 동작일 수 있습니다. 그러나 현재 단추만 사용하지 않도록 설정하여 사용자가 제거를 위해 여러 색을 큐에 대기하도록 허용한다고 가정합니다. 모든 단추 간에 공유되는 단일 명령이 있으므로 비동기 명령RunningCommandsCount 속성을 사용할 수 없습니다.

각 색에 대한 별도의 카운터를 갖도록 각 단추에 RunningCommandsCount 속성을 연결하여 목표를 달성할 수 있습니다. 이러한 기능은 http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml 네임스페이스에서 제공되므로 XAML에서 원격 UI 형식을 사용할 수 있습니다.

제거 단추를 다음으로 변경합니다.

<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 에는 해당 특정 이벤트와 관련된 활성 실행을 계산하여 vs:ExtensibilityUICommands.RunningCommandsCount 속성을 연결해야 하는 CounterTarget: UIElement 가 있을 수도 있습니다. 연결된 속성에 바인딩할 때 괄호(예 Path=(vs:ExtensibilityUICommands.RunningCommandsCount).IsZero)를 사용해야 합니다.

이 경우 각 단추에 활성 명령 실행의 별도 카운터를 연결하는 데 vs:EventHandler 사용합니다. 연결된 속성에 바인딩 IsEnabled 하면 해당 색이 제거될 때 특정 단추만 비활성화됩니다.

Targeted RunningCommandsCount가 있는 비동기 명령의 다이어그램

사용자 XAML 리소스 사전

Visual Studio 17.10부터 원격 UI는 XAML 리소스 사전을 지원합니다. 이렇게 하면 여러 원격 UI 컨트롤이 스타일, 템플릿 및 기타 리소스를 공유할 수 있습니다. 또한 다양한 언어에 대해 다양한 리소스(예: 문자열)를 정의할 수 있습니다.

원격 UI 컨트롤 XAML과 마찬가지로 리소스 파일은 포함된 리소스로 구성되어야 합니다.

<ItemGroup>
  <EmbeddedResource Include="MyResources.xaml" />
  <Page Remove="MyResources.xaml" />
</ItemGroup>

원격 UI는 WPF와 다른 방식으로 리소스 사전을 참조합니다. 리소스 사전은 컨트롤의 병합된 사전에 추가되지 않지만(병합된 사전은 원격 UI에서 전혀 지원되지 않음) 컨트롤의 .cs 파일에서 이름으로 참조됩니다.

internal class MyToolWindowContent : RemoteUserControl
{
    public MyToolWindowContent()
        : base(dataContext: new MyToolWindowData())
    {
        this.ResourceDictionaries.AddEmbeddedResource(
            "MyToolWindowExtension.MyResources.xaml");
    }
...

AddEmbeddedResource는 기본적으로 프로젝트의 루트 네임스페이스, 아래에 있을 수 있는 하위 폴더 경로 및 해당 파일 이름으로 구성되어 기본적으로 포함된 리소스의 전체 이름을 사용합니다. 프로젝트 파일의 LogicalNameEmbeddedResource을(를) 설정하여 해당 이름을 재정의할 수 있습니다.

리소스 파일 자체는 일반 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를 사용하여 원격 UI 컨트롤의 리소스 사전에서 리소스를 참조할 수 있습니다.

<Button Content="{DynamicResource removeButtonText}" ...

XAML 리소스 사전 지역화

원격 UI 리소스 사전은 포함된 리소스를 지역화하는 것과 동일한 방식으로 지역화할 수 있습니다. 동일한 이름과 언어 접미사를 사용하여 다른 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 개체를 해당 UI와 직접 공유할 수 없습니다. 확장은 대상 netstandard2.0 또는 net6.0 ( -windows 변형이 아님) WPF 형식에대한 액세스 권한이 없을 수도 있습니다.

원격 UI는 원격 사용자 정의의 데이터 컨텍스트에 WPF 개체의 XAML 정의를 포함할 수 있는 XamlFragment 형식을 제공합니다:

[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 데이터 컨텍스트 프록시의 개체로 변환 됩니다: 데이터 컨텍스트의 WPF 형식을 보여 주는 스크린샷

원격 UI 및 스레드

비동기 명령 콜백(및 INotifyPropertyChanged 데이터 입찰을 통해 UI에 의해 업데이트된 값에 대한 콜백)은 임의 스레드 풀 스레드에서 발생합니다. 콜백은 한 번에 하나씩 발생하며 코드가 식을 사용하여 await 제어를 생성할 때까지 겹치지 않습니다.

이 동작은 NonConcurrentSynchronizationContext를 RemoteUserControl 생성자에 전달하여 변경할 수 있습니다. 이 경우 해당 컨트롤과 관련된 모든 비동기 명령INotifyPropertyChanged 콜백에 제공된 동기화 컨텍스트를 사용할 수 있습니다.