Zamykanie okien we wzorcu MVVM

Wzorzec MVVM, co prawda wiele rzeczy ułatwia i porządkuje. Jednak potrafi sprawić też masę problemów. Pisząc swoją pierwszą aplikację, z użyciem tego wzorca natknąłem się na sporą ich ilość. Jednym z nich było przechwytywanie zdarzenia zamykania głównego okna aplikacji, w celu zapisania wprowadzonych zmian. Drugim problemem było zamknięcie innego okna – potomnego, które miało wyświetlać się na czas ładowania pliku, a później znikać. W przypadku code-behind zadania te są banalnie proste. Mamy zdarzenie zamykania okna, mamy też metodę this.Close(). Nic więcej nie jest nam potrzebne. Jednak co w przypadku wzorca MVVM?

Zamykanie okna z poziomu ViewModel

No właśnie z poziomu ViewModel… Nie tak dawno pisałem, że wszystko co związane z obsługą naszego View ma znajdować się w ViewModel’u. Z kolei kodu View – mamy nie ruszać wcale. Tutaj musimy jednak zrobić wyjątek. Przynajmniej ja nie znam innego sposobu, aby zrealizować to zadanie. Przejdźmy, więc do rzeczy.

Na samym początku przejdźmy do kodu *.cs naszego View. Musimy przecież jakoś wystawić naszą metodę Close();, dla ViewModel’u 😉 Wklepujemy tam taki oto kod, gdzie obiekt vm ma wskazywać na nasz ViewModel:

Najprościej rzecz ujmując – podpinamy tutaj nową akcję, dla naszego ViewModel’u. Jej wywołanie z poziomu ViewModel’u będzie skutkowało wywołaniem metody this.Close(); w naszym View. Skoro przypisujemy tutaj akcję do jakiegoś obiektu w vm to przydałoby się, żeby coś takowego tam w ogóle istniało. Przejdźmy więc do naszego ViewModel i dodajmy nową właściwość typu Action:

Po dodaniu tej linijki właściwie wszystko jest już gotowe. Aby zamknąć nasze okno z poziomu ViewModel, wystarczy wywołać akcję CloseAction();. Czas, więc na przykład:

Jak widać wyżej. Po wciśnięciu przycisku(defacto wywołaniu komendy) aktywne okno zostanie po prostu zamknięte.

Co jest jednak najważniejsze? To, że nie rozwiązaliśmy tego problemu poprzez przekazanie do konstruktora obiektu okna i nie wywołaliśmy czegoś takiego:

W sumie coś takiego też by działało… Jednak byłoby niezgodne ze wzorcem MVVM 😛 Dlaczego? Dlatego, że View nie może nic wiedzieć o ViewModelu, a w ten sposób kod naszego View, byłby po prostu pomieszany z ViewModel’em – co byłoby zupełnie pozbawione sensu. Już lepiej pisać w code-behind…

Zdarzenie zamykania okna

Pozostaje jeszcze kwestia wykrycia, kiedy okno jest zamykane. W końcu czasami trzeba coś zapisać, kiedy aplikacja jest zamykana.

W tym celu w naszym View przypisujemy zdarzenie zamknięcia okna, do metody w naszym ViewModel’u:

Z kolei w ViewModel dodajemy naszą metodę:

Teraz zawsze, kiedy okno będzie zamykane – zostanie wywołana ta metoda. Co też ważne, możemy tutaj manipulować wartością e.Cancel, którą znamy z code-behind. Do czego ona służy? Za jej pomocą możemy sprawić, że okno po wciśnięciu magicznego X, po prostu się nie zamknie ;). Jednak należy pamiętać, że po zmianie jej wartości na true, nasze okno nie zamknie się, również wtedy kiedy wywołamy CloseAction()!

Na koniec cały projekt do pobrania: Download

14 przemyśleń nt. „Zamykanie okien we wzorcu MVVM

  1. Mam wrazenie, ze troche przedobrzyles. 🙂
    Operacja zamkniecia okna nie jest operacja biznesowa a operacja interfejsu.
    Pewnie purysci MVVM beda sie ze mna klocic ale moim zdaniem to, co zajmuje sie operacja na samym widoku, nie powinno ladowac w logice biznesowej. To tak jakby resize’owania okna tez rozwiazywac w logice biznesowej.
    Oczywiscie kazdy ma prawo do innego zdania. 🙂

  2. Wydaje mi się, że to w XAMLu powinna być złapane zdarzenie Closing etc. (np. przez event to command) z widoku i obsłużona w ViewModelu właśnie, a jeśli chcemy ręcznie zamykać jakąś kontrolkę to raczej powinno to być robione przez jakiś button, niż przez wywoływanie close z ViewModelu.

    • Jak inaczej rozwiązałbyś taki problem:
      Mamy okienko z progressbarem, który pokazuje postęp wykonywania jakiegoś zadania. Po jego wykonaniu chcemy, żeby okienko się zamknęło. Jak to inaczej rozwiązać, niż przez wywołanie Close w VM? 😉

      PS: Co do samego zdarzenia zamykania okna, faktycznie można to też tak zrealizować 😉

        • Nie słyszałem o tym, a widzę że bardzo dobra rzecz 😉 Dzięki!

          Co do tego nieszczęsnego wywołania Close – z tego co zdążyłem zauważyć busyindicator, nie ma nawet krzyżyka, żeby okienko zamknąć, czyli tak czy owak musi w jego kodzie być coś w stylu this.Close();, które jest wywołane po wykonaniu zdań. Przynajmniej tak mi się wydaje 😛

          • Pamiętaj, że we wzorcu MVVM ViewModel nie odpowiada dokładnie za logikę biznesową (za tą odpowiada Model), więc jak najbardziej poprawne jest zamykanie ViewModeli w kodzie. Mi podoba się podejście stosowane w bibliotece Caliburn.Micro. Biblioteka ta rozszerzyła operację bindowania widoku (okna) z viewmodelem. Jeżeli viewmodel implementuje odpowiedni interfejs (nazwijmy go tutaj IClosable 🙂 to podczas bindowania jego metody są automatycznie powiązany z zamknięciem okna. Brzmi skomplikowanie ale w praktyce działa rewelacyjnie.

  3. Dziwi mnie to, że programiści boją się pisać code behind, gdy używają MVVM. Osobiście nie widzę w tym nic złego. Wzorzec ten służy do podziału aplikacji na warstwy, a nie upychanie w ViewModelu wszystkiego co się da, tylko po to żeby nie pisać code behindu. To co jest logiką widoku, powinno być napisane po stronie widoku. Otóż otwieranie i zamykanie okien jest cechą widoku i ViewModel nie powinien o tym nic wiedzieć.

    • No tak, ale zapisywanie danych już nie koniecznie jest sprawą widoku. To powinno się realizować w ViewModelu, a nawet wręcz w Modelu… Z tym, że to ViewModel, jest pośrednikiem między View, a Modelem. Tak, więc nie możemy sobie wywoływać niczego z Modelu, z poziomu View.

      Code-behind umieszczony w naszym widoku sprawia, że nie piszemy już w MVVM 😉 Założenia tego wzorca są inne. Tworzymy jakąś hybrydę. Czy to źle? Nie mnie osądzać. Jeżeli pisze się aplikację w pojedynkę, lub w małym zespole na pewno nie będzie to przeszkadzać.

      Tyle ode mnie, może się wypowie jeszcze ktoś z większym doświadczeniem 😛

      • Pofatygowałem się i napisałem przykładowy kod, który pokazuje jak można unikać takich kwiatków jak delegat z this.Close i nie łamać przy tym zasad MVVM 😉 Uważam, że jest to jedne z lepszych rozwiązań na twój problem, nie twierdzę jednak, że najlepsze.

        Tutaj link do kodu: https://github.com/fuffu/Wpf-Mvvm-Closing-Window

        Przy okazji powtórzę, że pisanie kodu w MVVM nie jest niczym złym, jeżeli kod wykonuje jedynie logikę widoku (na przykład jakieś scrollowanie czy coś). Zazwyczaj jednak code behind można zastąpić XAMLem lub własną kontrolką.

        • No i takie komentarze lubię, skłaniają do nauki innych rozwiązań – tym bardziej, że jestem początkujący 😉

          Już pobrałem projekt, w wolnej chwili przejrzę i „ogarnę” 😉
          Dzięki!

  4. Widze ze tutaj nikt nie zrozumial dlaczego uzywamy MVVM…

    Dzieki MVVM mozemy testowac wiekszosc logiki aplikacji (lacznie z GUI). Jezeli cos napiszemy do code behind, nie jest to otestowane i tym samym ztracamy zabezpieczenie przed nie celowa zmiana funkcjonalnosci aplikacji przez nas samych w przyszlosci, lub przez innego programiste.

    So please, never ever bother to follow MVVM (or any other patterns – MV*) if you don’t write unit tests.

Możliwość komentowania jest wyłączona.