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);
}
}