아발로니아에서 파일 다이얼로그와 MVVM 연동하기
- 참고 자료
-
서론 크로스 플랫폼 기능을 위해 아발로니아(Avalonia)를 사용하는 개발자라면, 윈도우 시스템에 특화된 파일 시스템 접근 방식을 사용하는 것은 여러 문제를 야기할 수 있습니다. 본 문서에서는 아발로니아에서 파일 다이얼로그를 사용하여 파일을 내보내는 방법을 간략히 소개합니다.
-
기본 사용 방법 아발로니아 프로젝트를 생성하고, MVVM 프레임워크로는 CommunityToolKit.MVVM 커뮤니티 패키지를 사용하겠습니다. 이 패키지는 현대적이면서도 낮은 침투성을 가진 최고의 MVVM 솔루션 중 하나입니다. 예제에서는 Windows 플랫폼을 사용하며, 다른 플랫폼에서 호환성 문제가 발생할 수 있습니다.
바인딩에 대해 알아보기 전에, 먼저 이벤트 기반으로 파일 다이얼로그를 여는 간단한 방법을 살펴보겠습니다.
MainView.xaml에 Button_Click 이벤트를 추가합니다:
<UserControl x:Class="AvaloniaFileDialogExample.Views.MainView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:AvaloniaFileDialogExample.ViewModels"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="vm:MainViewModel"
mc:Ignorable="d">
<Design.DataContext>
<vm:MainViewModel />
</Design.DataContext>
<Button Width="100"
Height="100"
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Click="Button_Click"
Content="파일 저장" />
</UserControl>
MainView.xaml.cs 파일에 관련 이벤트 처리 코드를 추가합니다:
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using System.Collections.Generic;
using System.IO;
namespace AvaloniaFileDialogExample.Views;
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
}
private async void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
var storageProvider = topLevel.StorageProvider;
// 파일 저장 다이얼로그 표시
var resultFile = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions()
{
FileTypeChoices = new List<FilePickerFileType>() { FilePickerFileTypes.TextPlain },
Title = "파일 저장",
SuggestedFileName = "output",
});
// 사용자가 취소한 경우
if (resultFile == null) return;
var content = "안녕하세요, 아발로니아!";
using var stream = await resultFile.OpenWriteAsync();
using var streamWriter = new StreamWriter(stream);
await streamWriter.WriteLineAsync(content);
}
}
- MVVM 패턴으로 파일 내보내기 구현하기
3.1 컨트롤과 ViewModel 간의 간격 해소 파일 다이얼로그를 사용하려면 컨트롤이 필요하지만, ViewModel과 컨트롤 사이에는 두꺼운 장벽이 존재합니다. 이 문제를 해결하기 위해 StorageProvider 객체를 별도로 참조하는 방법을 사용할 수 있습니다.
App.xaml.cs에서 MainView 인스턴스화를 통해 TopLevel을 얻고, 이를 통해 StorageProvider에 접근할 수 있습니다.
먼저 GlobalStorageHelper라는 정적 클래스를 생성하고 IStorageProvider 타입의 필드를 추가합니다:
using Avalonia.Platform.Storage;
namespace AvaloniaFileDialogExample.Helpers
{
internal static class GlobalStorageHelper
{
public static IStorageProvider StorageProvider { get; set; }
}
}
App.xaml.cs를 다음과 같이 수정합니다:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using AvaloniaFileDialogExample.Helpers;
using AvaloniaFileDialogExample.ViewModels;
using AvaloniaFileDialogExample.Views;
namespace AvaloniaFileDialogExample;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
BindingPlugins.DataValidators.RemoveAt(0);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var mainWindow = new MainWindow
{
DataContext = new MainViewModel()
};
desktop.MainWindow = mainWindow;
var topLevel = TopLevel.GetTopLevel(mainWindow);
GlobalStorageHelper.StorageProvider = topLevel.StorageProvider;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
var mainView = new MainView
{
DataContext = new MainViewModel()
};
singleViewPlatform.MainView = mainView;
var topLevel = TopLevel.GetTopLevel(mainView);
GlobalStorageHelper.StorageProvider = topLevel.StorageProvider;
}
base.OnFrameworkInitializationCompleted();
}
}
이제 이벤트 방식에서 StorageProvider를 가져오는 부분을 우리의 정적 클래스를 사용하도록 수정합니다:
private async void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var storageProvider = GlobalStorageHelper.StorageProvider;
var resultFile = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions()
{
FileTypeChoices = new List<FilePickerFileType>() { FilePickerFileTypes.TextPlain },
Title = "파일 저장",
SuggestedFileName = "output",
});
if (resultFile == null) return;
var content = "안녕하세요, 아발로니아!";
using var stream = await resultFile.OpenWriteAsync();
using var streamWriter = new StreamWriter(stream);
await streamWriter.WriteLineAsync(content);
}
3.2 컨트롤에서 분리하여 ViewModel에서 파일 다이얼로그 호출하기 이제 컨트롤에서 분리되었으므로 ViewModel에서 파일 다이얼로그를 호출할 수 있습니다. MainViewModel을 다음과 같이 수정합니다:
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.Input;
using System.Collections.Generic;
using System.IO;
using System.Windows.Input;
using AvaloniaFileDialogExample.Helpers;
namespace AvaloniaFileDialogExample.ViewModels;
public partial class MainViewModel : ViewModelBase
{
public ICommand SaveFileCommand { get; private set; }
public MainViewModel()
{
SaveFileCommand = new RelayCommand(ExecuteSaveFile);
}
private async void ExecuteSaveFile()
{
var storageProvider = GlobalStorageHelper.StorageProvider;
var resultFile = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions()
{
FileTypeChoices = new List<FilePickerFileType>() { FilePickerFileTypes.TextPlain },
Title = "파일 저장",
SuggestedFileName = "output",
});
if (resultFile == null) return;
var content = "안녕하세요, 아발로니아!";
using var stream = await resultFile.OpenWriteAsync();
using var streamWriter = new StreamWriter(stream);
await streamWriter.WriteLineAsync(content);
}
}
MainView.xaml의 Button을 바인딩 형식으로 변경합니다:
<UserControl x:Class="AvaloniaFileDialogExample.Views.MainView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:AvaloniaFileDialogExample.ViewModels"
d:DesignHeight="450"
d:DesignWidth="800"
x:DataType="vm:MainViewModel"
mc:Ignorable="d">
<Design.DataContext>
<vm:MainViewModel />
</Design.DataContext>
<Button Width="100"
Height="100"
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding SaveFileCommand}"
Content="파일 저장" />
</UserControl>
- 의존성 주입을 통한 서비스 구조 개선 데스크톱 애플리케이션 개발, 특히 단일 모듈 프로젝트에서 의존성 주입은 때때로 지나치게 복잡하게 느껴질 수 있습니다. 하지만 다중 모듈 협업 및 조립 과정에서는 중요한 역할을 할 수 있습니다. 프로젝트가 정적 클래스 호출에 머무르지 않고, 향후 대규모 프로젝트 유지보수를 고려하여 의존성 주입 방식으로 개선하는 것이 좋습니다.
CommunityToolkit.MVVM은 내장 의존성 주입 컨테이너를 제공하므로 이를 활용하겠습니다.
먼저 Microsoft.Extensions.DependencyInjection 라이브러리를 설치합니다.
App.xaml.cs를 다음과 같이 수정합니다:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using CommunityToolkit.Mvvm.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using AvaloniaFileDialogExample.Helpers;
using AvaloniaFileDialogExample.ViewModels;
using AvaloniaFileDialogExample.Views;
namespace AvaloniaFileDialogExample;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
BindingPlugins.DataValidators.RemoveAt(0);
var serviceCollection = new ServiceCollection();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var mainWindow = new MainWindow
{
DataContext = new MainViewModel()
};
desktop.MainWindow = mainWindow;
var topLevel = TopLevel.GetTopLevel(mainWindow);
serviceCollection.AddSingleton(topLevel.StorageProvider);
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
var mainView = new MainView
{
DataContext = new MainViewModel()
};
singleViewPlatform.MainView = mainView;
var topLevel = TopLevel.GetTopLevel(mainView);
serviceCollection.AddSingleton(topLevel.StorageProvider);
}
var serviceProvider = serviceCollection.BuildServiceProvider();
Ioc.Default.ConfigureServices(serviceProvider);
base.OnFrameworkInitializationCompleted();
}
}
MainViewModel.cs에서 Ioc를 사용하여 StorageProvider를 가져오도록 수정합니다:
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using System.Collections.Generic;
using System.IO;
using System.Windows.Input;
using AvaloniaFileDialogExample.Helpers;
namespace AvaloniaFileDialogExample.ViewModels;
public partial class MainViewModel : ViewModelBase
{
public ICommand SaveFileCommand { get; private set; }
public MainViewModel()
{
SaveFileCommand = new RelayCommand(ExecuteSaveFile);
}
private async void ExecuteSaveFile()
{
var storageProvider = Ioc.Default.GetService<IStorageProvider>();
var resultFile = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions()
{
FileTypeChoices = new List<FilePickerFileType>() { FilePickerFileTypes.TextPlain },
Title = "파일 저장",
SuggestedFileName = "output",
});
if (resultFile == null) return;
var content = "안녕하세요, 아발로니아!";
using var stream = await resultFile.OpenWriteAsync();
using var streamWriter = new StreamWriter(stream);
await streamWriter.WriteLineAsync(content);
}
}
편의를 위해 IocHelper를 만들어 등록된 서비스를 가져오는 것도 좋은 방법입니다. 이 방식은 정적 클래스와 유사하지만, 의존성 주입을 사용한다는 점에서 차이가 있습니다:
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.DependencyInjection;
namespace AvaloniaFileDialogExample.Helpers
{
internal static class IocHelper
{
public static IStorageProvider GetStorageProvider()
{
return Ioc.Default.GetService<IStorageProvider>();
}
}
}
이제 파일 다이얼로그를 MVVM 패턴과 효과적으로 통합하는 방법을 알아보았습니다. 의존성 주입을 사용하면 코드의 유연성과 테스트 용이성을 높일 수 있습니다.