RicH and FamouS

       Home         Glosar IT                                                                                                                                                                                                              SUBSCRIBE NOW!
        
Se afișează postările cu eticheta C#. Afișați toate postările
Se afișează postările cu eticheta C#. Afișați toate postările

13.04.2009

C# Data Layer

Exista multe stiluri de a scrie cod care ineractioneaza cu o baza de date. Este recomandata impartirea pe straturi a codului care se ocupa de asta. Un mod de a face asta este cel pe 3 straturi: Data Layer, BussinessLogic si Applicatie. Acest articlo se vrea o descriere a modului cum poate fi creat stratul DataLayer, insa o sa aiba ceva exemple si de cum poate fi folosit acest strat pentu a trimite informatii spre straturile superioare.

Lucrurile arata cam asa:

[Applicatie] <----> [Bussiness Layer] <----> [DataLayer] <----> [DataBase]



DataBase poate fi orice, M$ Sql, Oracle, Acces etc, insa codul o sa fie scris pentru M$ Sql.

Intr-un proiect nou, se creaza o noua clasa, cu numele dal.cs

Pentru a construi o clasa (pe care o voi numi DAL in continuare) trebuie sa importam in proiectul nostru namespace-urile System.Data.SqlClient si System.Data.SqlClient (in plus fatza de ce isi aduce default VisualStudio).

La mine arata cam asa (deocamdata):

Cod:
using System;
using System.Collections;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace TestEMNdb
{
public class DAL
{
}
}

Primul lucru de care avem nevoie este connectionString, unde definim parametrii necesari accesari serverului unde se aftla baza de date pe care o vom folosi.

Pentru acest exemplu vom folosi urmatorul string:
Data Source=EXPEED-L006SQLEXPRESS;
Initial Catalog=Northwind;
Integrated Security=SSPI;


Unde EXPEED-L006SQLEXPRESS este numele serverului care ruleaza pe calculatorul meu, Northwind este numele bazei de date pe care o voi folosi (vine ca exemplu cu sql server) isr ultimul parametru, Integrated Security=SSPI defineste nivelul de securitate pe care il va folosi connexiunea (se vor folosi drepturile userului curent al sistemului de operare).

Locul cel mai bun de pastrare a acestui string nu este in codul clasei, insa pentru moment, o sa il pun in interiorul clasei (hard coded). Pana la sfarsitul articolului o sa ii gasim un loc mai bun.

Deasemeni, avem nevoie de 2 obiecte, SqlConnection si SqlCommand (ambele se gasesc in numele de spatiu System.Data.SqlClient).

Acum ar trebui sa ne gandim si la un constructor pentru aceasta clasa, in interiorul caruia sa initializam ceea ce avem pana acum, SqlConnection si SqlCommand:

conn = new SqlConnection(connString); // definim un obiect de tipul SqlConnection, care sa aiba grija de conexiunea cu baza de date apoi,
command = new SqlCommand(); // un obiect care sa se ocupe de ceea ce vrem sa ii transmitem bazei de date, si la urma
command.Connection = conn; // ii zicem instantei obiectului command sa foloseasca obiectul conn pentru a transmite ce are de transmis bazei de date.

Pana acum clasa noastra arata asa:

Cod:
using System;
using System.Collections;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace TestEMNdb
{
public class DAL
{
string connString = @"Data Source=EXPEED-L006SQLEXPRESS; Initial Catalog=Northwind; Integrated Security=SSPI;";
SqlConnection conn;
SqlCommand command;

public DAL()
{
conn = new SqlConnection(connString);
command = new SqlCommand();
command.Connection = conn;
}
}
}

Deocamdata avem o clasa care nu prea face nimic. Initializeaza niste obiecte, si cam atat. Ca sa faca ceva, sa ii punem niste metode. Ar fi necesare o metoda Open() care sa deschida coneciunea la baza de date, si o alta metoda Close(), evident, sa inchida coneciunea deschisa de metoda Open(). Acestea sunt cele mai simple metode ale acestei clase.

Cod:
public void Open()
{
conn.Open();
}

public void Close()
{
if (conn != null)
conn.Close();
}

In metoda Close() inainte de a inchide connexiunea, verific daca obiectul conn este nenul. Daca obiectul conn este null, si se incearca executarea metodei conn.Close() o exceptie va fi aruncata. Daca acest obiect nu e null, inchid conexiunea. Nu trebuie sa verific altceva, intru-cat inchiderea unei conexiuni care nu este deschisa nu arunca o exceptie. Deci executarea acestei functii este destul de sigura.

Pentru c acum putem deschide o conexiune la baza de date, sa incepem sa creem niste metode care sa ne aduca si ceva date stocate in tabelele bazei noastre de date.

Una dintre cele mai simple interogari pe care le putem face asupra tabelelor, este sa cerem o singura valoare, ca de exemplu: cat costa produsul X? sau cati munitori sunt inregistrati? Sau in ce data am facut X? Cu alte cuvinte, rezultatul pe care il primim este o valoare care reprezinta rezultatul executiei interogarii noastre asupra bazei de date si care este returnata in coloana 1 randul 1. Orice alte rezultate returnate de baza de date, sunt ignorate.

Cum facem sa primim asa ceva? Destul de simplu! Obiectul command are o metoda care face asta petru noi: command.ExecuteScalar(). Avem nevoie numai sa transmitem obiectului command interogarea sql, si el se ocupa de restul.

Cod:
public object GetScalar(string sql)
{
command.CommandText = sql;
command.CommandType = CommandType.Text;
return command.ExecuteScalar();
}

De retinut este ca aceasta functie returneaza un obiect. Pentru afolosi valoarea returnata trebuie facut (in mometul folosiri ei) casting la tipul de data pe care ne asteptam sa il primim.

Doar ca exemplu de folosire a acestei functii (nu este parte a obiectului DAL - intru-cat aici nu trebuie sa avem decat functii generice, nu specifice unei tabele/baze de date/actiuni) aceasta functie se foloseste asa:

Cod:
string sql = "select count(*) from Products";
conn.Open();
int result = (int)GetScalar(sql);
conn.Close();

Urmatoarea functie utila este o functie care sa ne permita sa face actiuni DML asupra bazei de date. Obiectul connect are inca o metoda utila numita ExecuteNonQuery(). Aceasta executa o actiune DML si returneaza numarul de randuri afectate. Aceasta functie este folosita la inserarea de date, la stergerea de date sau modificarea datelor.

Cod:
public void ExecuteNonQuery(string sql)
{
command.CommandType = CommandType.Text;
command.CommandText = sql;
command.ExecuteNonQuery();
}

Ca sa recapitulam, pana in acest moment am avansat destul de mult. Avem o conexiune, pe care o putem controla, si avem posibilitatea sa primim (deocamdata putine) si sa trimitem date. Destul de bine pentru cateva linii de cod.

Insa in marea majoritate a cazurilor, avem nevoie de mai mult decat o singura valoare. Avem nevoie de un rand, cateva randuri sau chiar de un tabel intreg. Una dintre cele mai simple si eficiente metode de a citi randuri returnate de o instructiune select este folosind SqlDataReader. SqlDataReader este o clasa care ne permite sa citim date intr-un mod foarte eficient. SqlDataReader nu se poate folosi pentru scrierea datelor, iar citirea se face numai prin parcurgerea rezultatelor returnate de instructiunea select numai intr-un sens: inainte! Cu alte cuvinte, daca acum citim randu 4, urmatorul rand la care avem acces pentru citire este randul 5. Pentru a ajunge la randul 3 trebuie sa o luam cu cititul de la capat, de la randul 1 (parcurgand 1 si 2 pentru a avea acces la randul 3).
De regula, datele citite cu SqlDataReader sunt salvate in obiecte care permit parcurgerea datelor in ambele sensuri (sau accesarea prin index a unui element dorit).

Clasa SqlDataReader este definita in namespace-ul System.Data.SqlClient.

Pentru a crea o instantza a SqlDataReader se foloseste metoda ExecuteReader() a unui obiect SqlCommand.

Cod:
SqlDataReader reader = command.ExecuteReader();

Pentru a putea citi date folosind SqlDataReader avem nevoie de o conexiune deschisa la baza de date. De mentionat este ca aceasta conexiune va fi "ocupata" pana ce obiectul SqlDataReader o elibereaza.

Revenind la clasa noastra DAL, o sa o completam cu o functie noua, si anume una care sa returneze un obiect de tip SqlDataReader.

Cod:
public SqlDataReader GetReader(string sql)
{
command.CommandType = CommandType.Text;
command.CommandText = sql;
SqlDataReader dataReader = command.ExecuteReader();
return dataReader;
}

Acestei functii i se transmite ca parametru interogarea sql dorita, iar setul de randuri returnate de sql este accesibil prin intermediul obiectului de tip SqlDataReader returnat. Nu uitati ca inainte de a apela aceasta functie sa deschideti o conexiune, iar la final sa inchideti conexiunea deschisa!

Un exemple de folosire al acestei functii este (nu face parte din DAL.cs - ci dintr-o clasa de tip bussinessLogic):

Cod:
string sql = "select name from students";
con.Open();
SqlDataReader reader = GetReader(sql);
while (reader.Read())
{
// aici se salveaza/prelucreaza/etc datele primite
}
con.Close();

O alta metoda de a avea acces la datele returnate de o instructiune select este salvarea rezultatelor returnate in obiecte de tip DataTable sau DataSet. Descrierea acestor obiecte nu face parte din acestu tutorial. Se pot gasi tutoriale. Pe msdn sunt explicate aceste obiecte: DataTable si [url="http://msdn2.microsoft.com/en-us/librar /system.data.dataset.aspx"]DataSet[/url]

Deci, pe scurt, datele returnate dintr-o interogare sql se pot salva in obiecte de tip DataTable si DataSet. Aceste obiecte se trimit prin referintza functiei care face citirea datelor din baza de date. Aceasta functie se ocupa de "salvarea" datelor in obiect.

Cod:
public void Fill(DataTable dt, string sql)
{
SqlDataAdapter adapter = new SqlDataAdapter(sql, conn);
adapter.Fill(dt);
}

public void Fill(DataSet ds, string sql)
{
SqlDataAdapter adapter = new SqlDataAdapter(sql, conn);
adapter.Fill(ds);
}

Avantajele folosiri DataTable sau DataSet sunt ca datele pot fi parcurse apoi fara a necesita o conexiune deschisa la baza de date. Datele pot fi parcurse in ambele sensuri, sau prin intermediul indecsilor. Deasemeni obiectele de acest tip ofera multe moduri de a prelucra datele, de a cauta date specifice etc (documentatia msdn specifica toate metodele obiectelor de acest tip). Unul din dezavantajele folosiri acestui tip de obiect este consumul de resurse. Daca trebuiesc citite date care trebuiesc parcurse in ambele sensuri, date care trebuiesc parcurse de mai multe ori, date care trebuiesc prelucrate incrucisat, etc folositi datatable sau DataSet. Daca nu, folosirea SqlDataReader este indicata (citirea pentru afisare a unui tabel, incarcarea intr-un obiect specific a anumitor valor din tabele, popularea unor obiecte de tip colectie (ArrayList, HashTable etc)).

Cam asta e ceea ce trebuie sa contina (un minim) o clasa de tip DAL. Codul intregi clase este:

Cod:
using System;
using System.Collections;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace TestEMNdb
{
public class DAL
{
string connString = @"Data Source=EXPEED-L006SQLEXPRESS; Initial Catalog=Northwind; Integrated Security=SSPI;";
SqlConnection conn;
SqlCommand command;

public DAL()
{
conn = new SqlConnection(connString);
command = new SqlCommand();
command.Connection = conn;
}

public void Open()
{
conn.Open();
}

public void Close()
{
if (conn != null)
conn.Close();
}

public object GetScalar(string sql)
{
command.CommandText = sql;
command.CommandType = CommandType.Text;
return command.ExecuteScalar();
}

public void ExecuteNonQuery(string sql)
{
command.CommandType = CommandType.Text;
command.CommandText = sql;
command.ExecuteNonQuery();
}

public SqlDataReader GetReader(string sql)
{
command.CommandType = CommandType.Text;
command.CommandText = sql;
SqlDataReader dataReader = command.ExecuteReader();
return dataReader;
}

public void Fill(DataTable dt, string sql)
{
SqlDataAdapter adapter = new SqlDataAdapter(sql, conn);
adapter.Fill(dt);
}

public void Fill(DataSet ds, string sql)
{
SqlDataAdapter adapter = new SqlDataAdapter(sql, conn);
adapter.Fill(ds);
}
}
}

Accesul la functiile acestei clase trebuie sa se fac prin intermediul unei clase BusinessLogic, respectand principiile N-Tier.

Prin intermediul acestei clase putem executa (aproape) toate operatiile de baza asupra unei baze de date. Ea poate fi imbunatatita prin adaugarea altor functii si facilitati, precum suport pentru executarea Stored Procedures, interogarilor parametrizate samd.

12.04.2009

Pointeri in C#

In foarte multe locuri veti gasi C# descris ca un limbaj sigur, modern, obiectual, fara sa se vorbeasca prea mult de pointeri, ca in C++. Insa asta nu inseamna ca ei lipsesc cu desavirsire.

Exista cod C# ce poate fi marcat unsafe si folosit cu pointeri in mod cit se poate de traditional. De exemplu (din MSDN):

Cod:
// cs_operator_dereferencing.cs
// compile with: /unsafe
using System;
struct Point
{
public int x, y;
}

class Test
{
public unsafe static void Main()
{
Point pt = new Point();
Point* pp = &pt;
pp->x = 123;
pp->y = 456;
Console.WriteLine ( "{0} {1}", pt.x, pt.y );
}
}

//Output: 123 456

Tipurile de pointeri le gasiti aici, iar unsafe este aici, cu alte citeva exemple semnificative.

C# - Agenda (Command line)

Am hotarit sa rezolv asa o problema: Sa se scriu un program tip 'agenda telefonica' (pentru a se demonstra folosirea ArrayList)

Am scris urmatorul cod (impartit in 4 fisiere, pentru o mai usoara urmarire a codului):

Codul din fisierul agenda.cs (care contine si functia main)

Cod:
using System;
using System.Collections;

namespace agenda
{
class agenda
{
static void Main()
{
PhoneBook ph = new PhoneBook();
ph.Import(input.ReadFromFile());
int user_choice;
bool again = true;
while (again)
{
user_choice = output.DoMenu(ph.Count);
switch (user_choice)
{
case 1: //print all records from agenda
ph.PrintAgenda();
break;
case 2: // add a record to agenda
ph.Add(input.ReadNewRecord());
break;
case 3: // edit an record in agenda
string to_edit;
Console.Write("Enter the name of record you want to edit: ");
to_edit = Console.ReadLine();
if (ph.IsSet(to_edit))
ph.EditRecord(to_edit);
else
Console.WriteLine("Name not found!");
break;
case 4: // delete record
string to_delete;
Console.WriteLine("Warning!!! Delete is PERMANENT!");
Console.Write("Enter the name of record you want to delete: ");
to_delete = Console.ReadLine();
if (ph.IsSet(to_delete))
{
ph.Delete(to_delete);
}
else
{
Console.WriteLine("Name not found!");
}
break;
case 0: //exit
again = false;
Console.WriteLine("Bye! Bye! (Enter... exit!)");
output.WriteToFile(ph.Export());
Console.ReadLine();
break;
}
} //while
} //end Main()
}
}

Apoi am creat un nou fisier, PhoneBook.cs unde am creat class PhoneBook, clasa al carei rol este sa gestioneze un ArrayList. codul este urmatorul:

Cod:
using System;
using System.Collections;

namespace agenda
{
class PhoneBook
{
private ArrayList pBook = new ArrayList();
public void Add(record rec)
{
pBook.Add(rec);
} // Add(record rec)
public void Add_at_index (record rec, int index)
{
pBook.Insert(index, rec);
} // Add_at_index (record rec, int index)
public int Count
{
get { return pBook.Count; }
} //Count
public record this[int index]
{
get
{
if (index < 0 || index >= Count) throw new
Exception("Invalid index. Must be an integer between [0 .. " + (Count -
1) + "]");
//if (this[index] == null) throw new Exception("Null record");
return (record)pBook[index];
}
set
{
if (index < 0 || index > Count) throw new
Exception("Invalid index. Must be an integer between [0 .. " + (Count -
1) + "]");
pBook[index] = value;
}
}
public record GetbyName(string name)
{
if (name.Trim() == "") throw new Exception("Invalid request. Void name");
for (int i = 0; i < Count; i++)
if (((record)pBook[i]).FName == name) return (record)pBook[i];
throw new Exception("Invalid request. " + name + " cannot be found.");
} // GetbyName(string name)
public bool IsSet(string name)
{
if (name.Trim() == "") return false;
for (int i = 0; i < Count; i++)
if (((record)pBook[i]).FName == name) return true;
return false;
} //IsSet(string name)
public bool IsSet(int index)
{
if (pBook[index] == null) return false;
return true;
} //IsSet(int index)
public void Delete(string name)
{
if (!IsSet(name))
{
Console.WriteLine("Invalid name. Record NOT FOUND!");
return;
}
pBook.RemoveAt(GetIndexByName(name));
Console.WriteLine("{0} Removed Succesfuly!", name);
}
public int GetIndexByName(string name)
{
if (name.Trim() == "") return -1;
for (int i = 0; i < Count; i++)
if (((record)pBook[i]).FName == name) return i;
return -1;
} //GetIndexByName(string name) this return -1 if name is not in PhoneBook
public void PrintAgenda()
{
Console.WriteLine();
for (int i = 0; i < Count; i++)
Console.WriteLine(i.ToString() + ". " + (record)pBook[i]);
} // PrintAgenda() - full print of agenda
public void PrintAgenda(char FirstLetter)
{
for (int i = 0; i < Count; i++)
if (((record)pBook[i]).FName[0] == FirstLetter)
Console.WriteLine((record)pBook[i]);
} // PrintAgenda() print all records that start with specified char
public void EditRecord(string name)
{
int index = GetIndexByName(name);
if (index == -1) //invalid name
throw new Exception("Invalid Record Name.");
record current = GetbyName(name);
Console.WriteLine("Current First Name is: " + current.FName);
Console.Write("Do you want to change it (y/n)? ");
if (Console.ReadLine().ToLower() == "y")
{
current.FName = input.ReadFirstName();
}
Console.WriteLine("Current Second Name is: " + current.SName);
Console.Write("Do you want to change it (y/n)? ");
if (Console.ReadLine().ToLower() == "y")
{
current.SName = input.ReadSecName();
}
Console.WriteLine("Current phone no is: " + current.Tel_no);
Console.Write("Do you want to change it (y/n)? ");
if (Console.ReadLine().ToLower() == "y")
{
current.Tel_no = input.ReadTelNo();
}
//Add_at_index(current, index); greseala undeva
pBook[index] = current;
current.Print();
} //EditRecord(string name)
public void Import(string file_content)
{
bool new_records = false;
string[] records_row;
char separator = ';';
string[] found_rec;
records_row = file_content.Split('n');
if (records_row == null) return; //empty file (or file not found)
foreach (string row in records_row)
{
try
{
found_rec = row.Split(separator);
if (found_rec.Length != 4) continue; //invalid record
pBook.Add(new record(found_rec[0], found_rec[1], found_rec[2], DateTime.Parse(found_rec[3])));
new_records = true; // records found
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
if (new_records) Console.WriteLine("{0} record(s) loaded from file!", pBook.Count);
} // Import(string file_content)
public string Export()
{
string content = "";
foreach (record curent in pBook)
{
content += curent.FName + ";" + curent.SName + ";"
+ curent.Tel_no + ";" + curent.EntryData + Environment.NewLine;
}
return content;
} // Export() - export curent agenda (arraylist) to a string (to be write in a file)!
}
}

Urmeaza un nou fisier: record.cs care are functia de a defini ce inseamna record. codul este urmatorul:

Cod:
using System;
using System.Collections;

namespace agenda
{
class record
{
private string fName;
private string sName;
private string tel_no;
private DateTime entryData;
public string FName
{
get { return fName; }
set
{
string name = value.Trim();
if (name == "") throw new Exception("Name cannot be void");
fName = name;
}
}
public string SName
{
get { return sName; }
set
{
string name = value.Trim();
if (name == "") throw new Exception("Second Name cannot be void");
sName = name;
}
}
public string Tel_no
{
get { return tel_no; }
set
{
string no = value.Trim();
if (no == "") throw new Exception("Empty Phone Number");
for (int i = 0; i < no.Length; ++i)
if (no[i] < '0' || no[i] > '9')
throw new Exception("Phone numer must contain only digits. Invalid entery at pos " + (i+1));
tel_no = no;
}
}
public DateTime EntryData
{
get { return entryData; }
set { entryData = value; }
}
public record(string fname, string sname, string tel, DateTime dt)
{
FName = fname;
SName = sname;
Tel_no = tel;
EntryData = dt;
}
public record(string fname, string sname, string tel)
: this(fname, sname, tel, DateTime.Now)
{
}
public void Print()
{
Console.WriteLine("{0} [{1}]", ToString(), EntryData);
}
public override string ToString()
{
return FName + " " + SName + " - " + Tel_no;
}
}
}

Un ultim fisier, input.cs, are rolul de "helper"... sunt functii care au sarcina de a comunica (in si out) cu utilizatorul (citeste date, exporta date, importa.. etc). codul este:

Cod:
using System;
using System.Collections;
using System.IO;

namespace agenda
{
class input
{
public static string ReadFirstName()
{
string fn;
while(true)
{
Console.Write("First Name: ");
fn = Console.ReadLine();
fn = fn.Trim();
if (fn == "")
{
Console.WriteLine("Invalid input. Name cannot be empty");
continue;
}
if (fn.Length > 12) fn = fn.Substring(0, 12);
return fn;
}
} //ReadFirstName()
public static string ReadSecName()
{
string sn;
while (true)
{
Console.Write("Second Name: ");
sn = Console.ReadLine();
sn = sn.Trim();
if (sn == "")
{
Console.WriteLine("Invalid input. Second Name cannot be empty");
continue;
}
if (sn.Length > 12) sn = sn.Substring(0, 12);
return sn;
}
} //ReadSecName()
public static string ReadTelNo()
{
string no;
bool IsValidNo;
while (true)
{
IsValidNo = true;
Console.Write("Phone no: ");
no = Console.ReadLine();
no = no.Trim();
if (no == "")
{
Console.WriteLine("Invalid input. Tel number cannot be empty");
continue;
}
if (no.Length > 12)
{
Console.WriteLine("Invalid input. Phone Number cannot have more than 12 digits");
continue;
}
for (int i = 0; i < no.Length; ++i)
if (no[i] < '0' || no[i] > '9')
{
Console.WriteLine("Phone numer must contain only digits. Invalid entery at pos " + (i + 1));
IsValidNo = false;
break;
}
if (IsValidNo)
return no;
}
} //ReadTelNo()
public static record ReadNewRecord()
{
return new record(ReadFirstName(), ReadSecName(), ReadTelNo());
} //ReqadNewRecord()

public static string ReadFromFile()
{
string file_content = "";
try
{
StreamReader sr = new StreamReader("./agenda.txt");
file_content = sr.ReadToEnd();
sr.Close();
}
catch (Exception)
{
}
Console.WriteLine(file_content); //to remove after tests
return file_content;
}
}
class output
{
public static int DoMenu(int Counter)
{
string rec_no = "";
string user_chose;
if (Counter > 0) rec_no = Counter.ToString() + " record(s)"; else rec_no = "No records";
Console.WriteLine("nAgenda v.1.0 - " + rec_no + "n-----------------------------");
Console.WriteLine("1. Print Agenda");
Console.WriteLine("2. Add Record");
Console.WriteLine("3. Edit Record");
Console.WriteLine("4. Delete Record");
Console.WriteLine("0. Exit");
Console.WriteLine();
Console.Write("You chose: ");
while (true)
{
user_chose = Console.ReadLine();
if (user_chose == "0" || user_chose == "1" || user_chose == "2" || user_chose == "3" || user_chose == "4")
return int.Parse(user_chose);
Console.WriteLine("Wrong choice! Try again!");
Console.Write("Your choice: ");
}
} //DoMenu(int Counter)
public static void WriteToFile(string content)
{
try
{
StreamWriter sw = new StreamWriter("./agenda.txt", false);
sw.Write(content);
Console.WriteLine("Saved succesfuly!");
sw.Close();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
} //WriteToFile(string content) - write a string to file
}
}

Am postat acest articol (poate are si elemente de tutorial - desi am cam sarit comentariile) in sperantza ca va fi de folos cuiva.

    Blog din Moldova    FastCounter 

 
Copyright © 2008-2010 Foster1. All rights reserved.