Budapesti Műszaki és Gazdaságtudományi Egyetem 1117 Budapest, Magyar tudósok körútja 2. Q épület, B szárny, II. em. 207. Villamosmérnöki és Informatikai Kar Telefon: +36/1/463-2870, +36/1/463-3716 Fax: +36/1/463-2871 Automatizálási és Alkalmazott Informatikai Tanszék http:/www.aut.bme.hu, H-1521 Budapest SZAKDOLGOZAT FELADAT Trajber Barna Viktor szigorló mérnökinformatikus hallgató részére Költségnyilvántartó keretrendszer .NET alapon Amikor egy háztartás, vagy kisvállalat költségvetését szeretnénk nyilvántartani, kézenfekvő, hogy elektronikusan tároljuk a kiadásokat és bevételeket, a számlabefizetéseket. Ez lehetővé teszi az egyszerű, biztonságos tárolást, a könnyű adatbevitelt, különböző szűrések és rendezések eredményeinek megtekintését. A szakdolgozat célja egy kiadás nyilvántartó alkalmazáscsalád fejlesztése, melyet a költségkategóriák testreszabásával akár otthoni, illetve kisvállalati környezetben is használhatunk. Az elkészült keretrendszernek tartalmaznia kell egy szerver oldali alkalmazást, melynek elsődleges feladata, hogy kommunikáljon a különböző platformokon megvalósított kliens alkalmazásokkal. A hallgató feladatának a következőkre kell kiterjednie: Tervezze meg a keretrendszer felépítését és adatbázis sémáját! Készítsen egy szerveralkalmazást, amely WCF szolgáltatást nyújt a kliensek felé! Készítsen egy mobil kliens alkalmazást Windows Phone 8 platformra! Készítsen egy vékony kliens alkalmazást ASP.NET MVC keretrendszer segítségével! Tanszéki konzulens: Dr. Asztalos Márk Budapest, 2013. október 10. Dr. Vajk István egyetemi tanár tanszékvezető
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Budapesti Műszaki és Gazdaságtudományi Egyetem 1117 Budapest, Magyar tudósok körútja 2. Q épület, B szárny, II. em. 207.
Villamosmérnöki és Informatikai Kar Telefon: +36/1/463-2870, +36/1/463-3716 Fax: +36/1/463-2871
Automatizálási és Alkalmazott Informatikai Tanszék http:/www.aut.bme.hu, H-1521 Budapest
SZAKDOLGOZAT FELADAT
Trajber Barna Viktor
szigorló mérnökinformatikus hallgató részére
Költségnyilvántartó keretrendszer .NET alapon
Amikor egy háztartás, vagy kisvállalat költségvetését szeretnénk nyilvántartani,
kézenfekvő, hogy elektronikusan tároljuk a kiadásokat és bevételeket, a
számlabefizetéseket. Ez lehetővé teszi az egyszerű, biztonságos tárolást, a könnyű
adatbevitelt, különböző szűrések és rendezések eredményeinek megtekintését.
A szakdolgozat célja egy kiadás nyilvántartó alkalmazáscsalád fejlesztése, melyet
a költségkategóriák testreszabásával akár otthoni, illetve kisvállalati környezetben is
használhatunk. Az elkészült keretrendszernek tartalmaznia kell egy szerver oldali
alkalmazást, melynek elsődleges feladata, hogy kommunikáljon a különböző
Ebben a fejezetben a keretrendszer tervezésének és fejlesztésének lépéseit
mutatom be. Mivel a szakdolgozatom kereteibe nem férne bele minden lépés részletes
leírása, így próbáltam átadni a leglényegesebb információkat és egy-egy érdekesebb vagy
kihívást jelentő részletet kiemelni.
4.1 Adatbázis tervezése
A fejlesztés első állomása az adatmodell megtervezése volt. A tervezés során
először át kellett gondolnom, hogy milyen típusú adatokra lesz szükségem az alkalmazás
használata során. Továbbá azt is át kellett gondolnom, hogy az adatokat milyen formában
szeretném tárolni. A tárolás problémájára a megoldás viszonylag egyértelmű volt
számomra: a Microsoft SQL Server 2012 relációs adatbáziskelő rendszert választottam.
A döntés végkimenetelében szerepet játszott az, hogy a C#/.NET kiválóan együttműködik
ezen adatbázis kiszolgálóval, széleskörűen támogatott, valamint egyszerűen kezelhető. A
kiszolgálóval való kommunikáció kényelmesebbé tétele érdekében az Entity Framework
ORM (Object-relational mapping) [30] rendszert használtam.
4.1.1 Code First vagy Database First?
Az Entity Framework támogatja ezen kiszolgálóhoz a Code First megközelítést,
mely szemlélet szerint az egyes adatosztályok – mint entitások – és az osztályok között
levő kapcsolatok leképződnek adatbázis táblákra, valamint relációkba, amennyiben
megfelelő adatbázis kontextusba (DBContext) helyezzük őket. Ezt a módszert
választottam én is az adatbázis tervezése során, mert gyorsan létre tudtam hozni egy
adatbázist csupán az osztályok forráskódjának előállításával. Mindvégig kézben tudom
tartani az adatbázis tábláinak megfeleltethető osztályokat, nincsenek az adatbázis
sémából automatikusan generált osztályok (POCO entitások) ellenben a Database First
megközelítéssel szemben, ahol, ha a generált modellünket bővíteni szeretnénk valamilyen
új funkcióval vagy csupán egy új tulajdonsággal, akkor az csak úgy lehetséges, ha
kiterjesztjük a parciális modell osztályunkat. Ezzel sok-sok modell-kiterjesztés születhet,
ami átláthatatlanná teheti a projektet.
Természetesen megvannak a Database First technikának is az előnyei. Ide tartozik
az, hogy optimalizálhatjuk a táblák mezőit a megfelelő típusok, méretek megadásával
41
(igaz attribútumok segítségével a Code First módszert is finomíthatjuk valamilyen
mértékben), létrehozhatunk tárolt eljárásokat, triggereket. Az adatbázisokkal gyakran
foglalkozó szakértők talán jobban kedvelik a már bevált adatbázis létrehozási
módszereket, ahol teljesen testre szabhatják az igényeknek megfelelően az adatbázis
szerkezetét, és optimalizálhatják a rendszert. Kimerítőbb munka, azonban a befektetett
energiák könnyen megtérülhetnek.
A sémában történt változások frissítését mindkét technika támogatja. Code First
esetén az Entity Framework migrációs képességét használhatjuk ki, míg a Database First
módszernél frissíthetjük a generált modellt a séma aktuális állapota alapján. Ha
szeretnénk nyomon követni az adatbázisban történt változásokat, a különböző verziókat,
akkor a Code First migrációk hatalmas segítséget nyújthatnak számunkra, ahol egy
migráció hozzáadásával új fájl keletkezik időbélyeggel és egy általunk választott névvel,
majd ebben a fájlban automatikusan legenerálódnak a modellben történt változások egy
UP() és DOWN() függvényben. Az UP() függvény alkalmazza a modellben történt
változásokat, míg a DOWN() függvény eltávolítja a már nem kívánatos elemeket
(rollback). Mivel az adatbázis séma teljes mértékben a kódtól függ, a forráskód
verziózása is rendelkezésünkre áll. Bele tudunk avatkozni a kontextus inicializálásába is,
ahol például kezdeti üzleti adatokkal tölthetjük föl az adatbázisunkat. A verziózás
megvalósítása adatbázis oldal esetén már koránt sem olyan triviális feladat. [31]
4.1.2 Az adatbázis séma
Most áttérek a költségnyilvántartó rendszer adatbázis-tervének a
végeredményére. A tervezés több lépcsőből állt, először összegyűjtöttem a tárolandó
adatok listáját, amiket adatbázis entitásokba csoportosítottam, majd a folyamatos
finomítások után végül meghatároztam az entitások közötti kapcsolatokat is.
A tervezés befejeztével az alábbi Code First entitás modell készült el:
42
4-1. ábra: Adatbázis séma
43
4.1.3 Táblák és attribútumok
Az alábbi táblázatban a Code First segítségével generált adatbázis tábláit és
attribútumait tekinthetjük át. A táblák egyes oszlopaihoz rövid leírás tartozik, ami
tisztázza az attribútumok tárolási funkcióját.
Tábla Tárolási
funkció Oszlop Leírás
Users Felhasználók
adatai
Id Elsődleges kulcs
FullName A felhasználó teljes neve
Username A felhasználó azonosítója
Password A felhasználó jelszava (MD5
algoritmussal hashelt)
Email A felhasználó e-mail címe
Type Felhasználó szerepköre: admin /
user
Settings Felhasználó
beállításai
Id Elsődleges kulcs
BaseCurrency Alapértelmezett valuta
CurrencyConvert
Konvertálja az alapvalutát más
valuta esetén vagy csak az
alapvaluta tételei jelenjenek meg?
Language Használt megjelenítési nyelv
Transactions Tranzakció
adatai
Id Elsődleges kulcs
Note Megjegyzés
Date Tranzakció időpontja
Amount Tranzakció összege
IsExpense A tranzakció kiadás-e?
User_Id
Idegen kulcs
Payment_Id
Currency_Id
Location_Id
Category_Id
RecurringTransactions6
Ismételődő
tételek
(tranzakciók)
Id Elsődleges kulcs
LastDate Utolsó bekövetkezési időpont
NextDate Következő bekövetkezési időpont
Note Megjegyzés
Amount Tranzakció összege
IsExpense A tranzakció kiadás-e?
User_Id Idegen kulcs
Frequency_Id
Frequencies Ismétlődési
gyakoriság
Id Elsődleges kulcs
Freq Értesítési gyakoriság
Receipts Számlák,
blokkok
Id Elsődleges kulcs
Photo Számláról (bizonylatról) készült
fotó byte tömbként tárolva
Transaction_Id Idegen kulcs
Payments Fizetési
típusok
Id Elsődleges kulcs
Type Fizetőeszköz típusa
6 Ismételődő tételek jelenleg nincsenek kezelve az alkalmazásban, későbbi bővítési lehetőséghez
egy kiindulási alap ez a tábla
44
Categories Kategóriák
Id Elsődleges kulcs
Name Kategória megnevezése
IsExpense Azt jelöli, hogy a kategória kiadás-
e vagy bevétel
ParentCategory_Id
Idegen kulcs, egy adott kategória
szülőjére mutat. A
gyökérelemeknél a szülő kategória
azonosítója NULL
Locations Helyszínek
adatai
Id Elsődleges kulcs
Longitude Hosszúsági koordináta
Latitude Szélességi koordináta
Altitude Magassági koordináta
Postcode Irányítószám
City Város
Street Utca
Country_Id Idegen kulcs
Countries
Helyszínekhez
kapcsolódó
országok
Id Elsődleges kulcs
Name Ország neve
Currencies
Felhasználók
által használt
valuták adatai
Id Elsődleges kulcs
Code Valuta kód
Image Valuta szimbólum
Country_Id Idegen kulcs
4. táblázat: Az adatbázis tábláinak részletezése
4.1.4 Megjegyzések az adatmodellhez
Az adatmodell ebben a formájában nem támogatja a valuta átváltását bármely
dátumra visszamenőleg. Ha valamely kliens alkalmazás beállítások oldalán a valuta
átváltás opciót bekapcsoljuk, akkor a tranzakciós tételek mindig a lekérdezés aktuális
napjának árfolyamán lesznek konvertálva az alapvalutára. Az általam megvalósított
megoldás tehát nélkülözi a tétel dátumán érvényes valutaárfolyamot, mindig a
legfrissebbekkel dolgozik.
Az alábbiakban ismertetek egy elképzelést, mely szerint az adatbázis felkészíthető
lenne arra, hogy a tételeket a tranzakciós dátumnak megfelelő árfolyam szerint váltsuk
át. Ehhez először is szükségünk lenne az összes létező valutaárfolyamra azokon a
napokon, amelyeken történt tranzakció. Az architektúrában feltüntetett valuta kiszolgáló
támogatja a historikus valuta árfolyamok lekérdezését, azonban ez a lekérdezés időben
költséges művelet lenne minden egyes tétel esetén, amelyek külföldi valutában
szerepelnek. A művelet költségét csökkenthetjük azzal, ha a szerver adatbázisában
tároljuk a valutaárfolyamokat a számunkra szükséges dátumokon a következő tábla
felvételével: CurrencyRates(Id, Date, <Currency1...Currencyn>), ahol a
Currency1…Currencyn jelentik az egyes valuták értékét a mindenkori alapvalutához (1
dollárhoz) viszonyítva és az adott dátumra vonatkozóan. Még optimálisabb megoldás
45
esetén csak azon valuták értékét tároljuk, amelyeket használunk is az adott napon. Ezáltal
az adatbázisban állandóan rendelkezésre állhatnak historikusan a szükséges valuta
árfolyamok, melynek egy részét a szerver memóriájában tartva gyorsan hozzáférhetőek
lennének.
4.2 Szerver és a szolgáltatás
A specifikációban feltüntetett szerver központi szerepet tölt be a szolgáltatás-
orientált architektúrában (SOA – Service Oriented Architecture). Feladata, hogy IIS alatt
futasson egy WCF szolgáltatást. Ez a szolgáltatás gyakorlatilag egy .NET
osztálykönyvtár, mely kommunikál az adatbázissal és egy külső valuta szolgáltatóval. A
DLL (Dynamic-link library) egy WCF interfészt ajánl ki a kliensek számára, ami HTTP
protokollon keresztül érhető el a kliensek számára és megvalósítja az üzleti logikát
elfedve az adatbázis hozzáférést a kliens szoftverek fejlesztői elől. A szolgáltatás teljes
forráskódja a Service projektben található.
4.2.1 Üzleti logika
Az üzleti logikát teljesen egészében lefedi az alábbiakban ismertetett interfész és
annak implementációja a mögöttes adatbázissal együtt.
4.2.1.1 Adatbázis kontextus
Az adatbázis kontextust megvalósító osztály egy új projektbe került (Data), mely
akár egy külön szerveren is üzemelhet a szolgáltatást futtató szervertől, ha bármi
indokolná. A kontextus létrehozásnál figyelni kellett arra, hogy a Lazy Loading ki legyen
kapcsolva, ugyanis bekapcsolt állapotában a WCF nem tudja sorosítani a tábla-
osztályainkat7. Ezek a modell osztályok egyébként DataContract-ok is egyben, illetve
az adattagjaik DataMember attribútumokkal vannak ellátva, ha már a WCF
kontextusában használjuk őket. A Lazy Loading kikapcsolásával veszítünk ugyan a
kényelemből a lekérdezések során, azonban legalább két egyéb lehetőségünk is van
betölteni a kapcsolódó táblákat: Eagerly Loading és Explicitly Loading [32]. Eagerly
loading esetén a LinQ lekérdezés közben tudjuk betölteni a kívánt kapcsolódó entitásokat
az Include(<path>) függvény segítségével. Explicitly Loading esetén ki kell
7 Adatbázis táblákba képződő C# osztályok, Entity Framework és a Code First segítségével.
46
kényszerítenünk az explicit betöltését a kapcsolódó entitásnak, majd ezután ugyanúgy
használhatjuk az entitás már betöltött navigációs tulajdonságát, mintha a Lazy Loading
rendelkezésünkre állna. Lássunk egy-egy példát mindkét változatra a szolgáltatásból
hozott kódrészletekkel:
Eager loading példakód (egy felhasználó tranzakcióinak lekérdezése a kapcsolódó
táblákkal):
private IQueryable<Transaction> GetTransactionsByUser(int userID) { // Eagerly load related entites to a transaction var transactions = db.Transactions.Include("Category") .Include("Receipts").Include("Currency").Include("User"); // Get the user’s transactions var collection = from t in transactions where t.User != null && t.User.Id == userID select t; return collection; }
Explicit loading példakód (felhasználó törlése a kapcsolódó rekordokkal együtt):
public bool DeleteUser(int id, out string resultMsg) { try { var user = db.Users.Find(id); if (user == null) { resultMsg = "User not found!"; return false; } // Delete transactions of the user db.Entry(user).Collection(u => u.Transactions).Load(); var transactions = user.Transactions; if (transactions != null) { // Iterating over transactions of the user and delete them foreach (var t in transactions.ToList()) { db.Entry(t).Collection(t1 => t1.Receipts).Load(); . . . //Delete receipts associated with transaction db.Transactions.Remove(t); // Delete transaction } } . . . // Delete other related entites // Delete user db.Users.Remove(user); db.SaveChanges(); resultMsg = "Successfully deleted the user!"; return true; }
IPage.cs: Az egyes „oldalakra” vonatkozó adatok kérdezhetők le innen. 3
metódusa van: GetMainPageSource(…), GetOverviewSource(…),
GetDetailsSource(…), melyek mindegyike egy egyedi típusú
ObservableCollection-nel tér vissza. Alapvetően a mobil kliens korábban
elkészült felülete ihlette ezeket a metódusokat, hogy kiszolgáljon egy-egy
panoráma oldalt. Mivel azonban a web kliens is hasonló szerkezetű felületi
elemekkel és oldalakkal rendelkezik, ezért ott is tökéletesen használható volt.
IReport.cs: A kimutatásokhoz szükséges adatok lekérdező metódusai
tartoznak ide. Készült 4 különböző idő intervallumokra vonatkozó metódus (évi,
havi, heti és napi bontású), melyet később egyesítettem egy általánosabb
metódusba.
ITransaction.cs: Tranzakciók kezelése. Új tranzakció felvétele, meglévő
törlése, illetve lekérdezések tartoznak ide.
IUser.cs: A felhasználókra vonatkozó metódusgyűjtemény. Ide tartoznak a
következő funkciók: bejelentkezés, jelszó változtatás, törlés, lekérdezések,
beállítások mentése, illetve a jelszó egyezés ellenőrzése.
Természetesen az interfész metódusai által igényelt egyedi típusokhoz tartoznak
megfelelő osztályok, melyek mindegyike egy-egy DataContract.
4.2.1.3 Implementáció
A fentebb ismertetett interfészt egyetlen osztály valósítja meg, az
ExpenseService. Mint azt már olvashattuk, a teljes osztály itt is több fájlra lett bontva,
az interfész darabkákat tartalmazó fájlokkal összefüggésben:
ITransaction.cs TransactionOperations.cs,
ICurrency.cs CurrencyOperations.cs és így tovább…
Osztály szinten egy adatbázis kontextust tartok fenn, melyet az osztálypéldány
elpusztulásakor felszabadítok (Dispose minta [33]), így egy helyen cserélhető az aktuális
adatbázis kontextus és nincs szükség minden egyes adatbázis lekérdezésnél (valójában
LinQ to Entities lekérdezéseknél) létrehoznunk azt egy using blokkon belül. Hátránya,
hogy nincs lezárva az adatbázis kapcsolat a lekérdezés végrehajtása után.
49
A szolgáltatás implementációból egy kódrészletet emelnék ki némi magyarázattal
együtt. A valuta szolgáltatóval való kommunikáció részletébe pillanthatunk bele, mely
során az érkezett adatokat egy statikus gyűjteményben (Currencies) tárolom el a
szerver memóriájában.
CurrencyOperations.cs kódrészletek:
// Get currencies from provider if they are not up to date private void GetCurrenciesIfNotUpToDate() { if (Currencies == null || Currencies.Count <= 0 || CurrencyLastUpdatedDate.Date < DateTime.Now.Date) {
// block async operation (wait for the result) AsyncContext.Run(() => this.GetCurrenciesAsync()); } } // Trying to get currencies from the provider (openexchangerates) // in JSON format private async void GetCurrenciesAsync() { var httpClient = new HttpClient(); await httpClient.GetStreamAsync("http://openexchangerates.org/api" + "/latest.json?app_id=" + OpenExchangeApiKey) .ContinueWith(r => { Stream responseStream = r.Result; // Serializing response var serializer = new DataContractJsonSerializer( typeof(CurrencyResponse)); var jsonResponse = serializer.ReadObject(responseStream) as CurrencyResponse; if (jsonResponse == null) return; lock (CurrencyLock) {
// Uploading response to the currencies collection Currencies.Clear(); foreach (var item in jsonResponse.GetCurrencies()) { Currencies.Add(item); }
// Set currency last updated time to now CurrencyLastUpdatedDate = DateTime.Now; } }); }
50
A GetCurrenciesIfNotUpToDate() metódus minden valuta konvertálás
esetén hívódik meg, így egy ilyen környezetben képzeljük el. Hamar észrevehetjük, hogy
a metódus egyetlen feladata, hogy bizonyos feltétel esetén futassa a
GetCurrenciesAsync() függvényt. A feltétel akkor teljesül, ha üres a
valutaárfolyamokat tároló statikus kollekciónk (Currencies) vagy már elavult
árfolyamokat tartalmaz (jelenleg az 1 napnál régebbi árfolyamokat tekintem elavultnak).
Ekkor szeretnénk a külső árfolyam szolgáltatótól lekérdezni az aktuális állapotot és
feltölteni az árfolyam tárolónkat a friss adatokkal. Azt is megfigyelhetjük, hogy a hívandó
aszinkron valuta lekérdező függvény az AsyncContext.Run() metódusnak lett átadva
Action delegate típusú paraméterként. Ez az aszinkron kontextus a Nito AsyncEx
[35] osztálykönyvtárból származik. A feladata, hogy egy aszinkron függvényt szinkron
módon futtasson, tehát bevárja, míg feldolgozásra kerül a GetCurrenciesAsync()
függvényben indított httpClient.GetStreamAsync(<url>) kérés válasza, vagy
amíg nem keletkezik kivétel. Szükségem volt arra, hogy mindenképp szinkron módon
kérhessem el a valutákat, mert a hívás helye utána rögtön használni szeretném a friss
értékeket a valuta konvertáló függvényben.
Érdemes még figyelmet szentelni a valuták feltöltésekor használt zárra (lock).
Mivel egy web szolgáltatás kontextusában helyezkedik el az üzleti logika, ezért a
szolgáltatást megvalósító osztály statikus adattagjainak írásakor kétszer is fontoljuk meg
a zárak használatát. Úgy képzeljük el az egészet, mintha egy többszálú környezetben
lennénk, ahol párhuzamosan is indulhat egy-egy valuta-konvertálás. A többszálú
környezetet most a felhasználók kérései jelentik és bizony előfordulhat, hogy egyszerre
két kérés is befut, előteremtve a konkurenciát [35]. Ha nem szeretnénk, hogy hol az egyik,
hol a másik szál töltse fel a valuta gyűjteményt, ami könnyen duplikátumokat okozhat,
akkor érdemes zárolni a közösen használt erőforrást, hogy egyszerre csak egyvalaki férjen
hozzá, amíg a másik várakozni kényszerül.
Az üzleti logikát segítendő, a szolgáltatás tartalmaz pár statikus Helper metódust
is. Ide tartoznak a különböző bővítő metódusok (extension methods), a kimutatás diagram
színezéséhez használt színárnyalatot manipuláló függvények, adat-validációs
függvények, az e-mail küldéshez használt kódrészlet, illetve egy új véletlen jelszót
generáló függvény.
51
4.3 Kliens alkalmazások
Ebben az alfejezetben a megvalósított kliens alkalmazásokat tekinthetjük át.
Túlzottan részletes leírás nem férne bele a dolgozat keretébe, így a megvalósítás
mérföldköveit mutatom be, külön kiemelve a fejlesztői szempontból érdekes, kihívást
jelentő problémák megoldását.
4.3.1 Mobil kliens (WP8)
Az első kliens, ami a fejlesztés során elkészült, az a Windows Phone 8 platformra
íródott mobil alkalmazás.
4.3.1.1 Felhasználók kezelése
A felhasználói adatok tárolása úgy valósul meg, hogy a telefon helyi tárolójában,
az IsoStore-ban jegyzem fel (szerializálom9) az aktuálisan bejelentkezett felhasználóhoz
tartozó objektumot. Így, ha egyszer sikeresen beléptünk az alkalmazásba, akkor a további
használat során automatikusan be leszünk jelentkezve. A bejelentkező oldal csak akkor
jelenik meg, ha az alkalmazáshoz tartozó beállítások között nem található meg az aktuális
felhasználó adatai vagy éppen felhasználót váltunk.
4-2. ábra: Bejelentkezés és regisztráció mobilon
9 Szerializáció – segítségével egy objektum bájt-sorozatként reprezentálható egy fájlban. Ez a fájl
információt fog tartalmazni az objektum típusáról és minden adattagjáról, így az objektum állapotát
eltárolhatjuk.
52
A mobil kliens az aktuálisan bejelentkezett felhasználó mellett megjegyzi a
beállított nyelv, valuta konvertálás és az alapvaluta beállításokat is, mely beállításokat
szintén az IsoStore-ban tárolok.
4.3.1.2 Felület
A kliens felületét a METRO elveknek megfelelően próbáltam létrehozni, ahol a
hangsúly a tartalmon van. A kialakított felület így letisztult és átlátható képet ad a
felhasználók számára.
A kezdőképernyőn egy Panorama vezérlő található, három elemmel:
A Transactions (tranzakciók) oldal a tranzakciókhoz tartozó műveletek
lebonyolításáért felelős. A gombok felett található egy piros-zöld sáv, ami
a kiválasztott hónapban a kiadások/bevételek arányát szemlélteti. Továbbá
itt találhatók még a funkció gombok, melyek a kiválasztott művelethez
tartozó oldalra navigálnak minket.
Az Overview (áttekintő) oldalon megtekinthetjük a bevitt tételeket
részletesen napokra lebontva az aktuális hónapra vonatkozóan.
A Settings (beállítások) oldalon végezhetjük el az alkalmazásra érvényes
beállításokat, melyek automatikusan mentésre kerülnek az alkalmazás
bezárásakor, illetve deaktiválásakor. Itt állíthatjuk be a nyelvet
(magyar/angol), az alapvalutát, illetve a valuta átváltás módját (csak az
alapvalutabeli tételek megjelenítése vagy az összes tétel jelenjen meg
szükség esetén konvertálva az alapvalutára).
Az alábbi képen láthatjuk a panoráma vezérlő alkalmazását a főoldalon, melyen
minden lényeges funkciót hamar elérhetünk.
53
4-3. ábra: Panoráma főoldal
A legfontosabb funkció az alkalmazásban a tranzakciók bevitele. Tranzakció
hozzáadásakor a 4-4. ábra-n látható „Új tranzakció” feliratú képen is látható mezőket kell
megadnunk:
Amount (összeg): a tétel összege
Date (dátum): a tétel dátuma
Category (kategória): választhatunk megfelelő kategóriát attól függően,
hogy Income vagy Expense tételt rögzítünk. Lehetőségünk van új kategória
felvételére is, illetve egy kiválasztott kategória törlésére, ha admin
Location (helyszín): helyszín kiválasztása. Bing térkép segítségével tudjuk
kiválasztani a kívánt tartózkodási helyet (nem kötelező)
Az alábbi képen megtekinthetjük, hogyan néz ki egy új tranzakció hozzáadása és
néhány ehhez kapcsolódó választási lehetőség kezelőfelülete:
54
4-4. ábra: Új tranzakció hozzáadása
4.3.1.3 Kimutatás
A bevitt tranzakciókról célszerű készíteni valamilyen kimutatást is, hogy jobban
átlássuk a kiadott vagy éppen a bejövő összegek eloszlását. A tételek grafikus
reprezentálására mobil platformon a kördiagramot választottam, melyen éves, havi, heti,
illetve napi bontásban tekinthetjük meg kategóriánként csoportosítva a
bevételeinket/kiadásainkat.
4-5. ábra: Kördiagram kimutatás a kiadásokról és bevételekről a mobil kliensen
A kördiagram ábrázolásához készítettem egy saját vezérlőt (user control), melyet
egy külső osztálykönyvtárban (class library) implementáltam, lehetővé téve, hogy esetleg
más WPF/Silverlight felületű alkalmazásokban is felhasználható legyen.
Az alábbi metódus felelős egy körszelet kirajzolásáért:
55
private void drawPiChartSlice(double startAngle, double ratio) { Path path = new Path(); // . . . path kitöltés, vonalstílus Point point = new Point { X = 0.0, Y = 0.0 }; PathGeometry pathGeometry = new PathGeometry(); PathFigure pathFigure = new PathFigure(); pathFigure.StartPoint = new Point(0, 0); pathFigure.IsClosed = true; double angle = 360 * ratio / 100; if (angle == 360) { angle = 359.99; } // Starting Point LineSegment lineSegment = new LineSegment(); lineSegment.Point = new Point(this._radius, 0); // Arc ArcSegment arcSegment = new ArcSegment(); arcSegment.IsLargeArc = angle >= 180.0; arcSegment.Point = new Point(Math.Cos(angle * Math.PI / 180) * this._radius, Math.Sin(angle * Math.PI / 180) * this._radius); arcSegment.Size = new Size(this._radius, this._radius); arcSegment.SweepDirection = SweepDirection.Clockwise; pathFigure.Segments.Add(lineSegment); pathFigure.Segments.Add(arcSegment); pathGeometry.Figures.Add(pathFigure); path.Data = pathGeometry; RotateTransform rotate = new RotateTransform { CenterX = 0, CenterY = 0, Angle = startAngle }; path.RenderTransform = rotate; // Add pie chart slice to the placeholder this._piChartHolder.Children.Add(path); }
Bemeneti paraméterek:
startAngle – honnan kezdődjön a körszelet (fok)
ratio – körszelet mérete százalékban
56
Létrehozok egy PathGeomerty típusú változót, melyben felépítem a körcikk
alakzatot. A ratio értékéből kiszámítom a körcikk belső szögét (látószög). A
megjelenítéshez szükségem lesz LineSegment-re a határoló vonal megrajzolásához,
valamint ArcSegment-re, a cikkely kirajzolásához. A cikkely szegmenset 90 foktól
kezdve rajzolom ki, az előzőleg számított látószöget felhasználva. A felparaméterezett,
elkészült szegmenseket felveszem a PathGeometry alakzatok listájába. Mindezek után
szükségem lesz egy forgatásra, hogy a megfelelő irányba álljon a körcikk (itt használom
fel a startAngle paramétert). A visszatérés előtt hozzáadom az elkészült útvonalat (Path)
a tárolóhoz (placeholder), ami jelen esetben egy Canvas vezérlő.
4.3.1.4 Lokalizáció
Az alkalmazás jelenleg angol és magyar nyelv között képes váltani valós időben.
A Windows Phone telefonoknál egy szokás, hogy az operációs rendszer beállításaiban
kiválasztva a telefon használati nyelvét azt az alkalmazások figyeljék induláskor, így egy
közös helyen válthatunk nyelvet. Adódik azonban néhány probléma ezzel a megoldással:
nem tudunk valós időben nyelvet váltani
szükségünk lehet újraindítani a telefont egy új nyelv kiválasztása után
az éppen futó alkalmazásunk Running állapotból Tombstoned állapotba
kerül, tehát a használat folytonossága megtörik (lásd 2.3.2-es fejezetben)
Így feltétlenül szükségét éreztem annak, hogy a kliensalkalmazáson belül is
lehetőség legyen nyelvet változtatni. A megoldáshoz át kellett gondolnom, hogy
miképpen lehetne Culture-t váltani úgy, hogy arról az érintett elemek is értesüljenek.
Alapvetően minden UI elem, amin bármiféle szöveg van
a LocalizedStrings osztályra van kötve (bind-olva), ez tartalmazza az
aktuális AppResources referenciát (itt találhatóak a név – érték párokban megadott
szöveg fordítások).
Amikor a felületen a nyelvválasztó gombra kattintunk, akkor az alábbi metódus
gondoskodik arról, hogy kicserélje az aktuális alkalmazás Culture-t, ami egyben azt is
jelenti, hogy a megfelelő AppResources.resx fájl kerül felhasználásra (pl.
AppResources.hu.resx - magyar nyelv esetében). Az átadott paraméter a kívánt
kultúra rövidítése (hu, en-US):
57
public void SetUILanguage(string locale) { CultureInfo newCulture = new CultureInfo(locale); Thread.CurrentThread.CurrentCulture = newCulture; Thread.CurrentThread.CurrentUICulture = newCulture; AppResources.Culture = newCulture; ResourceTracker.OnPropertyChanged("Culture"); //. . . Set the FlowDirection of the RootFrame to match the new culture. //. . . Set the Language of the RootFrame to match the new culture. }
A bind-olt UI elemek azonban erről a változásról alapesetben nem fognak
értesülni, így létrehoztam egy statikus osztályt (ResourceTracker) egy eseménnyel,
amit minden AppResources.Culture értékadás után elsütök (kiemelt rész a kódban).
Erre az eseményre van feliratkozva a LocalizedStrings osztály (amire az UI
szövegek vannak kötve), amit kibővítettem, hogy implementálja
az INotifyPropertyChanged eventet. Így amikor elsül a ResourceTracker
eseménye, elsütök egy PropertyChanged eseményt a LocalizedStrings-en belül is
és így már frissülnek a felületen található szövegek is a kiválasztott nyelvnek
megfelelően.
58
4.3.2 Web kliens (ASP.NET MVC)
A web kliens fejlesztéséhez az ASP.NET MVC 4 –es keretrendszert használtam
fel. A szükséges üzleti logikát a szolgáltatás már tartalmazza, így az igényes megjelenés
és a felhasználóbarát környezet kialakítására került a fő hangsúly.
4-6. ábra: Web kliens főoldal
4.3.2.1 Felhasználók kezelése
A felhasználók kezelése során nem támaszkodtam az MVC 4 autentikációs
csomagjára, helyette a szolgáltatásban megvalósított felhasználó kezelést vettem igénybe
kiegészítve a Session használatával. Típusos és egységes Session kezelést valósítottam
meg kontroller szinten, az alábbi módon:
BaseController.cs részlet:
public class BaseController { . . . // Session handler property protected BaseSessionData BaseSession { get { string sessionName = this.GetType().Name; return (BaseSessionData)(Session[sessionName] ?? (Session[sessionName] = new BaseSessionData())); } } . . . }
59
[Serializable] public class BaseSessionData { public User User { get; set; } . . . }
Látható, hogy a BaseSessionData tároló osztályba csomagoltam a
felhasználóhoz tartozó adatokat. A Session kontrollerfüggő elemét a BaseSession
propertyben típusossá alakítva érhetjük el és így kényelmesen hozzáférhetünk az aktuális
felhasználói adatokhoz. Pl. int userid = BaseSession.User.Id értékadás a
Session-ből fogja előhalászni a felhasználó azonosítóját.
Szintén a BaseController-ben minden egyes kérés előtt ellenőrzöm, hogy az
aktuális felhasználó be van-e jelentkezve. Mivel a kontrollerek megvalósítják az
IActionFilter interfészt (Controller ősosztály implementálja), így lehetőségem
van beavatkozni az MVC szekvenciába, mielőtt egy Action-höz érkezne az eseménysor,
amennyiben felülírom (override) az OnActionExecuting(..) függvényt:
void IActionFilter.OnActionExecuting(ActionExecutingContext filterContext) { // If we are at the login page do nothing var action = filterContext.RouteData.Values["action"].ToString(); var controller = filterContext.RouteData.Values["controller"].ToString(); if (controller == "Account" && (action == "Login" || action == "Register" || action == "PasswordReminder")) return; // Check if the current user is logged in before executing any action if (!this.LoggedIn) { // We are not logged in -> Redirect to the Login page filterContext.Result = RedirectToAction("Login", "Account"); } // . . . Load user settings to a ViewBag base.OnActionExecuting(filterContext); }
Ha a bejelentkező oldal felől érkezik a kérés (Account controller,
Login/Register/PasswordReminder action-je felől), akkor tovább engedjük az action
végrehajtással kapcsolatos szekvenciát, különben pedig ellenőrizzük, hogy a felhasználó
valóban be van-e jelentkezve (ezt a BaseSession-ből könnyedén kideríthetjük). Ha
nincs, akkor a login oldalra irányítjuk.
60
A bejelentkezés, kijelentkezés, regisztráció, jelszó változtatás, beállítások
kezelése, valamint a felhasználó törlése funkciók természetesen már a szolgáltatással
kommunikálva valósulnak meg, szükség esetén módosítva a BaseSession-ben tárolt
aktuális felhasználóhoz tartozó tulajdonságok értékét.
4-7. ábra: Belépés és regisztráció felületének részlete
4.3.2.2 Kontrollerek
Minden egyedi vezérlőm a BaseController-ből származik, ami főleg a
felhasználók kezeléséről gondoskodik számunkra, mint ahogy azt az imént is láthattuk.
Emellett tartalmaz még néhány segédmetódust, melyekre szükségem lehet a leszármazott
vezérlőkben.
A kontrollerek fő feladata, hogy WCF-en keresztül kommunikáljanak a
szolgáltatással és átadják a szükséges adatokat a nézetek (View) számára. A vezérlőket
funkciójuk szerint csoportosítottam az alábbi osztályokra:
AccountController: Bejelentkező oldalhoz tartozó funkciók, illetve
bejelentkezés után a felhasználói profil funkciót menedzseli.
CategoryController: Ide tartozik a Categories oldal kiszolgálása
(kategóriák listázása), a kategória választó legördülő menük megjelenítése
(parciális nézetként), és természetesen a kategóriák kezeléséhez tartozó
segéd metódusok, action-ök.
CurrencyController: A valuta konvertáláshoz tartozó, listázó segéd
metódusok és a valutaválasztó legördülő elemet kiszolgáló action-ök
vannak itt.
61
HomeController: A főoldalon található elemek megjelenítése. A
kiadás/bevétel arányát megjelenítő „grafikon”-hoz szükséges adatokat
szolgáltatatja.
OverviewController: Az Overview oldal tranzakció listázásához, a
részletek megjelenítéséhez és egy-egy tétel törléséhez tartozó action-ök
tartoznak ide.
ReportController: A kimutatás diagramokhoz tartozó vezérlő.
Parciális nézetben jeleníti meg a kívánt szűrési feltételeknek megfelelő
grafikont.
TransactionController: Új tranzakció felvételére vonatkozó
kontroller.
4.3.2.3 Nézetek
Igyekeztem a megjelenítés során minél több komponenst a kliens oldalról indítva
AJAX10 hívásokkal betölteni, ezzel is kényelmesebbé és gyorsabbá téve a szoftver
használatát. Természetesen vannak olyan nézetek is, melyek nem tartalmaznak parciális
elemet, ezek a Controller megfelelő action-jének ViewResult-jaiből állnak elő. A
kliens oldali szkriptekre külön hangsúlyt fektettem, szintén a látvány és az élmény
fokozása érdekében. A 4-8. ábra-n megfigyelhetjük, hogyan zajlik azon nézetek
előállítása, ahol egy-egy komponens betöltése AJAX módon történik:
4-8. ábra: Nézet előállítása AJAX segítségével [7]
Először egy kontroller action-je kezdeményezi a teljes nézet előállítását (1), majd
a kliens oldali jQuery [36] kód segítségével aszinkron módon betöltjük a nézet hiányzó
10 AJAX – Asynchronous JavaScript and XML. Aszinkron kliens-oldali kérések valósíthatók meg
vele, az oldal újratöltése nélkül. Ez növeli a honlap interaktivitását, sebességét és használhatóságát.