Document-View architektúra¶
A gyakorlat célja¶
A gyakorlat céljai:
- UML alapú tervezés és néhány tervezési minta alkalmazása
- A Document-View architektúra alkalmazása a gyakorlatban
- UserControl szerepének bemutatása Window Forms alkalmazásokban, Document-View architektúra esetén
- A grafikus megjelenítés elveinek gyakorlása Window Forms alkalmazásokban (
Paint
esemény,Invalidate
,Graphics
használata)
A kapcsolódó előadások és korábbi gyakorlatok anyaga:
- UML alapú modellezés
- Windows Forms alkalmazásfejlesztés
- Szoftverarchitektúrák (Document-View architektúra)
Előfeltételek¶
A gyakorlat elvégzéséhez szükséges eszközök:
- Visual Studio 2022
- Windows 10 vagy Windows 11 operációs rendszer (Linux és macOS nem alkalmas)
A gyakorlat menete¶
Az alábbiak szerint fogunk dolgozni:
- A feladat/célok rövid ismertetése: egy interaktív fonteditor (betűtípus-szerkesztő) megtervezése
- A kész alkalmazást futtatva a feladat (a kész alkalmazás működésének) ismertetése
- Az alkalmazás architektúrájának megtervezése (osztálydiagram elkészítése)
- A kész alkalmazás forráskódjának alapján néhány fontosabb forgatókönyv megvalósításának áttekintése
Megjegyzés gyakorlatvezetők számára
A gyakorlat elején töltsük le a kész alkalmazást (innen klónozzuk ki: https://github.com/bmeviauab00/lab-docview-megoldas). A hallgatók ekkor még ne töltsék le, ne ezt kattintgassák, majd csak a gyakorlat második részében. A gyakorlatvezetőknek viszont szüksége lesz rá, mert ennek segítségével történik a feladat bemutatása.
1. Feladat - A feladat ismertetése¶
Interaktív FontEditor (betűtípus szerkesztő) készítése, amelyben lehet szerkeszteni a karaktereket, és az aktuális betűkészlet alapján tetszőleges példaszöveg megjeleníthető. Az alkalmazás felhasználói felülete futás közben:
A következő funkciókat kell támogatnia:
- Több betűtípus egyidejű szerkesztése. Ez egyes betűtípusok külön tab oldalakon szerkeszthetők (MDI – Multiple Document Interface).
- Új betűtípus a File/New menüelem kiválasztásával hozható létre (meg kell adni a nevét).
- Ez egyes betűtípusok elmenthetők (File/Save), betölthetők (File/Open), és az aktuális dokumentum bezárható (File/Close). Ezek helye megvan az alkalmazásban, de nincsenek részleteiben implementálva (a függvények törzse nincs kitöltve – opcionális HF).
- A felhasználói felület felépítése
- Az oldal tetején (Sample text) egy mintaszöveg adható meg, melyet az aktuális betűtípussal az alkalmazás megjelenít.
- Az oldalak közepén egy karaktersáv található. Egy adott karakteren duplán kattintva alatta megjelenik egy, az adott karakterhez tartozó szerkesztőnézet.
- Az oldal alján egymás mellett az eddig szerkesztésre megnyitott karakterek szerkesztőnézetei láthatók. Egy karakter többször is megnyitható szerkesztésre, ez esetben több szerkesztőnézet jön létre hozzá. Ennek az az értelme, hogy ugyanazt a karaktert különböző nagyítással is láthatjuk/szerkeszthetjük.
- A szerkesztőnézetek felépítése
- Nagy része (eltekintve a felső sáv) a szerkesztőfelület, ahol fekete háttéren sárgával jelennek meg az aktív pixelek. Egy adott pixelen az egérrel kattintva a pixel invertálódik.
- Bal felső sarokban a megjelenített karakter látható
- ’c’ gomb: Clear, minden aktív pixelt töröl
- ’+’ gomb: nagyítás
- ’-’ gomb: kicsinyítés
Futtassuk az alkalmazást, és vizsgáljuk meg a működését a fentieknek megfelelően. Azt mindenképpen nézzük meg, hogy ha egy karakter szerepel a mintaszövegben, valamint többször megnyitjuk szerkesztésre, akkor az egyik nézetben változtatva (egy pixelt invertálva) valamennyi nézete frissül.
Az alkalmazás a kódmennyiség minimális értéken tartása érdekében minimalisztikus, pl. a hibakezelés nincs általánosságában kidolgozva, hiányoznak ellenőrzések. Ugyanakkor kódmegjegyzésekkel el van látva, mely segíti a kód utólagos megértését.
2. Feladat - Az alkalmazás megtervezése¶
A cél az, hogy lássuk, milyen folyamatot követve, milyen lépésekben dolgozunk, mikor milyen tervezői lépéseket kell meghoznunk. Törekedjünk oktatói és hallgatói részről is az interaktivitásra, közösen hozzuk meg a döntéseket.
Hozzunk létre egy új C# nyelvű „Window Form App” projektet (.NET 8-ast), legyen a neve FontEditor. Vegyünk fel egy osztálydiagramot: projekten jobb katt, Add / New Item, majd a megjelenő ablakban Class Diagram kiválasztása, a neve maradhat az alapértelmezett. Állítsuk be, hogy a diagram mutassa majd a műveletek szignatúráit is (pl. jobb katt a háttéren, Change Members Format / Display Full Signature). A gyakorlat nagy részében ezt a diagramot fogjuk szerkeszteni.
A kész osztálydiagram a következő, eddig fogunk fokozatosan eljutni:
Document-View architektúra¶
Az első tervezői döntés: architektúrát kell választani. A Document-View esetünkben egyértelmű választás: dokumentumokkal dolgozunk, és több nézettel, melyeket szinkronban kell tartani. Az alábbi ábra ismerteti a működést. A nézetek az observerek, a document pedig a subject, melynek változásaira az egyes nézetek fel vannak iratkozva.
A D-V architektúrából adódóan szükségünk lesz dokumentum osztályra, amely a dokumentum adatait tárolja (tagváltozókban), mint pl. a név, elérési út, pixelmátrix. Tegyük fel, hogy a későbbiekben több dokumentum típust is támogatni kell majd: pl. megnyithatunk egy olyan tabfület, melyen a BKK járművekhez tudjuk rendelni a betűtípusokat (elektronikus kijelző). Vannak olyan dokumentum adatok, melyek minden dokumentum típusban megjelennek (pl. név, elérési út). Az egyes dokumentum típusoknak a közös tulajdonságait/műveleteit célszerű egy Document
ősosztályba kiszervezni, hogy ne legyenek duplikálva az egyes dokumentum típusokat reprezentáló dokumentum osztályokban.
- Vegyük fel a
Document
osztályt (ez az absztrakt ős). - Vegyünk fel bele egy
string Name
property-t (ez jelenik meg a tabfüleken).
A Document-View architektúrából adódóan szükség van egy nézet interfészre (egy Update
művelettel a nézet értesítéséhez), valamint a dokumentumoknak nyilván kell tartaniuk egy listában a nézeteiket:
- Vegyük fel az
IView
interfészt. - Vegyünk fel bele egy
Update
műveletet. - A
Document
osztályba vegyünk fel egyList<IView> views
mezőt (a Fields-nél). Jobb gombbal kattintsunk a mező nevén a diagramon, és a menüből Show as collection association kiválasztása. - A
Document
osztályba vegyünk fel avoid AttachView(IView view)
műveletet, mellyel új nézetet lehet beregisztrálni. - Végül vegyünk fel egy
void DetachView(IView view)
-t, mert nézetet bezárni is lehet.
Támogatnunk kell az egyes dokumentumok tartalmának perzisztálását (mentés/betöltés). Ezekhez vegyünk fel a Document
ősbe a megfelelő műveleteket:
Document
-beLoadDocument(string path)
felvétele.Document
-beSaveDocument(string path)
felvétele.- Mindkettő legyen absztrakt, hiszen csak az egyes dokumentum leszármazottakban tudunk implementációt megadni: szelektáljuk ki a két műveletet, és a Properties ablakban az Inheritence modifier legyen Abstract.
Az egyes dokumentumoknak támogatniuk kell a nézeteik frissítését, ez minden dokumentum típusra közös:
- A
Document
-be vegyük fel azUpdateAllViews()
-t (ez felel meg az Observer minta Notify műveletének).
Konkrét dokumentum és adatai¶
Szükség van egy olyan dokumentum típusra, ami a betűtípusok szerkesztéséhez tartozik, amely a tagváltozóiban nyilvántartja a szükséges adatokat: legyen a neve FontEditorDocument
.
- Vegyük fel a
FontEditorDocument
osztályt. - Származtassuk a
Document
-ből (Toolbox – Inheritence kapcsolat). - Ekkor a
LoadDocument
ésSaveDocument
műveletekre automatikusan megszületik az override-oló művelet. Ha mégsem lenne így- Jelöljük ki az ősben a két műveletet.
- Copy
- Jelöljük ki a
FontEditorDocument
osztályt. - Paste
- Jelöljük itt ki a két műveletet, és a Properties ablakban a Instance Modifier legyen
override
.
A dokumentumunk tagváltozókban tárolja az adatokat. Gondoljuk át, hogy ezt hogyan célszerű megvalósítani. Lehetne egy háromdimenziós tömb (karakter – x – y), de inkább emeljük ki egy külön osztályba az egy adott karakter pixeleinek tárolását/menedzselését: vezessük be a CharDef
osztályt.
Pixel tömb helyett
Azért nem a pixeltömböt használjuk közvetlenül, mert csak egy új osztály bevezetésével van lehetőségünk kifejezetten ide tartozó műveletek bevezetésére, vagyis az egységbezárás korrekt megvalósítására.
- Vegyük fel a
CharDef
osztályt. -
CharDef
-bebool[,] Pixels
tulajdonság felvétele.többdimenzoós tömbök C#-ban
A fenti példában egy többdimenziós tömböt használtunk
bool[,]
és nem tömbök tömbjétbool[][]
, mivel ezt nyelvi szinten is támogatja a C# és jobb teljesítményt nyújt, mint a tömbök tömbje, mert egy objektumként törolódik a heapen. -
CharDef
-bechar Character
felvétele: az egyesCharDef
osztályok tárolják magukról, hogy mely karakter pixeleit reprezentálják.
A dokumentumnak lesz egy gyűjteménye CharDef
objektumokból: minden karakterhez pontosan egy darab. Gondoljuk át, hogy a legcélszerűbb ezt megvalósítani. Az egyes karakterdefiníciókat a karakterkódjukkal akarjuk címezni, így a Dictionary<char, CharDef>
ideális választás: a karakterkód a kulcs, az hozzá tartozó CharDef
pedig az érték.
FontEditorDocument
-be:Dictionary<char, CharDef> charDefs
mező felvétele. Jobb katt, Show as collection association.
Dokumentumok menedzselése - App Singleton osztály¶
Az alkalmazásban nyilván kell tartani a megnyitott dokumentumok listáját. Mely osztály felelőssége legyen? Vezessünk be rá egy alkalmazásszintű osztályt: legyen a neve App
(Windows Forms alatt már van Application
, nem célszerű ezt a nevet választani). Ez lesz az alkalmazásunk „gyökérosztálya”.
- Vegyük fel az
App
osztályt. App
-baList<FontEditorDocument> documents
mező felvétele, majd Show as collection association.
Gondoljuk végig, hogyan történik majd egy új dokumentum létrehozása (mi történik a File/New menüelem kiválasztásakor): be kell kérni a felhasználótól a dokumentum nevét, létre kell hozni egy FontEditorDocument
objektumot, fel kell venni a megnyitott dokumentumok listájába stb. Ezt a logikát ne tegyük a GUI-ba (menüelem click eseménykezelő): tegyük abba az osztályba, melynek a felelőssége a megnyitott dokumentumok menedzselése, amely tárolja a szükséges adatokat hozzá (dokumentum lista). Így legyen ez az App
osztályunk feladata, benne vegyük fel a szükséges műveleteket:
App
-baNewDocument
ésOpenDocument
műveletek felvétele.
Most a dokumentum mentést gondoljuk végig: a File/Save mindig az aktív dokumentumra vonatkozik. Valakinek nyilván kell tartani, melyik az aktív dokumentum: legyen ez az App
, hiszen ő tárolja a dokumentumok listáját is.
- A Toolbox-on válasszuk ki az Association kapcsolatot. Az
App
-ból húzzunk egy nyilat aFontEditorDocument
-be. Válasszuk ki az újonnan létrehozott kapcsolatot, és nevezzük átActiveDocument
-re. App
-bavoid SaveActiveDocument()
felvétele.App
-bavoid CloseActiveDocumentá()
felvétele.
Konkrét dokumentumra vagy absztrakt ősre hivatkozzunk?
Mivel az App
osztályunk alkalmazás specifikus funkciókat lát el, nyugodtan hivatkozhat a konkrét dokumentum típusra, és felesleges az absztrakt őstől függenünk, mert az csak nem kívánt castolásokhoz vezetne.
Az App
objektumból értelemszerűen csak egyet kell/szabad létrehozni, amely a futó alkalmazást reprezentálja. Van még egy problémánk: a File/Save stb. menüelem click eseménykezelőben el kell érjük ezt az egy objektumot. Illetve, majd több más helyen is. Jó lenne, ha nem kellene minden osztályban külön elérhetővé tenni (tagváltozó vagy függvényparaméter formájában), hanem bárhonnan egyszerűen elérhető lenne. Erre nyújt megoldást a Singleton tervezési minta. Egy osztályból csak egy objektumot enged létrehozni, és ahhoz globális hozzáférést biztosít, mégpedig az osztály nevén és egy statikus Instance
property-n keresztül, pl. így:
App.Instance.SaveDocument
, stb. Nem valósítjuk meg teljes értékűen, de tegyük meg az alábbiakat:
App
-baApp Instance
property felvétele. Properties ablakban static: true.App
-ba privát konstruktor felvétele.
Az App
-osztállyal végeztünk.
Nézetek¶
A nézetekkel eddig nem foglalkoztunk, ez a következő lépés. Futtassuk a kész alkalmazást, és nézzük meg, hogy hány típusú nézetre van szükség, melyikből hány példány lesz:
- Két típusú nézetre van szükség: az egyik a mintaszöveget jeleníti meg, a másik egy adott karakter szerkesztését teszi lehetővé.
- Legyen az előző neve
SampleTextView
, az utóbbiéFontEditorView
. SampleTextView
-ból mindig egy van (egy adott dokumentumra vonatkozóan), aFontEditorView
objektumok igény szerint jönnek létre, 0..n példány létezhet.- Vegyük fel a két osztályt.
- Implementáltassuk velük az
IView
interfészt (Toolbox / Inheritence kapcsolat). AzUpdate
művelet automatikusan implementálva lesz.
Az egyes nézetek a dokumentumukból „táplálkoznak”, a a dokumentumukban tárolt adatokat jelenítik meg, azokat módosítják. Ehhez, a D-V architektúrának megfelelően el kell érjék a dokumentumukat.
- A
SampleTextView
ésFontEditorView
-ban vegyünk fel egyFontEditorDocument
típusúdocument
nevű mezőt (ha felvettük az egyikben, lehet copy-paste-tel másolni a másikba), majd "Show as Association". Megjegyzés: azért nem célszerű általánosDocument
típusút felvenni (és az interfészbe felvinni), mert a view-knak a konkrét dokumentum adatait (lásd alább) el kell érniük.
Gondoljuk végig, milyen adattagokkal rendelkeznek az egyes nézetek. Ehhez futtassuk az alkalmazást, és nézzük meg ismét a felhasználói felületét.
- A
SampleTextView
tárolja a mintaszöveget, melyet meg kell jeleníteni. Vegyünk fel egysampleText:string
mezőt. Ha el kellene menteni a mintaszöveget is, akkor aFontEditorDocument
-ben kellene tárolni (és onnan mindig lekérdezni), mert az adatok mentéséért a dokumentum osztályunk a felelős. - A
FontEditorView
két dolgot tárol:- A karakter kódja, melynek pixeleit megjeleníti. Vegyünk fel egy
editedChar: char
mezőt. - A nagyítási tényezőt (
zoom: double
felvétele)
- A karakter kódja, melynek pixeleit megjeleníti. Vegyünk fel egy
A nézetek maguk felelősek a kirajzolásukért:
Draw (g:Grapics)
felvétele mindkét nézetbe.
FontEditorDocument műveletek¶
A FontEditorDocument
-ben egy privát listában van egyelőre jelen a CharDef
-ek listája. A nézetek így nem tudják elérni, pedig a megjelenítéshez szükségük lenne rá. A dokumentumunkban be kell vezessünk olyan műveleteket, melyek a dokumentum által tárolt adatokat a nézetek számára elérhetővé teszik, és lehetőséget biztosítanak a módosításra is.
- Mindkét nézet el kell érje a megjelenített karakterek pixeleit tároló
CharDef
objektumokat. Ehhez vezessük be aFontEditorDocument
-ben aGetCharDef(c:char):CharDef
műveletet. Ezt hosszú távon majd úgy lesz célszerű megvalósítani, hogy aGetCharDef
nem az eredeti objektumot adja vissza, hanem annak egy másolatát (clone). Ha az eredetit adná vissza, akkor a nézetek KÖZVETLENÜL tudnák módosítani a pixelek értékét, ezt mi nem akarjuk (bár a funkciók bővítésével rákényszerülhetünk). - A
FontEditorView
-nak képesnek kell lennie egy adottCharDef
adott koordinátában levő pixel értékét invertálni (egér kattintáskor). Ehhez vezessük be aFontEditorDocument
-ben azInvertCharDefPixel(c:char, x: int, y: int)
műveletet.
A tervezés zárása¶
Eljutottunk oda, hogy megterveztük az architektúrát, minden igazán lényeges döntést meghoztunk. Az UML diagram alapján megszületett az osztályok váza. Ezt természetesen jelentősen bővíteni kell, még születnek új osztályok is (pl. Form-ok, vezérlők).
3. Feladat - A kész alkalmazás áttekintése¶
Idő hiányában nem valósítjuk meg az alkalmazást, hanem a kész megoldást nézzük át (laboron kb. 15 percben), annak is csak néhány lényeges használati esetét.
Töltsük le a kész megoldást. Ehhez parancssorban navigáljunk a c:\work\
git clone https://github.com/bmeviauab00/lab-docview-megoldas
Nyissuk meg a kész solution-t, futtassuk és próbáljuk ki az alkalmazás alapfunkcióit.
Nézetek megvalósítása¶
Nyissuk meg a FontEditorView
-t, először a kódot nézzük. A FontEditorView
egyrészt implementálja az IView
interfészt, másrészt a UserControl
-ból származik. Mégpedig azért, mert így a tervezőben (designer) tudjuk kialakítani a felhasználói felületét, pont úgy, mint egy űrlapnak. A Visual Studio designer felületén akár bele is módosíthatnánk a layoutba és a vezérlők tulajdonságaiba. Ha kíváncsiak vagyunk, ki is próbálhatjuk ezt (pl. a nagyítás és a kicsinyítés gombok helyének megváltoztatásával).
A SampleTextView
is UserControl
leszármazott, bár annak egyszerű a felülete (nincsenek rajta más vezérlők), így lehetett volna közönséges Control
leszármazott is.
Vonjuk le a tanulságot: Windows Forms környezetben a nézeteket tipikusan UserControl
-ként (esetleg Control
-ként) célszerű megvalósítani.
Egy oldal (tab) elrendezése¶
Futtassuk az alkalmazást. Valahogy ki kell alakítsuk egy adott oldal (tabpage) elrendezését. Lehetőleg tervezői nézetben, és nem futás közben, kódból pozícionálva az elemeket (legalábbis ahol nem muszáj). A UserControl
-ok alkalmazása jelenti számunkra a megoldást. Nyissuk meg a FontDocumentControl
-t tervezői nézetben. Ez egy olyan vezérlő, amely egy taboldalra kerül fel, azt tölti ki teljesen. Az oldalt a már ismert layout technikákkal alakítottuk ki (Label
, TextBox
, Panel
-ek Dock-kolva). Ha van időnk, akkor nézzük meg a Document Outline ablakban. Az igazi érdekesség pedig az, hogy a SampleTextView
-t is a Toolbox-ról drag&drop-pal került felhelyezésre (pont úgy, mintha egy beépített vezérlő lenne). Annyit nézzünk meg, hogy a SampleTextView
valóban ott van a Toolbox tetején.
Forgatókönyv 1 – Egy pixel invertálása, nézetek szinkronizálása¶
Ez egy kiemelt jelentőségű forgatókönyv, mert ezt illusztrálja a D-V architektúra alapmechanizmusát, a nézetek frissítését és konzisztensen tartását.
Keressük meg azt a függvényt, ahol az egész pixel invertálás folyamat elindul. A FontEditorView.FontEditorView_MouseClick
a kiindulópont. Itt az alább kiemelt sor a lényeg:
private void FontEditorView_MouseClick(object sender, MouseEventArgs e)
{
int x = e.X / zoom;
int y = (e.Y - offsetY) / zoom;
if (x >= CharDef.FontSize.Width)
return;
document.InvertCharDefPixel(editedChar, x, y);
}
Nézzük meg a FontEditorDocument.InvertCharDefPixel
-t. Az invertálja a megfelelő CharDef
pixelét, de a lényeg az utolsó sor:
public void InvertCharDefPixel(char c, int x, int y)
{
var charDef = GetCharDefCore(c);
if (charDef == null)
return;
charDef.Pixels[x, y] = !charDef.Pixels[x, y];
UpdateAllViews();
}
Az UpdateAllViews
a Document
ősben van, Update
-et hív minden nézetre. Ami érdekes, hogy az Update
hogyan van megírva az egyes nézetekben. Nézzük meg pl. a FontEditView
-t:
public void Update()
{
Invalidate();
}
Az Update
hatására a nézetek újra kell rajzolják magukat az aktuális dokumentum állapot alapján. De az Update
-ben nem tudunk rajzolni, csak az OnPaint
-ben. Így itt az Invalidate
hívással kiváltjuk a Paint
eseményt. Ez megint egy tanulság: Windows Forms alkalmazásokban a nézetek Update
függvényében tipikusan egy Invalidate
hívás szokott lenni.
Zárásképpen nézzük meg a FontEditView.OnPaint
megvalósítását. Egyetlen lényeges dolog van itt: a megjelenítéshez le kell kérni a dokumentumtól az aktuális CharDef
-et (mert a nézet a D-V architektúra alapelveinek megfelelően nem tárolja), majd ki kell azt rajzolni.
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var editedCharDef = document.GetCharDef(editedChar);
CharDefViewModel.DrawFont(e.Graphics, editedCharDef, 0, offsetY, zoom);
}
Kirajzolás logikája
Mivel a kirajzolás logikája a FontEditorView
-ban és a SampleTextView
-ban is azonosan működik a Graphics
osztály használatával, kiszerveztük ezt egy CharDefViewModel
segédosztályba az újrafelhasználhatóság kedvéért.
A CharDef
-be nem célszerű rakni ezt a logikát, mivel az egy nézet független adatreprezentáció, és sokkal inkább a dokumentumhoz tartozik, mint a nézethez.
Forgatókönyv 2 – Új dokumentum létrehozása (opcionális)¶
Azt nézzük meg, hogyan történik egy új dokumentum létrehozása, vagyis mi történik a File/New menüelem kiválasztásakor.
Nyissuk meg a MainForm
-ot tervezői nézetben, válaszuk a File/New menüelemet, majd ugorjunk el a Click
eseménykezelőhöz. Arra látunk példát, hogy az App
osztály, mint Singleton, hogy érhető el:
App.Instance.NewDocument();
Az összes többi menüelem eseménykezelője hasonló, nincs semmi logika a GUI-ban, csak egyszerű továbbhívás az App
-ba.
Tekintsük át az App.NewDocument
törzsét, és egy-egy mondatban fussuk át a fontosabb lépéseket.
NewDocForm
nézet megnyitása és várakozás a válaszra.- Sikeres válasz esetén új
FontEditorDocument
létrehozása és felvétele a dokumentumok közé, valamint aktívvá tétele. - Új tab létrehozása a nézetekkel.
public void NewDocument()
{
// Bekérjükk az új font típus (dokumentum) nevét a
// felhasználótól egy modális dialógs ablakban.
var form = new NewDocForm(GetDocumentNames());
if (form.ShowDialog() != DialogResult.OK)
return;
// Új dokumentum objektum létrehozása és felvétele a dokumentum listába.
var doc = new FontEditorDocument(form.FontName);
documents.Add(doc);
// Az új tab lesz az aktív, az activeDocument tagváltozót erre kell állítani.
UpdateActiveDocument(doc.Name);
CreateTabForNewDocument(doc);
}
App osztály felelősségi köre
Az egyszerűség érdekében az App
osztály most több felelősséggel is rendelkezik, de ideális esetben szét lenne szedve pl. a következő osztályokra a felelősségi köröknek megfelelően:
DocumentManager
: a megjelenítéstől függetlenül a dokumentumokat tárolná.ViewManager
: feladata a nézetek menedzselése, tabcontrolokhoz hozzáadása stb. lenne.
Az App.OpenDocument
művelet törzse nincs implementálva, de a lépések kódmegjegyzések formájában adottak, remek otthoni gyakorlási lehetőség a művelet tényleges megvalósítása.