Wzorzec MVVM jak zaimplementować zamykanie i otwieranie nowych okien

0

Witam
Zacząłem bawić się wzorcem MVVM, piszę małą aplikację w WPF. Mam problem związany z zamykaniem i otwieraniem nowych okien. Póki co zrobiłem takie głupie rozwiązanie, że przekazuje okno do ViewModelu i tam je zamykam/otwieram kolejne. Problemem jest to, że nie jest to zgodne z założeniami MVVM, bo w tym momencie ViewModel jest ściśle powiązany z widokiem. Pytanie jak to zrobić prawidłowo ?

 

public View()
{
      this.DataContext = new ViewModel(this);
}
 
public ViewModel(View window)
{
        this.window = window;
         NewWindowCmd = new RelayCommand(pars => NewWindow());
}

public void NewWindow()
{
// otwieranie zamykanie okna
}
3

Zrób w viewmodelu eventy:

public delegate void SimpleEventHandler();
public event SimpleEventHandler OpenNewWindow;
public event SimpleEventHandler Close;

dołóż metodę, która będzie sprawdzała czy handler nie jest równy null:

private void OnSimpleEvent(SimpleEventHandler handler){
	if(handler != null) handler();
}

w komendzie zamykającej okno wykonaj tę metodę:

this.OnSimpleEvent(this.Close);

tak samo w otwierającej nowe etc...

teraz w code-behind obsłuż połapany event:

var viewmodel = new WindowViewModel();
viewmodel.Close += () => { this.Close(); };
viewmodel.OpenNewWindow += () => {
	var window = new MyWindow();
	window.Show();
};

this.Datacontext = viewmodel;

To nieprawda, że code-behind ma być całkowicie pusty. Code-behind to zarządzanie widokiem więc możesz tam odpalać messageboksy czy przechwytywać eventy z viewmodelu i na ich podstawie tworzyć nowe okna czy zamykać inne etc...

Ciekawostka:
Dzięki funkcji sprawdzającej czy dany event nie jest nullem unikniesz wpadek przy unit-testach, bo dajmy na to event odpalający jakiegoś messagebox'a po prostu nie zadziała, a test pójdzie swobodnie dalej. A jeżeli messagebox jest pytający o coś to można zmienić delegata tak żeby nie zwracał void tylko to czego oczekujesz od odpowiedzi w messageboksie - enuma jakiegoś albo coś w tym stylu, generalnie rezultat. :)

0

Przepraszam, że odkopuję temat ale był mi bardzo pomocny, natomiast mam dodatkowe pytanie, w jaki sposób można otworzyć okno z konstruktorem, który zawiera parametr trzymając się wzorca MVVM? Byłbym wdzięczny za pomoc.

Pozdrawiam i jeszcze raz przepraszam ;)

0

Poczytaj o kontenerach IoC - Ninject, Unity Container - możesz za ich pomocą wstrzyknąć do konstruktora viewmodelu parametry nie wiążąc samego viewmodelu z klasami widoku.

0

Ok, więc poczytałem ale nie doszedłem o tego jak rozwiązać problem.
W ViewModel mam zeminną np. int abc; do niej przypisuję jakąś liczbę i chcę tą liczbę przekazać w konsruktorze bo od jej wartości będzie zależało to co będę wyświetlał, w tym jest cały problem. (tu użyję Twojego przykładu)

var viewmodel = new WindowViewModel();
viewmodel.Close += () => { this.Close(); };
viewmodel.OpenNewWindow += () => {
    var window = new MyWindow(); // chodzi o tą linię, nie można zrobić var window = new MyWindow(abc); bo niestety ale View byłby powiązany z VM
    window.Show();
};
 
this.Datacontext = viewmodel;

Jeśli chodzi o Ninject trafiłem na porównanie takie, że to przy małym problemie to jak używać armaty do zabijania muchy.

0

No właśnie bez IoC będziesz musiał przebijać się kolejno przez konstruktory:

Zrób takiego delegata:

public delegate void SimpleEventHandler(int abc);
public event SimpleEventHandler OpenNewWindow;

Przy wywołaniu eventa zrób:

this.OpenNewWindow?.Invoke(tutajMojaZmiennaInt);

W code behind natomiast:

viewmodel.OpenNewWindow += (tutajMojaZmiennaInt) => {
    var window = new MyWindow(tutajMojaZmiennaInt); // I tutaj kolejno przekazujesz zmienną przez konstruktury...
    window.Show();
};
0

Działa, dziękuję ale faktycznie to średnio wygląda, jednak będę musiał zagłębić się w IoC. Byłbym wdzięczny za parę słów na chłopski rozum jak takie coś wygląda, ponieważ (pierwsza z brzegu) strona http://www.altcontroldelete.pl/artykuly/biblioteki-warte-poznania-w-c-ninject/ przedstawia ninjecta dosyć dziwnie bo mówimy o tym żeby tak nie przebijać się przez te konstruktory ale jednak i tak działa to poprzez przekazywanie przez konstruktory, przykład ze strony MainApp mainApp = new MainApp(ninjectKernel.Get<ILogger>()); więc po mojemu i tak musiałbym zrobić delegata, event no i code behind, a rzecz w tym żeby to chyba uprościć o ile się nie mylę?

0

Raczej byłoby to tak: MainApp mainApp = ninjectKernel.Get<MainApp>();. Ninject sam będzie wiedział czy do konstruktora potrzebne są argumenty czy nie. Możesz operować nie tylko na interfejsach ale i na zwykłych klasach. Jeżeli MainApp posiada argument w konstruktorze to kontener zadba sam o jego przekazanie. Na tym to właśnie polega - nie trzeba pisać jakichś dziwnych łańcuszków przekazań tylko wystarczy zrobić np: taki konstruktor public SomeViewModel(ISomeService service) i Ninject wie, że ma przekazać do konstruktora odpowiedni obiekt.

Należy tylko zbindować odpowiedni interfejs do odpowiedniej klasy.

0

I jak by to wyglądało w Twoim przykładzie? Nie potrafię sobie tego wyobrazić bo póki co to przychodzi mi do głowy tylko taki obraz chociaż i tak nie jestem tego pewny bo w tedy nadal w gre wchodziłyby edlegaty i eventy a sam code behind byłby jeszcze bardziej zapisany.

viewmodel.OpenNewWindow += (tutajMojaZmiennaInt) => {
    MyWindow window = new ninjectKernel.Get<MyWindow>(); 
    window.Show();
};
0

Dokładnie tak tylko musiałbyś wrzucić tą zmienną do jakiegoś np. serwisu, który zająłby się przekazywaniem danych. Wtedy przekazywanie takiej zmiennej za pomocą delegata nie jest konieczne, bo przekaże ją IoC.

Tutaj trochę kłania się to "używanie armaty na muchy", ponieważ zanim zakodujesz różne rzeczy dobrze byłoby gdybyś logicznie zaprojektował przepływ danych. Wtedy taki kontener IoC sprawdza się wyśmienicie - co zresztą potwierdza jego wdrażanie out of box choćby w najnowszym ASP.NET Core.

PS: Można oczywiście przekazać argumenty poprzez ninjecta bezpośrednio: MyWindow window = new ninjectKernel.Get<MyWindow>(new ConstructorArgument( "zmienna", tutajMojaZmiennaInt)). Pewnie, że tak :) Wszystkiego należy używać adekwatnie do okoliczności.

Ja np. Ninjectem przekazuję z automatu serwisy przypięte w modelu do konstruktorów klas viewmodeli. Jakieś inne pierdoły typu zmienne do MessageBoksów to powiedzmy sobie szczerze - nie opłaca się i już lepiej wypuścić do przez delegata. Tego Twojego inta... to wiesz... można też przemyśleć :)

0

No ten mój int to w zasadzie ID z bazy :D wybieram coś na datagridzie, pobiera mi id, którego przekazuję do nowego okienka ze szczegółami wybranego rekordu :D
W sumie to myślałem nawet o klasie pomocniczej żeby mi przechowywała tą zmienną ale stwierdziłem, że nie ma sensu takie coś.

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