Как правильно закрыть окно без крестика, на кастомном окошке
Есть сервис окон в котором создается инстанс окна и для него 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 шт):
Сделал, не трогая code behind, и закрывая окно из порождающего класса, то есть сервиса окон/представлений:
Когда вызываю окно, передаю его тип, создается инстанс окна, зависимости для 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; }
В
VM
, получая сервис окон, мы обращаемся к его методу закрытия окна, и отправляем this - то есть vm, чтобы потом сопоставить в сервисе окон VM с окном и получить ссылку на окно из словаря:public MainViewModel(IServiceView serviceView) { Command2 = RaketaCommand.Launch(() => { serviceView.Close(this); }); }
Метод закрытия в сервисе окон:
public void Close(object viewModel) { var view = registryView[viewModel]; if(view is Window window) { window.Close(); registryView.Remove(viewModel); } }
По идее, все правильно, не использовал code behind, не использовал сильные связи, все абстрагировал и разделил ответственности.
Я так понимаю, без примера не обойтись... Чтож, давайте покажу пример. Но предупрежу, он будет очень кривой, но покажет суть того, что у вас +- должно быть.
И так, для начала нам нужно создать 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.