Czy mój WindowService nie łamie zasad MVVM?

0

Hej, zabrałem się za ogarnianie otwierania nowych okien w MVVM bez pomocy frameworków, co by lepiej zrozumieć mechanikę i się nieco podszkolić.

W tym celu utworzyłem WindowService, który zaczerpnąłem z SO, choć nieco w nim pozmieniałem, gdzie zamiast generycznej klasy mamy generyczną metodę:

//https://stackoverflow.com/a/36642835
    public class WindowService: IWindowService
    {
        private IContainer _container;

        public void ShowWindow<T>() where T : Window
        {
            _container = Startup.BootStrap();

            _container.Resolve<T>().Show();
        }
    }

Serwis jest wstrzykiwany do VM, oraz wcześniej rejestrowany w kontenerze IoC. Wykorzystuję go w VM w następujący sposób:

public class MainViewModel : ViewModelBase
    {
        private readonly IEventAggregator _eventAggregator;
        private readonly IWindowService _winService;

        public MainViewModel(IEventAggregator eventAggregator, IWindowService winService)
        {
            _eventAggregator = eventAggregator;
            _winService = winService;
        }

        private void OnUpdateStock(object obj)
        {
            _winService.ShowWindow<SecondView>();
            _eventAggregator.SendMessage<SelectionChangedEvent>(new SelectionChangedEvent("dupa"));
        }
}

I tutaj pojawia się moje pytanie, czy wywołując _winService.ShowWindow<SecondView>(); i podając nazwę widoku nie łamiemy zasad MVVM? Modele widoków bodajże nie powinny nic wiedzieć o widokach. Choć w tym wypadku nie jest przypisany do niego widok, a widok otwierany przez niego. Z drugiej strony przy pisaniu testów chyba będzie wygodniej sprawdzić, czy została wywołana metoda .ShowWindow<SecondView>() z nazwą konkretnego widoku.

Co o tym myślicie?

1

Wg mnie może tak być. Mógłbyś też operować na zdarzeniach, które wyłapuje widok, żeby widok otwierał konkretne okno.
Ja bym się bardziej czepił tego:
_container = Startup.BootStrap();
Wygląda to na ServiceLocator.

Czemu nie możesz wstrzyknąć po prostu IContainer?

0
Juhas napisał(a):

Ja bym się bardziej czepił tego:
_container = Startup.BootStrap();
Wygląda to na ServiceLocator.

Czemu nie możesz wstrzyknąć po prostu IContainer?

Startup (jako kontener IoC) wygląda u mnie tak. Wydaje mi się, że rejestruję tu serwisy podobnie jak w ASP.NET Core, czy się mylę?

public class Startup
    {
        /// <summary>
        /// IoC container
        /// </summary>
        /// <returns></returns>
        public static IContainer BootStrap()
        {
            var builder = new ContainerBuilder();

            builder.RegisterType<EventAggregator>()
              .As<IEventAggregator>().SingleInstance();

            builder.RegisterType<WindowService>()
              .As<IWindowService>().SingleInstance();

            builder.RegisterType<MainView>().AsSelf();
            builder.RegisterType<MainViewModel>().AsSelf().SingleInstance();

            builder.RegisterType<SecondView>().AsSelf();
            builder.RegisterType<SecondViewModel>().AsSelf().SingleInstance();

            builder.RegisterType<DialogView>().AsSelf();
            builder.RegisterType<DialogViewModel>().AsSelf().SingleInstance();

            return builder.Build();
        }
    }

Mój najnowszy ShowWindow wygląda teraz tak:

public Dictionary<object, object> ActiveViews { get; set; }

        /// <summary>
        /// Creates instance of Window of type T and shows it.
        /// </summary>
        /// <typeparam name="T">Window type</typeparam>
        public void OpenWindow<T>() where T : Window
        {
            //TODO: spr najpierw czy nie jest już otwarte
            //TODO: dodanie eventu na wypadek zamknięcia okna?

            Window window = (T)Activator.CreateInstance(typeof(T));

            window.Show();

            ActiveViews.Add(window, window.DataContext);
        }

Ale mam zamiar jeszcze spróbować go przerobić na modłę tego artykułu, gdzie będę mógł wołać metodę z instancją modelu widoku, i sparować go z widokiem przy użyciu konwencji nazewniczej.

Juhas napisał(a):

Wg mnie może tak być. Mógłbyś też operować na zdarzeniach, które wyłapuje widok, żeby widok otwierał konkretne okno.

Myślałem o wykorzystaniu Eventów, ale przy zamykaniu okien. Na chwilę obecną doszedłem do rozwiązania, gdzie tworzę słownik <widok, modelWidoku>, i przy zamykaniu okna robię coś takiego:
(VM)

private void Ok(object obj)
        {
            DialogResult = true;

            _winService.CloseWindow(this);
        }

Gdzie w WindowService odbywa się coś nastepującego:

/// <summary>
        /// Finds Window in ActiveViews dictionary basing on VM value, closes this window, removes from dictionary.
        /// </summary>
        /// <param name="viewModel">View model instance</param>
        public void CloseWindow(object viewModel)
        {
            if (viewModel != null)
            {
                Type type = viewModel.GetType();
                Window window = ActiveViews.FirstOrDefault(view => view.Value.GetType() == type).Key as Window;

                if (window != null)
                {
                    window.Close();
                    ActiveViews.Remove(window);
                }
            }
        }

Choć coś czuję, że przy oknach dialogowych, które zwracają jakikolwiek wynik, już nie obędzie się bez Eventów.

0

Niestety, w praktyce przy wywołaniu metody Stock newStock = await _winService.OpenResultWindow<DialogView>() as Stock; przy testowaniu pojawiły się problemy związane z odniesieniem do System.Windows, więc takie podejście było ostatecznie bardzo problematyczne. xUnit szukał odniesień do biblitek związanych z UI, a jak już je miał, to nie mógł ich użyć.

Zdecydowałem się ostatecznie na wywołania metody:

object newStock = await _winService.OpenResultWindow(_dialogVMCreator());

lub

Stock newStock = await _winService.OpenResultWindow(_dialogVMCreator()) as Stock;

które można stosować zamiennie, nawet z pominięciem ViewModelLokatora. Jedyny wymóg to posiadanie kontenera IoC. Ostatecznie mój cały WindowManager wygląda tak:

public class WindowManager : IWindowManager
    {
        /// <summary>
        /// Constructor, creates instance of OpenedViews list if not created yet.
        /// </summary>
        public WindowManager()
        {
            if (OpenedViews == null)
            {
                OpenedViews = new List<WindowModel>();
            }
        }

        /// <summary>
        /// List of currently opened WindowModels.
        /// </summary>
        public List<WindowModel> OpenedViews { get; set; }

        /// <summary>
        /// MessageBoxResult wrapper.
        /// </summary>
        /// <param name="messageBoxText">Passed text to be displayed</param>
        /// <param name="messageBoxTitle">Passed title of the window to be displayed</param>
        /// <returns>Nullable bool</returns>
        public bool? OpenDialogWindow(string messageBoxText, string messageBoxTitle)
        {
            bool? result = null;

            MessageBoxButton button = MessageBoxButton.YesNoCancel;
            MessageBoxResult messageResult = MessageBox.Show(messageBoxText, messageBoxTitle, button);

            switch (messageResult)
            {
                case MessageBoxResult.Yes:
                    result = true;
                    break;
                case MessageBoxResult.No:
                    result = false;
                    break;
                case MessageBoxResult.Cancel:
                    result = null;
                    break;
            }

            return result;
        }

        /// <summary>
        /// OpenFileDialog wrapper.
        /// </summary>
        /// <param name="messageBoxTitle">Passed title of the window to be displayed</param>
        /// <returns>String path</returns>
        public string OpenFileDialogWindow(string messageBoxTitle)
        {
            string result = string.Empty;

            OpenFileDialog openFileDialog = new OpenFileDialog();
            if (openFileDialog.ShowDialog() == true)
            {
                openFileDialog.Title = messageBoxTitle;
                result = openFileDialog.FileName;
            }

            return result;
        }

        /// <summary>
        /// Opens new window for specific view model using name convention. Window may return object. Attaches event handler for Window.Closed.
        /// </summary>
        /// <param name="viewModel">View model name</param>
        /// <returns>Object</returns>
        public async Task<object> OpenResultWindow(object viewModel)
        {
            if (!CheckIfAlreadyOpened(viewModel))
            {
                WindowModel model = CreateWindoModel(viewModel);

                model.OpenedWindow.Closed += new EventHandler((s, e) => ResultWindowClosed(s, e, model));
                model.OpenedWindow.ShowDialog();

                while (!model.IsValueReturned)
                    await Task.Delay(100);

                return model.ReturnedObjectResult;
            }

            return null;
        }

        /// <summary>
        /// Opens new modal dialog window for specific view model using name convention. Window may return nullable bool. Attaches event handler for Window.Closed.
        /// </summary>
        /// <param name="viewModel">View model name</param>
        /// <returns>Nullable bool</returns>
        public async Task<bool?> OpenModalDialogWindow(object viewModel)
        {
            if (!CheckIfAlreadyOpened(viewModel))
            {
                WindowModel model = CreateWindoModel(viewModel);

                model.OpenedWindow.Closed += new EventHandler((s, e) => DialogWindowClosed(s, e, model));
                model.OpenedWindow.ShowDialog();

                while (!model.IsValueReturned)
                    await Task.Delay(100);

                return model.ReturnedDialogResult;
            }

            return null;
        }

        /// <summary>
        /// Opens window for specific view model using name convention. Attaches event handler for Window.Closed.
        /// </summary>
        /// <param name="viewModel">View model name</param>
        public void OpenWindow(object viewModel)
        {
            if (!CheckIfAlreadyOpened(viewModel))
            {
                WindowModel model = CreateWindoModel(viewModel);

                model.OpenedWindow.Closed += new EventHandler((s, e) => WindowClosed(s, e, model));
                model.OpenedWindow.Show();
            }
        }

        /// <summary>
        /// Extracts window name from passed view model object and adds new WindowModel to OpenedViews list.
        /// </summary>
        /// <param name="viewModel">View model object</param>
        /// <returns>WindowModel object</returns>
        private WindowModel CreateWindoModel(object viewModel)
        {
            var modelType = viewModel.GetType();
            var windowTypeName = modelType.Name.Replace("ViewModel", "View");
            var windowTypes = from t in modelType.Assembly.GetTypes()
                              where t.IsClass && t.Name == windowTypeName
                              select t;

            WindowModel model = GetWindowModelFromWindowName(windowTypes.Single(), viewModel);
            OpenedViews.Add(model);

            return model;
        }

        /// <summary>
        /// Creates instance of new window basing on window type, returns new instance of WindowModel object.
        /// </summary>
        /// <param name="type">Window type</param>
        /// <param name="viewModel">View model related to the window</param>
        /// <returns>WindowModel object</returns>
        private WindowModel GetWindowModelFromWindowName(Type type, object viewModel)
        {
            Window window = (Window)Activator.CreateInstance(type);
            window.DataContext = viewModel;

            WindowModel model = new WindowModel()
            {
                OpenedWindow = window,
                AssignedViewModel = viewModel,
                IsValueReturned = false,
                ReturnedDialogResult = null,
                ReturnedObjectResult = null,
                ReturnedFilePathResult = null
            };

            return model;
        }

        /// <summary>
        /// Verify if WindowModel can be found in OpenedViews.
        /// </summary>
        /// <param name="viewModel">View model related to the window</param>
        /// <returns>Bool type, if the window is opened already or not</returns>
        private bool CheckIfAlreadyOpened(object viewModel)
        {
            return OpenedViews.Select(model => model.AssignedViewModel.GetType() == viewModel.GetType()).FirstOrDefault();
        }

        /// <summary>
        /// Handler for Closed event, triggered when window is about to close.
        /// </summary>
        /// <param name="sender">Window object</param>
        /// <param name="model">Window model object related to the window</param>
        private void WindowClosed(object sender, EventArgs args, WindowModel model)
        {
            Window window = (Window)sender;
            window.Closed -= new EventHandler((s, e) => WindowClosed(s, e, model));

            OpenedViews.Remove(model);
        }

        /// <summary>
        /// Handler for modal dialog window Closed event, triggered when dialog window is about to close.
        /// </summary>
        /// <param name="sender">Window object</param>
        /// <param name="model">Window model object related to the window</param>
        private void DialogWindowClosed(object sender, EventArgs args, WindowModel model)
        {
            Window window = (Window)sender;
            window.Closed -= new EventHandler((s, e) => DialogWindowClosed(s, e, model));

            OpenedViews.Remove(model);

            var vm = window.DataContext as IModalDialogViewModel;
            model.ReturnedDialogResult = vm.DialogResult;
            model.IsValueReturned = true;
        }

        /// <summary>
        /// Handler  for modal dialog window Closed event, triggered when dialog window is about to close.
        /// </summary>
        /// <param name="sender">Window object</param>
        /// <param name="model">>Window model object related to the window</param>
        private void ResultWindowClosed(object sender, EventArgs args, WindowModel model)
        {
            Window window = (Window)sender;
            window.Closed -= new EventHandler((s, e) => ResultWindowClosed(s, e, model));

            OpenedViews.Remove(model);

            var vm = window.DataContext as IResultViewModel;
            model.ReturnedObjectResult = vm.ObjectResult;
            model.IsValueReturned = true;
        }

        /// <summary>
        /// Finds Window in OpenedViews List basing on VM object type, closes this window, removes from dictionary.
        /// </summary>
        /// <param name="viewModel">View model object</param>
        public void CloseWindow(object viewModel)
        {
            if (viewModel != null)
            {
                Type type = viewModel.GetType();
                Window window = OpenedViews.FirstOrDefault(model => model.AssignedViewModel.GetType() == type).OpenedWindow as Window;

                if (window != null)
                {
                    window.Close();
                }
            }
        }
    }

1 użytkowników online, w tym zalogowanych: 0, gości: 1