자습서: 고급 원격 UI
이 자습서에서는 임의 색 목록을 표시하는 도구 창을 증분 방식으로 수정하여 고급 원격 UI 개념에 대해 알아봅니다.
학습 내용은 다음과 같습니다.
- 여러 비동기 명령 실행을 병렬로 실행할 수 있는 방법과 명령 이 실행 중일 때 UI 요소를 사용하지 않도록 설정하는 방법입니다.
- 여러 단추를 동일한 비동기 명령에 바인딩하는 방법입니다.
- 원격 UI 데이터 컨텍스트 및 해당 프록시에서 참조 형식이 처리되는 방식
- 이 비동기 명령을 이벤트 처리기로 사용하는 방법입니다.
- 여러 단추가 동일한 명령에 바인딩된 경우 비동기 명령의 콜백이 실행 중일 때 단일 단추를 사용하지 않도록 설정하는 방법입니다.
- 원격 UI 컨트롤에서 XAML 리소스 사전을 사용하는 방법입니다.
- 원격 UI 데이터 컨텍스트에서 복잡한 브러시와 같은 WPF 형식을 사용하는 방법입니다.
- 원격 UI에서 스레딩을 처리하는 방법입니다.
이 자습서는 소개 원격 UI 문서를 기반으로 하며 다음과 같은 VisualStudio.Extensibility 확장이 작동합니다.
- 도구 창을 여는 명령에 대한
.cs
파일입니다. - 이
MyToolWindow.cs
클래스에 대한ToolWindow
파일, - 이
MyToolWindowContent.cs
클래스에 대한RemoteUserControl
파일, - 이
RemoteUserControl
xaml 정의에 대한MyToolWindowContent.xaml
포함된 리소스 파일 - 이
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.Color
는string
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
에서 비동기 명령을 사용하고 매개 변수를 사용하여 제거해야 하는 색을 식별할 수 있습니다. 후자의 옵션은 클리너 디자인이므로 구현해 보겠습니다.
- 데이터 템플릿에서 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}}}" />
- 각
AsyncCommand
에서MyToolWindowData
에 해당하는 값을 추가합니다.
[DataMember]
public AsyncCommand RemoveColorCommand { get; }
- 다음 생성자
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를 적용합니다. 문자열을 제외하고 참조 형식 개체는 확장으로 다시 전송될 때 중복되지 않습니다.
그림에서 데이터 컨텍스트의 모든 참조 형식 개체(명령, 컬렉션, 각 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
하면 해당 색이 제거될 때 특정 단추만 비활성화됩니다.
사용자 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 리소스 사전은 포함된 리소스를 지역화하는 것과 동일한 방식으로 지역화할 수 있습니다. 동일한 이름과 언어 접미사를 사용하여 다른 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
데이터 컨텍스트 프록시의 개체로 변환 됩니다:
원격 UI 및 스레드
비동기 명령 콜백(및 INotifyPropertyChanged
데이터 입찰을 통해 UI에 의해 업데이트된 값에 대한 콜백)은 임의 스레드 풀 스레드에서 발생합니다. 콜백은 한 번에 하나씩 발생하며 코드가 식을 사용하여 await
제어를 생성할 때까지 겹치지 않습니다.
이 동작은 NonConcurrentSynchronizationContext를 RemoteUserControl
생성자에 전달하여 변경할 수 있습니다. 이 경우 해당 컨트롤과 관련된 모든 비동기 명령 및 INotifyPropertyChanged
콜백에 제공된 동기화 컨텍스트를 사용할 수 있습니다.