Model-View-Presenter w WinForms

90

Po raz pierwszy próbuję zaimplementować metodę MVP przy użyciu WinForms.

Próbuję zrozumieć funkcję każdej warstwy.

W moim programie mam przycisk GUI, który po kliknięciu otwiera okno dialogowe openfiledialog.

Więc używając MVP, GUI obsługuje zdarzenie kliknięcia przycisku, a następnie wywołuje presenter.openfile ();

Czy w ramach presenter.openfile () powinno to delegować otwarcie tego pliku do warstwy modelu, czy też ze względu na brak danych lub logiki do przetworzenia, powinno po prostu działać na żądanie i otworzyć okno openfiledialog?

Aktualizacja: Zdecydowałem się zaoferować nagrodę, ponieważ czuję, że potrzebuję dalszej pomocy w tej sprawie i najlepiej dostosowanej do moich konkretnych punktów poniżej, aby mieć kontekst.

OK, po przeczytaniu o MVP zdecydowałem się zaimplementować Widok pasywny. Efektywnie będę miał kilka kontrolek w Winform, które będą obsługiwane przez Prezenter, a następnie zadania delegowane do Modelu (ów). Moje konkretne punkty znajdują się poniżej:

  1. Kiedy winform ładuje się, musi uzyskać widok drzewa. Czy mam rację sądząc, że widok powinien zatem wywołać metodę taką jak: presenter.gettree (), to z kolei oddeleguje do modelu, który pobierze dane do drzewa, utworzy i skonfiguruje, zwróci do prezenter, który z kolei przejdzie do widoku, który następnie po prostu przypisze go, powiedzmy, do panelu?

  2. Czy to będzie to samo dla każdej kontroli danych w Winform, ponieważ mam również datagridview?

  3. Moja aplikacja ma wiele klas modelu z tym samym zestawem. Obsługuje również architekturę wtyczek z wtyczkami, które muszą być ładowane podczas uruchamiania. Czy widok po prostu wywołałby metodę prezentera, która z kolei wywołałaby metodę ładującą wtyczki i wyświetlającą informacje w widoku? Która warstwa będzie wtedy kontrolować odwołania do wtyczek. Czy widok zawiera odniesienia do nich lub do prezentera?

  4. Czy mam rację sądząc, że widok powinien obsługiwać każdą rzecz dotyczącą prezentacji, od koloru węzła drzewa, po rozmiar datagrid, itp.?

Myślę, że są to moje główne zmartwienia i jeśli rozumiem, jaki powinien być dla nich przepływ, myślę, że będzie dobrze.

Darren Young
źródło
Ten link lostechies.com/derekgreer/2008/11/23/… wyjaśnia niektóre style MVP. Mogłoby to okazać się pomocne, oprócz doskonałej odpowiedzi Johanna.
ak3nat0n

Odpowiedzi:

123

To jest moje skromne podejście do MVP i Twoich konkretnych problemów.

Po pierwsze , wszystko, z czym użytkownik może wchodzić w interakcje lub po prostu być pokazywane, to widok . Prawa, zachowanie i cechy takiego widoku są opisane przez interfejs . Ten interfejs można zaimplementować za pomocą interfejsu użytkownika WinForms, interfejsu konsoli, interfejsu internetowego lub nawet żadnego interfejsu użytkownika (zwykle podczas testowania prezentera) - konkretna implementacja nie ma znaczenia, o ile jest zgodna z prawami interfejsu widoku .

Po drugie , widok jest zawsze kontrolowany przez prezentera . Prawa, zachowanie i cechy takiego prezentera są również opisane przez interfejs . Ten interfejs nie jest zainteresowany implementacją widoku konkretnego, o ile jest zgodny z prawami interfejsu widoku.

Po trzecie , ponieważ prezenter kontroluje swój widok, aby zminimalizować zależności, tak naprawdę nie ma żadnej korzyści z tego, że widok w ogóle wie cokolwiek o swoim prezencie. Istnieje uzgodniona umowa między prezenterem a widokiem i jest to określone w interfejsie widoku.

Konsekwencje Trzeciego to:

  • Prezenter nie ma żadnych metod, które może wywołać widok, ale widok zawiera zdarzenia, które prezenter może zasubskrybować.
  • Prezenter zna swój pogląd. Wolę to osiągnąć za pomocą wtrysku konstruktora na betonowym prezencie.
  • Widok nie ma pojęcia, który prezenter go kontroluje; po prostu nigdy nie otrzyma żadnego prezentera.

W przypadku Twojego problemu powyższy kod może wyglądać tak w nieco uproszczonym kodzie:

interface IConfigurationView
{
    event EventHandler SelectConfigurationFile;

    void SetConfigurationFile(string fullPath);
    void Show();
}

class ConfigurationView : IConfigurationView
{
    Form form;
    Button selectConfigurationFileButton;
    Label fullPathLabel;

    public event EventHandler SelectConfigurationFile;

    public ConfigurationView()
    {
        // UI initialization.

        this.selectConfigurationFileButton.Click += delegate
        {
            var Handler = this.SelectConfigurationFile;

            if (Handler != null)
            {
                Handler(this, EventArgs.Empty);
            }
        };
    }

    public void SetConfigurationFile(string fullPath)
    {
        this.fullPathLabel.Text = fullPath;
    }

    public void Show()
    {
        this.form.ShowDialog();        
    }
}

interface IConfigurationPresenter
{
    void ShowView();
}

class ConfigurationPresenter : IConfigurationPresenter
{
    Configuration configuration = new Configuration();
    IConfigurationView view;

    public ConfigurationPresenter(IConfigurationView view)
    {
        this.view = view;            
        this.view.SelectConfigurationFile += delegate
        {
            // The ISelectFilePresenter and ISelectFileView behaviors
            // are implicit here, but in a WinForms case, a call to
            // OpenFileDialog wouldn't be too far fetched...
            var selectFilePresenter = Gimme.The<ISelectFilePresenter>();
            selectFilePresenter.ShowView();
            this.configuration.FullPath = selectFilePresenter.FullPath;
            this.view.SetConfigurationFile(this.configuration.FullPath);
        };
    }

    public void ShowView()
    {
        this.view.SetConfigurationFile(this.configuration.FullPath);
        this.view.Show();
    }
}

Oprócz powyższego zwykle mam IViewinterfejs podstawowy , w którym przechowuję Show()i dowolny widok właściciela lub tytuł, z którego zwykle korzystają moje widoki.

Na Twoje pytania:

1. Kiedy winform ładuje się, musi uzyskać widok drzewa. Czy mam rację sądząc, że widok powinien zatem wywołać metodę taką jak: presenter.gettree (), to z kolei oddeleguje do modelu, który pobierze dane do drzewa, utworzy go i skonfiguruje, zwróci do prezenter, który z kolei przejdzie do widoku, który następnie po prostu przypisze go, powiedzmy, do panelu?

Zadzwoniłbym IConfigurationView.SetTreeData(...)z IConfigurationPresenter.ShowView(), tuż przed wezwaniem doIConfigurationView.Show()

2. Czy wyglądałoby to tak samo dla każdej kontroli danych w Winform, ponieważ mam również datagridview?

Tak, wezwałbym IConfigurationView.SetTableData(...)to. Sformatowanie podanych danych zależy od widoku. Prezenter po prostu przestrzega umowy widoku, że chce danych tabelarycznych.

3. Moja aplikacja ma kilka klas modeli z tym samym zestawem. Obsługuje również architekturę wtyczek z wtyczkami, które muszą być ładowane podczas uruchamiania. Czy widok po prostu wywołałby metodę prezentera, która z kolei wywołałaby metodę ładującą wtyczki i wyświetlającą informacje w widoku? Która warstwa będzie wtedy kontrolowała odwołania do wtyczek. Czy widok zawiera odniesienia do nich czy do prezentera?

Jeśli wtyczki są powiązane z widokami, widoki powinny o nich wiedzieć, ale nie prezenter. Jeśli dotyczą danych i modelu, widok nie powinien mieć z nimi nic wspólnego.

4. Czy mam rację sądząc, że widok powinien obsługiwać każdą rzecz dotyczącą prezentacji, od koloru węzła drzewa, po rozmiar datagrid, itp.?

Tak. Pomyśl o tym jak o prezenterce dostarczającej XML opisujący dane i widok, który pobiera dane i stosuje do nich arkusz stylów CSS. Mówiąc konkretnie, prezenter może zadzwonić, IRoadMapView.SetRoadCondition(RoadCondition.Slippery)a widok renderuje drogę w kolorze czerwonym.

A co z danymi dla klikniętych węzłów?

5. Jeśli po kliknięciu treenodes powinienem przejść przez określony węzeł do prezentera, a następnie na tej podstawie prezenter powinien dowiedzieć się, jakich danych potrzebuje, a następnie poprosić model o te dane, zanim zaprezentuje je z powrotem w widoku?

Jeśli to możliwe, przekazałbym wszystkie dane potrzebne do przedstawienia drzewa w widoku w jednym ujęciu. Ale jeśli niektóre dane są zbyt duże, aby można je było przekazać od początku lub jeśli mają dynamiczny charakter i wymagają „najnowszej migawki” z modelu (przez prezentera), to dodałbym coś podobnego event LoadNodeDetailsEventHandler LoadNodeDetailsdo interfejsu widoku, aby prezenter może go zasubskrybować, pobrać szczegóły węzła w LoadNodeDetailsEventArgs.Node(prawdopodobnie za pośrednictwem pewnego rodzaju identyfikatora) z modelu, dzięki czemu widok może zaktualizować pokazane szczegóły węzła, gdy delegat programu obsługi zdarzeń powróci. Zwróć uwagę, że wzorce asynchroniczne mogą być potrzebne, jeśli pobieranie danych może być zbyt wolne, aby zapewnić dobre wrażenia użytkownika.

Johann Gerell
źródło
3
Nie sądzę, że trzeba koniecznie oddzielać widok od prezentera. Zwykle oddzielam model od prezentera, każąc prezenterowi słuchać wydarzeń modelu i odpowiednio działać (aktualizuję widok). Obecność prezentera w widoku ułatwia komunikację między widokiem a prezenterem.
kasperhj
11
@lejon: Mówisz, że obecność prezentera w widoku ułatwia komunikację między widokiem a prezenterem , ale zdecydowanie się z tym nie zgadzam. Mój punkt widzenia jest następujący: kiedy widok wie o prezenterze, to dla każdego zdarzenia widoku widok musi zdecydować, która metoda prezentera jest właściwa do wywołania. To „2 punkty złożoności”, ponieważ widok tak naprawdę nie wie, które zdarzenie widoku odpowiada której metodzie prezentera . Umowa tego nie określa.
Johann Gerell,
5
@lejon: Jeśli z drugiej strony widok odsłania tylko rzeczywiste wydarzenie, wówczas sam prezenter (który wie, co chce zrobić, gdy wystąpi zdarzenie widoku) po prostu subskrybuje go, aby zrobić właściwą rzecz. To tylko „1 punkt złożoności”, który w mojej książce jest dwa razy lepszy niż „2 punkty złożoności”. Ogólnie rzecz biorąc, mniej sprzężeń oznacza mniejsze koszty utrzymania w trakcie trwania projektu.
Johann Gerell,
9
Ja również mam tendencję do używania hermetycznego prezentera, jak wyjaśniono w tym linku lostechies.com/derekgreer/2008/11/23/ ... w którym widok jest jedynym posiadaczem prezentera.
ak3nat0n
3
@ ak3nat0n: W odniesieniu do trzech stylów MVP opisanych w linku, który podałeś, uważam, że ta odpowiedź Johanna może być najbardziej zbliżona do trzeciego stylu, który nazywa się Obserwujący styl prezentera : „Zaletą stylu obserwującego prezentera jest to, że całkowicie oddziela wiedzę prezentera od widoku, dzięki czemu widok jest mniej podatny na zmiany w obrębie prezentera ”.
DavidRR
11

Prezenter, który zawiera całą logikę w widoku, powinien zareagować na kliknięcie przycisku, jak mówi @JochemKempe . W praktyce wywołuje funkcję obsługi zdarzenia kliknięcia przycisku presenter.OpenFile(). Prezenter może wtedy określić, co należy zrobić.

Jeśli zdecyduje, że użytkownik musi wybrać plik, wywołuje z powrotem do widoku (za pośrednictwem interfejsu widoku) i pozwala widokowi, który zawiera wszystkie szczegóły techniczne interfejsu użytkownika, wyświetlić plik OpenFileDialog. Jest to bardzo ważne rozróżnienie, ponieważ prezenter nie powinien mieć możliwości wykonywania operacji związanych z używaną technologią UI.

Wybrany plik zostanie następnie zwrócony prezenterowi, który kontynuuje swoją logikę. Może to dotyczyć dowolnego modelu lub usługi, które powinny obsługiwać przetwarzanie pliku.

Głównym powodem stosowania wzorca MVP, imo, jest oddzielenie technologii interfejsu użytkownika od logiki widoku. W ten sposób prezenter organizuje całą logikę, podczas gdy widok oddziela ją od logiki interfejsu użytkownika. Ma to bardzo fajny efekt uboczny polegający na tym, że prezenter jest w pełni testowalny jednostkowo.

Aktualizacja: ponieważ prezenter jest ucieleśnieniem logiki znajdującej się w jednym określonym widoku , relacja widok-prezenter jest relacją IMO jeden do jednego. Ze wszystkich praktycznych powodów jedna instancja widoku (powiedzmy Forma) współdziała z jedną instancją prezentera, a jedna instancja prezentera tylko z jedną instancją widoku.

To powiedziawszy, w mojej implementacji MVP z WinForms prezenter zawsze wchodzi w interakcję z widokiem poprzez interfejs reprezentujący możliwości interfejsu użytkownika widoku. Nie ma ograniczeń co do tego, który widok implementuje ten interfejs, dlatego różne „widżety” mogą implementować ten sam interfejs widoku i ponownie wykorzystywać klasę prezentera.

Peter Lillevold
źródło
Dzięki. Czyli w metodzie presenter.OpenFile () nie powinien zawierać kodu do wyświetlania okna dialogowego openfiled? Zamiast tego powinien wrócić do widoku, aby pokazać to okno?
Darren Young,
4
Racja, nigdy nie pozwoliłbym prezenterowi bezpośrednio otwierać okien dialogowych, ponieważ to zepsułoby twoje testy. Albo prześlij to do widoku, albo, jak to zrobiłem w niektórych scenariuszach, użyj oddzielnej klasy „FileOpenService”, która zajmie się rzeczywistą interakcją w oknie dialogowym. W ten sposób możesz sfałszować usługę otwierania plików podczas testów. Umieszczenie takiego kodu w osobnej usłudze może dać fajne efekty uboczne
ponownego użycia
2

Prezenter powinien działać na końcu żądania, pokazując otwarte okno dialogowe, zgodnie z sugestią. Ponieważ od modelu nie są wymagane żadne dane, prezenter może i powinien obsłużyć żądanie.

Załóżmy, że potrzebujesz danych do utworzenia niektórych encji w modelu. Możesz albo przekazać koryto strumienia do warstwy dostępu, w której masz metodę tworzenia jednostek ze strumienia, ale sugeruję, abyś zajął się analizowaniem pliku w swoim prezenterze i użył konstruktora lub metody Create na jednostkę w swoim modelu.

JochemKempe
źródło
1
Dzięki za odpowiedzi. Czy miałbyś też jednego prezentera do widoku? A ten prezenter albo obsługuje żądanie, albo jeśli wymagane są dane, deleguje do dowolnej liczby klas modeli, które działają na określone żądania? Czy to właściwy sposób? Dzięki jeszcze raz.
Darren Young,
3
Widok ma jednego prezentera, ale prezenter może mieć wiele widoków.
JochemKempe