Eötvös Loránd Tudományegyetem Informatikai Kar Komponens alapú szoftverfejlesztés 9. előadás Grafikus felületű alkalmazások architektúrái Giachetta Roberto groberto@inf.elte.hu http://people.inf.elte.hu/groberto
A grafikus felületű alkalmazás A grafikus felületű (graphical user interface) alkalmazások jelentős részét képezik a mai szoftvereknek közvetlenül a felhasználóval állnak kapcsolatban, aki számára információkat jelenítenek meg, és feldolgozzák a felhasználói bemenetet, amihez eseményvezérlést használnak általánosan megfogalmazott grafikus elemekből (vezérlőkből) alkalmazás specifikus specifikus űrlapokat állítanak össze Grafikus felületű alkalmazások esetén a leggyakoribb felépítés a háromrétegű (3-tier) architektúra, amelyben elkülönül a nézet, a modell és a perzisztencia a nézet tartalmazza az adatok megjelenítésének módját, valamint a felhasználói interakció feldolgozását ELTE IK, Komponens alapú szoftverfejlesztés 9:2
Adatok állapotai A háromrétegű alkalmazásokban az adatok három állapotban jelennek meg megjelenített állapot (display state): a felhasználói felületen megjelenő tartalomként, amelyet a felhasználó módosíthat munkafolyamat állapot (session state): a memóriában, amely mindaddig elérhető, amíg a program és felhasználója aktív rekord állapot (record state): az adat hosszú távon megőrzött állapota az adattárban (perzisztenciában) display state session state record state ELTE IK, Komponens alapú szoftverfejlesztés 9:3
Adatok állapotai Az állapotok külön általában reprezentációt igényelnek, ezért az állapotok között transzformációkat kell végeznünk pl. objektumok közötti leképezés (object-object mapping), objektum-relációs leképezés (object-relational mapping) Az egyes állapotok kezelése történhet szinkron módon: a két állapot mindig megegyezik a megjelenített és munkafolyamat állapotnak célszerű megegyeznie, mivel a munkafolyamat állapoton hajtjuk végre a tevékenységeket aszinkron módon: a két állapot általában különbözik, de adott ponton szinkronizálható ELTE IK, Komponens alapú szoftverfejlesztés 9:4
Háromrétegű architektúra specializációja A háromrétegű architektúrában a felhasználóval a nézet réteg tartja a kapcsolatot, amely ezért tartalmazza a felületi logikát (UI logic) a felületi logika feladatai: felhasználói interakció fogadása, feldolgozása, és továbbítása az üzleti logika számára az adatok megjelenítése (a megjelentéshez szükséges konverzióval) a megfelelő nézet előállítása, új nézetek létrehozása a felületi logika és maga a megjelenítés két jól elhatárolható feladatkör, amelyet célszerű szeparálni a megjelenítés elsősorban nem programozói, hanem tervezői feladatkör ELTE IK, Komponens alapú szoftverfejlesztés 9:5
MVP architektúra A Model-View-Presenter (MVP) architektúra lehetőséget ad a felületi logika leválasztására egy prezentáció (presenter) számára a nézet felel az adatok megjelenítéséért (nézetállapot előállításáért), valamint a felhasználó interakció fogadásáért, és továbbításáért a prezentáció számára a prezentáció tartalmazza a felhasználói interakció feldolgozásáért felelős tevékenységeket, úgymint továbbítja a kéréseket az üzleti logika számára megadja az interakcióra válaszoló nézetet a prezentáció ismeri a nézetet, a nézet azonban nem ismeri a prezentációt ELTE IK, Komponens alapú szoftverfejlesztés 9:6
MVP architektúra MVP felhasználó nézet jelzés frissítés végrehajtás prezentáció modell ELTE IK, Komponens alapú szoftverfejlesztés 9:7
MVP architektúra (supervising controller) Az MVP architektúra két esete különböztethető meg: Supervising contoller (SC): a nézet ismeri a modellt (le tudja kérni a munkafolyamat állapotot), és el tud végezni alapvető tevékenységeket View + UpdateViewState() :void - Convert(SessionState) :DisplayState «event» + ComplexEvent() «event handler» - HandleViewEvent() Presenter «event handler» - HandleComplexEvent() 1..* 1..* Model ELTE IK, Komponens alapú szoftverfejlesztés 9:8
MVP architektúra (supervising controller) user View Presenter Model Click() ViewEvent() UpdateViewState() HandleEvent() GetState() :SessionState Convert(SessionState) : DisplayState ELTE IK, Komponens alapú szoftverfejlesztés 9:9
MVP architektúra (passive view) Passive View (PV): a nézet nem ismeri a modellt, így a prezentáció adja át a nézet számára a munkafolyamat állapotot szinte minden tevékenységet a prezentáció végez a nézet elsősorban grafikus vezérlők halmazának tekinthető View + UpdateViewState(SessionState) - Convert(SessionState) :DisplayState «event» + ViewEvent() Presenter - HandleViewEvent() 1..* Model ELTE IK, Komponens alapú szoftverfejlesztés 9:10
MVP architektúra (passive view) user View Presenter Model Click() ViewEvent() HandleEvent() GetState() :SessionState alt [requires new view] View UpdateViewState(SessionState) Convert(SessionState) : DisplayState UpdateViewState(SessionState) Convert(SessionState) : DisplayState ELTE IK, Komponens alapú szoftverfejlesztés 9:11
MVP architektúra Előnyei: a felületi logika leválasztható a nézetről, így a nézet ténylegesen nem lép interakcióba az üzleti logikával a prezentáció a nézettől függetlenül tudja biztosítani új nézetek létrehozását és a munkafolyamat állapot manipulálását a nézet független a prezentáció és a modell felületétől, így tetszőlegesen változtatható a prezentáció és a nézet egymástól függetlenül tesztelhetőek Hátrányai: a nézet továbbra is tartalmaz kódot (eseménykezelők formájában) a megjelenített állapot frissítése manuálisan történik ELTE IK, Komponens alapú szoftverfejlesztés 9:12
Példa Feladat: Készítsünk egy táblajáték programot MVP SC architektúrában, amelyben két játékos küzdhet egymás ellen. a korábbi nézet tevékenységeit felbontjuk a nézet (IBoardGameView) és a prezentáció (BoardGamePresenter) között a nézet interfésze egységes eseményeket definiál a játék tevékenységeire (GameStarted, GameStepped, ), valamint megadja a frissítés lehetőségét (UpdateViewState) a nézet megvalósítása (DrawingForm) felel a tényleges események átalakításáért játékbeli eseményekké a prezentáció kezeli a tevekénységeket, valamint a modell összetett eseményeit (GameWon, GameOver) ELTE IK, Komponens alapú szoftverfejlesztés 9:13
Példa Tervezés: «interface» View::IBoardGameView + UpdateViewState() :void «event» + GameStarted() :EventHandler<EventArgs> + GameStepped() :EventHandler<BoardEventArgs> + SaveGame() :EventHandler + LoadGame() :EventHandler «property» + Model() :BoardGameModel -view Presenter::BoardGamePresenter - model :BoardGameModel - view :IBoardGameView - savefiledialog :SaveFileDialog - openfiledialog :OpenFileDialog + BoardGamePresenter(BoardGameModel) - Model_GameWon(object, GameWonEventArgs) :void - Model_GameOver(object, EventArgs) :void - View_GameStarted(object, EventArgs) :void - View_GameStepped(object, BoardEventArgs) :void - View_LoadGame(object, EventArgs) :void - View_SaveGame(object, EventArgs) :void «property» + View() :IBoardGameView - model :BoardGameModel View::DrawingForm Form + DrawingForm() + UpdateViewState() :void - Panel_Paint(object, PaintEventArgs) :void - Panel_MouseDown(object, MouseEventArgs) :void - TicTacToeForm_Resize(object, EventArgs) :void - TicTacToeForm_KeyDown(object, KeyEventArgs) :void - Model_FieldChanged(object, FieldChangedEventArgs) :void + OnGameStarted() :void + OnGameStepped(Int32, Int32) :void + OnSaveGame() :void + OnLoadGame() :void «event» + GameStarted() :EventHandler<EventArgs> + GameStepped() :EventHandler<BoardEventArgs> + SaveGame() :EventHandler + LoadGame() :EventHandler «property» + Model() :BoardGameModel ELTE IK, Komponens alapú szoftverfejlesztés 9:14
Példa Feladat: Készítsünk egy táblajáték programot MVP PV architektúrában, amelyben két játékos küzdhet egymás ellen. a nézet (IBoardGameView) nem látja közvetlenül a modellt, így a prezentáció (BoardGamePresenter) kéri le a munkafolyamat állapotát (GetSessionState), és adja át a nézet számára a prezentáció veszi át a megmaradt modellbeli események kezelését a nézettől (Model_FieldChanged) ELTE IK, Komponens alapú szoftverfejlesztés 9:15
Példa Tervezés: Presenter::BoardGamePresenter - model :BoardGameModel - view :IBoardGameView - savefiledialog :SaveFileDialog - openfiledialog :OpenFileDialog + BoardGamePresenter(BoardGameModel) - Model_GameWon(object, GameWonEventArgs) :void - Model_GameOver(object, EventArgs) :void - Model_FieldChanged(object, FieldChangedEventArgs) :void - View_GameStarted(object, EventArgs) :void - View_GameStepped(object, BoardEventArgs) :void - View_LoadGame(object, EventArgs) :void - View_SaveGame(object, EventArgs) :void - GetSessionState() :PlayerData[] «property» + View() :IBoardGameView «interface» View::IBoardGameView + UpdateViewState(PlayerData[,]) :void «event» + GameStarted() :EventHandler<EventArgs> + GameStepped() :EventHandler<BoardEventArgs> + SaveGame() :EventHandler + LoadGame() :EventHandler -view View::DrawingForm - sessionstate :PlayerData ([,]) - boardwidth :Int32 - boardheight :Int32 Form + DrawingForm() + UpdateViewState(PlayerData[,]) :void - Panel_Paint(object, PaintEventArgs) :void - Panel_MouseDown(object, MouseEventArgs) :void - TicTacToeForm_Resize(object, EventArgs) :void - TicTacToeForm_KeyDown(object, KeyEventArgs) :void + OnGameStarted() :void + OnGameStepped(Int32, Int32) :void + OnSaveGame() :void + OnLoadGame() :void «event» + GameStarted() :EventHandler<EventArgs> + GameStepped() :EventHandler<BoardEventArgs> + SaveGame() :EventHandler + LoadGame() :EventHandler ELTE IK, Komponens alapú szoftverfejlesztés 9:16
MVC architektúra A Model-View-Controller (MVC) architektúra egy külön vezérlést (contoller) biztosít, amely kiszolgálja a felhasználói kéréseket a vezérlő fogadja közvetlenül a kérést a felhasználótól, feldolgozza azt (a modell segítségével), majd előállítja a megfelelő (új) nézetet így a nézet mentesül a kérések fogadásától, átalakításától általában egy vezérlőhöz több nézet is tartozik a nézet a felület (jórészt deklaratív) meghatározása, amely megjeleníti, szükség esetén átalakítja az adatokat elsősorban webes környezetben népszerű, mivel könnyen leválasztható a nézet (HTML) a vezérléstől (URL) ELTE IK, Komponens alapú szoftverfejlesztés 9:17
MVC architektúra MVC felhasználó nézet tevékenység frissítés végrehajtás vezérlő modell ELTE IK, Komponens alapú szoftverfejlesztés 9:18
MVC architektúra Az MVC architektúra két esete különböztethető meg: lehívás alapú (pull-based): ismeri a modellt, az adatokat a modelltől kéri le betöltés alapú (push-based): a vezérlő adja át a nézetnek a megjelenítendő adatokat (ez a nézet modellje, vagy view model) Előnye: a nézetnek nem kell foglalkoznia az események feldolgozásával, mivel azokat közvetlenül a vezérlő kapja meg Hátránya: a nézet továbbra is felel az adatok megfelelő átalakításáért, tehát tartalmaz valamennyi logikát ELTE IK, Komponens alapú szoftverfejlesztés 9:19
MVC architektúra (betöltés alapú) Controller Model user Event() HandleEvent() GetState() :SessionState View UpdateViewState(SessionState) Convert(SessionState) : DisplayState ELTE IK, Komponens alapú szoftverfejlesztés 9:20
Példa Feladat: Készítsünk egy táblajáték programot MVC architektúrában, amelyben két játékos küzdhet egymás ellen. a vezérlő (BoardGameController) látja a modellt, illetve létrehozza a nézetet, és kezeli a nézet eseményeit (billentyűzet lenyomás, átméretezés, táblakattintás) a nézet interfészét kibővítjük a táblával kapcsolatos információkkal (BoardWidth, BoardHeight) a táblakattintás (BoardClicked) egy összetett esemény, amelyet a nézetben váltunk ki betöltés alapú megközelítést alkalmazunk, így továbbra is a nézet feladata az állapotfrissítés (UpdateViewState) a kapott nézet modell alapján ELTE IK, Komponens alapú szoftverfejlesztés 9:21
Példa Tervezés: Controller::BoardGameController - model :BoardGameModel - view :BoardGameView - savefiledialog :SaveFileDialog - openfiledialog :OpenFileDialog + BoardGameController(BoardGameModel) - Model_GameWon(object, GameWonEventArgs) :void - Model_GameOver(object, EventArgs) :void - Model_FieldChanged(object, FieldChangedEventArgs) :void - View_Shown(object, EventArgs) :void - View_BoardClicked(object, BoardEventArgs) :void - View_KeyDown(object, KeyEventArgs) :void - View_Resize(object, EventArgs) :void - LoadGame() :void - SaveGame() :void - GetSessionState() :PlayerData[] «property» + View() :BoardGameView -view View::BoardGameView + UpdateViewState(PlayerData[,]) :void # OnBoardClicked(Int32, Int32) :void «property» + BoardWidth() :Int32 + BoardHeight() :Int32 Form «event» + BoardClicked() :EventHandler<BoardEventArgs> View::DrawingForm -model Model::BoardGameModel ELTE IK, Komponens alapú szoftverfejlesztés 9:22
Adatkötés Az állapotok automatikus szinkronizálását adatkötés (data binding) segítségével érhetjük el, amely két lehetséges módon biztosíthatja a szinkronizációt az érték módosítás hatására átíródhat a másik állapotba módosítás megjelenített állapot módosítás munkafolyamat állapot az érték módosítás hatására jelzést adhat a másik állapot frissítésére megjelenített állapot változásjelzés állapotlekérés munkafolyamat állapot módosítás ELTE IK, Komponens alapú szoftverfejlesztés 9:23
Adatkötés Az adatkötésért egy olyan adatkötés objektum (Binding) felel, amelyet a megjelenített állapot tárolója (nézet) és a munkafolyamat állapot tárolója (modell) közé helyezünk az adatkötés ismeri mind a nézetet mind a modellt, ezért lehívhatja az értékeket (GetState), és elvégezheti a módosítást (SetState) elvégezheti az átalakítást (Convert) a munkafolyamat állapot és a nézet állapot között általában minden szinkronizálandó adathoz külön adatkötés tartozik, többszörös előfordulás esetén akár előfordulásonként is tartozhat adatkötés a nézet ismerheti az adatkötést, így közvetlenül kezdeményezheti a módosítást az adatkötésen keresztül ELTE IK, Komponens alapú szoftverfejlesztés 9:24
Adatkötés (modell irányába) client Button_Clicked() View Binding Controller SetState(DisplayState) Convert(DisplayState) : SessionState SetState(SessionState) ELTE IK, Komponens alapú szoftverfejlesztés 9:25
Adatkötés (nézet irányába) View Binding Contoller SetState(s : SessionState) SetState(DisplayState) Convert(s : SessionState) : DisplayState ELTE IK, Komponens alapú szoftverfejlesztés 9:26
Adatkötés megvalósítása Az adatkötés nyelvi támogatásként érhető el modern.net platformokon (pl. WPF, WinRT, UWP, Xamarin) a grafikus felületet deklaratívan, XAML segítségével írjuk le a felületen a kötéseket a Binding típus segítségével adjuk meg a cél helyén, és megadjuk a forrást (Source), továbbá megadhatjuk az átalakítás módját (Converter), pl.: <Label Text="{Binding Source=Value}" /> <!-- a feliratot adatkötéssel adjuk meg --> magára a teljes felületre az adatforrást (a nézet modellt) a DataContext tulajdonság segítségével helyezhetjük, pl.: ViewModel vm = new ViewModel; // nézet modell vm.value = 0; // érték beállítása view.datacontext = vm; // adatforrás megadása ELTE IK, Komponens alapú szoftverfejlesztés 9:27
Parancsok A parancs (command) tervminta célja egy művelet kiemelése egy külön objektumba, így azt több objektum többféleképpen is igénybe veheti a végrehajtandó tevékenység (Action) formája, paraméterezése tetszőleges lehet, ezért nem lehet egyazon módon különböző környezetekben kezelni a parancs (Command) egy egységes formát biztosít egy tevékenység végrehajtására (Execute), a konkrét parancs (ConcreteCommand) pedig biztosítja a megfelelő tevékenység végrehajtását a kezdeményező (Invoker) csupán a parancsot látja, így a tényleges végrehajtás előle rejtett marad ELTE IK, Komponens alapú szoftverfejlesztés 9:28
Parancsok Invoker «interface» Command + Execute() Receiver + Action() Client -receiver ConcreteCommand - state + Execute() receiver.action() ELTE IK, Komponens alapú szoftverfejlesztés 9:29
Parancsok megvalósítása A parancsok nyelvi támogatásként érhetőek el modern.net platformokon (pl. WPF, WinRT, UWP, Xamarin) a parancs (ICommand) lehetőséget ad egy tetszőleges tevékenység végrehajtására (Execute), illetve a végrehajthatóság ellenőrzésére (CanExecute) célszerű a tevékenységeket lambda-kifejezések (Action) formájában megvalósítani a parancsokat adatkötés segítségével, a nézet modellen keresztül kapcsolhatjuk a grafikus felületre, ahol a vezérlők parancs (Command) tulajdonságához köthetjük, pl.: <Button Command="{Binding MyCommand}" /> <!-- a parancsot adatkötéssel adjuk meg --> ELTE IK, Komponens alapú szoftverfejlesztés 9:30
MVVM architektúra A Model-View-Viewmodel (MVVM), vagy prezentációs modell (PM) architektúra a nézettel kapcsolatos tevékenységeket, illetve a nézetállapot frissítését egy nézetmodell (viewmodel) komponensbe helyezi a nézetmodell tartalmazza a felületi adatokat (megjelenített állapot), valamint a tevékenységek végrehajtásához szükséges műveleteket (parancsok formájában) a nézethez a megjelenített állapotot adatkötéssel kapcsoljuk, a nézetmodell pedig automatikusan jelez a nézetállapot megváltozásáról (figyelő segítségével) Az összetett tevékenységeket (pl. nézetek létrehozása) egy külön alkalmazás vezérlés (application control) biztosítja ELTE IK, Komponens alapú szoftverfejlesztés 9:31
MVVM architektúra MVVM felhasználó nézet létrehozás adatkötés jelzés alkalmazás vezérlés végrehajtás nézetmodell jelzés modell ELTE IK, Komponens alapú szoftverfejlesztés 9:32
Figyelő A figyelő (observer) tervminta célja összefüggőség megadása az objektumok között, hogy egyik állapotváltozása esetén a többiek értesítve lesznek a figyelő (Observer) ehhez biztosítja a változás jelzésének metódusát (Update) a megfigyelt objektumok (Subject) az értékeikben tett változtatásokat jelzik a felügyelőknek (Notify) egy objektumot több figyelő is nyomon kísérhet az MVVM architektúrában a figyelő szerepét az adatkötés tölti be, míg a megfigyelt objektum a nézetmodell ELTE IK, Komponens alapú szoftverfejlesztés 9:33
Figyelő Subject - observers :List<Observer> + Attach(Observer) + Detach(Observer) :Observer + Notify() for (all o in observers) o.update() -observers 1..* Observer + Update() ConcreteSubject - subjectstate + GetState() return subjectstate + SetState(State) Notify() -subject ConcreteObserver - observerstate + Update() observerstate = subject.getstate() ELTE IK, Komponens alapú szoftverfejlesztés 9:34
MVVM architektúra A nézetmodell tekinthető egy átjárónak (proxy), amely a nézet és a modell között helyezkedik el ViewModel Application Control View * Command * + HandleEvent() - Convert(Value) :DisplayedValue + GetState() :DisplayedValue «event» + Notify() + ComplexEvent() + Execute() * Model ELTE IK, Komponens alapú szoftverfejlesztés 9:35
MVVM architektúra client View Command ViewModel Model Execute(Value) HandleEvent() GetState() :SessionState Notify() Convert(SessionState) : DisplayedState GetState() :DisplayedState ELTE IK, Komponens alapú szoftverfejlesztés 9:36
MVVM architektúra Előnyei: a nézet csak a felület deklaratív leírását tartalmazza, minden tevékenység és adatkezelés (átalakítás) külön rétegben található a felület teljesen függetlenül alakítható ki a nézetmodelltől Hátrányai: összetett architektúra, alapos átgondolást, és sok beépített elemet igényel a nézet és a nézetmodell összekötése közötti inkonzisztenciák csak futási időben derülnek ki ELTE IK, Komponens alapú szoftverfejlesztés 9:37
MVVM architektúra megvalósítása Az MVVM architektúra megvalósítása nyelvi támogatásként érhető el modern.net platformokon (pl. WPF, WinRT, UWP, Xamarin) használjunk az adatkötést (Binding) és a parancsokat (ICommand) az adatokban (tulajdonságokban) történt változások nyomon követése a nézetmodellben történik (INotifyPropertyChanged) a megadott tulajdonság módosításakor kiválthatjuk a NotifyPropertyChanged eseményt gyűjteményekben bekövetkezett változásokat is nyomon követhetünk (INotifyCollectionChanged) ELTE IK, Komponens alapú szoftverfejlesztés 9:38
MVVM architektúra megvalósítása FrameworkElement Binding View Binding * +DataContext * ViewModelCommand ViewModel * ViewModelItem «interface» ICommand + CanExecute(Object) :bool + Execute(Object) :void ObservableCollection «interface» INotifyPropertyChanged Model «event» + PropertyChanged() :PropertyChangedEventHandler ELTE IK, Komponens alapú szoftverfejlesztés 9:39
Példa Feladat: Készítsünk egy táblajáték programot MVVM architektúrában, amelyben két játékos küzdhet egymás ellen. külön projektet hozunk létre a nézetmodellnek (ViewModel), valamint a nézetnek (View.Presentation) kiegészítjük a nézetmodellt a változásjelzéssel (INotifyPropertyChanged), amelyet egy közös ősosztályban kezelünk, és váltunk ki (ViewModelBase) a tevékenységeket felbontjuk a nézetmodell és az alkalmazás (App) között új nézetet igénylő tevékenységekről (pl. betöltés, mentés, játék vége) a nézetmodell, vagy a modell eseményt küld az alkalmazásnak ELTE IK, Komponens alapú szoftverfejlesztés 9:40
Példa Tervezés: Model::BoardGameModel -model App - persistence :IPersistence - model :BoardGameModel - viewmodel :BoardViewModel - window :BoardGameWindow - openfiledialog :OpenFileDialog - savefiledialog :SaveFileDialog Application + App() - App_Startup(object, StartupEventArgs) :void - Model_GameWon(object, GameWonEventArgs) :void - Model_GameOver(object, EventArgs) :void - ViewModel_LoadGame(object, System.EventArgs) :void - ViewModel_SaveGame(object, System.EventArgs) :void - ViewModel_GameExit(object, System.EventArgs) :void -window Window View::BoardGameWindow + BoardGameWindow() -model -viewmodel - model :BoardGameModel ViewModel::BoardViewModel ViewModelBase ViewModel::BoardItemViewModel - player :String + BoardItemViewModel(Action<Object>) «property» + Column() :Int32 + Row() :Int32 + Number() :Int32 + Player() :String + BoardItemCommand() :DelegateCommand ViewModelBase + BoardViewModel(BoardGameModel) - NewGame() :void - GetDisplayedState(PlayerData) :String - CreateBoard() :void - Model_FieldChanged(object, FieldChangedEventArgs) :void - OnLoadGame() :void - OnSaveGame() :void - OnGameExit() :void «event» + GameExit() :EventHandler + LoadGame() :EventHandler + SaveGame() :EventHandler «property» + NewGameCommand() :DelegateCommand + LoadGameCommand() :DelegateCommand + SaveGameCommand() :DelegateCommand + ExitGameCommand() :DelegateCommand + BoardWidth() :Int32 + BoardHeight() :Int32 + BoardItems() :ObservableCollection<BoardItemViewModel> * ELTE IK, Komponens alapú szoftverfejlesztés 9:41