Winforms wzorzec MVP - moje boje

0

Witam

To moje początki z wzorcem Model-View-Presenter. Napisałem malutką aplikację, która jak mi się wydaje
korzysta właśnie z tego wzorca. Chciałbym abyście rzucili okiem i ocenili, czy wszystko jest zgodnie ze sztuką.

Aplikacja pobiera dane z datatable i wyświetla je w datagridview.

Chciałbym otrzymać odpowiedzi na poniższe pytania:

  • czy logika znajduje się w odpowiednich warstwach
  • czy w solution explorer panuje odpowiedni ład i porządek
  • czy dobrze zrealizowany jest dostęp do kontrolek View z poziomu Presentera

Załączam solucję (VS2010) Update: Złe rozwiązanie

Piszę w VS2010Express i czasami podczas robienia builda wyrzuca mi kupę błędów,
ale nie ma z tego powodu żadnych problemów (np. jakaś klasa "does not contain a definition for" dla jakiegoś pola)
Z czym to jest związane?

1

Masz rację - wydaje Ci się. ;) Ale dobrze, że pytasz.

  1. Model, View i Preseneter masz w jednym projekcie, do drugiego wydzieliłeś jedynie interfejsy. Nic to nie zmienia, bałagan jest wszędzie.
    Sugeruję zrobić oddzielne projekty dla każdej z warstw (Model i Preseneter jako dll library, View jako WinForms application). Do tego w dllce z P należy zrobić dwa katalogi: Interfaces i Presenters. (W większej aplikacji należałoby rozbić to na dwa projekty.)
  2. Presenter NIE WIE nic o GUI, nie używa żadnych klas specyficznych dla niego klas. Nie możesz zatem operować na czymś takim jak DataGridView. Ogólna idea MVP jest taka, że w dowolnym momencie zmieniasz sobie V, a reszta działa bez żadnych zmian. Ty tego teraz nie zrobisz, nie przerobisz aplikacji na wersję konsolową czy webową, bo wszędzie korzystasz ze specyficznych dla WinFormsów klas.

Temat bardziej pasuje do Inżynierii Oprogramowania niż tutaj.

0

Muszę zadać pytania pomocnicze.
Po dodaniu 3 projektów do solucji (1 - View - window forms, 2 - Model -class library, 3 - Presenter -class libary)
każdy z tych projektów ma inną przestrzeń namespaces. Czy zmienić tak, żeby były w tej samej, czy dodać referencje do pozostałych
(tu miałem jakieś problemy z project dependecies).
A może miałbyś chwilę i utworzył szkielet poprawnie zbudowanego projektu?

Odnośnie drugiej Twojej odpowiedzi - jak na przykład pobrać aktualną pozycję z datagridview i wartość z komórki, nie odwołując się do
kontrolki?

0

Jeśli chodzi o przestrzenie nazw, to jeden katalog = jedna namespace. Wszystkie klasy w jednym katalogu mają mieć tę samą namespace, do tego zgodną z nazwą katalogu - dzięki temu wszystko jest czytelne. U mnie P siedzi w dllce o nazwie AppLogic, mam w tym namespace Interfaces oraz Presenters, a ponadto dzielę to jeszcze wg funkcjonalności aplikacji. Np. jeśli jest to program do obsługi sprzedaży samochodów, to będę miał np. AppLogic.Interfaces.Cars, tam np. ICarsListView, ICarEditorView oraz AppLogic.Interfaces.Customers, tam ICustomersListView oraz ICustomerEditorView. Analogiczny podział w AppLogic.Presenters: Cars z CarsListPresenter i CarEditorPrestenter oraz Customers z CustomersListPresenter i CustomerEditorPresenter.

Co do referencji - jeśli jest wiele projektów w solucji, to i tak musisz mieć odpowiednio ustawione referencje. W tym przypadku V ma referencje do P i M, a P ma referencję do M. M nie ma referencji do niczego.

Do kontrolki odwołujesz się jedynie w kodzie View. Dlatego takie rzeczy jak aktualnie zaznaczony element DataGridView musisz udostępnić jako właściwość odpowiadającego interfejsu (indeks czy tam cały obiekt), tak aby prezenter mógł na niej operować.

Poza tym - co ten Twój program ma robić i co tak właściwie masz w Modelu?

0
somekind napisał(a)

Jeśli chodzi o przestrzenie nazw, to jeden katalog = jedna namespace. Wszystkie klasy w jednym katalogu mają mieć tę samą namespace, do tego zgodną z nazwą katalogu - dzięki temu wszystko jest czytelne. U mnie P siedzi w dllce o nazwie AppLogic, mam w tym namespace Interfaces oraz Presenters, a ponadto dzielę to jeszcze wg funkcjonalności aplikacji. Np. jeśli jest to program do obsługi sprzedaży samochodów, to będę miał np. AppLogic.Interfaces.Cars, tam np. ICarsListView, ICarEditorView oraz AppLogic.Interfaces.Customers, tam ICustomersListView oraz ICustomerEditorView. Analogiczny podział w AppLogic.Presenters: Cars z CarsListPresenter i CarEditorPrestenter oraz Customers z CustomersListPresenter i CustomerEditorPresenter.

A może poświęciłbyś chwilę i utworzył przykładowy projekt z podziałem o jakim mówisz?
Gotowe rozwiązanie łatwiej jest przyswoić. Nie chcę sobie wyrabiać złych nawyków od początku.

somekind napisał(a)

Co do referencji - jeśli jest wiele projektów w solucji, to i tak musisz mieć odpowiednio ustawione referencje. W tym przypadku V ma referencje do P i M, a P ma referencję do M. M nie ma referencji do niczego.

Rozumiem, że tu chodzi o MVP odmiana Supervising Controller? W Pasive View, view chyba nie ma kontaktu z modelem.

somekind napisał(a)

Poza tym - co ten Twój program ma robić i co tak właściwie masz w Modelu?

To będzie program do katalogowania książek oparty na bazie sqlite.
W modelu będzie wszystko dotyczące odczytu i zapisu danych.
Jaki model MVP SC czy PV wybrać w tym konkretnym przypadku?

0
somekind napisał(a)

Co do referencji - jeśli jest wiele projektów w solucji, to i tak musisz mieć odpowiednio ustawione referencje. W tym przypadku V ma referencje do P i M, a P ma referencję do M. M nie ma referencji do niczego.

Właśnie natknąłem się na problem. Do modelu MyModel, który jest w osobnym projekcie, chciałbym zastosować interfejs IMyModel,
ale jeśli umieszczę go razem z innymi interfejsami w AppLogic, nie będzie dostępny. Można umieścić go w projekcie Model?

1

Wydaje mi się, że powinieneś zacząć od utworzenia Modelu - najpierw opracować sobie jakie klasy z jakimi atrybutami będziesz przechowywał w aplikacji, potem dopiero zajął się Presenterem, a na końcu View (mając gotowy Model będzie to fajne, bo użyjesz sobie BindingSource'ów powiązanych ze swoimi klasami Modelu i wygenerujesz automatycznie prawie gotowe formatki).

W załączniku jest przykładowy projekcik. Jest to jakieś rozwiązanie, nie twierdzę, że najlepsze na świecie, ogólnie można zrobić to inaczej. Jak już sam wiesz, wariacji MVP trochę jest - ja się szczegółowo na nich nie znam, bo to taki trochę fetyszyzm teoretyczny. ;)
W każdym razie u mnie View zna Model (inaczej nie mógłby ich wyświetlić ani edytować) i View ma referencję na swojego Presentera.

Do tego dodałem dodatkowy projekt - DataAccess służący od obsługi przechowywania klas z Modelu gdzieś tam - u mnie w pamięci, Ty to jakoś do bazy danych zapiszesz.

0
somekind napisał(a)

W załączniku jest przykładowy projekcik.

Nie wszystko jest dla mnie jasne, więc zapytam:

  • do czego służy właściwość, w której jest instancja MainForm
  • dlaczego w Winclient zastosowałeś namespace "SZP.WinClient"? (SZP to jakiś skrót?)
  • dlaczego w interfejsie IDataRepository zastosowałeś IList, a nie List? (nie wiem czy to jest dobre pytanie)

Ogólnie ten przykład zapewni początkującemu zajęcie na długie zimowe wieczory :)

0
  1. To jest zwykły singleton - dba o to, by w całej aplikacji była tylko jedna instancja MainForm.
  2. Tak, SZP to skrót. ;) Kod przeklejałem ze swojego projektu, który tak się nazywa i nie zapomniałem tego usunąć. Ogólnie zazwyczaj projekty w solucjach nazywa się NazwaSolucji.NazwaProjektu, zaś NazwaSolucji to NazwaFirmy.NazwaAplikacji. Np. XYZ.SuperApp.WinClient, XYZ.SuperApp.DataAccess, itd. Ja nie mam firmy, więc u mnie było samo SZP, ale to skasuj u siebie.
  3. Bo interfejsy są dobre, a klasy są złe. :) A konkretniej - dzięki temu, implementacja metody GetAll będzie mogła zwrócić zarówno List, jak i dowolną inną klasę implementującą ten interfejs, jeśli zajdzie taka potrzeba. Np. gdy zechcesz skorzystać z jakiejś biblioteki do bazy danych, która nie korzysta z klasy List, tylko zwraca jakieś swoje obiekty IList, ukryjesz to w swoim Repository, a reszta aplikacji pozostanie bez zmian.
    W zasadzie, chyba lepiej by było zmienić to IList na ICollection, bo i tak nigdzie nie są używane metody ani właściwości z IList.
0

Mam dwa kolejne pytania:

  • do czego wykorzytujesz zmienną public new bool DesignMode?
  • dlaczego w AuthorsRepository lista autorów ma modyfikator private static?
0

Pytanie przeniesione.

0
  1. To nie zawsze jest potrzebne, ale to jest obejście jakiegoś dość częstego błędu z designerem formatek. Bez tego próbuje on wykonywać jakieś operacje oprócz tworzenia kontrolek, przez co potrafi się wywalić. Ale w tak prostym przykładzie nie jest to raczej potrzebne (na razie).
  2. Normalnie metody z klasy Repository odwołują się do bazy danych/pliku, więc tej listy nie powinno być. Ona tylko udaje źródło danych, a jest static dlatego, że dane do niej wprowadzone/zmienione zostaną w pamięci przez cały czas pracy programu. Gdyby nie to, nie zauważyłbyś żadnej zmiany po dodaniu nowej książki, bo wszystko by się odświeżyło do pierwotnej postaci.
0

Stanąłem jeszcze przed jednym, ale zasadniczym problemem - jak ma wyglądać mój model.

Baza zawiera spis książek, każdą opisuje ok 30 różnych cech: autor, ilość stron, cena, itd.
Wymyśliłem tak: na starcie ładuję cała bazę do dataset (później nie będę już nic odczytywać z dysku).
Na zawartości tabeli (dataset.table) operuje za pomocą widoku (dodawanie, usuwanie, sortowanie, filtrowanie itd).
Bazę na dysku uaktualniam dopiero, kiedy użytkownik zaakceptuje zmiany, a nie od razu po każdej modyfikacji.
Pozycje do zapisania uzyskuję za pomocą metody GetChanges z dataset i przy użyciu transakcji zapisuję do bazy,
bo użycie adaptera jest bardzo powolne.
Nowa pozycję tworzę przy pomocy metody dataview.Table.NewRow().
Dane są prezentowanie na zmodyfikowanym datagridview (wyświetla tylko 1 pozycję, nazwy kolumn ułożone pionowo)
We wszytko zamieszana jest jeszcze kontrolka treeview, która wybiera wiersz z dataview.

A jeśli chciałbym załadować te dane do listy List<Book>,
czy z ustawieniem odpowiednich właściwości klasy Book wartościami z bazy będzie dużo pracy?
Każda kolumna w bazie ma nazwę, jak podstawić wartości z tych kolumn do odpowiadającym im nazwami właściwości klasy?
Jak inicjować nową instancje klasy jeśli ma on ze 30 pól?
new Book() {pole1=, pole2=, ...., pole30=} to chyba za dużo roboty.
Czy lepiej może zostać przy metodzie z wykorzystaniem widoku?

0

DataSety nie mają nic wspólnego z programowaniem obiektowym. Ja z własnej woli ich nie używam, moim zdaniem są gównem totalnym. Nie mam nawet pomysłu, jak mogłyby współpracować z MVP, więc Ci nie pomogę.

shifttab napisał(a)

A jeśli chciałbym załadować te dane do listy List<Book>,
czy z ustawieniem odpowiednich właściwości klasy Book wartościami z bazy będzie dużo pracy?
Każda kolumna w bazie ma nazwę, jak podstawić wartości z tych kolumn do odpowiadającym im nazwami właściwości klasy?

Do tego służy ORM. Ja najczęściej używam LINQ To SQL, ale to działa z bazami MSSQL. Chociaż ponoć jest też coś współpracującego z SQLite: http://code.google.com/p/dblinq2007/ Najbardziej rozbudowany jest NHibernate, też można w nim obsługiwać SQLite: http://devlicio.us/blogs/krzysztof_kozmic/archive/2009/08/14/testing-with-nhibernate-and-sqlite.aspx
Ja uważam, że lepiej mieć obiektowy model i użyć gotowego rozwiązania do obsługi źródła danych niż stosować archaiczne i niezbyt elastyczne technologie typu DataSety.

Jak inicjować nową instancje klasy jeśli ma on ze 30 pól?
new Book() {pole1=, pole2=, ...., pole30=} to chyba za dużo roboty.
Czy lepiej może zostać przy metodzie z wykorzystaniem widoku?

Ja nowe obiekty wprowadzałbym przez odpowiednie kontrolki formularza, które byłyby zbindowane z obiektem z BindingSource i wszystko działoby się samo - tak zresztą jak w przykładzie. Podobny mechanizm można zastosować jeśli dane wprowadzasz jako nowy wiersz w gridzie, a nie w oddzielnym okienku. Generalnie odpowiednio podpięte do kontrolek BindingSource automatycznie przepisze wartości z kontrolek do nowego obiektu.

0

Zdecydowałem się na dblinq. Mam klasę MyDbDataContext dziedziczącą po DataContext i (tylko raz na starcie) za pomocą metody GetTable<>
i zapytania linq, pobieram wszystkie rekordy z bazy sqlite. Dostaję dane typu IQueryable<Book> i ładuję je do listy<Book>.
Bindingsource wskazuje na encję Book i jest podłączony do gridview (datasource to otrzymana lista<Book>)
i wszystko jak do tej pory działa poprawnie.

Mam jednak problem do rozwiązania:

  • Chcę zapisać zmiany na dysk kiedy użytkownik tego sobie zażyczy.
    Jak zaktualizować dane w IQueryable<Book> (takie jak dodawanie, usuwanie elementów)?
    W MyDbDataContext mam metodę Submitchanges, i mógłbym to zapisać do bazy.
    Jeśli usuwam coś z listy w IQueryable<Book> nic o tym nie wiedzą.

Mam małe doświadczenie. Proszę o wskazówki.

0

Można nie trzymać pobranych z bazy danych w liście w pamięci, tylko to, co się pobierze od razu wyświetlić w DataGridView. Potem, gdy coś zostanie zmodyfikowane, dodane lub usunięte, to od razu robi się SubmitChanges, i od nowa wczytuje z bazy. W tym rozwiązaniu DataContext istnieje lokalnie w poszczególnych metodach służących do operowania na danych.

Można tez mieć jeden DataContext na czas całej pracy, na nim wykonać szereg różnych operacji - które zostaną wykonane w pamięci, a SubmitChanges zawołać na końcu.

Tak czy siak nie ma chyba po co robić jakiejś kopii danych do innej kolekcji. Wszystko masz w Contexcie.

0

Natknąłem się chyba na problem nie do przeskoczenia.
Założenie: użytkownik sam ma decydować kiedy i czy zostaną zapisane na dysku.

Używam tabeli z datacontextu. Dodaję nową pozycję za pomocą InsertOnSubmit. Niestety takie informacje jak nowy rozmiar tabeli po
dodaniu tej pozycji, czy choćby dostęp do tej pozycji, jest dopiero po wywołaniu SubmitChanges (zapis na dysk), co kłóci się z moim założeniem.
Niezapisane zmiany dostępne są poprzez GetChangeSet, ale tam są tylko zmiany, a ja chcę operować na całośći. Taką funkcjonalność zapewnia (ble!) dataset.

Czy bez dodatkowej kopii danych jak lista czy coś tam innego, obejdzie się?

0

Ostatecznie postawiłem na typowany dataset i bazę sql ce (chcę operować na bazie w pamięci)
Pomogło mi to rozwiązanie: http://codeguru.pl/frmThread.aspx?id=488571#488571

Ale do rzeczy: mam pytanie związane z oddzieleniem logiki od warstwy prezentacji we wzorcu model-view-presenter.

Jeśli dane wyświetlam w datagridview (dgv), (zapełniam go ręcznie, bez używanie datasource, bo potrzebuję nietypowego zachowania tej kontrolki)
to czy logika dotycząca obsługi dgv ma znajdować się w widoku czy prezenterze?

Prezenter ma niby nic nie wiedzieć na jakich kontrolkach operuje, więc czy trzeba mu dostarczyć tych informacji za pomocą właściwości?
Byłoby ich dużo, bo potrzebne są np. ilość wierszy dgv, aktualna pozycja, dostęp do wyglądu komórek, koloru tesktu itd.

I jeszcze jedno pytanie odnośnie słowa this.
Czy odwołując się do elementów klasy (np. kontrolek) zawsze powinno używać się tego słowa czy można pominać?
Oczywiście są sytuacje kiedy jest wymagane, ale mi chodzi o ogólną zasadę.

0
shifttab napisał(a)

Prezenter ma niby nic nie wiedzieć na jakich kontrolkach operuje, więc czy trzeba mu dostarczyć tych informacji za pomocą właściwości?
Byłoby ich dużo, bo potrzebne są np. ilość wierszy dgv, aktualna pozycja, dostęp do wyglądu komórek, koloru tesktu itd.

Chyba tak...

I jeszcze jedno pytanie odnośnie słowa this.
Czy odwołując się do elementów klasy (np. kontrolek) zawsze powinno używać się tego słowa czy można pominać?
Oczywiście są sytuacje kiedy jest wymagane, ale mi chodzi o ogólną zasadę.

Wymagane jest jedynie, gdy w danym scopie są widoczne dwa obiekty o tej samej nazwie, jeden jest elementem klasy, a drugi jest lokalny.
Ja używam zawsze, dla każdego pola, metody i właściwości. Dzięki temu od razu widać, że jest to coś, co należy do klasy, a nie zmienna lokalna, stała czy statyczna. Czytelność przede wszystkim.

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