Сервис создания модальных и немодальных окон в контексте паттерна MVVM

sp7

Как в контексте паттерна MVVM правильно и красиво реализовать сервис создания модальных и немодальных окон. Хотелось бы что-то вроде:

myWindowSrv.ShowWindow(myChildViewModel);

Т.е. мы вызываем сервис создания окон, передаем ему нужную ViewModel и сервис на основе типа этой ViewModel отображает нужную View.

1 ответ

sp7

В качестве базы вы можете сделать так:

class DisplayRootRegistry
{
    Dictionary<type, type=""> vmToWindowMapping = new Dictionary<type, type="">();

    public void RegisterWindowType<vm, win="">() where Win: Window, new() where VM : class
    {
        var vmType = typeof(VM);
        if (vmType.IsInterface)
            throw new ArgumentException("Cannot register interfaces");
        if (vmToWindowMapping.ContainsKey(vmType))
            throw new InvalidOperationException(
                $"Type {vmType.FullName} is already registered");
        vmToWindowMapping[vmType] = typeof(Win);
    }

    public void UnregisterWindowType<vm>()
    {
        var vmType = typeof(VM);
        if (vmType.IsInterface)
            throw new ArgumentException("Cannot register interfaces");
        if (!vmToWindowMapping.ContainsKey(vmType))
            throw new InvalidOperationException(
                $"Type {vmType.FullName} is not registered");
        vmToWindowMapping.Remove(vmType);
    }

    public Window CreateWindowInstanceWithVM(object vm)
    {
        if (vm == null)
            throw new ArgumentNullException("vm");
        Type windowType = null;

        var vmType = vm.GetType();
        while (vmType != null && !vmToWindowMapping.TryGetValue(vmType, out windowType))
            vmType = vmType.BaseType;

        if (windowType == null)
            throw new ArgumentException(
                $"No registered window type for argument type {vm.GetType().FullName}");

        var window = (Window)Activator.CreateInstance(windowType);
        window.DataContext = vm;
        return window;
    }
}
</vm></vm,></type,></type,>

Имея это, можно накручивать сверху разную логику открытия/закрытия. Например, вы можете писать команды открытия/закрытия вручную:

class DisplayRootRegistry
{
    // начало см. выше

    Dictionary<object, window=""> openWindows = new Dictionary<object, window="">();
    public void ShowPresentation(object vm)
    {
        if (vm == null)
            throw new ArgumentNullException("vm");
        if (openWindows.ContainsKey(vm))
            throw new InvalidOperationException("UI for this VM is already displayed");
        var window = CreateWindowInstanceWithVM(vm);
        window.Show();
        openWindows[vm] = window;
    }

    public void HidePresentation(object vm)
    {
        Window window;
        if (!openWindows.TryGetValue(vm, out window))
            throw new InvalidOperationException("UI for this VM is not displayed");
        window.Close();
        openWindows.Remove(vm);
    }

    public async Task ShowModalPresentation(object vm)
    {
        var window = CreateWindowInstanceWithVM(vm);
        window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
        await window.Dispatcher.InvokeAsync(() => window.ShowDialog());
    }
}
</object,></object,>

Пример использования:

public partial class App : Application
{
    DisplayRootRegistry displayRootRegistry = new DisplayRootRegistry();
    MainVM mainVM;

    public App()
    {
        displayRootRegistry.RegisterWindowType<mainvm, mainwindow="">();
        displayRootRegistry.RegisterWindowType<askvm, askdialog="">();
    }

    protected override async void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        await RunProgramLogic();
        Shutdown();
    }

    async Task RunProgramLogic()
    {
        while (true)
        {
            mainVM = new MainVM();

            displayRootRegistry.ShowPresentation(mainVM);

            while (true)
            {
                var askCloseVM = new AskVM("Do you want to close the application?");
                await Task.Delay(TimeSpan.FromSeconds(2));
                await displayRootRegistry.ShowModalPresentation(askCloseVM);
                if (askCloseVM.Answer == true)
                    break;
            }
            displayRootRegistry.HidePresentation(mainVM);
            await Task.Delay(TimeSpan.FromSeconds(2));

            var askReopenVM = new AskVM("Maybe reopen again?");
            await displayRootRegistry.ShowModalPresentation(askReopenVM);
            if (askReopenVM.Answer != true)
                break;
        }
    }
}
</askvm,></mainvm,>
public class MainVM
{
    public string Message => "hello world!";
}
<window x:class="MvvmShowWindow.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" title="Main Window" height="350" width="525">
    <grid>
        <textblock text="{Binding Message}" horizontalalignment="Center" verticalalignment="Center">
    </textblock></grid>
</window>
public class AskVM : INotifyPropertyChanged
{
    public AskVM(string question) { Question = question; }

    public string Question { get; private set; }

    bool? answer = null;
    public bool? Answer
    {
        get { return answer; }
        set { if (answer != value) { answer = value; NotifyPropertyChanged(); } }
    }

    void NotifyPropertyChanged([CallerMemberName] string propertyName = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    public event PropertyChangedEventHandler PropertyChanged;
}
<window x:class="MvvmShowWindow.AskDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" title="Question" height="150" width="300">
    <grid>
        <grid.rowdefinitions>
            <rowdefinition height="*">
            <rowdefinition height="Auto">
        </rowdefinition></rowdefinition></grid.rowdefinitions>
        <textblock margin="5" text="{Binding Question}">
        <stackpanel orientation="Horizontal" horizontalalignment="Right" grid.row="1">
            <button minwidth="75" margin="0,5,5,5" click="OnYes" isdefault="True">Yes</button>
            <button minwidth="75" margin="0,5,5,5" iscancel="True">No</button>
        </stackpanel>
    </textblock></grid>
</window>
public partial class AskDialog : Window
{
    public AskDialog()
    {
        InitializeComponent();
        Closed += (o, args) => BindableDialogResult = DialogResult;
        SetBinding(BindableDialogResultProperty, new Binding("Answer"));
    }

    void OnYes(object sender, RoutedEventArgs e)
    {
        DialogResult = true;
    }

    #region dp bool? BindableDialogResult
    public bool? BindableDialogResult
    {
        get { return (bool?)GetValue(BindableDialogResultProperty); }
        set { SetValue(BindableDialogResultProperty, value); }
    }

    public static readonly DependencyProperty BindableDialogResultProperty =
        DependencyProperty.Register("BindableDialogResult", typeof(bool?), typeof(AskDialog),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    #endregion
}

Или вы можете «доверить» закрытие/открытие окна самому окну, связываясь с ним через Binding — здесь делайте как вам кажется лучше.

По поводу того, как организовать модальные окна, ещё можно заглянуть в этот вопрос: Метод, ожидающий действие пользователя.

licensed under cc by-sa 3.0 with attribution.