Sunteți pe pagina 1din 31

Catedra Automatică și Tehnologii

Informaționale

Disciplina MIDPS
l.u. Boleac Ruslan

Introducere in .Net si C#

Structura unei aplicatii C#.


O aplicatie C# (.Net in general) este formata din una sau mai multe clase, grupate in spatii de
nume (namespaces)*. Este obligatoriu ca una din aceste clase (si numai una) sa contina un
"punct de intrare" (entry point), si anume metoda Main**.

O parte din clasele folosite de aplicatie sunt deja definite. Folosirea acestora se face fie
"importand" spatiile de nume care contin definitia acestora, fie apeland clasa folosind numele
complet. Prima metoda este folosita atunci cand clasele dintr-un namespace sunt folosite de mai
multe ori.

1 //"Importarea" spatiilor de nume ce vor fi folosite.


2 using System;
3
4 //Definirea unui nou namespace
5 namespace FirstApplication
6 {
7 //Definirea unei clase
8 public class FirstClass
9 {
10 //Definitiile si declaratiile variabilelor si metodelor
11 ...
12 //Metoda Main
13 }
14 }

Fig. 1: Structura unei aplicatii foarte simple

*Nu exista restrictii la alegea numelor pentru clase si fisiere (cum exista in Java), fisierele putand
avea orice nume si contine definitia a mai mult de o singura clasa. Singura restrictie este sa nu
existe ambiguitati (sa nu existe doua clase cu acelasi nume in acelasi spatiu de nume, etc.)

**De fapt punctul de intrare intr-o aplicatie poate fi modificat (sa fie diferit de metoda Main);
acest lucru poate fi facut manipuland direct codul MSIL.

Metoda Main
Metoda Main este punctul de intrare in aplicatie - este functia care se executa atunci cand
aplicatia este pornita. La fel ca si in C++, metoda Main poate avea mai multe signaturi, fiecare
dintre acestea fiind corecta.

La fel ca orice alta metoda C# (.Net) Main nu poate fi globala. Ea trebuie definita ca fiind
membru intr-o clasa.

1 public class FirstClass


2 {
3 ...
4
5 static void Main() {...}
6 static int Main() {...}
7 static void Main (string[] args) {...}
8 static int Main (string[] args) {...}
9
10 ...
11 }

Fig. 2: Singnaturile metodei Main

Se observa ca metoda Main trebuie sa fie statica (Puteti explica de ce?). De asemenea poate
intoarce o valoare intreaga. Acea valoare reprezinta rezultatul executiei programului (la fel ca in
C++). Prin conventie, daca un program intoarce valoarea 0 inseamna ca s-a executat cu succes. O
valoare diferita de 0 inseamna eroare, si codurile de eroare sunt definite de programator. (La fel
ca si in C++ aceasta este doar o conventie si nu un standard.)

Intr-o aplicatie nu poate exista mai mult de un singur punct de intrare (mai multe definiri ale
metodei Main, fie si in clase diferite). Definirea in mai multe locuri va genera erori la compilare.

Daca Main este definita cu parametru (string[] args), atunci aplicatia poate accepta parametri.
Acestia vor fi trimisi functiei in parametrul args.
Spatii de nume
Toate clasele, interfetele, etc. existente in .Net Framework formeaza o ierarhie. La baza acestei
ierarhii sta spatiul de nume System. Aici sunt unele clase des folosite (ex: clasa Console pentru
lucrul cu tastatura si afisarea la ecran.).

Alte namespace-uri importante sunt:

 System.IO - pentru lucrul cu dispozitivele input/output (ex: accesul la fisiere)


 System.Collections - pentru lucrul cu colectii de obiecte de acelasi tip (liste, stive,
etc.)
 System.Xml - pentru manipularea datelor in format xml
 System.Windows.Forms - pentru lucrul cu forme windows (controale windows)

Prima aplicatie
Fie urmatoarea aplicatie C#:

1 using System;
2
3 namespace Laborator1
4 {
5 public class HelloWorld
6 {
7 static void Main ()
8 {
9 Console.Write ("Hello World!");
10 }
11 }
12 }

Fig. 3: Aplicatia "Hello World"

In programul anterior am definit o clasa HelloWorld (pe care am pus-o in spatiul de nume
Laborator1) care contine o singura metoda: Main. Functia Main apeleaza, la randul sau, functia
Write din clasa Console. Clasa Console este definita in spatiul de nume System si contine
functii pentru lucrul cu iesirea/intrarea standard (consola). O parte dintre aceste medode sunt
statice (Puteti da un exemplu? ) astfel nefiind necesara declararea unui obiect de tip Console.

In cazul in care ar fi lipsit linia 1 din program ("importarea" unui namespace) ar fi trebuit sa
apelam functia Write folosind numele ei complet (si anume: System.Console.Write ("Hello
World!"))

MSIL - Microsoft Intermediate Language


Unul din marile avantaje ale platformei .Net este interoperabilitatea interlimbaj. Acest lucru a
fost incercat si anterior dar cu un succes limitat. Un exemplu in acest sens il reprezinta COM-ul.
In COM este posibil sa creezi o componenta intr-un limbaj (de exemplu Visual Basic) si sa o
folosesti in alt limbaj (de exemplu C++). Totusi COM-ul nu permite mostenirea unei clase
definita intr-un limbaj in alt limbaj. .Net framework permite acest lucru.

Pentru a intelege cum este posibila interoperabilitatea interlimbaj trebuie urmarit ce se intampla
atunci cand se compileaza o aplicatie. La compilare nu este creat un fisier binar care poate apoi fi
executat, ci un fisier (sau mai multe) care contine (printre altele) cod MSIL. Acest cod MSIL
seamana destul de mult cu assembler-ul (dar nu este assembler). Fisierul obtinut dupa
compilare are tot extensia exe, la fel ca orice binar normal.

Cand aplicatia este rulata, codul MSIL este incarcat si executat de CLR (Common Language
Runtime). Pentru a intelege mai bine se poate spune ca CLR-ul este un fel de Masina Virtuala
Java (cu observatia ca CLR ofera mult mai multe servicii codului executat decat JVM. Desi se
poate face o paralela intre cele doua medii de executie acestea nu trebuiesc confundate sau
considerate echivalente.)

Fisierul obtinut in urma compilarii unui program .Net este un assembly. Un assembly contine, pe
langa cod MSIL si metadata si resurse. In metadata sunt incluse toate informatiile despre tipurile
din assembly (signaturile tuturor metodelor, claselor, etc.) Pentru ca in orice moment signatura
unei clase este cunoscuta (indiferent daca avem codul sursa sau un assembly care contine clasa)
aceasta poate fi extinsa prin mostenire din orice limbaj. Datorita metadatei nu mai este nevoie de
bibliotecile de tipuri, folosite foarte mult in COM, pentru ca metada actioneaza ca o biblioteca de
tipuri. Cu alte cuvinte crearea componentelor in .Net este mult mai simpla.

Pentru a vedea cum arata codul MSIL putem face o decompilare a aplicatiei anterioare folosind
utilitarul ildasm.exe (care se instaleaza o data cu .Net Framework)

1 .class public beforefieldinit HelloWorld extends object


2 {
3 .method public hidebysig specialname rtspecialname instance void
4 .ctor() cil managed
5 {
6 // Code Size: 7 byte(s)
7 .maxstack 1
8 L_0000: ldarg.0
9 L_0001: call instance void object::.ctor()
1 L_0006: ret
0 }
1
1 .method private hidebysig static void Main() cil managed
1 {
2 .entrypoint
1 // Code Size: 11 byte(s)
3 .maxstack 1
1 L_0000: ldstr "Hello World!"
4 L_0005: call void [mscorlib]System.Console::Write(string)
1 L_000a: ret
5 }
1 }
6
1
7
1
8
1
9
2
0
2
1

Fig. 4: Codul MSIL pentru aplicatia HelloWorld.exe

La o privire sumara asupra codului de mai sus se remarca doua lucruri:

1. Clasa HelloWorld este derivata din clasa Object (la linia 1 avem extends object imediat
dupa declararea clasei.). In .Net toate tipurile mostenesc clasa Object. Aceasta derivare
nu trebuie facuta in mod explicit de programator, compilatorul adaugand automat codul
necesar.
2. Clasa HelloWorld contine doua metode, desi in codul C# am definit numai una.
Compilatorul a adaugat si un constructor (constructorul implicit), identificat cu numele
.ctor (linia 3). Trebuie amintit ca toti constructorii de instanta sunt identificati in codul
MSIL cu numele .ctor, In cazul de fata constructorul nu face nimic altceva decat sa
apeleze constructorul clasei de baza (linia 8).

Manipularea parametrilor aplicatiei


Aplicatiile .Net pot primi parametri de la tastatura. Asa cum se arata in figura 2, functia Main are
2 signaturi cu parametru (liniile 7 si 8). Acel parametru este un vector de string-uri si va contine
parametrii aplicatiei care pot fi accesati folosind indici: args[0], args[1], etc.

string[] este tip si, in consecinta, poate avea metode si date membru. Lungimea vectorului args
poate fi obtinuta folosind astfel de date membru. args.Length va contine lungimea vectorului
args.

Tipuri de date fundamentale in .Net


In .Net tipurile se impart in doua categorii, si anume tipuri valoare si tipuri referinta. Cele mai
importante doua tipuri referinta sunt object si string. Acestea sunt cele mai importante pentru ca
sunt folosite intens in orice aplicatie. Asta nu inseamna ca celelalte nu sunt importante.

Atunci cand un obiect este de tipul referinta, pe stiva nu este pusa valoarea acelui obiect, ci o
referinta la el. Obiectul va avea memorie alocata in heap si pe stiva este pusa o referinta catre
zona de memorie ocupata in heap. In consecinta referinta este un pointer type-safe - adica avem
siguranta ca la adresa de memorie de pe stiva avem un obiect de tipul corect (sau valoarea null).

Urmatoarele tipuri sunt tipuri referinta: clasele, array-urile, interfetele si tipurile delegat.
Pe de alta parte o variabila de tip valoare va contine pe stiva direct valoarea, si nu o referinta.

Exista mai multe tipuri valoare predefinite in .Net printre care se numara structurile (struct),
enumeratile (enum) si tipurile primitive* (int, byte, short, long, double, etc.)

Principala diferenta intre class si struct este ca primul e tip referinta si cel de-al doilea e tip
valoare. Din acest motiv nu erste recomandat sa se creeze structuri care au o dimensiune mare.

*Tipurile primitive sunt toate niste structuri, numele mai sus mentionate fiind niste alias-uri.
Astfel int este un alias pentru System.Int32, short pentru System.Int16, byte pentru
System.Int8, long pentru System.Int64, etc. Folosirea numelui complet al tipului sau al alias-
ului nu produce decat o diferenta sintactica, codul MSIL aratand la fel.

Tip CTS C# Alias Descriere


System.Object object Tipul de baza pentru toate tipurile CTS.
System.String string String
System.Sbyte Sbyte Valoare intreaga pe 8 biti (cu semn).
System.Byte byte Valoare intreaga pe 8 biti (fara semn).
System.Int16 short Valoare intreaga pe 16 biti (cu semn).
System.UInt16 ushort Valoare intreaga pe 16 biti (fara semn).
System.Int32 int Valoare intreaga pe 32 biti (cu semn).
System.UInt32 uint Valoare intreaga pe 32 biti (fara semn).
System.Int64 long Valoare intreaga pe 64 biti (cu semn).
System.UInt64 ulong Valoare intreaga pe 64 biti (fara semn).
System.Char char Caracter unicode (pe 16 biti).
System.Single
Numar in virgula mobila pe 32 biti, conform
float
IEEE.
System.Double
Numar in virgula mobila pe 64 biti, conform
double
IEEE.
System.Boolean bool Valoare booleana (true/false)
Valoare pe 128 de biti care retine 28 de
System.Decimal
zecimale semnificative, folosit in calcule unde
decimal
este necesara o precizie mare (ex: calcule
finaciare)
C#. Tipuri de date. Instructiuni de control.
Colectii
Colectii - clasele Array si ArrayList
Clasa Array, definita in spatiul de nume System, este clasa de baza pentru vectori in .Net. Desi
este o clasa abstracta, furnizeaza o metoda CreateInstance pentru crearea de vectori. De
asemenea furnizeaza metode pentru manipulare, cautare si sortare.

Proprietate Descriere
IsFixedSize
Intoarce o valoare booleana care indica daca dimensiunea
vectorului poate fi modificata.
IsReadOnly
Intoarce o valuare booleana care indica daca vectorul e read-
only sau nu.
IsSyncronized Indica daca vectorul e thread-safe.
Length
Numarul total de elemente din vector (numarand toate
dimensiunile).
Rank Intoarce numarul de dimensiuni ale vectorului.
SyncRoot
Intoarce un obiect folosit pentru a sincroniza accesul la
elementele vectorului.

Fig. 1: Proprietatile clasei Array

Moduri de creare a unui array:

1 Declararea unui vector de intregi.


2 int[] intregi;
3
4 //Initializarea vecatorului
5 intregi = new int[3] {1, 2, 3};
6
7 //Creeaza un vector cu doua dimensiuni.
8 Array nume = Array.CreateInstance( typeof(string), 2, 4 );
9
10 //Alta metode a crea un vector cu doua dimensiuni, si initializare
11 //Un vector declarat astfel se numeste vector rectangular
12 string[,] alteNume = { { "Ion", "Vasile"} , {"Popescu", "Ionescu"} };
13 string[,] adrese = new string[2, 3];
14
15 //Jagged Arrays - elementele unui astfel de array sunt array-uri
16 int[][] altiIntregi = new int[2][];
17 altiIntregi[0] = new int[3];
18 altiIntregi[1] = new int[10];

Fig. 2: Moduri de decrare si initializare a vectorilor


.Net Framework pune la dispozitia programatorilor mai multe clase folosite pentru manipularea
colectiilor de obiecte de acelasi tip*. Acestea sunt definite in spatiul de nume
System.Collection.

ArrayList este clasa cel mai des folosita atunci cand se lucreaza cu liste de obiecte. ArrayList
lucreaza intern cu un Array de object. Pe masura ce obiecte noi sunt adaugate in lista, array-ul
este redimensionat. Redimensionarea este facuta doar atunci cand este necesar. In acest mod este
furnizat un mecanism de acces rapid la elemente.

1 //Creem doua obiecte de tipul ArrayList


2 ArrayList nume = new ArrayList();
3 ArrayList prenume = new ArrayList();
4
5 //Adaugam valori in aceste liste
6 nume.Add ("Ionescu");
7 nume.Add ("Popescu");
8 prenume.Add ("Ion");
9 prenume.Add ("Vasile");
10 prenume.Add ("Cristi");
11
12 //Afisam la consola continutul listelor
13 for (int i = 0; i < prenume.Count; i++)
14 Console.WriteLine (prenume[i]);
15
16 //O metoda mai eleganta de parcurgere a listelor
17 foreach (string str in nume)
18 Console.WriteLine (str);

Fig. 3: ArrayList in actiune.

La linia 13 din figura 3 am folosit proprietatea Count pentru a prelua numarul de elemente din
lista. Aceasta nu trebuie confundata cu proprietatea Size care intoarce dimensiunea array-ului
folosit intern de ArrayList. Size reprezinta numarul de elemente care pot fi retinute fara a se
mai face o redimensionare a array-ului (Obs: aceasta redimensionare este transparenta pentru
programator, facandu-se automat cand se adauga un nou element si nu mai este loc).

La linia 17 am folosit o noua metoda de acces a elementelor unei liste, si anume instructiunea
foreach. Instructiunea foreach poate fi folosita doar asupra tipurilor care implementeaza interfata
IEnumerable. Aceasta interfata este implementata de toate colectiile din .Net precum si de clasa
Array.

Desi in exemplul anterior am lucrat doar cu stringuri, intr-un ArrayList pot fi adaugate
elemente de orice tip (pentru ca toate sunt de tipul object).

*Pentru a face colectiile cat mai generale, acestea vor lucra intern cu tipul object. Cum toate
tipurile sunt derivate din tipul object colectiile pot lucra cu orice tip definit de programator.
Acest lucru are atat avantaje cat si dezavantaje.

Accesori
Accesorii sunt modalitati de acces la variabilele membru ale unui obiect intr-un mod sigur.
Accesorii mai sunt numiti si proprietati ale unei clase (a se vedea figura 1 cu proprietatile clasei
Array).

Sintaxa pentru definirea unei proprietati este urmatoarea:

1 public class Student


2 {
3 string m_nume;
4 string m_prenume;
5 int m_nota;
6
7 //getter si setter
8 public string Nume
9 {
1 get { return m_nume; }
0 set { m_nume = value; }
1 }
1
1 //setter
2 public string Prenume
1 {
3 set { m_prenume = value; }
1 }
4
1 //getter
5 public string NumeComplet
1 {
6 get { return m_nume + " " + m_prenume; }
1 }
7
1 public static void Main ()
8 {
1 Student student = new Student();
9 //setam numele si penumele studentului
2 student.Nume = "Popescu";
0 student.Prenume = "Vasile";
2
1 //Afisam numele studentului pe ecran
2 Console.WriteLine (student.NumeComplet); //Pe ecran se va
2 afisa: Popescu Vasile
2 }
3 }
2
4
2
5
2
6
2
7
2
8
2
9
3
0
3
1
3
2
3
3
3
4
3
5
3
6

Fig. 4: Accesori

La definire accesorii au sintaxa asemanatoare cu a unei metode, si la apel se foloseste sintaxa


unei variabile membru.

Accesorii simplifica foarte mult sintaxa pentru obtinerea unor valori ale datelor membru sau
setarea lor, si nu numai. Daca pentru a pune o anumita variabila membru pe o valoare folosim
proprietati, putem face niste verificari inainte. De exemplu la setarea numelui unui student putem
testa daca numele nu contine caractere interzise (cum ar fi @). Asemenea teste erau posibile si in
alte limbaje, dar trebuie construite functii speciale atat pentru setarea valorii (ex: SetName.), iar
pentru obtinerea valorii trebuia implementata alta functie (ex: GetName). Introducerea accesorilor
a simplificat mult sintaxa.

Proprietatile pot fi numai read-only (este definit numai getter-ul - cum este NumeComplet), write-
only (este definit numai setter-ul - cum este Prenume) sau atat read cat si write (cand este definit
atat getter-ul cat si setter-ul - cum este Nume). In exemplul de mai sus, daca incercam sa obtinem
doar valoarea pentru prenume (ex: string prenumeStudent = student.Prenume;) vom
obtine o eroare la compilare.

Pentru a seta valoarea, accesorii au un parametru spcial: value (liniile 11 si 17). Acesta contne
valoarea care urmeaza sa fie setata (e un fel de parametru al unei functii, doar ca nu e declarat
explicit). Acesta are ca tip tipul proprietatii (string in cazul de mai sus).

Indexatori
Indexatorii sunt o alta modalitate de a usura scrierea unui program. Indexatorii permit accesul la
un obiect folosind indici, la fel ca in cazul vectorilor.

Programului de mai sus i-am putea adauga urmatorul indexator:

1 public class Student


2 {
3 ...
4
5 //Definirea indexatorului
6 public string this[string camp]
7 {
8 get
9 {
1 if (camp == "nume")
0 return m_nume;
1 else if (camp == "prenume")
1 return m_prenume;
1 else
2 return null;
1 }
3 set
1 {
4 if (camp == "nume")
1 m_nume = value;
5 else
1 //fa ceva
6 }
1 }
7
1 ...
8
1 public static void Main ()
9 {
2 ...
0 Console.WriteLine (student["nume"]);//Pe ecran se va afisa:
2 Popescu
1 student["nume"] = "Ionescu";//Am schimbat numele
2 studentului :)
2 }
2 }
3
2
4
2
5
2
6
2
7
2
8
2
9
3
0
3
1
3
2
3
3
3
4
Fig. 5: Indexatori

Implementarea indexatorului este similara cu cea a unei proprietati, putand avea numai setter,
getter sau pe amandaua.

Indexatorul din figura 5 primeste un parametru de tip string (intre parantezele patrate - la linia 6).
Nu este obligatoriu ca acel parametru sa fie de tipul string, el putand avea orice alt tip, ramanand
in sarcina programatorului utilizarea lui corecta. (In figura 3 la linia 14 am folosit un indexator -
implementat de ArrayList - care primea ca parametru in paranteza patrata o valoare de tipul int)

Instructiuni de control
 if
 for
 foreach
 while
 switch
 goto
 return

Operatori C#
Categorie Operatori Asociativitate
Primari
(x), x.y, f(x), a[x], x++, x--, new,
typeof, sizeof, checked, unchecked
Unari +, -, !, ~, ++x, --x, (T)x
Operatori de multiplicitate *, /, % stanga
Operatori de adunare +, - stanga
Shiftare <<, >> stanga
Relationali <, >, <=, >=, is stanga
Egalitate == stanga
SI logic & stanga
XOR logic (sau exclusiv) ^ stanga
SAU logic | stanga
SI conditional && stanga
SAU conditional || stanga
Operator conditional ?: dreapta
Asignare
=, *=, /=, %=, +=, -=, <<=, >>=, &=,
dreapta
^=, ?=

Fig. 6: Operatorii C#
In tabelul de mai sus sunt prezentati operatorii C#, precedenta fiind de sus in jos (operatorii cu
precedenta cea mai mare sunt cei primari, si operatorii cu precedenta cea mai mica sunt
operatorii de asignare).

String-uri
Clasa System.String (sau string) reprezinta un sir de caractere imutabil - adica valoarea unui
string nu poate fi modificata dupa creare. Metodele care par sa modifice un string creaza de fapt
un alt sir de caratere care contine modificarile si nu-l modifica pe primul. In afara de clasa strin
.Net Framework ofera si alete clase pentru lucrul cu siruri de caractere: StringBuilder,
StringFormat, StringCollection etc. care ofera metode pentru comparare, inserare, indexare,
cautare, joining, splitting, inlocuire, copiere, formatare etc. a sirurilor de caractere.

Figura urmatoare prezinta formatatorii pentru stringuri folosind exemple:

1 string.Format (123);//123
2 string.Format ("{0}", 123);//123
3 string.Format ("{0:D3}", 123);//123
4 string.Format ("{0,5} {1,5}", 123, 456);//Aliniere la dreapta: " 123
5 456"
6 string.Format ("{0,-5} {1,-5}", 123, 456);//Aliniere la stanga: "123 456
7 "
8 string.Format ("{0,-10:D6} {1,-10:D6}", 123, 456);//"000123 0000456
9 "
1 string.Format ("{0:C}", 123456);//$123,456.00
0 string.Format ("{0:C5}", 123456);//$123,456.00000
1 string.Format ("{0:D}", 123456);//123456
1 string.Format ("{0:D5}", 123456);//123456
1 string.Format ("{0:E}", 123456);//1.234560E+005
2 string.Format ("{0:E5}", 123456);//1.23456E+005
1 string.Format ("{0:F}", 123456);//123456.00
3 string.Format ("{0:F5}", 123456);//123456.00000
1 string.Format ("{0:G}", 123456);//123456
4 string.Format ("{0:G5}", 123456);//1.23456E5
1 string.Format ("{0:N}", 123456);//123,456.00
5 string.Format ("{0:N5}", 123456);//123,456.00000
1 string.Format ("{0:P}", 123456);//12,345,600.00 %
6 string.Format ("{0:P5}", 123456);//12,345,600.00000 %
1 string.Format ("{0:X}", 123456);//1E240
7 string.Format ("{0:X5}", 123456);//1E240
1
8
1
9
2
0
2
1
2
2

Fig. 7: Formatarea stringurilor.


1. Implementati interfata IList din spatiul de nume System.Collections. Implementarea
se va face in fiecare din urmatoarele doua clase:
 SimpleList - lista de stringuri, in sensul obisnuit, in care adaugarea unui element
se face la sfarsitul listei.
 SortedList - lista va contine stringuri sortate alfabetic, in care adaugarea unui
element se face la pozitia corespunzatoare ordinii alfabetice.

Se cere implementarea tuturor metodelor interfetei IList, pentru ambele clase, cu


functionalitatea specifica fiecareia.

Proiectati o functie Main cu urmatorul schelet:

 se declara o variabila de tip IList


 utilizatorul opteaza pentru unul din cele doua tipuri de liste
 in functie de optiune, se face instantierea variabilei de tipul IList declarate mai
sus
 se adauga 10 stringuri in lista
 se apeleaza celelalte functii implementate.
 se afiseaza lista folosind iteratori.

Observatie: interfata IList mosteneste interfata IEnumerable (care permite iterarea


asupra elementelor din lista)!

Windows Forms
Exceptii
Unul din scopurile principale ale CLR este acela de a elimina erorile furnizand mecanisme
pentru managementul automat al memoriei si resurselor precum si prin prinderea erorilor la
compilare, dat fiind faptul ca limbajele .Net sunt puternic tipizate (un tip nu poate minti
"spunand" ca este altceva decat ceea ce este). Pornind cu aceste concepte erori de genul buffer
underrun sau buffer overrun, folosirea unui obiect dupa ce memoria ocupata de el a fost
dealocata nu mai sunt posibile. Totusi unele erori (care tin de logica programului) nu pot fi
tratate decat la rulare (la runtime): erori de genul impartire la zero, anumite fisiere solicitate de
aplicatie nu exista, etc. Pentru tratarea acestor erori sunt folosite exceptii.

Pentru tratarea corecta a exceptiilor trebuie facuta diferenta intre o exceptie si un eveniment
asteptat. De exemplu daca este citit continutul unui fisier si este gasit sfarsitul fisierului (adica nu
se mai poate citi mai departe din acel fisier) am intalnit un eveniment asteptat si nu o exceptie
(faptul ca nu mai putem citi nu este o exceptie.). In cazul in care in timpul citirii din fisier
sistemul de operare raporteaza ca nu mai poate citi pentru ca exista o eroare pe disc am intalnit o
exceptie (nu ne asteptam sa nu mai putem citi din cauza unei erori pe disc).

Notiunii de exceptie trebuie sa-i asociem si un context. Si anume daca incercam sa ne conectam
la alt computer din retea si nu reusim (adica primim o exceptie) contextul acestei exceptii este
motivul pentru care nu am reusit conexiunea: username/password nu erau corecte, computerul
respectiv nu exista in retea, computerul exista in retea dar nu accepta conexiuni la programul
nostru, etc. Avand aceste informatii suplimentare (adica contextul exceptiei) putem sa gasim o
solutie pentru problema (ex: daca username/password nu sunt corecte putem sa cerem
utilizatorului sa le mai introduca o data).

Folosirea exceptiilor in C# are la baza 4 cuvinte cheie: throw, try, catch si finally. Atunci cand
executia unei metode nu mai poate continua din cauza unei situatii exceptionale aceasta va
"arunca" o exceptie. In exemplul urmator este prezentata o functie care verifica daca executia
programului poate continua. In cazul in care gaseste o eroare grava o exceptie de tipul
BigErrorException este aruncata (linia 4).

1 public void CheckForErrors ()


2 {
3 if (bigError)
4 throw new BigErrorException();
5 else
6 ContinueExecution();
7 }

Fig. 1: O functie care arunca o exceptie.

Prinderea unei astfel de exceptii este facuta intr-o instructiune de tipul try...catch... finally.

1 public void ExecuteProgram ()


2 {
3 try
4 {
5 //do something
6 CheckForErrors();
7 Console.WriteLine ("Everything looks fine. No errors ... yet!");
8 //do something else
9 }
1 catch (BigErrorException ex)
0 {
1 Console.WriteLine ("A big error occured. Panic [y/n]?");
1 //Do something to solve the error, if possible.
1 //All the necessary information about the error should be in the
2 'ex' object.
1 }
3 catch (Exception ex)
1 {
4 Console.WriteLine ("Unknown error!");
1 //do something here
5 return;
1 }
6 finally
1 {
7 Console.WriteLine ("Done! With or without errors.");
1 }
8 }
1
9
2
0
2
1
2
2
2
3
2
4
2
5
2
6

Fig. 2: Prinderea unei exceptii.

Existenta blocului finally este optionala. In cazul in care acesta exista se va executa indiferent
daca o exceptie a aparut sa nu. In cazul in care apare o exceptie CLR-ul verifica tipul acelei
exceptii pentru a selecta care bucata de cod se va ocupa de tratarea ei. Daca exceptia este de tipul
BigErrorException codul de la liniile 11-15 se va ocupa de tratarea ei. Daca exceptia aparuta e
de un alt tip va fi tratata de codul de la liniile 17-21.

In .Net toate exceptiile sunt derivate din clasa Exception. Din acest motiv instructiunea catch de
la linia 16 va prinde toate exceptiile neprinse pana in acel moment. (Ordinea in care declarati
blocurile te tip catch este importanta!).

La linia 20 se remarca o instructiune return. Executia acestei instructiuni nu inseamna iesirea


imediata din functie. Iesirea din functie va avea loc doar dupa executia blocului finally. Tot ca o
observatie relativ la iesirea dintr-o functie, instructiunea return este ilegala intr-un bloc de tip
finally.

Ce se intampla in cazul in care nu exista nici o instructiune pentru prinderea unei exceptii intr-un
cod care poate genera exceptii?

Cum poate fi rescrisa linia 16 din figura 2 pentru a prinde toate exceptiile fara a ne mai folosi de
tipul Exception?

Exceptii customizate
In .Net programatorul are posibilitatea sa-si creeze propriile exceptii, specifice aplicatiei pe care
o face. Pentru a crea propria exceptie tot ce trebuie sa faca un programator este sa creeze o clasa
derivata din Exception sau ApplicationException. Este recomandat ca un programator sa-si
deriveze propriile exceptii din ApplicationException, facand-use astfel distinctie intre
exceptiile customizate si cele definite in BCL (Base Class Library).

Delegati si evenimente
O alta mare noutate existenta in CTS (Common Type System) o reprezinta tipurile numite
generic delegates. Din punctul de vedere al cuiva care foloseste delegati, acestia sunt tipuri
referinta care incapsuleaza o metoda cu o anumita signatura. (Atentie: un delegat nu este o
functie, ci un tip referinta!!!). In .Net delegatii sunt folositi pentru a furniza functionalitatea de
callback precum si tratarea in mod asincron a evenimentelor.

Folosita in mod intensiv in programarea windows, functionalitatea de callback permite trimiterea


unui pointer la o functie altei functii (ca parametru). De exemplu functia EnumWindows (aflata in
API-ul Win32), enumera toate ferestrele deschise si pentru fiecare fereastra apeleaza o functie pe
care a primit-o ca paramatru.

Figura urmatoare prezinta modul de declarare si folosire a delegatilor.

1 public delegate void WorkDoneDelegate (string name);


2 public class Supervisor
3 {
4 private string m_name;
5 public Supervisor (string name)
6 {
7 m_name = name;
8 }
9 public void EmployeeWorkDone (string name)
1 {
0 Console.WriteLine ("{0}: {1} has finished his work.", m_name,
1 name);
1 }
1 }
2 public class Employee
1 {
3 public WorkDoneDelegate workDone;
1 private string m_name;
4
1 public Employee (string name)
5 {
1 m_name = name;
6 }
1 public void Work()
7 {
1 //do work.
8 if (workDone != null)
1 workDone(m_name);
9 }
2 }
0 public class TestApplication
2 {
1 static void Main ()
2 {
2 Supervisor boss = new Supervisor ("Boss");
2 Employee worker = new Employee ("John");
3 worker.workDone = new WorkDoneDelegate (boss.EmployeeWorkDone);
2 worker.Work();
4 }
2 }
5
2
6
2
7
2
8
2
9
3
0
3
1
3
2
3
3
3
4
3
5
3
6
3
7
3
8
3
9

Fig. 3: Declararea si folosirea delegatilor.

La linia 1 din figura anterioara am declarat tipul delegat. Desi pare ca am dclarat o functie
WorkDoneDelegate este o clasa derivata din clasa System.MulticastDelegate. Compilatorul
C# adauga codul necesar pentru aceasta, sintaxa de mai sus fiind doar syntactic sugar. De fapt,
acest tip va contine o metoda care are signatura de la linia 1.

Tipul Employee contine ca data membru o instanta de tipul WorkDoneDelegate. Inainte de


terminarea executiei funtiei Work este apelat acest delegat (in cazul in nu care exista nimeni
interesat ca acest worker si-a terminat treaba - adica testul de la linia 26 intoarce false - nu se
face nici un apel - nu exista listeneri).

In exemplul anterior totul pare OK. Cand "John" isi va termina treaba seful va fi instiintat. Dar ce
se inampla in cazul in care si presedintele vrea sa fie instiintat de acet lucru? Adaugand
instructiunea worker.workDone = new WorkDoneDelegate (president.EmployeeWorkDone)
nu se va obtine efectul dorit pentru ca este suprascris listenerul sefului, si seful nu va mai fi
instiintat.

Solutia este ca tipul Employee sa declasenze un eveniment atunci cand isi termina treaba.

1 public class Employee


2 {
3 ...
4 public event WorkDoneDelegate workDone;
5 ...
6 }
7 public class TestApplication
8 {
9 static void Main ()
1 {
0 Supervisor boss = new Supervisor ("Boss");
1 Supervisor president = new Supervisor ("President");
1 Employee worker = new Employee ("John");
1 worker.workDone += new WorkDoneDelegate (boss.EmployeeWorkDone);
2 worker.workDone += new WorkDoneDelegate
1 (president.EmployeeWorkDone);
3 worker.Work();
1 }
4 }
1
5
1
6
1
7
1
8

Fig. 4: Evenimente.

Se observa ca acum dupa ce workDone a devenit eveniment adaugarea listenerilor nu se mai face
folosind operatorul = ci operatorul +=. Asfel oricine doreste sa asculte la acel eveniment isi poate
adauga propriul listener, fara sa interfereze cu ceilalti listeneri. Acum workDone va mentine o
lista cu toti listenerii. In momentul in care este declansat acest eveniment este apelat fiecare
listener din lista. Ordinea de apelare este aceeasi cu cea a adaugarii lor in lista.

Eliminarea unui listener din lista se face folosind operatorul -=.

ADO.Net
ADO.Net este o multime de biblioteci orientate obiect care permit interactiunea cu sistemele de
stocare a informatiilor. De obicei aceste sisteme sunt reprezentate de bazele de date, dar pot fi si
fisiere text, fisiere XML, fisiere Excel, etc. In continuare ne vom ocupa de interactiunea cu
bazele de date.
Un server de baze de date pe care se pot rula exemplele urmatoare este MSDE (Microsoft
Desktop Engine 2000) si poate fi descarcat gratuit de pe site-ul Microsoft.

Data Provider
ADO.Net permite interactiunea cu diverse tipuri de baze de date. Totusi nu exista un singur set
de clase care sa pemita acest lucru. Deoarece fiecare tip de baza de date foloseste alte protocoale
trebuie sa gasim o metoda sa folosim protocolul corect in fiecare caz. Unele sisteme mai vechi
folosesc protocolul ODBC, altele mai noi, folosesc protocolul OleDb, etc.

ADO.Net furnizeaza metode asemanatoare de comunicare cu fiecare sistem de stocare a


informatiilor, dar fiecare este implementata separat intr-o biblioteca. Aceste biblioteci sunt
numite Data Providers si poarta numele protocolului sau sistemului cu care permite
interactiunea. Tabelul urmator prezinta o lista cu o parte dintre acesti provideri.

Provider Prefix API Descriere

Data Sources care folosesc protocolul ODBC (in


ODBC Data Provider Odbc
mod normal baze date mai vechi).

Data Sources care expun interfata OleDb (Access,


OleDb Data Provider OleDb
Excel, etc.)

Oracle Data Provider Oracle Pentru bazele de date Oracle.

SQL Data Provider Sql Pentru Microsoft Sql Server si MSDE

Ofera acces la mai multe tipuri de baze de date


Borland Data Provider Bdp
cum ar fi Interbase, SQL Server, IBM DB2 si Oracle.

Fig. 1: Data Providers

Obiectele ADO.Net
SqlConnection

Primul lucru pe care un programator trebuie sa-l faca atunci cand vrea sa comunice cu o baza de
date este sa deschide o conexiune la aceasta. Conexiunea "spune" celorlalte obiecte cu ce baza de
date lucreaza. Conexiunea se ocupa de logica low-level asociata protocolului. Acest lucru
usureaza foarte mult munca unui programator, acesta neavand decat sa instantieze obiectul
conexiune, sa deschida conexiunea, sa faca operatiile de care are nevoie asupra bazei de date si
apoi sa inchida conexiunea. Datorita modului in care celelalte clase ADO.Net sunt implementate
uneori este nevoie de chiar mai putin decat atat.
Desi folosirea conexiunilor este mult simplificata in ADO.Net, programatorul trebuie sa le
inteleaga foarte bine pentru a lua deciziile corecte. O conexiune este o resursa foarte importanta.
Daca aplicatia va fi folosita pe o singura masina, asupra unei singure baze de date, importanta
acestei resurse este mai putin clara. Dar daca este vorba de o aplicatie enterprise, folosita
simultan de multi utilizatori asupra aceleiasi baze de date importanta conexiunii este mult mai
clara. Fiecare conexiune reprezinta overhead pentru server si nu exista nici un server care sa
suporte overhead infinit.

Un obiect SqlConnection este la fel ca orice alt obiect C#. de cele mai multe ori declararea si
instantierea se face in acelasi timp:

1 SqlConnection sqlConn = new SqlConnection(


2 "Data Source=(local);Initial Catalog=Northwind;Integrated
3 Security=SSPI");
4
Conectarea folosind un cont anume
5 SqlConnection sqlConn1 = new SqlConnection(
6 "Data Source=DatabaseServer;Initial Catalog=Northwind;User
7 ID=YourUserID;Password=YourPassword");
8
9 //sau pentru OleDb
1 OleDbConnection oleDbConn = new OleDbConnection(
"Provider=Microsoft.Jet.OLEDB.4.0;Data
0
Source=MyDatabase.mdb");

Fig. 2: Instantierea unui obiect de tipul SqlConnect

Obiectul SqlConnection de mai sus este instantiat folosind un constructor care primeste ca
parametru un string. Acest argument este stringul de conectare.

Parametru al stringului de conectare Descriere

Identifica masina server. Poate sa fie masina locala,


Data Source numele unui computer din domeniu sau o adresa
IP.

Initial Catalog Numele bazei de date.

Setat la valoarea SSPI pentru a face conexiunea


Integrated Security
folosind contul windows al utilizatorului.

User ID Numele de utilizator configurat pe serverul SQL.

Password Parola atasata utilizatorului de la User ID.

Fig. 3: Stringurile de conectare contin perechi de tipul cheie/valoare despre modul in care se
va face conectarea la baza de date.

Scopul instantierii unui obiect de tip SqlConnection este ca alte obiecte ADO.Net sa poata lucra
cu baza de date. Alte obiecte, cum ar fi SqlDataAdapter si SqlCommand, au constructori care
primesc obiectul conexiune ca parametru. Atunci cand se lucreaza cu o baza de date trebuie
urmati pasii:

1. Instantierea unui obiect SqlConnection;


2. Deschiderea conexiunii;
3. Trimiterea conexiunii ca parametru altor obiecte ADO.Net;
4. Realizarea operatiunilor asupra bazei de date;
5. Inchiderea conexiunii.

Exemplul urmator prezinta modul de folosire a unei conexiuni, pas cu pas:

1 using System;
2 using System.Data;
3 using System.Data.SqlClient;
4
// Prezinta modul de lucru cu un obiect SqlConnect
5 class SqlConnectionDemo
6 {
7 static void Main()
8 {
9 // 1. Instantiaza conexiunea
1 SqlConnection conn = new SqlConnection(
"Data Source=(local);Initial
0
Catalog=Northwind;Integrated Security=SSPI");
1
1 SqlDataReader rdr = null;
1
2 try
1 {
3 // 2. Deschide conexiunea
conn.Open();
1
4 // 3. Trimite conexiunea altui obiect ADO.Net
1 SqlCommand cmd = new SqlCommand("SELECT * FROM
5 Customers", conn);
1
6 //
// 4. Realizarea operatiunilor asupra bazei de date
1
//
7
1 // Obtine rezultatul interogarii
8 rdr = cmd.ExecuteReader();
1
9 // Afiseaza valoarea CustomerID a fiecarei
2 inregistrari
while (rdr.Read())
0
{
2
1 Console.WriteLine (rdr[0]);
2 }
2 }
finally
2
{
3 // inchide reader-ul
2 if (rdr != null)
4 {
2 rdr.Close();
5 }
2
// 5. Inchide conexiunea
6
if (conn != null)
2 {
7 conn.Close();
2 }
8 }
2 }
9 }
3
0
3
1
3
2
3
3
3
4
3
5
3
6
3
7
3
8
3
9
4
0
4
1
4
2
4
3
4
4
4
5
4
6
4
7
4
8
4
9
5
0
5
1
5
2

Fig. 4: Folosirea unui obiect obiect de tipul SqlConnect

Asa cum se vede la linia 19 deschiderea conexiunii se face apeland metoda Open() a instantei
SqlConnection. Orice operatii asupra unei conexiuni care nu a fost inca deschisa genereaza o
exceptie.

Inainte de a folosi conexiunea trebuie sa instiintam celelalte obiecte ADO.Net despre care
conexiune este vorba. Facem acest lucru la linia 22 din figura 4, trimitand ca parametru
constructorului SqlCommand obiectul conn. Orice operatie pe care o va face instanta cmd va
folosi aceasta conexiune.

Obiectul care foloseste conexiunea este cmd, de tipul SqlCommand. Acesta face o interogare in
baza de date adupra tabelului Customers. Rezultatul este intors intr-un obiect de tipul
SqlDataReader, iar in bucla while este afisat continutul primei coloane din fiecare rand din
tabelul obtiunut (noi stim ca aceasta coloana este coloana CustomerID). Important de retinut este
ca obiectele SqlCommand si SqlDataReader folosesc un obiect de tipul SqlConnection, si astfel
stiu cu care baza de date sa lucreze.

Atunci cand am terminat de folosit obiectul conn inchidem conexiune (linia 48). Lasarea
conexiunii deschise are implicatii grave in performanta aplicatiei.

SqlCommand

Obiectele de tipul SqlCommand permit specificare tipului de actiune asupra bazei de date. De
exemplu se poate face o interogare, inserare, modificare sau stergere.

Instantierea unui obiect de tipul SqlCommand se face ca la linia 22 din figura 4. Aceste este
modul cel mai des intalnit de instantiere. Ia ca parametru un string, care este comanda ce va fi
executata, si o referinta la un obiect de tipul SqlConnect.
Atunci cand faci o interogare in baza de date, obtii un tabel rezultat care trebuie sa poata fi
vizualizat. Pentru a obtine acest lucru folosind obiecte SqlCommand este folosita metoda
ExecuteReader care intoarce un obiect de tipul SqlDataReader. Exemplul de mai jos arata
modul de obtinere a unei instante SqlDataReader.

1 // 1. Instantiaza o noua comanda cu o fraza select si o conexiune


SqlCommand cmd = new SqlCommand ("SELECT CategoryName FROM Categories",
2 conn);
// 2. Executa interogarea
3 SqlDataReader rdr = cmd.ExecuteReader();

Fig. 5: Realizarea unei interogari in baza de date.

Pentru a insera valori intr-o baza de date trebuie apelata functia ExecuteNonQuery pe un obiect
SqlCommand. Exemplul urmator arata modul de inserare intr-o baza de date.

1 // pregateste stringul comanda


string insertString = @"INSERT INTO Categories (CategoryName, Description)
2 VALUES ('Miscellaneous', 'Whatever doesn''t fit
elsewhere')";
3 // 1. Instantiaza o noua comanda
SqlCommand cmd = new SqlCommand (insertString, conn);
4
// 2. Apeleaza ExecuteNonQuery pentru a executa comanda
5 cmd.ExecuteNonQuery();

Fig. 6: Inserarea intr-o baza de date.

Modificarea si stergerea datelor dintr-o baza de date se face la fel ca si inserarea, dar ca punem
primul parametru al constructorului SqlCommand pe valoarea corespunzatoare.

Uneori avem nevoie dintr-o baza de date doar de o singura valoare, care poate fi suma, media,
etc. inregistrarilor dintr-un tabel. Apeland ExecuteReader si apoi calculand acea valoare in
program nu este cea mai eficienta metoda de a ajunge la rezultat. Cea mai buna metoda este sa
lasam baa de date sa faca ceea ce este necesar si sa intoarca o singura valoare. Acest lucru il face
metoda ExecuteScalar:

1 // 1. Instantiaza o noua comanda


2 SqlCommand cmd = new SqlCommand ("SELECT count(*) FROM Categories", conn);
3
// 2. Apeleaza ExecuteScalar pentru a executa comanda
4
int count = (int) cmd.ExecuteScalar();
5

Fig. 7: Obtinerea unei singure valori.

SqlDataReader

Tipul SqlDataReader este folosit pentru a citi date in cea mai eficienta metoda posibila. NU
poate fi folosit pentru scriere. O data citita o informatie nu mai poate fi citita inca o data.
SqlDataReader citeste secvential date.

Datprita faptului ca citeste doar inainte (forward-only) permite acestui tip de date sa fie foarte
rapid in citire. Overhead-ul asociat este foarte mic (overhead generat cu inspectarea rezultatului
si a scrierii in baza de date). Daca intr-o aplicatie este nevoie doar de informatii care vor fi citite
o singura data, sau rezultatul unei interogari este prea mare ca sa fie retinut in memorie (caching)
SqlDataReader este solutia cea mai buna.

Obtinerea unei instante de tipul SqlDataReader este putin diferita de instantierea normala -
trebuie apelata metoda ExecuteDataReader. Daca pentru instantiere este folosit operatorul new
veti obtine un obiect cu care nu puteti face nimic pentru ca nu are o conexiune si o comanda
atasate.

SqlDataReader obtine datele intr-un stream secvential. Pentru a citi aceste informatii trebuie
apelata metoda Read; aceasta citeste un singur rand din tabelul rezultat. Metoda clasica de a citi
informatia dintr-un SqlDataReader este de a itera intr-o bucla while asa cum se vede in figura 4
la liniile 32-35.

Metoda Read intoarce true cat timp mai este ceva de citit din stream.

SqlDataReader implementeaza si indexatori (am obtinut prima coloana in exemplul din figura 4
folosind indexatori numerici). In exemplul din figura 4 nu este foarte clar pentru cineva care
citeste codul ca acolo este vorba de coloana CustomerID (decat daca s-a uitat si in baza de date).
Din aceasta cauza este preferata utilizarea indexatorilor de tipul string. In acest caz codul devine:

1 // Obtine rezultatul interogarii


2 rdr = cmd.ExecuteReader();
3
// Afiseaza valoarea CustomerID a fiecarei inregistrari
4
while (rdr.Read())
5 {
6 Console.WriteLine (rdr["CustomerID"]);
7 }
8

Fig. 8: Folosirea indexatorilor asupra unui SqlDataReader.

Valoeare indexului trebuie sa fie numele coloanei din tabelul rezultat.

Indiferent ca se foloseste un index numeric sau unul de tipul string indexatorii intorc totdeauna
un obiect de tipul object fiind necesara conversia.

Dupa ce un reader nu mai este folosit acesta trebuie inchis apeland metoda Close (linia 42 din
figura 4)

SqlDataAdapter

Pana acum am vazut cum putem efectua operatii asupra unei baze de date folosind obiecte de
tipul SqlCommand si SqlDataReader. Problema cu aceasta abordare este ca pe parcursul intregii
tranzactii conexiunea trebuie sa fie deschisa.

Voi prezenta in continuare o metoda care nu necesita o conexiune permanenta la o baza de date -
si anume folosind obiecte de tipul DataSet si SqlDataAdapter.

Un DataSet este o reprezentare in memorie a unui data store (un sistem de stocare si obtinere a
datelor). Un DataSet contine o multime de tabele asupra carora se pot executa diverse operatii.
Un DataSet doar retine informatii si nu interactioneaza cu un data source. SqlDataAdapter este
cel care se ocupa administrarea conexiunilor cu data source si ofera comportamentul de lucru in
mod deconectat. SqlDataAdapter deschide o conexiune doar atunci cand este nevoie si o
inchide imediat ce si-a terminmat treaba. De exemplu SqlDataAdapter realizeaza urmatoarele
operatiuni atunci cand trebuie sa populeze un DataSet:

1. deschide conexiunea;
2. populeaza DataSet-ul;
3. inchide conexiunea;

si urmatoarele operatiuni atunci cand trebuie sa faca update in baza de date:

1. deschide conexiunea;
2. scrie modificarile din DataSet in baza de date;
3. inchide conexiunea;

Intre operatiunea de populare a DataSet-ului si cea de update conexiunile la data source sunt inchise.
Intre aceste operatii in DataSet se poate scrie sau citi. Acestea sunt mecanismele de a lucra in mod
deconectat. Pentru ca aplicatia tine deschisa conexiunea la baza de date doar atunci cand este necesar,
devine mai scalabila.
Crearea unui obiect de tipul DataSet se face folosind operatorul new

1 DataSet dsCustomers = new DataSet ();

Fig. 9: Instantierea unui DataSet.

Constructorul unui DataSet nu necesita parametri. Exista totusi o supraincarcare a acestuia care
primeste ca parametru un string, si este folosit atunci cand trebuie sa se faca o serializare a
datelor intr-un fisier XML. In acest moment avem un DataSet gol si avem nevoie de un
SqlDataAdapter pentru a-l popula.

Un obiect SqlDataAdapter contine mai multe obiecte SqlCommand si un obiect SqlConnection


pentru a citi si scrie date.

1 SqlDataAdapter daCustomers = new SqlDataAdapter ("SELECT CustomerID,


CompanyName FROM Customers", conn);

Fig. 10: Instantierea unui SqlDataAdapter.

Codul de mai sus creaza un obiect de tipul SqlDataAdapter, daCustomers. Comanda SQL
specifica cu ce date va fi populat un DataSet, iar conexiunea conn trebuie sa fi fost creata
anterior, dar nu si deschisa. Responsabilitatea deschiderii conexiunii revine adapterului in la
apelul metodelor Fill si Update.

SqlDataAdapter contine mai multe obiecte comanda: cate unul pentru inserare, update, delete si
select. Prin intermediul constructorului putem instantia doar comanda de interogare. Instantierea
celorlalte se face fie prin intermediul proprietatilor pe care le expune SqlDataAdapter, fie
folosind obiecte de tipul SqlCommandBuilder.

1 SqlCommandBuilder cmdBldr = new SqlCommandBuilder (daCustomers);

Fig. 11: Instantierea unui SqlCommandBuilder.

La initializarea unu SqlCommandBuilder am apelat un constructor care primeste ca parametru un


adapter, pentru care vor fi construite comenzile. SqlCommandBuilder are limitari: nu poate
construi decat comenzi simple si care se aplica unui singur tabel. Atunci cand trebui ca sa facem
comenzi care vor folosi mai multe tabele este recomandata construirea separata a comnezilor si
apoi atasarea lor adapterului folosind proprietati.

O data ce avem cele doua instante, DataSet si SqlDataAdapter, putem sa populam DataSet-ul.

1 daCustomers.Fill (dsCustomers, "Customers");

Fig. 12: Popularea unui DataSet.


Metoda Fill din exemplul anterior primeste doi parametri: un DataSet pe care-l va popula si un
string care va fi numele tabelului (nu numele tabelului din baza de date, ci al tabelului rezultat in
DataSet) care va fi creat. Scopul acestui nume este identificarea ulterioara a tabelului. In cazul
in care nu este specificat nici un nume de tabel, acestea vor fi adaugate in DataSet sub numele
Table1, Table2, ...

Un DataSet poate fi folosit ca data source pentru un DataGrid atat in ASP.Net cat si pentru cel
din Windows Forms. Mai jos este prezentat un exemplu de legare a unui DataSet la un
DataGrid:

1 DataGrid dgCustomers = new DataGrid();


2 dgCustomers.DataSource = dsCustomers;
3 dgCustomers.DataMembers = "Customers";

Fig. 13: Legarea unui DataSet la un DataGrid.

La linia 2 setez un DataSet ca DataSource pentru un DataGrid. Acest lucru ii spune grid-ului ce
informatii sa afiseze. Un grid stie sa afiseze mai multe tabele dintr-un DataSet, afisand un semn
"+" permitandu-i utilizatorului sa aleaga care tabel sa fie afisat din cele disponibile. Pentru a
suprima afisarea acelui semn "+" din GUI am setat si proprietatea DataMembers pe numele
tabelului care va fi afisat. Numele tabelului este acelasi care l-am folosit ca parametru in apelul
metodei Fill

Dupa ce au fost facute modificari intr-un DataSet acestea trebuie scrise si in baza de date.
Actualizarea se face prin apelul metodei Update

1 daCustomers.Update (dsCustomers, "Customers");

Fig. 14: Actualizarea bazei de date.

SqlParameter

Atuci cand lucrati cu bazele de date veti avea nevoie, de cele mai multe ori sa filtrati rezultatul
dupa diverse criterii. De obicei acest lucru se face in functii de niste criterii pe care utilizatorul le
specifica (ex: vreti sa vedeti doar clientii anume oras).

Dupa cum am vazut, o interogare asociata unui obiect SqlCommand este un simplu string. Cea
mai simpla metoda de filtrare a rezultatelor este sa construiti acel string in mod dinamic, dar
aceasta metoda nu este recomandata.

1 // sa nu faceti asa!!
2 SqlCommand cmd = new SqlCommand(
3 "SELECT * FROM Customers WHERE city = '" + inputCity + "'";
Fig. 15: Un exemplu despre construirea nerecomandata a interogarii.

Motivul pentru care o astfel de construire a unei interogari este nerecomandata este ca nu se
poate avea incredere in input-ul utilizatorului. De obicei inputCity este introdus de utilizator
intr-un TextBox. Folosind acel TextBox un utilizator rau intentionat poate sa introduca cod care
poate duce la coruperea bazei de date, accesarea informatiilor confidentiale, etc.

In loc sa construiti dinamic stringul de interogare folositi interogari cu parametri. Orice valoare
pusa intr-un parametru nu va fi tratata drept cod SQL, ci ca valoare a unui camp, facand aplicatia
mai sigura. Pentru a folosi interogari cu parametri urmati pasii:

1. Construiti stringul pentru SqlCommand folosind parametri;


2. Creati un obiect SqlParameter asignand valorile corespunzatoare;
3. Adaugati obiectul SqlParameter la obiectul SqlCommand, folosind proprietatea Parameters.

Deci primul pas este construirea unui string de interogare parametrizat. Pentru a specifica locul
unde vor fi inserate valorile parametrilor folositi marcatorul @. Sintaxa este urmatoarea:

1 // 1. Declarati un obiect SqlCommand care are stringul de interogare


parametrizat
2 SqlCommand cmd = new SqlCommand(
"SELECT * FROM Customers WHERE city = @City", conn);
3

Fig. 16: Obiect comanda cu stringul de interogare parametrizat.

In contructorul din exemplul anterior stringul de interogare contine un parametru @City. Atunci
cand comanda va fi executata in string @City va fi inlocuit cu valoarea aflata in obiectul
SqlParameter atasat. In exemplul din figura 16 folosim o interogare cu un singur parametru, dar
pot exista oricati parametri, si pentru fiecare din acestia trebuie sa asociem un obiect
SqlParameter. In cazul in care pentru un parametru din stringul de interogare nu avem asociata
o instanta de tipul SqlParameter vom obtine o eroare la rulare. Acelasi lucru se intampla si daca
avem mai multe instante SqlParameter pentru un parametru.

Acum trebuie sa declaram o instanta de tipul SqlParameter:

1 // 1. Definiti parametrii utilizati in stringul de interogare


2 SqlParameter param = new SqlParameter();
3 param.ParameterName = "@City";
param.Value = inputCity;
4

Fig. 17: Declararea unei instante de tipul SqlParameter.

Si acum trebuie sa adaugam acet parametru obiectului comanda:


1 // 1. Adauga parametrii definiti obiectului comanda
2 cmd.Parameters.Add(param);

Fig. 18: Adaugararea parametrilor obiectului comanda.