MVVM 패턴으로 OK Cancel 구현

MVVM 패턴으로 OK Cancel 구현

이철우

아주 사소하지만, 고객이 어떤 버튼(예로 ‘삭제’)에 대해 한 번 생각할 시간을 달라고 하면, 대개 대화상자를 띄우고 OK/Cancel에 따라 프로그램이 작동한다. 그런데, MVVM 패턴에서는 이것이 자신의 패턴을 깨는 작업이 된다.

그래서, MVVM 패턴으로 OK/Cancel 을 구현하는 사용자 컨트롤(ConfirmButton.xaml)과 그에 대응하는 ViewModel(ConfirmButtonViewModel.cs)을 작성하고 적용하였다.

닷넷 9.0, CommunityToolkit.Mvvm 8.4.0, Visual Studio Community 최신 버전을 사용하여 WPF 앱 프로젝트를 만들자. 프로젝트 속성-일반에서 출력형식을 'Console Application’으로 바꾼다. 그리고 윈도우 설정은 Height=“225” Width=“400” FontSize=“14” 로 하였다. 이 프로젝트는 한 개의 버튼(“출력”)을 가지고 있고 그 버튼을 누르면 "Start."를 출력하고 1초 뒤에 "End."를 출력한다. 이 기능을 구현하는 ViewModel.cs를 프로젝트에 추가하자.

    public sealed class ViewModel : ObservableObject
    {
        public ViewModel()
        {
            PrintCommand = new AsyncRelayCommand(Print);
        }

        public IAsyncRelayCommand PrintCommand { get; }

        private async Task Print()
        {
            Console.WriteLine("Start.");
            await Task.Delay(1000).ConfigureAwait(false);
            Console.WriteLine("End.");
        }
    }

MainWindow.xaml 윈도우 설정에 아래 줄을 추가한다.

d:DataContext="{d:DesignInstance Type=local:ViewModel, IsDesignTimeCreatable=False}"

그리고 MainWindow.xaml UI를 아래와 같이 한다.

<Grid>
        <Button Content="출력" Command="{Binding PrintCommand}"/>
</Grid>

이제 UI와 ViewModel을 MainWindow.xaml.cs에서 연결하자.

public MainWindow()
{
    InitializeComponent();

    DataContext = new ViewModel();
}

실행하면 아래와 같이 된다.
OK_Cancel_1

이제 본론으로 돌아와 MVVM 패턴으로 OK/Cancel 을 구현하자. 먼저 클래스 ConfirmButtonViewModel.cs를 만들자.

    public class ConfirmButtonViewModel : ObservableObject
    {
        public ConfirmButtonViewModel(IAsyncRelayCommand missionCommand, string missionCommandContent)
        {
            MissionCommand = missionCommand;
            FrontCommandContent = missionCommandContent;

            FrontCommand = new RelayCommand(ShowBack);
            OKCommand = new AsyncRelayCommand(OK);
            CancelCommand = new RelayCommand(Cancel);
        }

        public IRelayCommand FrontCommand { get; }
        public IRelayCommand OKCommand { get; }
        public IRelayCommand CancelCommand { get; }
        public IAsyncRelayCommand MissionCommand { get; }

        private void ShowBack()
        {
            IsBack = true;
        }

        private void ShowFront()
        {
            IsBack = false;
        }

        private async Task OK()
        {
            await MissionCommand.ExecuteAsync(null).ConfigureAwait(false);
            ShowFront();
        }

        private void Cancel()
        {
            ShowFront();
        }

        public bool IsBack
        {
            get => _isBack;
            private set => SetProperty(ref _isBack, value);
        }
        private bool _isBack = false;

        public string FrontCommandContent
        {
            get => _frontCommandContent;
            set => SetProperty(ref _frontCommandContent, value);
        }
        private string _frontCommandContent = string.Empty;
    }

프로젝트에 사용자 컨트롤 ConfirmButton.xaml을 추가하고 ConfirmButton.xaml.cs에 아래와 같이 코드를 넣자.

    public partial class ConfirmButton : UserControl
    {
        public ConfirmButton()
        {
            InitializeComponent();
        }
    }
    [ValueConversion(typeof(bool), typeof(Visibility))]
    public sealed class TrueToVisibleConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (bool)value ? Visibility.Visible : Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    [ValueConversion(typeof(bool), typeof(Visibility))]
    public sealed class FalseToVisibleConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (bool)value ? Visibility.Collapsed : Visibility.Visible;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

ConfirmButton.xaml 에 아래처럼 자원을 추가한다.

    <UserControl.Resources>
        <local:TrueToVisibleConverter x:Key="TrueToVisibleConverter"/>
        <local:FalseToVisibleConverter x:Key="FalseToVisibleConverter"/>
    </UserControl.Resources>

ConfirmButton.xaml UI를 구성하자.

    <Grid>
        <Button Content="{Binding FrontCommandContent}" Command="{Binding FrontCommand}" Visibility="{Binding IsBack, Converter={StaticResource FalseToVisibleConverter}}"/>
        <UniformGrid Columns="2" Visibility="{Binding IsBack, Converter={StaticResource TrueToVisibleConverter}}">
            <Button Content="O.K." Command="{Binding OKCommand}"/>
            <Button Content="Cancel" Command="{Binding CancelCommand}"/>
        </UniformGrid>
    </Grid>

주 ViewModel에서 ConfirmViewModel을 활용하면 ViewModel.cs는 다음과 같다.

    public sealed class ViewModel : ObservableObject
    {
        public ViewModel()
        {
            PrintCommand = new AsyncRelayCommand(Print);
            ConfirmButtonViewModel = new ConfirmButtonViewModel(PrintCommand, "출력");
        }

        public IAsyncRelayCommand PrintCommand { get; }
        public ConfirmButtonViewModel ConfirmButtonViewModel { get; }


        private async Task Print()
        {
            Console.WriteLine("Start.");
            await Task.Delay(1000).ConfigureAwait(false);
            Console.WriteLine("End.");
        }
    }

주 UI인 MainWindow.xaml에서 ConfirmButtonViewModel과 ConfirmButton을 활용하자.

    <Grid>
        <!--<Button Content="출력" Command="{Binding PrintCommand}"/>-->
        <local:ConfirmButton DataContext="{Binding ConfirmButtonViewModel}"/>
    </Grid>

실행하고 출력 버튼을 누르면 다음과 같다.

주 UI인 MainWindow.xaml에서 ConfirmButton.xaml으로 DataContext가 Tunneling됨을 알 수 있다.

(*ConfirmButton.xaml 설정 mc:Ignorable=“d” 다음에 아래 줄을 넣으면 성가신 것이 사라진다.)

d:DataContext="{d:DesignInstance Type=local:ConfirmButtonViewModel, IsDesignTimeCreatable=False}"

5개의 좋아요