2024. április 26., péntek

Gyorskeresés

Windows Phone fejlesztés: MVVM és adatbázis példakód

Írta: | Kulcsszavak: fejleszés . WP8 . fórum

[ ÚJ BEJEGYZÉS ]

Ez a bejegyzés eredetileg egy hozzászólásnak készült a C# programozás topikban, de annyira hosszúra sikeredett, hogy inkább kiszerveztem ide. Ennek fényében bárminemű hozzászólást itt és ott is szívesen fogadok. Az eredeti kérdés meg itt érhető el.

És akkor jöjjön a magyarázat Crytec210 kérdéséhez - eredetileg neki címezve.

---

[ÚJ!] A kész projekt GitHubon érhető el.

Perpillanat csak 2013-as Visual Studio van a kezem ügyében, így WP8 projektet tudtam csak létrehozni. De a benne foglaltak módosítás nélkül kell hogy menjenek 7-en is.

Van egy kis plusz körítés a kódban, mert azt a vázat követi amit minden WP appomban használok. Ebből végeredményben kivágható a téged érintő szakasz, majd kitérek rá, de lehet látsz benne még valami hasznosat.

Magáról a vázról:
- Az MVVMLight és a WPToolkit NuGet csomagok kulcsfontosságúak. Az előbbiből jön az ObservableObject ősosztály a modell osztályokhoz, hogy ne kelljen kézzel implementálni a linkeken emlegetett INotifyPropertyChanging és -Changed interfészeket, helyette egy sorral letudhatóak a setterek illetve a kézi értesítések is. Az utóbbiból meg a ListPicker controlt használom a szűréshez.
- Utálom a kézzel megírt SQL utasításokat. Ennek anekdotikus oka van: aki nekem elkezdett kardozni mellette a LINQ to SQL-lel vagy bármilyen magasabb szintű megoldással szemben, kritikán aluli egyszerírható szutykot rakott a projektekbe DB réteg gyanánt. YMMV. Ennek örömére a projektben LINQ to SQL van, és csak javasolni tudom, hogy vedd át. Kevesebbet írni mindig jobb.
- Az MVVMLightból kihasználom a ViewModelLocatort és az IOC konténerét is. Ez egy nagyobb falat, de akár figyelmen kívül is hagyhatod. A gyakorlatban annyit tesz itt, hogy kevesebbet kell írni, és ugyanaz a VM osztály használható designerben és futásidőben is.

És akkor az appról:

1) Csináltam egy modell osztályt, aminek olyasmi mezői vannak, mint ami a menetrendes feladatodban lehet az eddigi kérdések alapján: indulási- és célállomás, illetve idők. Tekintsd meg a TimeTableItem.cs fájlt.
- A már említett ObservableObjectből származik, úgyhogy alkalmas mind a UI mind a DB felé adatkötésre. Ehhez kell még, hogy a propertyk mögött tagváltozó álljon, és az eseménykezelők meg legyenek hívva - ami meg is történt.
- A [Table] és a [Column] attribútumokkal kész is van az adatbázishoz mappelés. Az osztály egyébként nem tud arról, hogy hol tárolod. Az én hitvallásom szerint az attribútumok hozzáadása nem jelenti azt, hogy maga az osztály szennyezett lenne, hiszen az interfészén semmi se látszik belőle. Sőt, ha JSON serializálni kéne, akkor még a DataContract attribútumokat is erre az osztályra szórnám fel, szemrebbenés nélkül.

2) A LINQ to SQL alappillére a DataContext, mert ezen keresztül érhetőek el az adatok, és ez végzi el a tényleges DB kezelést.
- Kell a projektben egy saját leszármazott a konkrét táblák miatt, nekem ez a TimeTableDataContext.cs-ben található. Azért ilyen hosszú, mert első indításkor nyom bele némi adatot; egyébként csak a Table típusú tagváltozó és a konstruktor legeleje kellene.
- A beszúrást láthatod is: az InsertOnSubmit/InsertAllOnSubmitnak objektumokat kell adni, majd a SubmitChanges hívással menteni a módosításokat.
- Vicces dolgot vettem észre: az SQL CE-ben a minimum ábrázolható dátum 1753. január 1., ezért inkább a mai nappal tároltam le az indulási időket, mintsem az 1. évvel (ahogy a korábbi hozzászólásomban spekuláltam).

3) Mivel nem szeretném, hogy a UI függjön az adatbázistól (így designerben elég körülményes lenne dolgozni vele), bevezettem az IDataSource interfészt. Lásd a DataSource.cs fájlt.
- Athlon64+ jogos korrekciója után az IQueryable<T> interfészt választottam erre, mert ez lehetővé teszi a LINQ to SQL számára, hogy elvégezhesse a lekérdezések fordítását, viszont a statikus demóadatokkal is átjárható.
- A DesignDataSource osztály kiköp pár demóadatot, ami majd a designerben megjelenik.
- A DbDataSource pedig a DB tábláját passzolja tovább.
- A kettő közötti váltás az IOC konténer feladata, ez a része kicsit hardcore.

4) Akkor most ugorjunk a másik felére a történetnek. A felületet egy fájlba sűrítettem, ez a MainPage.xaml.
- Control szinten van rajta egy ListPicker, amiben a DB-ben lévő összes város jelenik meg; valamint egy ListBox amiben a menetrendsorok jelennek meg.
- Ha a ListPickerben kiválasztasz egy várost, akkor csak azok az elemek jelennek meg, amik onnan indulnak vagy oda mennek.
- Amit itt látni kell: a MainPage.xaml.cs fájl a konstruktoron kívül üres. Nincs szükség itt logikára.
- A ListBoxnak van egy komplex ItemTemplate-je, amin az összes adat megjelenik. Az óra:perc megjelenítés is itt látható, végül maradtam a telefon beállításaira érzékeny (korrektebb) megoldásnál.
- A ListPickernek csak egy FullModeItemTemplate-je van, ami öt vagy több lehetőségnél lesz látható. Egy kicsit nagyobb feliratokat raktam be az alapnál, semmi extra.
- A feliratok mindenhol lokalizáltak, azaz angol nyelvnél angol, magyarnál magyar, német és stb-nél meg angol. Ehhez arra a LocalizedStringsre volt szükség, ami egyébként is benne van a VS sablonjában.
- Az adatok pedig a DataContexten (nem összekeverendő a LINQ-essel!) jönnek be, ami a fájl elején kerül bekötésre. A konkrét bindingot lásd a ListPicker és a ListBox ItemsSource-ánál, vagy a SelectedIndexnél. A hivatkozott propertyk mind a viewmodel részét képezik, ami a következő szakasz tárgya.

5) A QueryPageViewModel.cs fájlban található az a viewmodel, ami a(z egyetlen) képernyőt kiszolgálja.
- A VM ugyebár arra szolgál az MVVM mintában, hogy bindingon keresztül szolgáltasson adatot és elérhető parancsokat a View felé, így abba minimális logikát kell csak írni. Más szóval egy olyan osztály, aminek a mezői megfeleltethetőek a UI elemeinek, és még változáskövetést is tud.
- Ez utóbbit az MVVMLightos ViewModelBase tudja, ami szintén ObservableObject származék, úgyhogy a szabályok ugyanazok.
- Tehát a UI elemek miatt szükség van egy állomáslistára és egy opcionálisan szűrt menetrendlistára, illetve a kiválasztott állomásnak is be kell jönnie valahol. Ezek mind propertyk lesznek, melyből a legutolsó írható-olvasható, a többi csak olvasható.
- Most jött jól a harmadik pontban megírt DataSource: mindegy hogy DB-ből, tömbből, random generátorból jönnek az elemek, akkor is ugyanúgy kell elvégezni rajtuk a szűrést vagy a városok leválogatását. A getterekbe csak ennyi kerül.
- A SelectedStationIndex amellett, hogy a szokásos értesítéses formát hozza, még egy extra jelzést is lead, amitől a ListBox újrahúzza a tartalmát. Az Items getter már az új szűrőfeltétel használatával adja vissza a találatokat.

Nagyjából ennyi az áttekintés. Az újrafelhasználás kapcsán: a legfontosabb a XAML és a viewmodel. Ha valamiért nem tetszik a LINQ to SQL és inkább a lábbalhajtós megoldás mellett maradsz, a XAML-nek akkor se kell eltérnie ettől. A viewmodel is csak annyiban más, hogy a getter mögött egy valódi lista áll, meg mondjuk egy private setter, amivel az új eredményeket beállítod és kiküldöd a jelet.

Hozzászólások

(#1) Peter Kiss


Peter Kiss
senior tag
LOGOUT blog

Az IEnumerable<T> messze nem a legjobb interface az adatkapcsolathoz, mivel valahányszor hozzányúlsz, lehúzza az egész adattáblát memóriába. :U

Egy ilyen attribútummal mennyi vagy független az adatbázistól?

[Table]
public class TimeTableItem : ObservableObject
{ /* ... */ }

Kicsit furcsán fest.

[ Szerkesztve ]

(#2) Karma válasza Peter Kiss (#1) üzenetére


Karma
félisten

Attól, hogy attribútum van rajta, még mindig egy POCO-ról van szó, szerintem ez elég független az adatbázistól. Na jó, az ID és a Version mezők tényleg rontanak rajta, de szerintem nem vészes.

Javaban használtál már JAXB-t, Springet vagy Camelt? Ott is elég gyakori, hogy annotációkkal tolnak meg POJO-kat, hogy ne kelljen feleslegesen duplikálni dolgokat.

Tudtommal az lenne a lényege az IEnumerable-nek, hogy addig nem végez semmilyen műveletet, amíg nem akarja valaki kiértékelni, márpedig azt csak a ListBox fogja megtenni property change-enként egyszer, meg induláskor. Az induláskor teljes felnyalás a példa egyszerűsége miatt ilyen (lehetne limitálni meg stb), szűréskor meg a LINQ to SQL már berakja a WHERE feltételt magától. Tévednék?

[ Szerkesztve ]

“All nothings are not equal.”

(#3) Peter Kiss válasza Karma (#2) üzenetére


Peter Kiss
senior tag
LOGOUT blog

Tévedsz. Az IEnumerable<T> extension-ök Func<>-torokkal operál, ezekben tud a rendszer pl. SQL kódot generálni, mivel nem lát bele. Az IQueryable<T> extension method-jai használnak Expression<Func<>> típusú paramétereket, ezekbe lát bele a rendszer. Ennél fogva egy Where() hívás a te megoldásoddal azt eredményezné, hogy a szűrés a memóriában valósul meg, nem pedig pl. az SQL szerveren (és nem csak a limitált adathalmaz jön vissza).

És önmagában az IEnumerable<T> semmi köze a deferred executing-hoz.

(#4) Karma válasza Peter Kiss (#3) üzenetére


Karma
félisten

Hupsz. Akkor itt tényleg képzavarban voltam, mert igazából mindig csak objektumokra használom a LINQ-t és láttam hogy később értékeli ki a dolgokat az IEnumerable...

Odáig oké, hogy rossz amit írtam, de mit javasolnál helyette akkor? IQueryable interfészt kiajánlani helyette esetleg? Nem hiszem, hogy az absztrakciós igényem ördögtől való azért.

[ Szerkesztve ]

“All nothings are not equal.”

(#5) Peter Kiss válasza Karma (#4) üzenetére


Peter Kiss
senior tag
LOGOUT blog

Persze, simán jó lehet az IQueryable<T>.

(#6) Karma válasza Peter Kiss (#5) üzenetére


Karma
félisten

Köszi, majd átírom és cserélem a szöveget is meg a kódot is.

Valami más észrevételed van még esetleg? Szeretek tanulni a hibáimból :)

[ Szerkesztve ]

“All nothings are not equal.”

(#7) Karma


Karma
félisten

Megcsináltam a módosításokat, de előtte tettem be egy loggert a DataContextnek, így szépen látszik hogy pontosan úgy van ahogy leírtad :R

Az eredeti formában, ahogy feltöltöttem, a minden szűrés a memóriában történt, a DB-ből teljes felolvasásokat végzett. Ez a sor ismétlődik a logban minden DB műveletnél:

SELECT [t0].[_version], [t0].[Id], [t0].[FromStop], [t0].[ToStop], [t0].[StartTime], [t0].[EndTime]
FROM [TimeTableItem] AS [t0]

Miután kidobtam az AsEnumerable hívást, mindjárt normalizálódott.

Ez lett a városnevek gyűjtéséből:

SELECT [t3].[FromStop]
FROM (
SELECT [t2].[FromStop]
FROM (
SELECT [t0].[FromStop]
FROM [TimeTableItem] AS [t0]
UNION
SELECT [t1].[ToStop]
FROM [TimeTableItem] AS [t1]
) AS [t2]
) AS [t3]
ORDER BY [t3].[FromStop]

És ez a városra szűrésből:

SELECT [t0].[_version], [t0].[Id], [t0].[FromStop], [t0].[ToStop], [t0].[StartTime], [t0].[EndTime]
FROM [TimeTableItem] AS [t0]
WHERE ([t0].[FromStop] = @p0) OR ([t0].[ToStop] = @p1)
-- @p0: Input String (Size = 9; Prec = 0; Scale = 0) [Kecskemét]
-- @p1: Input String (Size = 9; Prec = 0; Scale = 0) [Kecskemét]

“All nothings are not equal.”

(#8) Karma


Karma
félisten

Now with 100% more GitHub! Kicsit kényelmesebb mindenkinek, mint a zip fájlt tologatni :P

“All nothings are not equal.”

(#9) Peter Kiss válasza Karma (#7) üzenetére


Peter Kiss
senior tag
LOGOUT blog

Megnyugodtam. :DDD

(#10) Pttypang válasza Karma (#8) üzenetére


Pttypang
veterán

Itt is szeretném megköszönni a részletes magyarázatot :R

Everybody lies.

További hozzászólások megtekintése...
Copyright © 2000-2024 PROHARDVER Informatikai Kft.