티스토리 뷰

728x90

■ 목차

1. SimpleToDoList 들어가기

2. 프로그램 동작 및 구조

3. 어노테이션과 코드 생성기

4. 프로그램 시작 및 종료와 JSON 파일 읽고 쓰기

5. 뷰와 아발로니아 요소들

 

1. SimpleToDoList 들어가기

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

 

 

SimpleToDoList은 CommunityToolkit.Mvvm 패키지를 활용하는 예제로, 전형적인 MVVM 응용 프로그램의 모습을 만날 수 있다.

 

2. 프로그램 동작 및 구조

 

하단의 텍스트 박스에 새로운 항목을 입력하고 엔터키를 입력하거나 우측의 버튼을 클릭하는 것으로 새로운 TODO를 등록할 수 있다. 각 TODO 항목은 체크 박스로 완료 여부를 관리할 수 있고 우측의 버튼으로 삭제도 가능하다. 편집 내용은 JSON 형태로 MyToDoList.txt 파일에 저장한다. 단, 프로그램 종료 시점에만 파일을 저장하고 기존 내역 읽기도 프로그램 시작 시점에만 수행한다.

 

 

프로젝트는 단순한 MVVM 패턴 구조를 가진다. TODO 항목을 가지는 모델 한 개, TODO 리스트를 관리하는 뷰모델과 개별 항목을 관리하는 뷰모델, 그리고 한 개의 뷰로 구성된다. Services 폴더에 있는 코드는 프로그램 시작 및 종료 시 JSON 파일을 읽고 저장하는 코드를 확인할 수 있다.  

 

3. 어노테이션과 코드 생성기

public ToDoItemViewModel(ToDoItem item)
{
  IsChecked = item.IsChecked;
  Content = item.Content;
}

private bool _isChecked;
public bool IsChecked
{
  get { return _isChecked; }
  set { SetProperty(ref _isChecked, value); }
}

[ObservableProperty] 
private string? _content;

public ToDoItem GetToDoItem()
{
  return new ToDoItem()
  {
    IsChecked = this.IsChecked,
    Content = this.Content
  };
}

 

위의 코드는 개별 TODO 항목에 대한 뷰모델인 ToDoItemViewModel.cs의 내용으로 어노테이션을 통한 코드 생성기의 사용 효과를 명확하게 확인할 수 있다. [ObservableProperty]라고 어노테이션을 기술한 _content는 자동적으로 _content를 적용한 public Content 속성을 코드가 컴파일 시점에 생성된다. 반면에 _isChecked는  어노테이션을  기술하지 않고 자동 생성되고 속성 코드를 직접 작성한 것이다.  

 

public MainViewModel()
{
  if (Design.IsDesignMode)
  {
    ToDoItems = new ObservableCollection<ToDoItemViewModel>(new[]
    {
      new ToDoItemViewModel() { Content = "Hello" },
      new ToDoItemViewModel() { Content = "Avalonia", IsChecked = true}
    });
  }
}

public ObservableCollection<ToDoItemViewModel> ToDoItems { get; } = new ObservableCollection<ToDoItemViewModel>();

[RelayCommand (CanExecute = nameof(CanAddItem))]
private void AddItem()
{
  ToDoItems.Add(new ToDoItemViewModel() {Content = NewItemContent});
  NewItemContent = null;
}

[ObservableProperty] 
[NotifyCanExecuteChangedFor(nameof(AddItemCommand))]
private string? _newItemContent;

private bool CanAddItem() => !string.IsNullOrWhiteSpace(NewItemContent);

[RelayCommand]
private void RemoveItem(ToDoItemViewModel item)
{
  ToDoItems.Remove(item);
}

 

TODO 목록을 관리하는 뷰모델에서도 CommunityToolkit.Mvvm에서 제공하는 어노테이션을 통한 코드 생성기를 사용하는데 [RelayCommnd] 어노테이션을 사용하면 RemoveItem의 경우 아래와 같은 코드를 생성한다.

 

 

private RelayCommand<ToDoItemViewModel>? removeItemCommand;

public IRelayCommand<ToDoItemViewModel> RemoveItemCommand => removeItemCommand ??= new RelayCommand<ToDoItemViewModel>(RemoveItem);

 

 


4. 프로그램 시작 및 종료와 JSON 파일 읽고 쓰기

프로그램은 프로그램을 시작할 때 기존 TODO 목록을 읽어 뷰에 반영하고 프로그램 종료 시점에는 TODO 목록을 JSON 파일 형태로 MyToDoList.txt 파일에 저장한다.

 

public override void Initialize()
{
  AvaloniaXamlLoader.Load(this);
}

private readonly MainViewModel _mainViewModel = new MainViewModel();
public override async void OnFrameworkInitializationCompleted()
{
  if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
  {
    desktop.MainWindow = new MainWindow
    {
      DataContext = _mainViewModel 
    };
    desktop.ShutdownRequested += DesktopOnShutdownRequested;
  }
  base.OnFrameworkInitializationCompleted();
  await InitMainViewModelAsync();
}

private bool _canClose; // This flag is used to check if window is allowed to close
private async void DesktopOnShutdownRequested(object? sender, ShutdownRequestedEventArgs e)
{
  e.Cancel = !_canClose;
  if (!_canClose)
  {
    var itemsToSave = _mainViewModel.ToDoItems.Select(item => item.GetToDoItem());
    await ToDoListFileService.SaveToFileAsync(itemsToSave);
    _canClose = true;
    if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
    {
      desktop.Shutdown();
    }
  }
}

private async Task InitMainViewModelAsync()
{
  var itemsLoaded = await ToDoListFileService.LoadFromFileAsync();
  if (itemsLoaded is not null)
  {
    foreach (var item in itemsLoaded)
    {
      _mainViewModel.ToDoItems.Add(new ToDoItemViewModel(item));
    }
  }
}

 

위의 코드는 App.axaml.cs의 코드로 아발로니아 프레임워크가 정상적으로 로딩된 다음에 호출되는 OnFrameworkInitializationCompleted를 오버라이드하여 프로그램 종료 시점 인식을 위한 이벤트 처리 루틴을 등록하고 프로그램 시작 시 처리할 TODO 목록 읽기를 수행한다. 실제 파일에 대한 처리는 Services 폴더의 코드로 수행한다.

 

public static class ToDoListFileService
{
  private static string _jsonFileName = 
    Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
    "Avalonia.SimpleToDoList", "MyToDoList.txt");

  public static async Task SaveToFileAsync(IEnumerable<ToDoItem> itemsToSave)
  {
    Directory.CreateDirectory(Path.GetDirectoryName(_jsonFileName)!);
    using (var fs = File.Create(_jsonFileName))
    {
      await JsonSerializer.SerializeAsync(fs, itemsToSave);
    }
  }

  public static async Task<IEnumerable<ToDoItem>?> LoadFromFileAsync()
  {
    try
    {
      using (var fs = File.OpenRead(_jsonFileName))
      {
        return await JsonSerializer.DeserializeAsync<IEnumerable<ToDoItem>>(fs);
      }
    }
    catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
    {
      return null;
    }
  }
}

 

JSON 형태의 파일로 TODO 목록을 읽고 쓰는 코드는 위와 같다. JSON 관련 기능은 System.Text.Json을 이용한다.

 

5. 뷰와 아발로니아 요소들

뷰는 텍스트 블록, 스크롤 뷰어, 텍스트 블록이 그리드 형태로 구성된 형태이며 스크롤 뷰어는 TODO 항목들을 목록으로 표시한다. 

<Grid RowDefinitions="Auto, *, Auto" x:Name="Root">
  <TextBlock Text="My ToDo-List" Classes="h1" />
  <ScrollViewer Grid.Row="1">
    <ItemsControl ItemsSource="{Binding ToDoItems}">
      <ItemsControl.ItemTemplate>
        <DataTemplate DataType="vm:ToDoItemViewModel">
          <Grid ColumnDefinitions="*, Auto">
            <CheckBox IsChecked="{Binding IsChecked}" Content="{Binding Content}" />
            <Button Grid.Column="1"
                Command="{Binding #Root.((vm:MainViewModel)DataContext).RemoveItemCommand}"
                CommandParameter="{Binding .}">
              <PathIcon Data="{DynamicResource DeleteIconData}" Height="15" Foreground="Red" />
            </Button>
          </Grid>
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
  </ScrollViewer>
  <TextBox Grid.Row="2" Text="{Binding NewItemContent}" Watermark="Add a new Item">
    <TextBox.InnerRightContent>
      <Button Command="{Binding AddItemCommand}">
        <PathIcon Data="{DynamicResource AcceptIconData}" Foreground="Green" />
      </Button>
    </TextBox.InnerRightContent>
    <TextBox.KeyBindings>
      <KeyBinding Gesture="Enter" Command="{Binding AddItemCommand}" />
    </TextBox.KeyBindings>
  </TextBox>
</Grid>

 

728x90
댓글
최근에 올라온 글
최근에 달린 댓글
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함