다음을 통해 공유


자습서: 고급 원격 UI

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

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

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

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

이 자습서는 입문 원격 UI 문서를 기반으로 하며 다음과 같은 VisualStudio.Extensibility 확장이 필요합니다:

  1. .cs 도구 창을 여는 명령에 대한 파일입니다.
  2. 클래스를 위한 MyToolWindow.cs 파일입니다ToolWindow.
  3. 클래스를 위한 MyToolWindowContent.cs 파일입니다RemoteUserControl.
  4. MyToolWindowContent.xaml xaml 정의를 위한 RemoteUserControl 포함된 리소스 파일
  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에서 제공하는 ObservableList<T>를 사용합니다. 이는 ObservableCollection<T>를 확장한 것입니다.
  • MyToolWindowDataMyColor는 현재 모든 속성이 읽기 전용이기 때문에 INotifyPropertyChanged를 구현하지 않습니다.

장기 실행 비동기 명령 처리

원격 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;
    }
});

사용자가 단추를 클릭하면 명령 콜백이 확장에서 비동기적으로 실행되고 콜백 CanExecutefalse이 설정된 후 Visual Studio 프로세스의 프록시 데이터 컨텍스트로 비동기적으로 전파되어 단추가 비활성화되기 때문에 이 솔루션에는 여전히 불완전한 동기화가 있습니다. 사용자는 단추를 사용하지 않도록 설정하기 전에 연속해서 단추를 두 번 클릭할 수 있습니다.

더 나은 해결 방법은 RunningCommandsCount의 속성을 사용하는 것입니다.

<Button Content="Add color" Command="{Binding AddColorCommand}" IsEnabled="{Binding AddColorCommand.RunningCommandsCount.IsZero}" Grid.Row="1" />

RunningCommandsCount 는 현재 진행 중인 명령의 동시 비동기 실행 수에 대한 카운터입니다. 이 카운터는 단추를 클릭하는 즉시 UI 스레드에서 증가하므로 바인딩 IsEnabledRunningCommandsCount.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. 해당하는 AsyncCommandMyToolWindowData에 추가합니다.
[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 개체는 비동기 명령의 매개 변수로 수신되고, List<T>.Remove 호출의 매개 변수로 사용되며, 참조 형식을 사용하지만, 이는 MyColorEquals를 재정의하지 않으므로 제거할 요소를 식별합니다. 이는 매개 변수가 UI에서 수신되더라도 현재 데이터 컨텍스트의 MyColor 일부인 정확한 인스턴스가 복사본이 아니라 수신되기 때문일 수 있습니다.

의 프로세스

  • 원격 사용자 제어의 데이터 컨텍스트 프록시
  • 확장에서 Visual Studio로 또는 그 반대로 업데이트 보내기 INotifyPropertyChanged
  • 확장에서 Visual Studio로 관찰 가능한 컬렉션 업데이트 보내기 또는 그 반대의 경우도 마찬가지입니다.
  • 비동기 명령 매개 변수 보내기

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

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

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

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 는 기본적으로 프로젝트의 루트 네임스페이스, 아래에 있을 수 있는 하위 폴더 경로 및 파일 이름으로 구성된 포함된 리소스의 전체 이름을 사용합니다. 프로젝트 파일에서 LogicalName을(를) 설정하여 EmbeddedResource 이름을 재정의할 수 있습니다.

리소스 파일 자체는 일반 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 리소스 사전은 포함된 리소스를 지역화하는 것과 동일한 방식으로 지역화할 수 있습니다. 예를 들어 MyResources.it.xaml 이탈리아어 리소스와 같은 이름과 언어 접미사를 사용하여 다른 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와 직접 공유할 수 없습니다. 확장이 WPF 형식에 대한 액세스 권한이 없을 수도 있는데, 이는 netstandard2.0 또는 net6.0를 대상으로 할 수 있지만, -windows 변형은 아닙니다.

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

[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 콜백에 제공된 동기화 컨텍스트를 사용할 수 있습니다.