wpf mvvm window show 질문입니다.

요약: mvvm 방식에서 버튼 클릭 시 인터페이스 등의 것을 사용하지 않고 윈도우 창을 열고 싶습니다. 안된다면 주로 많이 사용하는 방식을 알려주시면 감사하겠습니다.

제가 기존에 안드로이드를 개발하면서 mvvm 형태로 개발을 했는데요.
그때 당시 mvvm을 적용할 때 새 창을 여는 부분을 singleLiveData을 구현해서 버튼 클릭 시 viewModel에서 SingleLiveData을 통해 view로 넘어가서 화면을 열었습니다.

이걸 wpf로 굳이 비유를 하자면 viewModel에서 view로 이동해서 윈도우 창을 연다라고 봐야될 꺼 같네요.(안드로이드 처럼 SingleLiveData의 역할을 하는 걸 찾을 수 없어서 비유가 안되네요…)

다만 궁금한 것은 아무리 찾아봐도 viewmodel에서 view로 이동해서 윈도우를 여는 부분이 안보이더라고요. 방식으로는 인터페이스도 있고 하던데 이런걸 사용하지 않고 viewModel에서 View로 이동해서 윈도우를 열 수는 없나요? 그게 아니라면 wpf의 경우 그냥 버튼을 누르면 viewModel에서 ICommand 방식으로 받아서 viewModel에서 그냥 윈도우를 열어버리는 건가요?

좋아요 1

사용하시는 mvvm 프레임워크가 어떻게 되시나요?

좋아요 1

어;;; wpf 닷넷 프레임워크 4.7.2 는 쓰고 있지만 따로 mvvm을 위한 프레임워크는 사용하지 않습니다.
다들 사용하시나요? 사용한다면 사용하기 간편한 걸로 추천 좀 부탁드립니다. ㅠㅠ 하나하나 구현하려니 힘드네요.

좋아요 1

MAUI mvvm 프레임워크에 대한 글이지만, WPF도 동일하게 지원하고 있습니다.

mvvm 프레임워크로 Prism을 사용하신다면 제가 이전에 작성한 demo 소스에서 dialogservice 부분을 참조하시면 좋을 것 같아요.

복잡해서 다른 프레임워크를 사용하신다면 dialogservice를 직접 구현하시거나 적당한 패키지를 찾아야 되는데요.
GitHub - FantasticFiasco/mvvm-dialogs: Library simplifying the concept of opening dialogs from a view model when using MVVM in WPF 가 쓰기 쉬웠던 기억이 있어서 공유 드립니다.
NuGet Gallery | MvvmDialogs 9.1.0

좋아요 2

ViewModel로 직접 View를 넘겨 처리 하는것은
MVVM 패턴 위반에 해당 됩니다.

그래서 이를 우회 하기 위해 View를 추상화 한 IView같은 인터페이스를 ViewModel에서 처리 하는 방법 등 여러가지 방법이 있습니다.

방법1 - 개인적인 의견

개인적으로 사실 제일 좋은 방법은 팝업창을 다이얼로그 윈도우로 처리 하지 않고
Popup 컨트롤이나 직접 Border 컨트롤 등을 이용해서

기존 View위에 오버레이로 표시 되는 팝업 스타일로 처리 하는 것이 현재 UX 트렌드(?)에도 맞고 가장 쉽고 깔끔하게 해결하는 방법이라고 생각 합니다.
물론 이렇게 처리 하면 팝업의 뒷 부분 영역은 딤 처리해서 사용자의 입력이 안되도록 추가 조치 처리 같은게 필요합니다.
이런것을 고려해서 공통으로 편하게 사용할 수 있는 유저컨트롤 등을 만들어 사용하는 것이 좋다는 의견입니다.

참고로 Popup 컨트롤은 전체 화면의 75% 까지만 커버 할 수 있습니다.

방법2

하지만 혼자 개발하는 것이 아닌 이상 기획서의 요구사항에 따라야 하고 필수적으로 다이얼로그 윈도우를 띄워야 하는 상황이 있을 수 있습니다.

이런경우는 다이얼로그서비스 같은것을 만들어서 MVVM 패턴을 지키면서 처리할 수 있습니다.
다음은 간단한 팝업 윈도우를 ViewModel에서 띄울수 있는 심플한 다이얼로그 서비스 제안 코드 입니다.

샘플 코드 제안

우선 팝업 윈도우로 쓸 View를 간단하게 만들어 봅니다.

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Border Grid.Row="0"
                Grid.RowSpan="2"
                BorderBrush="#FF999999"
                BorderThickness="1"
                CornerRadius="2">
            <Border.Effect>
                <DropShadowEffect BlurRadius="10"
                                  Direction="-90"
                                  RenderingBias="Performance" ShadowDepth="1"/>
            </Border.Effect>
        </Border>

        <Grid x:Name="xTitleGrid"
              Grid.Row="0"
              MouseMove="TitleGrid_MouseMove"
              Background="Transparent">
            <Grid.RowDefinitions>
                <RowDefinition Height="25"/>
                <RowDefinition Height="40"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <controls:IconButton x:Name="xCloseBtn"
                                 Grid.Row="0"
                                 HorizontalAlignment="Right"
                                 VerticalAlignment="Center"
                                 Cursor="Hand"
                                 IconImage="/Views;component/Images/titlebar_btn_close_bl.png"
                                 Click="xCloseBtn_Click"
                                 Command="{Binding CloseCommand}"
                                 Margin="0, 5, 0, 0"/>

            <TextBlock Grid.Row="1"
                       Text="{Binding ElementName=xPopupWindow, Path=Title}"
                       FontSize="15"
                       VerticalAlignment="Bottom"
                       Foreground="#FF000000"
                       Margin="15, 0, 0, 10"
                       Style="{StaticResource xNanumSquareFont}"/>
            <Border Grid.Row="2"
                    BorderBrush="#FFB3B2B2"
                    BorderThickness="0.2"/>
        </Grid>

        <ContentControl x:Name="xPopupContent"
                        Grid.Row="1"
                        Content="{Binding PopupVM}" />
    </Grid>


보시는 이미지와 같이 팝업 윈도우로 쓰일 그냥 단순한 껍데기 View 입니다.
(팝업에 사용되는 내용(컨텐츠)은 별개로 처리 합니다.)


사용되는 팝업의 스타일이 여러개일 것을 고려하여 팝업 스타일에 대한 상수를 다음과 같이 정의해 봅니다.

public enum EDialogHostType
{
    /// <summary>
    /// 기본 모양 뷰 팝업 타입
    /// </summary>
    BasicType,
    /// <summary>
    /// 기본 모양 외 다른 모양 뷰 팝업 타입
    /// </summary>
    AnotherType,
    // 각기 View가 다른 다이얼로그 타입을 여기에 추가 정의할 수 있다.
}

그리고 팝업 View의 ViewModel이 되는 Base를 다음과 같이 작성합니다.

public abstract class PopupDialogViewModelBase : ObservableObject
{
    private ViewModelBase? _popupVM;

    public ViewModelBase? PopupVM
    {
        get => _popupVM;
        set => SetProperty(ref _popupVM, value);
    }

    private RelayCommand? _closeCommand;
    public RelayCommand? CloseCommand
    {
        get
        {
            return _closeCommand ??
                (_closeCommand = new RelayCommand(
                    () =>
                    {
                        PopupVM = null;
                    }));
        }
    }

    public virtual void Cleanup()
    {
        WeakReferenceMessenger.Default.Cleanup();
    }
}

다음으론 팝업 윈도우에서 타이틀 설정, 크기 설정, Close 이후 후 처리 할 수 있는 콜백 기능 등을 사용할 수 있도록
인터페이스를 정의합니다.

이 인터페이스는 팝업 윈도우가 되는 껍데기 View에서 사용합니다.

public interface IDialog
{
    public string? Title { get; set; }

    public double Width { get; set; }

    public double Height { get; set; }

    object? DataContext { get; set; }

    bool Activate();

    void Show();

    bool? ShowDialog();

    void Close();

    Action? CloseCallback { get; set; }
}

팝업 윈도우의 코드 비하인드에 팝업 닫기 버튼 이벤트 처리 등을 구현합니다.
[팝업View.xaml.cs]

/// <summary>
/// PopupWindow.xaml에 대한 상호 작용 논리
/// </summary>
public partial class PopupWindow : Window, IDialog
{
    public PopupWindow()
    {
        this.DataContext = new PopupViewModel();
        InitializeComponent();
    }

    public Action? CloseCallback { get; set; }

    private void TitleGrid_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            this.DragMove();
        }
    }

    private void xCloseBtn_Click(object sender, RoutedEventArgs e)
    {
        if (CloseCallback is not null)
            CloseCallback();

        this.xPopupContent.Content = null;
        this.Close();
    }
}

마지막으로 다이얼로그 서비스를 다음과 같이 작성합니다.

public interface IDialogService
{
    /// <summary>
    /// 팝업 다이얼로그 호스트 등록
    /// </summary>
    /// <param name="dialogHostType">다이얼로그 타입</param>
    /// <param name="dialogWindowHostType">다이얼로그 호스트 Window 타입</param>
    void Register(EDialogHostType dialogHostType, Type dialogWindowHostType);

    bool CheckActivate(string title);

    /// <summary>
    /// 팝업 컨텐츠 설정
    /// </summary>
    /// <param name="vm">컨텐츠 뷰모델</param>
    /// <param name="title">팝업창 타이틀</param>
    /// <param name="dialogHostType">컨텐츠가 표시될 팝업 다이얼로그 호스트 타입</param>
    void SetVM(ViewModelBase vm, string? title, double width, double height, EDialogHostType dialogHostType, bool isModal = true);

    /// <summary>
    /// 팝업 다이얼로그 정리
    /// </summary>
    void Clear();
}

public class DialogService : IDialogService
{
    private Dictionary<EDialogHostType, Type> _dialogWindowHostDic;

    public DialogService()
    {
        // 기본 capacity 3으로 설정
        _dialogWindowHostDic = new(3);
    }

    public void Register(EDialogHostType dialogHostType, Type dialogWindowHostType)
    {
        _dialogWindowHostDic.Add(dialogHostType, dialogWindowHostType);
    }

    public bool CheckActivate(string title)
    {
        var popupWin = Application.Current.Windows.Cast<Window>().FirstOrDefault(p => p.Title == title);
        if (popupWin is not null)
        {
            popupWin.Activate();
            return true;
        }
        else
        {
            return false;
        }
    }

    public void SetVM(ViewModelBase vm, string? title, double width, double height, EDialogHostType dialogHostType, bool isModal = true)
    {
        Type dialogWindowHostType = _dialogWindowHostDic[dialogHostType];
        var popupDialog = Activator.CreateInstance(dialogWindowHostType) as IDialog;

        if (popupDialog is null)
        {
            throw new Exception("팝업 다이얼로그를 생성할 수 없습니다. IDialog 타입인지 체크해 보세요");
        }

        // 팝업창 닫힐때 콜백
        popupDialog.CloseCallback = () =>
        {
            popupDialog.CloseCallback = null;

            if (popupDialog.DataContext is PopupDialogViewModelBase vm)
            {
                vm.Cleanup();
            }
            popupDialog.DataContext = null;
        };

        if (popupDialog.DataContext is PopupDialogViewModelBase viewModel)
        {
            popupDialog.Width = width;
            popupDialog.Height = height;
            popupDialog.Title = title;
            viewModel.PopupVM = vm;

            if(isModal)
            {
                popupDialog.ShowDialog();
            }
            else
            {
                popupDialog.Show();
            }
        }
    }

    public void Clear()
    {
        foreach(var window in Application.Current.Windows)
        {
            if(window is IDialog popupDialog)
            {
                popupDialog.CloseCallback = null;

                if (popupDialog.DataContext is PopupDialogViewModelBase vm)
                {
                    vm.Cleanup();
                }
                popupDialog.DataContext = null;
            }
        }

        _dialogWindowHostDic.Clear();
    }
}

이젠 IDialogService를 IoC에 등록해서 의존성 주입으로 해당 서비스를 이용해서 팝업 다이얼로그를 띄울 수 있습니다.

IDialogService 를 통해 사용되는 모든 팝업을 Register()를 통해 미리 추가 시켜 놓고

dialogService!.Register(EDialogHostType.BasicType, typeof(PopupWindow));

이렇게 팝업 다이얼로그를 띄울 수 있습니다.

// 팝업의 컨텐츠로 사용할 ViewModel
// 팝업 타이틀
// width, height 사이즈 
// 팝업 스타일
_dialogService.SetVM(new 팝업컨텐츠ViewModel(),
"타이틀",
500, 650,
Common.Enums.EDialogHostType.BasicType);

리소스에 팝업의 컨텐츠로 사용되는 View (UserControl)를 해당 ViewModel과 매핑시켜 놓고
팝업 컨텐츠의 ViewModel 객체를 생성해서 넘기면

위에서 미리 추가 해놓았던 팝업 껍데기 뷰들은 _dialogWindowHostDic 딕셔너리에 보관되고 있는데
해당 팝업 스타일에 맞게 팝업 뷰 인스턴스를 생성하고

팝업 ViewModel의 PopupVM속성에 할당됨과 동시에 바인딩으로 인해 해당 팝업 컨텐츠 View가 표시 됩니다.

그리곤 최종적으로 IDialog의 ShowDialog() 또는 Show()를 호출해서 화면에 표시 합니다.


전체 코드는 아래 제 github를 참고 하시면 됩니다.
WPFMusic/ShellViewModel.cs at main · tyeom/WPFMusic (github.com)

115 line MainSettingExecute() 메서드 부분

private void MainSettingExecute()
    {
        if (_dialogService.CheckActivate("설정") is true)
        {
            // CheckActivate에서 해당 팝업 창 활성화
        }
        else
        {
            _dialogService.SetVM(new MainSettingPopupViewModel(), "설정", 500, 650, Common.Enums.EDialogHostType.BasicType);
        }
    }
좋아요 8