티스토리 뷰
■ 목차
└ 5. ReactiveUI와 CommunityToolkit.Mvvm
1. CommandSample 들어가기
크로스플랫폼 닷넷 UI 프레임워크인 아바로니아 맛보기를 넘어서서 본격적인 활용에 들어가기 위한 학습 방법으로 필자는 예제 프로그램 리뷰를 하나씩 수행하기로 했다. 기술 자료가 넉넉하고, 비주얼스튜디오의 WPF 디자이너와 같은 도구가 있다면 넘어갈 수도 있는 문제겠지만 지금까지 접하지 않았던 새로운 도구와 친숙해지는 방법은 역시 예제를 통하는 것이 아닌가 싶다. 공식 예제 코드는 깃허브 https://github.com/AvaloniaUI/Avalonia.Samples 에서 받을 수 있다. 커뮤니티에서 발굴해 놓은 예제와 참조 프로젝트도 있는데 깃허브 https://github.com/AvaloniaCommunity/awesome-avalonia 에서 확인할 수 있다.

CommandSample은 MVVM(Models, Views, View Models) 패턴으로 개발하는 앱에서 버튼 클릭을 통한 명령 수행이 필요한 경우 참조할만한 예제이다. MVVM 패턴을 적용하기 위하여 Reactive UI 프레임워크와 함께 마이크로소프트의 CommunityToolkit.Mvvm을 사용하는 뷰모델을 제시하고 있는 것도 주목할만하다.
2. 프로그램 동작 및 구조

프로그램의 동작은 단순하다. 상단의 탭으로 서로 다른 뷰 모델의 동작을 선택한다. 탭 제목처럼 Reactive UI와 CommunityToolkit.Mvvm 기반의 뷰모델을 선택할 수 있지만 각각의 뷰모델과 연동하는 뷰의 형태는 동일하다. 버튼으로 인공지능 컴퓨터 HAL 9000에 명령을 내리면 해당 명령에 응답하는 모양새로 동작한다. 내부적으로는 버튼 단위의 명령이 내려지면 명령에 해당하는 메시지를 하단에 출력하는 방식이다.

프로젝트는 MVVM 패턴에서 별도의 모델 없이 뷰모델과 뷰로만 구성된 단순한 형태이다. MainWindow 뷰에서 다른 뷰인 ReactiveUiCommandsSampleView, CommunityToolkitCommandsSampleView를 내포시키는 방법을 적용했다.
3. 서브 뷰 내포시키기
<TabControl>
<TabItem Header="ReactiveUI">
<views:ReactiveUiCommandsSampleView DataContext="{Binding ReactiveUiCommandsViewModel}" />
</TabItem>
<TabItem Header="CommunityToolkit-MVVM">
<views:CommunityToolkitCommandsSampleView DataContext="{Binding CommunityToolkitCommandsViewModel}" />
</TabItem>
</TabControl>
MainWindow 뷰를 살펴보면 TabItem 컨트롤 내부에 "views" 태그를 통해서 서브 뷰를 내포하고 있음을 확인할 수 있다. 뷰 이름을 지정하면서 DataContext에 뷰모델을 지정했다. 메인뷰는 "Window" 컨트롤이지만 서브 뷰는 "UserControl"로 정의했다. 관련 컨트롤의 세부 내용은 아래와 같다.
- Window : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_window
최상단 레벨의 윈도우를 표현한다. Icon, Title, TransparencyLevelHint, Background, ExtendClientAreaToDecorationsHint, WindowStartupLocation, Width, Height, Padding 등 사용 - UserControl : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_UserControl
사용자 컨트롤을 정의하기 위한 베이스 클래스로 Window 컨트롤처럼 하나의 뷰 클래스 단위가 되고 뷰모델과 연동한다. UserControl.Resources 등 사용 - TabControl : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_TabControl
내부에 TabItem 항목으로 탭 요소를 둔다. TabStripPlacement 속성을 "Bottom"으로 지정하면 탭을 하단에 표시한다. - TabItem : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_TabItem
Header 속성으로 탭 제목 지정.
4. 뷰와 아발로니아 요소들
<Grid RowDefinitions="Auto, Auto, *" ColumnDefinitions="Auto, *">
<TextBlock Grid.Column="0" Grid.Row="0" Text="Command:"
FontWeight="Bold" VerticalAlignment="Center" Margin="5" />
<StackPanel Grid.Column="1" Grid.Row="0" Spacing="5" Margin="5" >
<Button Command="{Binding OpenThePodBayDoorsDirectCommand}"
Content="Open the pod bay doors, HAL." />
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBox Text="{Binding RobotName}" Watermark="Robot Name" />
<Button Command="{Binding OpenThePodBayDoorsFellowRobotCommand}"
Content="{Binding RobotName, StringFormat='Open the Pod Bay for {0}'}"
CommandParameter="{Binding RobotName}" />
</StackPanel>
<Button Command="{Binding OpenThePodBayDoorsAsyncCommand}"
Content="Start Pod Bay Opening Sequence" />
</StackPanel>
<Separator Grid.Row="1" Grid.ColumnSpan="2"
HorizontalAlignment="Stretch" Margin="5" Height="2"
Background="LightGray"/>
<TextBlock Grid.Column="0" Grid.Row="2"
Text="HAL 9000:" FontWeight="Bold" Margin="5"
VerticalAlignment="Center"/>
<Border Grid.Column="1" Grid.Row="2"
CornerRadius="10" Margin="5"
BorderThickness="1" BorderBrush="{DynamicResource SystemAccentColor}">
<Grid ColumnDefinitions="*,Auto">
<ItemsControl Margin="5" ItemsSource="{Binding ConversationLog}" />
<Button Grid.Column="1"
VerticalAlignment="Stretch" VerticalContentAlignment="Center"
Background="Transparent"
Command="{Binding ConversationLog.Clear}" Content="❌"/>
</Grid>
</Border>
</Grid>
- Grid : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_Grid
행과 열로 구성되는 표 형태의 영역을 정의하는 데 사용한다. RowDefinitions, ColumnDefinitions으로 행과 열의 크기를 지정. - StackPanel : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_StackPanel
내부에 요소들을 수직 또는 수평으로 배열하는 컨테이너. 예제에서는 스택 패널에 개별 뷰모델을 바인딩했다.
Spacing, Width, Orientation, Background, Margin, DataContext, VerticalAlignment 등 사용. - TextBlock : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_TextBlock
텍스트 출력, 태그 내에 출력할 문자열을 입력하거나 Text 속성에 정적/동적 텍스트 입력. FontWeight, FontSize, VerticalAlignment, TextAlignment, TextWrapping 등 사용 - TextBox : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_TextBox
단순 텍스트 입출력에 사용. Text로 뷰모델의 속성 지정, Watermark, IsReadOnly, FontWeight, VerticalAlignment, FontSize, PasswordChar, Name, UseFloatingWatermark 등 사용 - Button : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_Button
표준 버튼 컨트롤. HorizontalAlignment, VerticalAlignment, Content, Command, CommandParameter, IsVisible, FontSize, CornerRadius, Background, Classes 등 사용 - Separator : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_Separator
분리자 컨트롤. HorizontalAlignment, VerticalAlignment, Margin, Height, Background 등 사용 - Border : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_Border
하위 컨트롤 들을 경계선과 배경으로 장식하는 컨트롤. Padding, CornerRadius, ClipToBounds, Background, Margin, BorderThickness, BorderBrush 등 사용 - ItemsControl : https://api-docs.avaloniaui.net/docs/T_Avalonia_Controls_ItemsControl
항목 그룹을 출력하는 컨트롤. Margin, ItemsSource, ItemsControl.ItemTemplate, ItemsControl.ItemsPanel 등 사용
5. ReactiveUI와 CommunityToolkit.Mvvm
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace CommandSample.ViewModels;
public partial class CommunityToolkitCommandsViewModel : ObservableObject
{
public CommunityToolkitCommandsViewModel()
{
OpenThePodBayDoorsDirectCommand = new RelayCommand(OpenThePodBayDoors);
}
public ICommand OpenThePodBayDoorsDirectCommand { get; }
private void OpenThePodBayDoors()
{
ConversationLog.Clear();
AddToConvo("I'm sorry, Dave, I'm afraid I can't do that.");
}
[RelayCommand(CanExecute = nameof(CanRobotOpenTheDoor))]
private void OpenThePodBayDoorsFellowRobot(string? robotName)
{
ConversationLog.Clear();
AddToConvo($"Hello {robotName}, the Pod Bay is open :-)");
}
private bool CanRobotOpenTheDoor() => !string.IsNullOrWhiteSpace(RobotName);
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(OpenThePodBayDoorsFellowRobotCommand))]
private string? _robotName;
[RelayCommand]
private async Task OpenThePodBayDoorsAsync()
{
ConversationLog.Clear();
AddToConvo("Preparing to open the Pod Bay...");
await Task.Delay(1000);
AddToConvo("Depressurizing Airlock...");
await Task.Delay(2000);
AddToConvo("Retracting blast doors...");
await Task.Delay(2000);
AddToConvo("Pod Bay is open to space!");
}
public ObservableCollection<string> ConversationLog { get; } = new ObservableCollection<string>();
private void AddToConvo(string content)
{
ConversationLog.Add(content);
}
}
예제에서는 MVVM 패턴을 사용하기 위해서 ReactiveUI와 함께 CommunityToolkit.Mvvm 패키지를 사용하는 코드를 보여준다. 두 방식 모두 ICommand 인터페이스를 구현하는데 ReactiveUI를 사용하는 뷰모델은 ReactiveObject를 상속받아 ReactiveCommand로 명령을 생성하지만 CommunityToolkit.Mvvm를 사용하는 뷰모델은 ObservableObject를 상속받아 RelayCommand로 명령을 생성한다. 뷰에서 해당 명령의 수행 가능 여부를 나타내는 CanExecute 설정 방식도 약간의 차이가 있다.
'프로그래밍' 카테고리의 다른 글
| 아발로니아 UI 예제 프로그램 리뷰 4 - ValueConversionSample (0) | 2025.10.27 |
|---|---|
| 아발로니아 UI 예제 프로그램 리뷰 3 - ValidationSample (0) | 2025.10.22 |
| 아발로니아 UI 예제 프로그램 리뷰 1 - BasicMvvmSample (0) | 2025.10.16 |
| VS2022에서 아발로니아 UI 맛보기 (0) | 2025.10.14 |
| VS Code에서 아발로니아 맛보기 (0) | 2025.09.30 |