Как правильно закрыть окно без крестика, на кастомном окошке

Есть сервис окон в котором создается инстанс окна и для него ViewModel это все создается по запросу нужного окна обращаясь к сервису окон, в ходе создания пробрасывается window.Close во ViewModel, для этого во ViewModel в ctor имеется ctor(Action exitWindow), чтобы туда поместился метод закрытия окна.

Примерно такой код:

List<object> dependencies = _containerDi.Resolve(viewModel.Key, identifiers, arguments);
dependencies.Add(window.Close);
window.DataContext = Activator.CreateInstance(viewModel.Key, dependencies.ToArray());

Закрытие окна происходит не по крестику, а по отрисованной кнопке, так как окно кастомное и крестик убрал, команда просто вызывает метод exitWindow.

Можно ли, верно ли так если все это делает сервис окон или же нужно использовать какой-то другой подход/механизм?


Ответы (2 шт):

Автор решения: Voenkomat

Сделал, не трогая code behind, и закрывая окно из порождающего класса, то есть сервиса окон/представлений:

  1. Когда вызываю окно, передаю его тип, создается инстанс окна, зависимости для Vm, сама Vm, устанавливается контекст и самое главное я регистрирую то, что окно создано в Dictionary<object, object> registryView = new(); для того, чтобы отлавливать ссылку на окно по ViewModel.

    public ServiceView Window<View>(string[] identifiers = null, params object[] arguments) where View : Window
    {
        window = Activator.CreateInstance<View>();
        var viewType = typeof(View);
        foreach (var viewModel in containerView)
        {
            if (viewModel.Value.Item1 == viewType)
            {
                List<object> dependencies = _containerDi.Resolve(viewModel.Key, identifiers, arguments);
                var vm = Activator.CreateInstance(viewModel.Key, dependencies.ToArray());
                window.DataContext = vm;
                registryView.Add(vm, window);
                break;
            }
         }
         return this;
    }
    
  2. В VM, получая сервис окон, мы обращаемся к его методу закрытия окна, и отправляем this - то есть vm, чтобы потом сопоставить в сервисе окон VM с окном и получить ссылку на окно из словаря:

     public MainViewModel(IServiceView serviceView)
     {
         Command2 = RaketaCommand.Launch(() =>
         {
             serviceView.Close(this);
         });
     }
    
  3. Метод закрытия в сервисе окон:

    public void Close(object viewModel)
    {
        var view = registryView[viewModel];
        if(view is Window window)
        {
            window.Close();
            registryView.Remove(viewModel);
        }
    }
    

По идее, все правильно, не использовал code behind, не использовал сильные связи, все абстрагировал и разделил ответственности.

→ Ссылка
Автор решения: EvgeniyZ

Я так понимаю, без примера не обойтись... Чтож, давайте покажу пример. Но предупрежу, он будет очень кривой, но покажет суть того, что у вас +- должно быть.

И так, для начала нам нужно создать 2 интерфейса, которые будут реализовывать наши окна и VM слои, это будет как их идентификатор, по которому мы сможем ограничить наши методы (мы ведь не хотим в методе регистрации окна принимать что-то кроме окна, верно?). Собственно, создаем:

public interface IWindow {}
public interface IViewModel {}

Далее давайте создадим интерфейс самого сервиса, в нем мы пропишем всю логику, которая нас интересует, в моем примере это будет открытие, закрытие, ну и регистрация. Получаем такое:

public interface IWindowService
{
    void Show<TViewModel>() where TViewModel : IViewModel;
    void Close<TViewModel>() where TViewModel : IViewModel;
    void Register<TViewModel, TWindow>()
      where TViewModel : IViewModel
      where TWindow : class, IWindow;
}

Теперь мы можем написать сам сервис:

  • Метод регистрации - Его задача сохранить типы регистрируемых окон и VM, чтобы потом мы могли быстро их сопоставить и вызвать нужное окно. Пусть хранит все это в словаре, получаем в итоге такое:

    private readonly Dictionary<Type, Type> _windows = [];
    
    public void Register<TViewModel, TWindow>() where TViewModel : IViewModel where TWindow : class, IWindow
    {
        _windows.Add(typeof(TViewModel), typeof(TWindow));
    }
    

    Замечу то, что тут нет проверки на уже зарегистрированный тип, это сами сделаете если надо, моя задача показать сейчас максимально просто задумку.

  • Метод показа - Его задача состоит в том, чтобы по типу из контейнера достать нужный экземпляр окна и его VM, задать окну DataContext и показать само окно. В качестве контейнера я использую контейнер Microsoft (Microsoft.Extensions.DependencyInjection), вы подстраивайте под себя:

    public void Show<TViewModel>() where TViewModel : IViewModel
    {
        var windowType = _windows[typeof(TViewModel)];
        var window = services.GetService(windowType) as Window;
        var viewModel = services.GetService<TViewModel>();
    
        window.DataContext = viewModel;
        window.Show();
    }
    

    И опять, тут нету проверок на NULL, учитывайте это!

  • Метод закрытия - Очевидно, он отвечает за закрытие окна. Для этого нам надо взять из контейнера по типу нужный объект и вызвать у него .Close() метод.

    public void Close<TViewModel>() where TViewModel : IViewModel
    {
        var windowType = _windows[typeof(TViewModel)];
        var window = services.GetService(windowType) as Window;
        window?.Close();
    }
    

Весь код получается такой:

public class WindowService(IServiceProvider services) : IWindowService
{
    private readonly Dictionary<Type, Type> _windows = [];

    public void Register<TViewModel, TWindow>() where TViewModel : IViewModel where TWindow : class, IWindow
    {
        _windows.Add(typeof(TViewModel), typeof(TWindow));
    }

    public void Show<TViewModel>() where TViewModel : IViewModel
    {
        var windowType = _windows[typeof(TViewModel)];
        var window = services.GetService(windowType) as Window;
        var viewModel = services.GetService<TViewModel>();

        window.DataContext = viewModel;
        window.Show();
    }
    public void Close<TViewModel>() where TViewModel : IViewModel
    {
        var windowType = _windows[typeof(TViewModel)];
        var window = services.GetService(windowType) as Window;
        window?.Close();
    }
}

Обратите внимание на то, что все методы завязаны на VM слоях, все методы просят именно VM, а не V (окна), по этим VM уже ищется окно. Это важно, ибо по правилам MVVM слой ViewModel не должен знать ничего про View и наоборот, ну а запрашивая окна, мы будем требовать в VM слое указывать V слой, что является нарушением.

Собственно, сервис у нас есть, дальше давайте для удобства создадим несколько методов расширения, которые сократят нам код:

public static class Extensions
{
    public static IServiceCollection AddView<TViewModel, TWindow>(this IServiceCollection services)
        where TViewModel : class, IViewModel
        where TWindow : class, IWindow
    {
        services.AddSingleton<TViewModel>();
        services.AddSingleton<TWindow>();

        return services;
    }

    public static IServiceProvider RegisterView<TViewModel, TWindow>(this IServiceProvider services)
        where TViewModel : class, IViewModel
        where TWindow : class, IWindow
    {
        var service = services.GetService<IWindowService>();
        service?.Register<TViewModel, TWindow>();
        return services;
    }
}

Как видите, в методах расширения нет ничего магического, просто объединяем 2 вызова в один удобный метод. Но как по мне, это костыль, ибо 1. для регистрации нам надо вызвать 2 разных метода, что по сути можно было бы сделать в виде одного. 2. В методе регистрации постоянно дергается IWindowService, что в теории не очень хорошо (хотя я часто вижу подобное в других проектах).

Так, теперь регистрируем все в контейнер. Открываем класс App.xaml.cs и переопределяем OnStartup, для примера пусть будет что-то такое:

protected override void OnStartup(StartupEventArgs e)
{
    var services = new ServiceCollection();

    services.AddSingleton<IWindowService, WindowService>();
    services.AddView<MainViewModel, MainWindow>();

    var provider = services.BuildServiceProvider();
    provider.RegisterView<MainViewModel, MainWindow>();

    var windowService = provider.GetRequiredService<IWindowService>();
    windowService.Show<MainViewModel>();
}

Супер, остается только реализовать интерфейсы у окна и VM:

public partial class MainWindow : IWindow {...}

и

public partial class MainViewModel : IViewModel {...}

Ну и также, из-за того, что за старт окна теперь отвечаем мы, нам надо убрать в App.xaml свойство StartupUri. Все, теперь можем запускать, должно открыться окно.

Вот вам базовый сервис, который управляет окнами.


Теперь давайте поговорим про закрытие и выполнения логики в VM. Логично будет требовать через интерфейс реализации нужного нам метода в каждой VM, а затем, в методе закрытия, прописать вызов нужного метода. Собственно давайте так и сделаем.

Дописываем интерфейс VM:

public interface IViewModel 
{
    void OnClose();
}

Для того, чтобы не реализовывать везде этот метод, давайте сделаем абстрактный класс, который будет иметь базовую реализацию для всех VM и который можно будет переопределить:

public abstract class ViewModelBase : IViewModel
{
    public virtual void OnClose() { }
}

Теперь допишем метод Close нашего сервиса, в нем мы теперь будем запрашивать еще и VM, у которой вызовем метод закрытия.

public void Close<TViewModel>() where TViewModel : IViewModel
{
    var windowType = _windows[typeof(TViewModel)];
    var window = services.GetService(windowType) as Window;
    var viewModel = services.GetService<TViewModel>();
    viewModel?.OnClose();
    window?.Close();
}

Осталось дописать VM. Наследуем ее теперь не от интерфейса, а от абстрактного класса class MainViewModel : ViewModelBase. Заметьте, реализовывать метод не обязательно, вы можете запустить проект и все будет работать. Ну а если нам надо сделать что-то в VM при закрытие, то пишем override и выбираем метод OnClose, студия сгенерирует нам что-то такое:

public override void OnClose()
{
    
}

Собственно этот метод и будет теперь вызываться при закрытие окна. Ну а дальше уже ваша фантазия.

А да, закрыть окно вы можете из любого класса, который лежит в контейнере. Допустим, если хотим закрыть по клику кнопки, то запрашиваем IWindowService через конструктор и вызываем в команде кнопки windowService.Close<MainViewModel>();, все, окно закроется.


Недостатки:

  • Очевидный недостаток, дублируется код, я бы подумал как избавиться от этого. По хорошему тут стоит отказаться от словаря сопоставления в сервисе и через рефлексию искать VM и M через имена. Условно окно зовется MainWindow, VM под нее MainViewModel, вот Main тут уникальное, имея его, мы можем через рефлексию найти как VM так и окно.

  • Другой очевидный недостаток, это то, что окна регистрируются как синглтон, а это означает, что мы не можем открыть одно окно несколько раз. Чтоб избавиться от этого недостатка стоит наверно хранить открытые окна и их VM, ну и уже по этому "хранилищу" искать того, кого надо закрыть.


Собственно вот и все. Заметьте, что в моем коде VM и V отделены друг от друга, они независимы, про них знает только сервис, который ими и руководит. Также у меня нету создания объектов вручную, за это отвечает контейнер, у которого я просто прошу нужные мне объекты по типу. Ну и ключевое, повторю, в VM только VM слои, там нет чего-то по типу .Show<MainWindow>, ибо это окно, это View, чего быть не должно в VM.

→ Ссылка