Czy mój WindowService nie łamie zasad MVVM?

Czy mój WindowService nie łamie zasad MVVM?
bakunet
  • Rejestracja:prawie 8 lat
  • Ostatnio:około 7 godzin
  • Lokalizacja:Polska
  • Postów:1596
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ę:

Kopiuj
//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:

Kopiuj
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?

JU
  • Rejestracja:około 22 lata
  • Ostatnio:29 dni
  • Postów:5042
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?

bakunet
  • Rejestracja:prawie 8 lat
  • Ostatnio:około 7 godzin
  • Lokalizacja:Polska
  • Postów:1596
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ę?

Kopiuj
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:

Kopiuj
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)

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

            _winService.CloseWindow(this);
        }

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

Kopiuj
/// <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.

bakunet
  • Rejestracja:prawie 8 lat
  • Ostatnio:około 7 godzin
  • Lokalizacja:Polska
  • Postów:1596
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:

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

lub

Kopiuj
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:

Kopiuj
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();
                }
            }
        }
    }
edytowany 2x, ostatnio: bakunet
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)