-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 1 of 46
Programare Microsoft Visual Studio .NET
Lucrari de laborator
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 2 of 46
Cuprins
1 INTRODUCERE
....................................................................................................................................................
4
2 PREZENTAREA MEDIULUI DE PROGRAMARE VISUAL STUDIO .NET
............................................... 5
2.1 PRINCIPALELE FERESTRE DE LUCRU DIN VISUAL STUDIO
..................................................................................
7 2.2 COMENZILE DE LUCRU UTILIZATE IN VS
...........................................................................................................
7
3 DEFINIREA CLASELOR IN OOP
......................................................................................................................
9
3.1 PROPRIETATI, CONSTRUCTORI. TIPUL ENUM SI DATETIME
...............................................................................
9 3.2 INSTANTIEREA CLASELOR
................................................................................................................................
10 3.3 CONSTRUCTORI PRIVATI. OBIECTELE TIMER SI RANDOM
................................................................................
11
4 PREZENTAREA DATELOR
.............................................................................................................................
15
4.1 SOLUTII CU PROIECTE MULTIPLE. IERARHIZAREA PROIECTELOR
......................................................................
15 4.2 FEREASTRA OUTPUT
.......................................................................................................................................
16 4.3 PROGRAMARE BAZATA PE EVENIMENTE. OBIECTELE "DELEGATE" SI
"EVENT" ............................................... 17 4.4
PREZENTAREA DATELOR IN DATAGRID
...........................................................................................................
19
4.4.1 Controalele de tip GUI nu sunt thread-safe
...........................................................................................
21 4.5 CONTROALE VIZUALE: CHECKBOX, TEXTBOX. CLASA DICTIONARY
..............................................................
23
5 SALVAREA INFORMATIILOR IN BAZA DE DATE
...................................................................................
30
5.1 CREAREA BAZEI DE DATE SQLITE
...................................................................................................................
30 5.2 DATA ACQUISITION LAYER : INTERFATA INTRE BAZA DE DATE SI
APLICATIA GUI .......................................... 31
5.2.1 Definirea string-ului de conectare la baza de date in
pagina de proprietati a proiectului .................... 32 5.3
CLASA DE INTERFATA CU BAZA DE DATE
.........................................................................................................
33
5.3.1 Salvarea informatiilor in baza de date
...................................................................................................
33 5.3.2 Citirea informatiilor din baza de date
....................................................................................................
36
6 TRANSMITEREA DATELOR INTRE APLICATII. SOCKET TCP/IP
....................................................... 40
6.1 OBIECTUL TCPCOMMCLIENT
.........................................................................................................................
41 6.1.1 Impachetarea datelor pentru transmisie
.................................................................................................
42 6.1.2 Crearea unui nou fir de executie pentru fiecare dialog cu
serverul ....................................................... 43
6.1.3 Trimiterea datelor catre
server...............................................................................................................
44 6.1.4 Functia Dispose inchide firele de executie
suspendate...........................................................................
45
6.2 OBICETUL TCPCOMMSERVER
........................................................................................................................
46
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 3 of 46
Lista de figuri Figura 1: Visual Studio apare ca devenv.exe in
lista de procese din Task Manager
....................................... 5 Figura 2: Crearea unui
nou proiect de tip "Console Application" in Visual Studio .NET
.................................. 6 Figura 3: Principalele
ferestre de lucru ale mediului Visual Studio
..................................................................
7 Figura 4: Clasa "SensorBase" si enumerarea "SensorType"
...........................................................................
9 Figura 5: Constructorii clasei SensorBase
.....................................................................................................
10 Figura 6: Selectarea unei valori dintr-o enumerare
........................................................................................
10 Figura 7: Instantierea clasei "SensorBase" folosind
constructorul implicit
..................................................... 11 Figura 8:
Constructorii clasei PumpSensorValues
.........................................................................................
12 Figura 9: Functia de generare si afisare a valorile aleatoare
pentru senzori ................................................. 13
Figura 10: Intantierea clasei "PumpSensorValues" si pornirea
ceasului ....................................................... 13
Figura 11: Selectarea unui proiect ca referinta pentru proiectul
curent ......................................................... 15
Figura 12: Lansarea in executie a functiei de simulare a senzorilor
.............................................................. 16
Figura 13: Fereastra Output afiseaza mesajele trimise la consola
................................................................ 16
Figura 14: atasarea functiei "bTest_Click" la evenimentul
"bTest.Click"
........................................................ 17 Figura
15: Definirea obiectelor de tip delegate si event in clasa de
generare a datelor de masurare ........... 17 Figura 16: Lansarea
evenimentului "newSensorValueEvent"
........................................................................
18 Figura 17:Atasarea functiei "OnNewSensorValueHandler" la
evenimentul "newSensorValueEvent" ........... 18 Figura 18:Valorile
primite prin eveniment de la clasa "PumpSensorValue" sunt afisate
in MessageBox ..... 19 Figura 19: Controlul DataGridView din
ToolBox
.............................................................................................
19 Figura 20:Setarea sursei de date pentru gridul de afisare
.............................................................................
20 Figura 21: Popularea listei sensorValueList si setarea ei ca
DataSource pentru grid ................................... 21
Figura 22: Eroare: un alt thread acceseaza datagridul
..................................................................................
21 Figura 23: Fereastra principala de prezentare a datelor cu
datagrid-ul legat la lista de valori ...................... 23
Figura 24: Adaugarea codului de pacient in clasa cu valorile
senzorilor .......................................................
24 Figura 25: Patient code trebuie adaugat si in clasa
PumpSensorValues
...................................................... 24 Figura
26: Evenimentul "newSensorValueEvent" include si codul de pacient
............................................... 24 Figura 27:
Adaugarea coloanei PatientCode in
datagrid................................................................................
25 Figura 28: Fereastra DataPresentation cu butoanele de startare
si oprire a monitorizarii ............................. 25 Figura
29: Enumerarea ce include codurile de pacienti
.................................................................................
26 Figura 30: Atasarea unei enumerari la un
CheckBox.....................................................................................
26 Figura 31: Codul aferent butonului "Start Monitoring"
....................................................................................
26 Figura 32: Functia StartPumping cu parametrii cod pacient si
perioada de timp ........................................... 27
Figura 33: Oprirea monitorizarii pentru un pacient si eliminarea
lui din dictionarul pacientilor activi ............. 28 Figura 34:
Crearea bazei de date PatientData in SQLite Administrator
...................................................... 30 Figura
35: Crearea tabelului PatientData pentru salvarea datelor ce vin de
la pacienti .............................. 31 Figura 36: Adaugarea
proiectului DataStore de tip Class Library la solutie
.............................................. 32 Figura 37:
Definirea string-ului de conectare la baza de date SQLite in pagina
de proprietati a proiectului . 33 Figura 38: Functia de inserare a
unei valori de masurare in baza de date
.................................................... 34 Figura 39:
Tratarea evenimentului newSensorValueEvent: salvare in baza si
afisare in datagrid .............. 35 Figura 40: Comanda SQL de
vizualizare a datelor din tabela PatientData
.................................................... 35 Figura 41:
Vizualizarea datelor din baza in SQLite Administrator
..................................................................
35 Figura 42: Fereastra DataPresentation dupa ce s-a adaugat
sectiunea de filtrare ...................................... 36
Figura 43: Functia de citire din baza de date a datelor de masurare
pentru un pacient si o zi stabilita ........ 37 Figura 44:
Proiectul CommonReferences contine toate definitiile particulare
ale tipurilor de date ............. 38 Figura 45: Afisarea
valorilor din baza de date
................................................................................................
39 Figura 46: Functia handler pentru butonul Display Received Data
.............................................................
39
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 4 of 46
1 Introducere Scopul lucrarilor de laborator prezentate in acest
document este de familiariza studentul cu mediul de programare
Visual Studio .NET si aplicarea practica a cunostintelor prezentate
la curs. In acest scop, se propune in cele ce urmeaza realizarea
unei miniaplicatii numite "HealthMonitor" care sa monitorizeze de
la distanta starea unui pacient cu probleme de sanatate, in sensul
ca pacientul poate sa ramana acasa purtand cu el diversi senzori de
masurare a bio-parametrilor (temperatura, ritm cardiac, glicemie,
etc) si aceste valori sunt trimise prin internet la cabinetul
doctorului care urmareste astfel starea pacientului si se
deplaseaza numai la aparitia unei alarme. Ce presupune aceasta
monitorizare?
Citirea parametrilor bio
Salvarea valorilor masurate intr-o baza de date locala
Transmisia valorilor la aplicatia ce ruleaza in cabinetul
doctorului
Afisarea acestor valori sub forma grafica sau text pentru ca
doctorul sa poata trage concluziile corecte referitoare la starea
de sanatate a pacientului
Prin implementarea acestor taskuri, studentul va trebui sa-si
insuseasca si sa lucreze cu urmatoarele concepte:
Definirea claselor in OOP
Instantierea claselor
Mostenire
Controale grafice
Salvarea datelor in baza de date
Comunicatii TCP/IP
Multithreading
Programare bazata pe evenimente
... Evident ca aceste concepte nu pot fi acoperite in totalitate
intr-un numar atat de mic de ore, dar acest exemplu poate
reprezenta un punct de plecare pentru dezvoltarea de alte aplicatii
serioase si aprofundarea cunostintelor legate de programare obiect
si Visual Studio .NET.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 5 of 46
2 Prezentarea mediului de programare Visual Studio .NET Mediul
Visual Studio se deschide prin lansarea in executie a programului
"C:\Program Files\Microsoft Visual Studio
9.0\Common7\IDE\devenv.exe". Trebuie retinut numele programului
"devenv.exe" deoarece de multe ori este utila sa deschidem
aplicatia "Task Manager" din Windows si sa analizam modul de
ocupare a memoriei si a microprocesorului in timpul rularii unui
program. Faptul ca apare "devenv.exe" in capul listei din tab-ul
"Processes" cu o portiune mare de memorie utilizata, inseamna de
multe ori ca programul a ramas agatat pe un fir de executie infinit
si trebuie oprit din TaskManager.
Figura 1: Visual Studio apare ca devenv.exe in lista de procese
din Task Manager Dupa lansarea VS, primul pas este de a deschide un
proiect deja existent pe dicul local, sau putem crea un nou proiect
(meniul File/New/Project..")
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 6 of 46
Figura 2: Crearea unui nou proiect de tip "Console Application"
in Visual Studio .NET Putem alege din mai multe tipuri de proiecte,
dupa cum urmeaza:
Windows Form application: este pentru dezvoltarea de aplicatii
de tip GUI, bazate pe ferestre, controale grafice, etc.
WPF Application: dezvolta acelasi tip de aplicatii de tip GUI,
dar bazate pe noua tehnologie WPF (Windows Presentation Foundation)
ce expune posibilitati mult mai largi de desenare grafica a
interfetelor utilizator. WPF lucreaza cu limbajul XAML prin care se
pot descrie imagine grafice de pe ecran. Folosind acest limbaj,
utilizatorul are posibilitatea sa deseneze interfata grafica intr-o
aplicatie specifica de desenare, exporta imaginea in format XAML si
aceasta se importa in Visual Studio unde i se poate atasa
evenimente si logica necesara aplicatiei.
Console Application: cel mai simplu proiect, nu are interfata
grafica, este foarte util in testarea rapida a unor concepte de tip
OOP.
Windows Service: dezvoltarea unui serviciu Windows. Serviciile
sunt acele aplicatii care pornesc automat la deschiderea
calculatorului si ofera suport in background pentru diverse
functionalitati ale sistemului. De exemplu, baza de date Oracle
lucreaza ca un serviciu, porneste odata cu sistemul si deschide un
"Listener" ce asculta in mod permanent la un port pentru a raspunde
eventualelor comenzi SQL trimise de un client.
Proiecte de tip "Library": nu au interfata cu utilizatorul, nu
pot fi pornite direct pentru ca nu contin functia "Main". Ele sunt
folosite doar pentru crearea diverselor functii de biblioteca ce
vor fi utilizate in programele de tip "application". Librariile
sunt salvate pe disc sub forma de fisiere DLL ce trebuie importate
in spatiul de lucru al proiecteor ce vor face apel la ele.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 7 of 46
2.1 Principalele ferestre de lucru din Visual Studio Dupa
deschiderea unui proiect, Visual Studio afiseaza un ecran de lucru
ce cuprinde urmatoarele ferestre utile:
Figura 3: Principalele ferestre de lucru ale mediului Visual
Studio
Fereastra "Toolbox": contine majoritatea controalelor utilizate
in crearea aplicatiilor: butoane, TextBox-uri, grid-uri,
conexiuni,etc. Sunt foarte multe controale implicite ale mediului
VS, dar pot fi adaugate de asemenea alte controale din diverse
librarii sau instalari de aplicatii.
Fereastra de editare a codului de program: aici se introduce
efectiv codul sursa al programului
Fereastra "Solution Explorer" afiseaza structura solutiei:
proiectele ce apartin de aceasta solutie, proprietatile fiecarui
proiect in parte, fisierele ce compun proiectul.
Properties: este fereastra ce afiseaza proprietatile si
evenimentele asociate obiectului curent selectat in pagina de
lucru
Error List: listeaza erorile aparute la compilarea sau executia
programului
2.2 Comenzile de lucru utilizate in VS Comenzile des folosite in
editarea si rularea unui program sunt:
F5: compileaza si lanseaza in executie programul curent (in
modul Debug)
CTRL+S: salveaza fisierele de lucru
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 8 of 46
F6 (Build solution): compileaza solutia
SHIFT+F5: opreste executia programului din modul Debug
F10: executie pas cu pas (functia apelata se considera a fi un
pas)
F11: executie pas cu pas cu intrarea in functia apelata
F9: defineste sau sterge un breakpoint in linia curenta
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 9 of 46
3 Definirea claselor in OOP
3.1 Proprietati, Constructori. Tipul Enum si DateTime Se
deschide mediul VS si creaza un nou proiect de tip "Console
Application" numit "Test OOP". Se creeaza un nou folder in cadrul
proiectului numit "Sensor" si in acest folder se defineste o noua
clasa numita "SensorBase". In cadrul acestei clase se definesc
urmatoarele componente:
Membri privati: type, value, timeStamp
Proprietatile publice Type, Value, TimeStamp
Constructorii: - Implicit - public SensorBase(SensorType type,
double value, DateTime timeStamp) - public SensorBase(SensorType
type, double value, string timeStamp)
Figura 4: Clasa "SensorBase" si enumerarea "SensorType"
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 10 of 46
Figura 5: Constructorii clasei SensorBase
Lista de senzori potentiali cu care poate lucra programul este
tinuta de o enumerare publica numita "SensorType". Enumerarile sunt
constructii simple in care se pot da nume sugestive pentru numerele
naturale. De exemplu, este mult mai sugestiv sa primesti un
parametru numit "HeartRate" decat numarul 2 cat reprezinta acel
parametru. Mai mult, daca se sterge sau se adauga un termen din
enum, programul nu se modifica cu nimic, in continuare functia va
primi parametrul "HeartRate", chiar daca acum acel parametru are
valoarea 1. Alegerea unei valori din enum se face simplu, se scrie
numele enum-ului si se alege valoarea corespunzatoare:
Figura 6: Selectarea unei valori dintr-o enumerare
Se observa ca in lista de proprietati a clasei proprietatea
TimeStamp (timpul la care s-a facut masurarea) apare de doua ori.
Prima varianta TimeStamp primeste si returneaza o clasa de tip
DateTime, iar a doua varianta primeste un string si converteste
acel string intr-o structura DateTime. Conversia unei date in
string
se face cu metoda ToString (timeStamp.ToString("dd-MMM-yy
HH:mm") ), metoda ce primeste ca
parametru stringul de formatare a datei calendaristice.
Conversia inversa se face cu functia statica a clasei DateTime
numita ParseExact : DateTime.ParseExact(value, "dd-MMM-yy
HH:mm",CultureInfo.InvariantCulture)
3.2 Instantierea claselor Se deschide fereastra programului
principal (Program.cs) si se scrie codul pentru instantierea clasei
SensorBase folosind constructorul implicit. Dupa crearea obiectului
"sensor1" se folosesc proprietatile acestuia pentru introducerea
valorile de masurare (Type, TimeStamp si Value). Toate aceste
valori se afiseaza apoi la consola prin apelul functiei
DisplaySensorValue. Aceasta functie primeste doi parametri:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 11 of 46
o un string numit "headerText" care va fi afisat in capul listei
de valori o o instanta a clasei "SensorBase" care contine
informatiile de afisat
Figura 7: Instantierea clasei "SensorBase" folosind
constructorul implicit Sa se defineasca un alt obiect de tip
"SensorBase", dar utilizand de data aceasta constructorul cu
parametri Sa se afiseze valorile acestui obiect prin apelul
functiei "DisplaySensorValue()". Intrebare: de ce a fost necesar sa
se declare functia "DisplaySensorValue()" de tip static?
Stergeti
atributul "static" din definitia functiei si observati
rezultatul.
3.3 Constructori privati. Obiectele Timer si Random Cum nu avem
senzorii in mod efectiv, va trebui sa construim o clasa care sa
simuleze functionarea reala a acestor senzori. Aceasta clasa va
trebui sa instantieze in mod aleatoriu un senzor din lista
"SensorType" si apoi sa genereze o valoare aleatoare intr-un
domeniu specific acelui senzor:
Temperatura poate varia intre 36 si 40 de grade
Glicemia intre 80 si 300 mg/dl
Pulsul inimii intre 30 si 200 Functia de simulare trebuie sa
creeze aceasta valoare aleatoare in mod continuu cu o frecventa
data de functia principala Main.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 12 of 46
In folderul "Sensor" se creeaza o noua clasa numita
"PumpSensorValues". Aceasta va avea doar un singur constructor
public, cel ce primeste numarul de secunde intre doua valori ale
senzorilor.
Figura 8: Constructorii clasei PumpSensorValues
Constructorul implicit va fi declarat privat pentru a nu permite
instantierea unei clase fara sa se stabileasca perioada intre doua
valori.
Incercati sa creati un obiect de tipul "PumpSensorValues" prin
constructorul implicit si observati eroarea generata de VS
In constructorul clasei sunt create doua obiecte de uz general.
Prima clasa instantiata este de tipul Random. Acesta clasa este
folosita pentru generarea numerelor aleatoare. Se instantiaza o
singura data la intrarea in constructor si se poate apela oriunde
in program pentru a obtine un numar aleator intre doua limite date.
Generarea numarului aleator se face prin apelul functiei Next cu
cele doua variante:
Next(intMax): genereaza un intreg aleator mai mic decat
intMax
Next(intMin, intMax): numarul aleator este cuprins intre intMin
si intMax. Al doilea obiect utilizat este de tipul
System.Timers.Timer. Acest obiect lucreaza ca un ceas ce poate fi
programat sa apeleze o functie periodic la un interval de timp dat.
De fapt, la sfarsitul fiecarui interval de timp, timerul va genera
un eveniment "Elapsed" la care noi trebuie sa ne abonam ca sa-l
interceptam in mometul emiterii: timerBase.Elapsed += new
ElapsedEventHandler(timerBase_Elapsed);
Functia ce va fi apelata la fiecare tact al timerului este
"timerBase_Elapsed". In aceasta functie trebuie sa selectam in mod
aleator un tip de senzor si sa-i dam o valoare, apoi sa afisam
proprietatile senzorului la consola.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 13 of 46
Figura 9: Functia de generare si afisare a valorile aleatoare
pentru senzori
Tot ce mai trebuie facut ca sa apara valorile la consola, este
sa instantiem clasa PumpSensorValues si sa pornim ceasul din clasa
respectiva:
Figura 10: Intantierea clasei "PumpSensorValues" si pornirea
ceasului
Nu trebuie uitat la iesirea din program sa se opreasca ceasul
pentru a nu mai trimite evenimente pe care sa nu le intercepteze
nimeni.
Intrebare: de ce am putut apela functia "DisplaySensorValues" in
clasa "PumpSensorValues" fara sa am vreo instanta la clasa
"Program" ?
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 14 of 46
Functiile StartPumping si StopPumping sunt urmatoarele: public
void StartPumping()
{
timerBase.Start();
}
public void StopPumping()
{
timerBase.Stop();
}
In functia "StartPumping()" pornesc ceasul timerBase si acesta
imi va trimite evenimente la fiecare "Interval" de timp.
"StopPumping" trebuie apelata la sfarsitul programului pentru a
opri ceasul sa mai trimita evenimente.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 15 of 46
4 Prezentarea datelor
4.1 Solutii cu proiecte multiple. Ierarhizarea proiectelor Pana
acum am reusit sa generam date pentru senzorii de masurare si sa le
afisam in fereastra de tip consola. Dar acest tip de afisare nu
este convenabila pentru doctor. Doctorul ar avea nevoie de un ecran
care sa-i afiseze valorile sosite continuu de la senzor, sa le
poata sorta dupa tipul de senzor, sa-i fie semnalate alarme in
cazul unei valori ce depaseste anumite limite. In acest scop vom
dezvolta un nou proiect de tip Windows Form Application care va
avea drept scop afisarea intr-o maniera grafica a rezultatelor de
masurare. Mai intai, se redenumeste solutia in "HealthMonitor"
(click dreapta pe numele solutiei si alegem meniul "Rename"), iar
proiectul TestOOP se redenumeste "SensorInput". Deci ne propunem o
solutie pentru monitorizarea starii de sanatate a pacientului, iar
in cadrul acestei solutii proiectul deja construit are rol de a
simula functionarea reala a senzorilor. Se deschide un nou proiect
in cadrul solutiei pentru prezentarea datelor venite de la senzori.
Acest nou proiect se denumeste "DataPresentation" si se alege sa
fie proeictul de pornire a solutiei. Cand o solutie contine mai
multe proiecte trebuie ales proiectul de pornire, cel ce va lansa
functia Main(). Acest nou proiect trebuie sa afiseze datele
furnizate de celalalt proiect (SensorInput). Totusi, proiectele
sunt independente si nu se pot apela in mod direct functii dintr-un
proiect in altul. De aceea,trebuie alcatuita o ierarhie de
proiecte, in care un proiect poate face apel la clasele dintr-un
alt proiect aflat pe o ramura inferioara in cadrul ierarhiei.
Includerea unui proiect copil in cadrul ierarhiei se face prin
adaugarea acelui proiect la referintele proiectului tata. Se face
click dreapta pe "References" la proiectul "DataPresentation" si se
alege Add Reference:
Figura 11: Selectarea unui proiect ca referinta pentru proiectul
curent
Referintele pot fi adaugate din universul .NET unde se gasesc
majoritatea bibliotecilor de programe utile, sau din tab-ul "COM"
ce contine controale particularizate, sau "Projects" unde sunt
listate toate proiectele curente ale solutiei curente.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 16 of 46
Se alege tab-ul "Projects" si se selecteaza proiectul
"SensorInput" pentru a se adauga la referintele proiectului curent.
Din acest moment proiectul "DataPresentation" poate instantia si
utiliza toate clasele publice declarate in proiectul
"SensorInput".
4.2 Fereastra Output Se modifica constructorul proiectului
"DataPresentation" pentru a lansa in executie functia
"StartPumping" din clasa "PumpSensorValues":
Figura 12: Lansarea in executie a functiei de simulare a
senzorilor
Chiar daca functia de simulare a senzorilor a pornit, in
fereastra "DataPresentation" nu se vede nimic. Nici nu are cum, din
moment ce functia de simulare trimite valorile generate la consola.
Iar acum consola nu este vizibila pentru ca proiectul curent
"DataPresentation" este de tip "Windows Form Application" si nu
afiseaza consola. Totusi, putem vedea toate mesajele trimise la
consola in timpul executiei unui proiect daca se activeaza
fereastra "Output" (meniul View/Output din VS). Aceasta fereastra
se deschide in timpul executiei programului si afiseaza mesajele de
consola ale aplicatiei:
Figura 13: Fereastra Output afiseaza mesajele trimise la
consola
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 17 of 46
4.3 Programare bazata pe evenimente. Obiectele "Delegate" si
"Event" Am vazut ca clasa "PumpSensorValues" afiseaza datele doar
la consola. Si totusi, noi am dori ca aceste date sa fie afisate de
fereastra "DataPresentation". In schimb, "PumpSensorValues" nu are
nici o informatie despre clasa container in care va fi utilizata,
deci nu are acces la nici un obiect din aceasta clasa. Este aceeasi
problema pe care o are obiectul "Button" dintr-o forma oarecare. In
momentul cand utilizatorul face click pe buton, acesta ar trebui sa
execute o actiune in clasa container, cea care il contine. Dar
evident, butonul nu stie nimic despre clasa container, el este un
obiect din biblioteca .NET si este instantiat in mii de aplicatii
de tipul "Windows Form". Tot ce poate sa faca un buton in momentul
cand user-ul face "click" este sa lanseze evenimentul "Click". Daca
este cineva sa-l asculte (adica s-a scris o functie care sa prinda
evenimentul) atunci actiunea click va avea afect, altfel
evenimentul se pierde in eter. Se poate face o asemanare cu
actiunile unui catel de paza. Catelul de paza (ca si butonul) cand
se naste, nu stie unde va pazi si cine-l va asculta. Daca este pus
sa pazeasca o vie de exemplu, tot ce poate el este sa faca galagie,
adica sa emita evenimente (in cazul de fata evenimentul
"Thief_Inside"). Daca este cineva sa-l asculte bine, daca nu,
evenimentele se pierd. Pentru a putea lansa si prinde evenimente,
trebuie realizate cateva actiuni:
Definirea obiectulului "Delegate": reprezinta amprenta functiei
care trebuie sa prinda acel eveniment.
Definirea obiectulului "Event": este evenimentul
propriu-zis.
Lansarea evenimentului
Prinderea evenimentului de functia "handler" Toate aceste
actiuni se fac automat cand se face double-click pe un buton in
fereastra de design. VS-ul creeaza automat o functie (handler) care
sa trateze evenimentul trimis de buton si tot ce avem noi de facut
este sa introducem cod in acea functie. Daca ne uitam in background
(Designer.cs), observam ca in acel fisier ,VS-ul introduce cod
pentru legarea evenimentului la functie:
Figura 14: atasarea functiei "bTest_Click" la evenimentul
"bTest.Click"
Revenim la aplicatia noastra si sa parcurgem pasii pentru
lansarea si prinderea evenimentului ce anunta o noua valoare de la
senzori. Mai intai, in clasa ce pompeaza date definim delegatul si
evenimentul:
Figura 15: Definirea obiectelor de tip delegate si event in
clasa de generare a datelor de masurare
Intai se defineste delegatul in afara oricarei clase. Este ca
orice definitie de functie, numai ca i se pune in fata termenul
"delegate". In acest fel se defineste amprenta functiei care
trebuie sa prinda evenimentul. Evenimentul se declara in interiorul
clasei si trebuie sa fie public pentru a fi cunoscut si in afara
clasei. Lansarea acestuia se face simplu, este ca un apel de
functie:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 18 of 46
Figura 16: Lansarea evenimentului "newSensorValueEvent"
Intotdeauna, inainte de lansare trebuie verificat daca
evenimentul este diferit de null. Un eveniment este nul pana in
momentul cand se ataseaza un handler la el. Daca nu are atasat nici
un handler care sa-l intercepteze, nu are sens sa se trimita
evenimentul, de aceea VS-ul il mentine nul. Atasarea handler-ului
la eveniment se face in clasa container "DataPresentation":
Figura 17:Atasarea functiei "OnNewSensorValueHandler" la
evenimentul "newSensorValueEvent"
In constructorul clasei "DataPresentation" se instantiaza clasa
"PumpServerValues(3)", se porneste functia de generare a datelor
(StartPumping) si se ataseaza handlerul la eveniment. La fiecare 3
secunde functia "StartPumping" va genera un eveniment avand
valorile senzorului ca argument care in final va fi tratat in
handler. Pentru moment, handler-ul doar afiseaza un "MessageBox" cu
valorile senzorului. Dupa lansarea in executie a proiectului, in
fereastra clasei "DataPresentation" va aparea la fiecare 3 secunde
un mesaj cu valorile primite in argument.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 19 of 46
Figura 18:Valorile primite prin eveniment de la clasa
"PumpSensorValue" sunt afisate in
MessageBox Stop 15 Martie 2010
4.4 Prezentarea datelor in DataGrid Scopul final al proiectului
e sa prezentam datele nu prin mesaje, ci folosind un grid care sa
se populeze cu datele venite de la senzori. Proiectul
"DataPresentation" este de tip "Windows Form Application", deci in
cadrul lui avem acces la controalele vizuale din ToolBox. Putem
gasi la sectiunea "Data" un control de vizualizare de tip grid:
Figura 19: Controlul DataGridView din ToolBox
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 20 of 46
Selectam acest control si facem drag-and-drop in fereastra
principala a proiectului. El va fi instantiat automat in functia
"InitializeComponents" a formei, noi trebuie doar sa-i setam
proprietatile in stilul dorit. Dar in primul rand trebuie setate
numele gridului si "DataSource", adica sursa de unde gridul isi va
lua datele ca sa le afiseze:
Figura 20:Setarea sursei de date pentru gridul de afisare
Se pune numele "dgSensorValueList" iar pentru DataSource se
merge pe butonul de rulare in jos si se alege din fereastra aparuta
linkul "Add project data source..". Se alege apoi data source de
tip Object unde regasim toate clasele definite de noi si care pot
juca rolul de sursa pentru grid. Din namespace-ul "SensorInput"
alegem clasa "SensorBase" drept sursa pentru grid. Odata selectata
sursa, VS-ul creeaza automat un obiect de legatura intre grid si
clasa sursa. Acest obiect este de tipul "BindingSource" si apare
automat in partea de jos a ferestrei de design. Daca ne uitam in
functia "InitializeComponents" putem gasi codul scris in mod
automat pentru legarea gridului la BindingSource si BindingSource
la SensorBase:
Se creeaza un obiect de tipul BindingSource:
this.sensorBaseBindingSource = new
Forms.BindingSource(this.components);
Se alege SensorBase ca DataSource pentru acest BindingSource:
this.sensorBaseBindingSource.DataSource =
typeof(SensorInput.SensorBase);
Se ataseaza BindingSource la datagrid:
this.dgSensorValueList.DataSource =
this.sensorBaseBindingSource;
Dupa aceasta bindare gridul va avea coloanele setate dupa
proprietatile publice din SensorBase. Se poate modifica modul de
prezentare a datelor (coloanele afisate, fontul dorit, headere si
multe altele) din fereastra de proprietati a gridului. Dupa setarea
tuturor proprietatilor pentru datagrid se ruleaza programul si se
observa ca gridul apare in fereastra cu toate coloanele stabilite
in design. Si totusi, nu are nici o linie de date inclusa.Si asta
deoarece clasa "SensorBase" este doar o definitie, ea nu are
efectiv date in ea. Se populeaza gridul cu date numai daca ii dam
ca sursa o lista de obiecte de tip SensorBase, fiecare obiect avand
valorile date de clasa PumpSensorValues.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 21 of 46
Figura 21: Popularea listei sensorValueList si setarea ei ca
DataSource pentru grid
Definim in clasa DataPresentation o lista de tipul SensorBase
List sensorValueList = new List();
Iar in handler-ul de tratare a evenimentului primit de la
sensor, comentam linia cu MessageBox-ul (nu mai vreau sa afisez
informatia prin mesaj) si inserez linia: this.BeginInvoke(new
VoidFunctionDelegate(BindDataGridToListOfValues));
4.4.1 Controalele de tip GUI nu sunt thread-safe In loc de linia
cu BeginInvoke, as fi vrut sa apelez direct functia
BindDataGridToListOfValues(). Dar daca facem asa ceva, apare o
eroare foarte interesanta la executie:
Figura 22: Eroare: un alt thread acceseaza datagridul
Care este problema de fapt? Eroare spune ca un alt thread (fir
de executie) decat cel care l-a creat, acceseaza datagridul. Ce
sunt thread-urile sau firele de executie. Se stie ca Windows-ul
poate lansa mai multe aplicatii simultan. Pentru ficare aplicatie
cand e pornita, Windows-ul creeaza un nou fir de executie
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 22 of 46
pentru acea aplicatie si-i da drumul sa ruleze. In acest fel se
aduna mai multe fire de executie, toate cerand timp microprocesor
pentru executie. In functie de prioritatea lor, Windows-ul le
acorda la fiecare un timp microprocesor in care acel thread sa-si
faca treaba. Deci microprocesorul porneste un thread, apoi il
opreste si da microprocesorul la alt thread, apoi la altul, apoi se
intoarce iar la primul si tot asa. Acelasi multithreading se
intampla si-n aplicatia noastra. Clasa PumpSensorValues are o
functie de pornire a ceasului: public void StartPumping()
{
timerBase.Start();
}
Aceasta functie aparent nevinovata de fapt creeaza un alt thread
in care va lucra ceasul. Ceasul este un obiect ce lucreaza in
background si nu are sens sa ocupe din threadul curent de executie.
El isi deschide un thread separat de unde trimite din cand in cand
evenimente de tip "Time_Elapsed". Tehnica programarii de tip
multithreading este foarte utila atunci cand trebuie indeplinite
taskuri mari consumatoare de timp: download-area unui fisier mare
de pe internet, accesul la o baza de date, deschiderea de fisiere
mari, etc. Daca aceste taskuri s-ar executa in acelasi fir de
executie cu cel pricipal ce raspunde de interfata cu utilizatorul,
atunci aceasta interfata ar fi inghetata (froozen) pe timpul
rularii lor. Adica programul nu mai raspunde la nici o actiune a
utilizatorului deoarece este captiv in acel task lung. De aceea se
prefera ca acel task se porneasca pe un alt thread, iar threadul
principal sa ramana activ pentru a raspunde la comenzile
utilizatorului, si cand threadul secundar se termina, sa trimita un
eveniment catre threadul principal. Cam asa lucreaza si Timer-ul,
el ruleaza pe un thread secundar de unde trimite evenimente catre
threadul nostru principal: cel ce afiseaza fereastra si raspunde la
butoane. Toate bune si frumoase pana cand threadul secundar arunca
un eveniment, acesta este prins intr-o functie si acea functie vrea
sa acceseze datagrid-ul (sau orice alt control vizual din
fereastra). In acel moment apare eroarea descrisa mai sus, deoarece
controalele vizuale nu sunt "thread-safe", adica nu lucreaza bine
pe mai multe fire de executie. Nici nu au cum daca ne gandim ca
threadurile lucreaza intretesut, adica se intrerup unul pe altul in
mod permanent. Si se pot intampla situatii cand un thread modifica
culoarea la un control in verde de exemplu si este intrerupt in
acest timp de un alt thread care modifica in rosu. Ce culoare ar
trebui sa aiba controlul la sfarsit? De aceea s-a hotarat ca toate
controalele vizuale nu lucreaza multithreading si nu accepta
controlul decat de la threadul care l-a instantiat. Pentru a sari
in threadul principal din threadul secundar, apelam functia
BeginInvoke, care pune in coada de mesaje al threadului principal
functia ce trebuie executata. Este aceeasi functie
"BindDataGridToListOfValues", dar prin apelul BeginInvoke eu o
trimit spre executie thread-ului
principal. Fiind executata de thread-ul principal, se paote
umbla la controlul datagrid si sa-i setam DataSource pe lista de
valori obtinuta in celalalt thread. private void
BindDataGridToListOfValues()
{
dgSensorValueList.DataSource = null;
dgSensorValueList.DataSource = sensorValueList;
}
Deci, la fiecare eveniment primit de la celalalt thread,
eveniment ce vine dupa el cu o noua valoare de senzor, noi adaugam
in lista acea valoare si reconectam lista la datagrid pentru
afisare.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 23 of 46
Figura 23: Fereastra principala de prezentare a datelor cu
datagrid-ul legat la lista de valori
4.5 Controale vizuale: CheckBox, TextBox. Clasa Dictionary In
constructorul clasei de prezentare a datelor se starteaza o singura
instanta a clasei PumpSensorValues. In realitate sunt mai multi
pacienti conectati la aplicatia de monitorizare ce ruleaza pe
calculatorul doctorului. In afara de valorile de masurare, gridul
de vizualizare a datelor trebuie sa afiseze si codul pacientului de
la care provin datele. In acest scop se modifica clasa "SensorBase"
sa contina si codul pacientului pentru care trimite valorile.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 24 of 46
Figura 24: Adaugarea codului de pacient in clasa cu valorile
senzorilor
Constructorul clasei "PumpSensorValues" se modifica pentru a
primi ca parametri codul pacientului pentru care trimite valori,
cat si intervalul de timp intre doua masurari succesive.
Figura 25: Patient code trebuie adaugat si in clasa
PumpSensorValues
Iar cand se trimite evenimentul "newSensorValueEvent" acesta
trebuie sa contina in argument si codul de pacient:
Figura 26: Evenimentul "newSensorValueEvent" include si codul de
pacient
Ramane ca si in gridul de prezentare a datelor sa se adauge o
coloana de afisare a codului de pacient. Se deschide fereastra de
proprietati a gridului si sa da click pe Add pentru a adauga noua
coloana "PatientCode" definita in clasa "SensorBase" folosita ca
sursa de date pentru grid:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 25 of 46
Figura 27: Adaugarea coloanei PatientCode in datagrid
Acum ca avem si codul de pacient in SensorBase, putem construi o
interfata ce ne permite sa startam un pacient nou, sau sa oprim
trimiterea datelor de la un pacient deja pornit. Vom introduce in
fereastra "DataPresentation" doua butoane "Start Pumping" si "Stop
Pumping" care sa starteze un pacient selectat dintr-un comboBox. De
asemenea, intervaulul de timp dintre doua masuratori succesive se
citeste dintr-un textBox:
Figura 28: Fereastra DataPresentation cu butoanele de startare
si oprire a monitorizarii
CheckBox-ul de selectare a pacientilor are ca DataSource o
enumerare unde se introduc toate codurile de pacienti.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 26 of 46
Figura 29: Enumerarea ce include codurile de pacienti
Iar aceasta enumerare se pune ca sursa de date pentru
checkBox:
Figura 30: Atasarea unei enumerari la un CheckBox
Butonul "Start Monitoring" trebuie sa porneasca monitorizarea
pentru pacientul selectat in checkBox avand ca interval de timp
valoarea din textBox-ul aferent:
Figura 31: Codul aferent butonului "Start Monitoring"
Functia trebuie sa verifice ca checkBox-ul are un pacient
selectat si ca textBox-ul pentru numarul de secunde contine un
numar intreg valid. De exemplu, daca utilizatorul introduce numarul
"12ax", acest text nu poate fi convertit la un intreg si trebuie
afisat mesj de eroare. Prinderea acestei situatii se face printr-o
constructie de tip try-catch. Se scrie o functie "startPumping"
care primeste ca parametri codul pacientului si numarul de secunde
intre doua masurari pentru acel pacient si aceasta functie trebuie
sa starteze monitorizarea pentru acel pacient:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 27 of 46
Figura 32: Functia StartPumping cu parametrii cod pacient si
perioada de timp
Problema care apare in momentul cand se lucreaza cu mai multi
pacienti este ca putem din greseala sa pornim acelasi pacient de
mai multe ori si am primi de la acelasi cod de pacient seturi de
date diferite. Putem indrepta aceasta greseala daca construim un
dictionar care sa includa toti pacientii activi impreuna cu
obiectul "SensorBase" atasat. Clasa "Dictionary" lucreaza ca o
colectie de perechi de obiecte, primul obiect din pereche avand
semnificatia unei kei. Perechile de obiecte se adauaga in dictionar
foarte simplu prin comanda Add si apoi sa poate cauta o anumita
pereche daca se da keia pentru acea pereche:
Se defineste o noua instanta a clasei Dictionary pentru perechea
de obiecte: PatientCodeEnum si PumpSensorValues:
Dictionary dictPatientPump = new
Dictionary();
Dupa startarea unui nou pacient, perechea
pacient-pumpSensorValue se adauga in dictionar:
dictPatientPump.Add(patCodeEnum, sensorValuesPump);
Inainte de startare se cauta in dictionar daca nu cumva acest
pacient este deja activ. In caz adevarat se da mesaj de eroare:
if (dictPatientPump.ContainsKey(patCodeEnum))
{
MessageBox.Show("The selected patient has the pump already
started");
return;
}
Functia aferenta butonului "Stop Monitoring" trebuie sa opreasca
monitorizarea pentru acel pacient si apoi sa elimine pacientul din
dictionarul cu pacientii activi:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 28 of 46
Figura 33: Oprirea monitorizarii pentru un pacient si eliminarea
lui din dictionarul pacientilor activi
In sfarsit am obtinut ceva... un grid care se incarca periodic
cu o noua linie ce afiseaza ultima masuratoare obtinuta de la
senzori aferenti unui pacient activ, putem de asemenea starta sau
stopa monitorizarea unui pacient cu o perioada de timp
programabila. Totusi treaba nu e gata, mai trebuie lucrat la
interfata (butoane de filtrare a informatiei, alarme, etc) cat si
alte taskuri ramase in aer:
Salvarea datelor intr-o baza de date (ce ne facem daca doctorul
vrea sa vada valorile de acum o saptamana si noi am pierdut lista
de valori?)
Comunicare TCP/IP intre PumpSensorValues si datagrid (sa nu
uitam ca in principiu, pacientul sta acasa si PumpSensorValues
ruleaza la pacient acasa, pe cand DataPresentation ruleaza la
doctor in cabinet, deci pe calculatoare diferite).
Si altele...
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 29 of 46
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 30 of 46
5 Salvarea informatiilor in baza de date Pana in acest moment,
datele ce vin de la pacient sunt puse intr-o lista dinamica si
afisate in datagrid. Daca se inchide aplicatia, atunci toate
informatiile primite se vor pierde, doctorul nu are nici o
posibilitate sa analizeze o informatie din trecut. Nu este
suficient sa afisam datele, trebuie ca aceste date sa fie salvate
pe un suport permanent si sa poata fi restaurate la un moment dat,
daca doctorul doreste sa faca o examinare a pacientului si are
nevoie de istoricul datelor de masurare. In acest capitol vom
rezolva problema salvarii datelor intr-o baza de date si problema
inversa, de citire a informatiilor din baza pentru perioade de timp
stabilite si un pacient dat. Totusi, nu vom folosi o baza de date
foarte mare gen Oracle, deoarece nu sunt foarte multe date si
foarte complexe de salvat si nu are sens sa ocupam memoria cu
motorul bazei de date. Mai ales ca, e posibil ca aceasta aplicatie
sa ruleze pe un telefon mobil ce are constrangeri evidente de
memorie disponibila. De aceea vom utiliza SQLite, o baza de date
mult mai mica si fara sa necesite instalari complexe. Acest SQLite
este un simplu DLL care se salveaza odata cu aplicatia si poate fi
apelat in orice moment pentru accesarea bazei de date ce este
formata dintr-un singur fisier.
5.1 Crearea bazei de date SQLite Mai intai se instaleaza SQLite
database prin rularea aplicatiei SQLite-1.0.62.0-setup.exe. Se
deschide apoi sqliteadmin.exe, aplicatia cu care administram baza
de date SQLite si se creeaza o noua baza de
date (butonul ).
Figura 34: Crearea bazei de date PatientData in SQLite
Administrator
Se deschide apoi baza de date si se creeaza un table pentru
salvarea datelor ce vin de la pacient:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 31 of 46
Figura 35: Crearea tabelului PatientData pentru salvarea datelor
ce vin de la pacienti
In afara de cele patru campuri de valori ce vin de la pacient se
mai adauga un camp numit id de tip varchar si care va fi coloana de
tip primary key pentru acest tabel. Tabelul se mai poate crea si
prin instructiunea SQL specifica:
CREATE TABLE [PatientData] ( [id] VARCHAR(16) PRIMARY KEY NULL,
[patient_code] VARCHAR(10) NULL, [sensor_type] VARCHAR(20) NULL,
[timestamp] DATE NULL, [value] NUMERIC NULL )
5.2 Data Acquisition Layer : interfata intre baza de date si
aplicatia GUI Nu este indicat sa se acceseze direct baza de date
din functiile ce apartin de clasa GUI. Conexiunea la baza de date,
functiile SQL, trebuie sa fie create pe un nivel intermediar, de
sine statator si care va fi apelat de clasa superioara de tip GUI
pentru orice accesare a bazei de date. Adaugam un nou proiect la
solutie, dar de data aceasta de tip Class Library (deci un simplu
fisier DLL, fara nici o interfata cu utilizatorul), pe care-l
denumim DataStore:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 32 of 46
Figura 36: Adaugarea proiectului DataStore de tip Class Library
la solutie
Dorim sa utilizam SQLite ca baza de date pentru acest proiect,
deci trebuie adaugate la proiect referintele System.Data si
System.Data.SQLite. In cadrul proiectului vom utiliza si clasa
SensorBase definita in proiectul SensorInput, deci va trebui
adaugata si aceasta referinta (add reference, sectiunea
Projects).
5.2.1 Definirea string-ului de conectare la baza de date in
pagina de proprietati a proiectului
String-ul de conectare la baza de date (unde se gaseste baza de
date si modul de conectare la ea) reprezinta o informatie care este
accesata ori de cate ori trebuie sa lucram cu baza de date. De
aceea, acesta informatie se salveaza o singura data in pagina de
proprietati a proiectului si va fi citita din pagina ori de cate
ori este nevoie de ea. Pagina de proprietati este o modalitate de
gestionare simpla si rapida a resurselor comune proiectului. O
resursa (cum este acest string de conectare) se salveaza o singura
data in pagina de proprietati si apoi va fi vizibila pe tot
cuprinsul proiectului. Se deschide pagina de proprietati a
proiectului si se defineste la sectiunea Settings stringul de
conectare la baza de date SQLite:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 33 of 46
Figura 37: Definirea string-ului de conectare la baza de date
SQLite in pagina de proprietati a
proiectului Se pune numele proprietatii ConnStringSQLite, se
alege la Type tipul resursei Connection String, iar la
value se da click pe butonul pentru a defini conexiunea cu
ajutorul Wizard-ului. Se deschide Wizard-ul si se selecteaza la
DataSource driver-ul .NET Framework Data Provider for SQLite, iar
la DataBase se merge cu butonul Browse si se selecteaza fisierul
creat pentru baza de date. La sfarsit se poate face si un test de
verificare daca conexiunea este setata corect (butonul test
Connection).
5.3 Clasa de interfata cu baza de date
5.3.1 Salvarea informatiilor in baza de date
Odata ce am definit conexiunea cu baza de date, putem construi
functia de adaugare a unei valori de masurare (o instanta de
SensorBase) in baza de date. Adaugam o noua clasa la proiectul
DataStore numita DAL_PatientData :
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 34 of 46
Figura 38: Functia de inserare a unei valori de masurare in baza
de date
Etapele ce trebuie parcurse pentru salvarea valorilor de
masurare in baza de date sunt urmatoarele:
Se defineste un obiect de tipul SQLiteConnection cu care putem
sa ne conectam la baza de date. String-ul de conectare se citeste
din fisierul de proprietati ale proiectului
(Properties.Settings.Default.ConnStringSQLite).
Se defineste obiectul SQLiteCommand cu care vom executa
instructiunea SQL de inserare in baza de date
Setam instructiunea SQL ce trebuie executata de
SQLiteCommand
Setam parametrii ce trebuie trimisi la SQLiteCommand cu valorile
de masurare primite prin obiectul sensorData.
Deschidem conexiunea la baza de date si executam comanda SQL
Inchidem conexiunea la baza de date Trebuie observat ca accesul
la baza de date s-a facut prin constructia try-catch-finally pentru
a ne asigura ca tratam eroarea aparuta in cazul cand baza de date
nu raspunde. Tot ce mai ramane de facut este de a apela functia de
salvare in baza de date in momentul cand proiectul DataPresentation
primeste o noua valoare de masurare prin evenimentul
newSensorValueEvent:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 35 of 46
Figura 39: Tratarea evenimentului newSensorValueEvent: salvare
in baza si afisare in datagrid Nu trebuie sa uitam sa adaugam
referinta la proiectul DataStore in cadrul proiectului
DataPresentation. In sfarsit, putem verifica daca totul lucreaza
OK, pornim aplicatia cu F5, startam pacientul 0101 si dupa catva
timp ne uitam in baza de date cu ajutorul programului SQLite
Adminstrator. Deschidem o fereastra de comenzi SQL si introducem
comanda SQL de vizualizare a tabelei PatientData:
Figura 40: Comanda SQL de vizualizare a datelor din tabela
PatientData Executam comanda SQL cu tasta F9 si ar trebui sa
observam datele salvate in tabela de aplicatia Health Monitor:
Figura 41: Vizualizarea datelor din baza in SQLite
Administrator
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 36 of 46
5.3.2 Citirea informatiilor din baza de date
Stocarea informatiilor in baza de date permite doctorului in
orice moment sa analizeze datele din trecut pentru un pacient dat.
In primul rand trebuie creata o sectiune de filtrare a datelor unde
doctorul sa-si selecteze pacientul pe care vrea sa-l analizeze si
intervalul de timp pentru care vrea sa vada datele. Se adauga un
fereastra DataPresentation un control de tip GroupBox ce contine
doua controle de filtrare:
comboBox pentru selectia pacientului
monthCalendar ce selecteaza ziua pentru care se doreste a fi
vizualizate datele
buton pentru efectuarea filtrarii: Display Selected data.
Figura 42: Fereastra DataPresentation dupa ce s-a adaugat
sectiunea de filtrare Dupa selectia pacientului si a zilei de
vizualizare, utilizatorul face click pe butonul Display Data si in
acel moment trebuie citita din baza de date lista tuturor
masuratorile salvate in acea zi pentru respectivul pacient. In
acest scop, clasa DAL_PatientData trebuie sa fie completata cu o
functie de citire a informatiilor din baza de date:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 37 of 46
Figura 43: Functia de citire din baza de date a datelor de
masurare pentru un pacient si o zi stabilita Functia de citire din
baza de date are urmatoarea structura:
se creeaza o noua lista sensorValueList unde vor fi salvate
informatiilor gasite in baza se defineste un nou obiect de tip
SQLiteConnection si un obiect SQLiteCommand
se scrie instructiunea SQL de selectie din baza de date si se
ataseaza obiectului SQLiteCommand
se adauga parametrii de filtrare: patient_code, minTime si
maxTime
se deschide un SQLiteReader in care se salveaza toate liniile
aduse din baza de date de fraza select (reader =
cmd.ExecuteReader();).
Se parcurge reader-ul linie cu linie, se face conversia datelor
aduse de fraza select la tipul SensorBase si se adauga noul item la
lista de valori.
La sfarsit se inchide conexiunea, reader-ul si se returneaza
lista de valori. Totusi avem o mica problema: proiectul DataStore
nu cunoaste tipul de date PatientCodeEnum cu care trebuie sa
lucreze functia GetData pentru ca aceasta enumerare este definita
in proiectul DataPresentation. Ar trebui ca proiectul
DataPresentation sa fie adaugat in lista de referinte a
proiectului
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 38 of 46
DataStore. Dar acest lucru nu este posibil pentru ca, deja
proiectul DataStore este referinta la proiectul DataPresentation si
ar rezulta o referinta circulara. Nu este posibil ca un proiect P1
se fie referinta la alt proiect P2 si in acelasi timp proiectul P2
sa fie referinta la P1. Deci nu se poate sa un proiect sa fie tata
la alt proiect si in acelasi timp sa fie fie si copil la acelasi
proiect. Din acest motiv, se creeaza un nou proiect numit
CommonReferences care va contine doar un fisier unde vor fi
definite toate tipurile de obiecte comune celorlalte proiecte.
Acest proiect va fi adaugat ca referinta la toate proiectele ce
lucreaza cu acele tipuri de date. Evident, ca acest proiect nu
trebuie sa faca referire la nici un alt proiect din solutie.
Figura 44: Proiectul CommonReferences contine toate definitiile
particulare ale tipurilor de date Am mutat definitiile
PatientCodeEnum si SensorType in CommonReferences astfel incat
acestea sa fie vizibile in toata solutia fara probleme de referinta
circulara. Tot ce mai ramane pentru vizualizarea datelor din baza
de date e sa implementam codul pentru butonul Display selected
Data:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 39 of 46
Figura 45: Afisarea valorilor din baza de date
Afisarea se face in acelasi datagrid, trebuie doar sa schimbam
proprietatea dataSource pentru acest grid pe lista de valori adusa
de functia DAL_PatientData.GetData(). Totusi, folosind acelasi grid
pentru afisarea ambelor tipuri de valori, atat cele primite online
de la patientii activi, cat si cele aduse din baza de date, trebuie
definita a variabila logica care sa specifice tipul de date ce
trebuie vizualizate:
private bool displayTheReceivingData = true;
Iar aceasta variabila va fi setata corespunzator pe cele doua
butoane Display Selected Data si Display Received Data:
Figura 46: Functia handler pentru butonul Display Received
Data
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 40 of 46
6 Transmiterea datelor intre aplicatii. Socket TCP/IP Pana acum,
atat proiectul care producea date (SensorInput), cat si proiectul
care consuma datele (DataPresentation), ruleaza sub aceeasi
solutie, in acelasi calculator si atunci evenimentele trimise de
furnizorul de date pot fi receptionate de consumatorul de date. Si
totusi, realitatea sta un pic altfel: furnizorul de date ruleaza la
pacient acasa, iar receptorul ruleaza la medic in cabinet. Intre
ele exista o legatura internet sau intranet prin care trebuie
transmise datele. In acest capitol vom analiza modul de conectare a
celor doua aplicatii printro legatura de tip Socket TCP/IP.
Tehnologia Socket lucreaza pe arhitectura client-server. Se
deschide un server TCP/IP care ruleaza un listener ce asculta la un
port dat cererile de conectare de la diversi clienti TCP/IP din
retea. Cand soseste o cerere de conectare, se deschide un canal de
comunicatii intre cele doua aplicatii prin care pot fi transferate
date in ambele sensuri. Se modifica proiectul "DataPresentation"
prin adaugarea unor radio butoane care sa selecteze modul de
transmitere a datelor:
prin TCP/IP la un calculator dat de o adresa IP
direct la calculatorul curent
Figura 47: Proiectul Data Presentation cu posibilitatea de a
alege canalul de comunicare a datelor
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 41 of 46
Pentru a realiza o comunicatie TCP/IP trebuie construite
urmatoarele obiecte:
TcpClient ce primeste datele de masurare de la sensor, le
impacheteaza intr-un format text si le transmite la server
TcpServer care deschide listenerul si accepta cererile de
conexiune sosite de la client, primeste textul de la client, il
despacheteaza in format SensorBase si-l trimite mai departe la
"DataPresentation"
Evenimentul prin care serverul sa transmita la interfata
DataPresentation valorile primite de la client Se creeaza un nou
proiect in cadrul solutiei numit "TCPCommunication", proiect ce
include doua clase: TCPCommClient si TCPCommServer:
Figura 48:Solutia "HealthMonitor" cu toate proiectele
componente
6.1 Obiectul TCPCommClient In continuare se prezinta clasa
"TCPCommClient" cu urmatoarele functionalitati:
Primeste data de tip "SensorBase" pentru a fi trimisa la
server
Porneste un nou fir de execuie (thread) care sa se ocupe separat
de deschiderea conexiunii si trimiterea datelor
Impachetarea clasei "SensorBase" intr-un text dupa un anumit
format standard
Inchiderea firelor de executie ramase suspendate daca serverul
refuza sa raspunda la cererea de dialog
Sa luam aceste functii pe rand:
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 42 of 46
6.1.1 Impachetarea datelor pentru transmisie
Clasa TCPCommClient afiseaza urmatoarele functii publice:
Constructorul clasei
Figura 49: Constructorii clasei TCPCommClient Se observa ca
constructorul implicit al clasei a fost declarat privat astfel
incat sa nu poata fi posibila o instantiere a clasei de forma "new
TCPCommClient()", adica sa se creeze o instanta fara a da si adresa
IP a serverului la care trebuie sa se conecteze clientul pentru a
trimite date. Singurul constructor disponibil din exterior este cel
ce primeste ca parametru adresa IP a serverului. In acest fel ne
asiguram ca toti clientii TCP creati vor avea setata adresa IP a
serverului cu care trebuie sa dialogheze.
"SendSignalData" ce primeste cele patru valori din clasa
SensorBase in scopul trimiterii lor catre server.
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 43 of 46
Figura 50: Functiile de primire si impachetare a datelor ce
trebuie trimise la server
Functia ce primeste datele pentru transmisie apeleaza mai intai
functia de impachetare a datelor in format text, dupa care face
apel catre functia de trimitere a textului. Este mai avantajoasa
trimiterea sub forma de text a datelor pentru ca pot fi procesate
cu ajutorul functiilor de tip string si in plus, sender-ul si
receiver-ul pot avea structuri de date diferite, totul e sa se
stabileasca formatul de impachetare care sa fie respectat de ambele
parti. S-a decis asupra urmatorului format:
- Simbolul "#" semnifica inceputul si sfarsitul mesajului -
Simbolul "," separa campurile de date specifice clasei "SensorBase"
- Ordinea de impachetare este urmatoarea: SensorType,
TimeStamp,
PatientCode, Value. Concatenarea acestor string-uri in mesajul
de trimis se face cu jutorul functiei "Append" din cadrul clasei
"StringBuilder".
6.1.2 Crearea unui nou fir de executie pentru fiecare dialog cu
serverul Functia ce trimite efectiv mesajul text catre server este
listata in continuare:
Figura 51: Startarea unui nou thread pentru trimiterea datelor
Crearea unui nou fir de executie pentru trimiterea textului la
server se face prin apelul constructorului clasei
"Thread" din namespace-ul "using System.Threading;":
Se creeaza instanta clasei "Thread":
Thread newThread = new Thread(new
ParameterizedThreadStart(SendSignalTextNewThread));
Se adauga thread-ul format in lista de thread-uri a clasei
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 44 of 46
myThreadList.Add(newThread);
se porneste noul fir de executie avand ca parametru textul ce
trebuie trimis la server: newThread.Start(signalText);
Dar mai intai se inchid toate thread-urile ramase in suspensie
de la vechile incercari de dialog cu serverul. Daca serverul nu
raspunde in timp util, aceste fire de executie raman suspendate si
ocupa memoria in mod abuziv. Daca sunt mii de date de trimis si in
tot acest timp serverul nu raspunde, sunt mii de thread-uri ramase
in suspensie. De aceea trebuie facuta curatenie din cand in
cand:
Figura 52: Inchiderea firelor de executie inactive Se creeaza o
noua lista de thread-uri in care sa vor salva toate firele de
executie care sunt inca active. Daca thread-ul nu este activ se
inchide prin apelul functiei Abort(). La sfarsit lista de
thread-uri a clasei va fi initializata cu noua lista, fiind
alcatuita numai din firele de executie active.
6.1.3 Trimiterea datelor catre server Trimiterea efectiva a
textului catre server se face in functia ce ruleaza in firul de
executie nou creat. Aceats functie urmeaza urmatorii pasi:
se obtine o instanta a clasei de biblioteca TcpClient cu
parametrii: _serverIP si _port; se obtine un obiect de tip
"NetworkStream" pe care-l returneaza acest TcpClient. Acest
stream
poate trimite octetii unul dupa altul pe canalul deschis intre
client si server despachetam mesajul de tip text intr-un sir de
octeti ce pot fi trimisi pe canalul de comunicatie:
ASCIIEncoding encoding = new ASCIIEncoding();
byte[] buffer = encoding.GetBytes(signalText);
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 45 of 46
Apelam functia stream.Write pentru a trimite datele. Aceasta
functie "Write" lucreaza ca si cum am scrie intr-un fisier,
problemele hardware de low level de transmitere efective a datelor
fiind transparente pentru noi ca utilizatori.
Figura 53: Dialogul cu serverul
6.1.4 Functia Dispose inchide firele de executie suspendate Daca
utilizatorul inchide brusc aplicatia (cand inca sunt active fire de
executie ce trimit date catre server)
atunci trebuie apelata o functie pe evenimentul
"DataPresentation_FormClosing" (deci cand fereastra
este pe punctul de a se inchide) care sa inchida toate resursele
aferente acestei aplicatii.
Figura 54: Tratarea evenimentului "FormClosing" in clasa
"DataPresentation"
Functia apelata la inchiderea aplicatiei pentru inchiderea
tuturor firelor de executie aferente clientului este "Dispose()".
Pentru a nu uita ca trebuie sa scrim aceasta functie, am mostenit
clasa "TCPCommClient" din interfata "Idisposable" si astfel, in mod
automat VS-ul te obliga sa implementezi functia Dispose():
-
Programare Visual Studio .NET - LABORATOR Project no. PVS 2008
Product name: Programare Visual Studio Date: Feb - 2010
Document no. Version no.: 0.1 CR no.: Author: Lucian Nita
Version no.: 1 Page 46 of 46
Figura 55: Componentele interne ale clasei "TCPCommClient"
Figura 56: Inchiderea firelor de executie la terminarea
programului
6.2 Obicetul TCPCommServer Acest obiect trebuie sa deschida un
listener care sa receptioneze cererile de conectare de la clienti,
sa deschida canalele de comunicatie, sa receptioneze mesajele, sa
despacheteze aceste mesaje si sa trimita valorile primite mai
departe la "DataPresentation".