Home Page Home Page Articoli Tutorial ADO.NET ed esempi pratici - Parte 2

Tutorial ADO.NET ed esempi pratici - Parte 2

Dopo aver fatto un po' di teoria su ADO.NET nella Parte 1 dell'articolo procediamo con un Tutorial pratico che ci condurrà alla creazione di un'applicazione basilare che consenta l'accesso e la modifica dei dati su un Database Access.
Autore: Stefano Passatordi Livello:
Introduzione
Nel precedente articolo abbiamo introdotto ADO.NET, il componente per l?accesso ai dati del Microsoft .NET Framework, studiandone la struttura per cercare di capire come sfruttare al meglio tutte le sue potenzialità.
In questo articolo, invece, svilupperemo un esempio pratico che, meglio di ogni altra teoria, ci farà intuire quanto ADO.NET sia potente e facile da utilizzare, noterete ,infatti, che con poche righe di codice e in poco tempo otterremo una applicazione che tramite una DataGrid interagisce con una sorgente dati.

Descrizione dell?esempio
Il nostro è un esempio classico di architettura distribuita a tre livelli (Three-Layer) :
- Client (il vostro browser)
- Server applicativo (IIS + Framework)
- Server database (Access 2000)



La nostra applicazione si occuperà di gestire automobili e relativi proprietari, ci permetterà di visualizzare tutte le auto in base al loro proprietario, di inserire nuove auto o nuovi proprietari, di effettuare modifiche, cancellazioni e ordinamenti vari, insomma, quasi tutte le operazioni possibili con un database. In questo esempio non ci occuperemo di intercettare e gestire eventuali errori di input perché ci concentreremo sul funzionamento di ADO.NET.
Per lo sviluppo dell?intera applicazione verrà utilizzato il famoso IDE di casa Microsoft , il potentissimo Visual Studio .Net (per brevità in seguito denominato VS) e cercheremo di sfruttare al massimo la magia del ?drag&drop? che ci farà risparmiare molto codice, nonchè tempo, ma ricordate una cosa... maggiore facilità vuol dire anche minore flessibilità, infatti, se da un lato la nostra piattaforma di sviluppo ci facilita la vita permettendoci l?utilizzo di oggetti che sono già ?pre-confezionati?, dall?altro, rende le nostre applicazioni meno flessibili e scalabili.
Il database si chiamerà cars.mdb, è composto da due semplici tabelle (Proprietari,Auto) che sono in relazione tra loro (uno ? molti) e le cui strutture sono visibili nelle immagini di seguito :



Iniziamo...
Ecco di seguito tutti i passi preliminari per creare e impostare la nostra applicazione di esempio.

1) Apriamo Visual Studio.Net e creiamo un Nuovo Progetto per Applicazioni Windows che chiameremo Cars.

2) Dopo aver premuto ?OK? , Visual Studio .Net crea la soluzione e avremo, così, un form vuoto a disposizione per la nostra applicazione. Sulla sinistra dovreste vedere il tab ?Casella degli Strumenti? (se non lo vedete selezionate nella barra dei menù ?Visualizza? e poi scegliete ?Casella degli Strumenti?), selezionatela e vi comparirà un menu stile Outlook in cui dovrete selezionare la voce ?Dati?.
Questo è un elenco di tutti gli oggetti ?pre-confezionati? che VS mette a nostra disposizione e che riguardano essenzialmente l?accesso e la manipolazioni di dati. Tutti questi oggetti sono stati descritti nel precedente articolo e quindi spero che non abbiate grosse difficoltà a capire cosa siano e a cosa servano, anche se in futuro avremo modo di osservarli da molto vicino.

3) Selezionate l?oggetto ?OleDbDataAdapter? e trascinatelo all?interno del nostro form, subito dopo partirà il wizard che ci aiuterà a configurare correttamente il componente.



4) Selezionando ?Avanti? si aprirà un'ulteriore finestra; quindi premiamo su "Nuova Connessione". Ora è necessario selezionare il tab ?Provider? e scegliere ?Microsoft Jet 4.0 OLE DB Provider? perché noi stiamo utilizzando un Database Access, ma, come potete vedere, avete una vasta scelta.

Successivamente tornate nella linguetta ?Connessione? e da qui potrete selezionare la sorgente dati fisica sfogliando il vostro HD, subito dopo cliccate su ?Verifica connessione? per testare se ci sono problemi con la sorgente dati. Infine ciccate su ?OK? e tornerete alla finestra iniziale e dovrete scegliere ?Avanti?, vi comparirà allora la seguente finestra:



Come vedete è possibile ora scegliere la modalità di accesso del DataAdapter al Database. Visto che stiamo utilizzando Microsoft Access, l'unica opzione disponibile è la prima. Se avessimo usato SQL Server potevamo anche creare una nuova Stored Procedure o utilizzarne una esistente.

Si procede nello Wizard premendo semplicemente "Avanti" e ci troveremo di fronte un bivio, potremo infatti digitare direttamente la query di estrazione dati oppure utilizzare il "Generatore di Query" che con una modalità tipica di Access ci farà scegliere prima le tabelle e successivamente le colonne da cui pescare i dati.

Per comodità digitiamo questa query direttamente nella casella di testo:

SELET * FROM AUTO

Procediamo con lo Wizard fino alla fine. Se verrà richiesto se includere la password nella stringa di connessione selezioniamo "Non Includere".

5) Ripetiamo i passi 3 e 4 però scegliendo la tabella "Proprietari".

6) Sempre da ?Casella degli Strumenti? sotto la voce ?Windows Form? selezioniamo e trasciniamo nel form il controllo DataGrid e successivamente due Button (uno sarà ?Aggiorna? e l?altro ?Elimina?) . Noterete, anche , che nella barra di progettazione in basso ci sono tre nuovi oggetti che sono stati creati in precedenza durante i passi 5 e 6.



Il wizard, usato in precedenza, ha la duplice funzione di creare, non solo, l'OleDbDataAdapter ma anche l?oggetto OleDbConnection a cui è collegato in maniera diretta, infatti , se vi ricordate, il DataSet non è collegato direttamente al componente Connection ma tramite il DataAdapter che funge , quindi, da ponte tra i due. In genere viene creato un oggetto Connection per ogni DataAdapter ma in questo caso la fonte dati è la stessa e quindi entrambi gli adattatori di dati sfruttano la stessa connessione. Selezionando uno degli adattatori e guardandone le proprietà potremo facilmente individuare i quattro oggetti Command contenuti all?interno dell?OleDbDataAdapter e visionarne le proprietà, tra cui, la più importante è sicuramente la CommandText.
Per quanto riguarda l?oggetto OleDbConnection tra le sue proprietà spicca ConnectionString che ci permette di individuare in maniera univoca una fonte dati fisica. La forma di questa stringa di connessione varia in basa al tipo di base di dati utilizzata.(Piccolo consiglio : se avete la necessità di creare una Connection da codice e non sapete come impostare la proprietà ConnectionString basta fare ?drag&drop? dell?oggetto Connection sul form , seguire il wizard e infine copiare la stringa creata in automatico dal sistema, così siete sicuri di non sbagliare!)



7) A questo punto manca un solo oggetto...il che sarà utilizzato come sorgente dati per la DataGrid. Fate in modo che il form abbia il focus e quindi selezionate dalla barra dei menu la voce ?Dati? e scegliete poi ?Genera DataSet?. (Attenzione : se il form non ha il focus non potrete Generare il DataSet!).

In questo modo abbiamo creato un oggetto DataSet che è legato ad entrambi gli OleDbAdapter ed è quindi in grado di mappare in memoria sia la tabella Auto che quella Proprietari. Fate bene attenzione a questo concetto perché è fondamentale e se non vi è chiaro vi consiglio di leggere il precedente articolo sulla parte teorica di ADO.NET.

8) Selezioniamo ora il datagrid nel form e nelle proprietà impostiamo il DataSource e il DataMember :



Impostiamo la DataGrid in modo che la sua sorgente dati sia il nostro DataSet e in particolare la tabella Proprietari. A questo punto ecco il nuovo scenario :



Iniziamo ora a scrivere un po' di codice.
La prima cosa da fare è scrivere una funziona che avrà il compito di prelevare i dati dalla nostra DataSource e passarli al DataSet che è a sua volta fonte dati della DataGrid.
La funzione sarà CaricaDatagrid() e va inserita nell?evento Load del form :


private void CaricaDatagrid()
{
try
{
dataSet11.Clear();
oleDbDataAdapter1.Fill(dataSet11.Auto);
oleDbDataAdapter2.Fill(dataSet11.Proprietari);
}
catch(Exception e)
{
MessageBox.Show(e.ToString(),"Errore",MessageBoxButtons.OK);
}
}


Ora commentiamo un pò questo codice :

Nella prima rigachiamiamo il metodo Clear() dell?oggetto DataSet, in questo modo lo ripuliamo da tutti gli eventuali dati che, potrebbero essere stati caricati in precedenza, in pratica eliminiamo tutte le possibili righe delle ?ipotetiche? tabelle. La chiamata di questo metodo non è sempre obbligatoria, come in questo caso, ma a volte risulta indispensabile perciò vi consiglio di usarlo sempre prima di caricare i dati in un dataset a meno di esigenze specifiche.

Nella seconda rica chiamiamo il metodo Fill() dell?oggetto OleDbDataAdapter che ha il compito di aggiungere o aggiornare dati nell?oggetto DataSet che gli passiamo come argomento.
Il metodo Fill() ha molti overload, circa una decina, ed è molto potente, in quanto, gestisce l?apertura della connessione verso la sorgente dati, richiama il comando SELECT che è all?interno dell?adapter per recuperare tutti i dati e li passa al dataset, chiudendo infine la connessione.
Nel caso dell? OleDbDataAdapter1 sappiamo che, tramite il wizard, la SELECT era stata impostata per recuperare dati dalla tabella Auto e quindi come argomento del metodo Fill() gli passiamo sì il DataSet. in cui copiare i record, ma specificando esattamente in quale tabella. In questo modo la tabella Auto della datasource viene mappata nella stessa tabella del dataset.
Per quanto riguarda la terza riga vale lo stesso discorso della precedente ma questa volta usando la tabella Proprietari.
Tutte le eventuali eccezioni , che in questo caso potrebbero essere dovute ad un errore durante l?apertura e/o alla lettura dei dati dalla sorgente, vengono intercettate tramite un banale blocco try...catch e viene mostrato un MessageBox che descrive il tipo di errore.


private void Form1_Load(object sender, System.EventArgs e)
{
CaricaDatagrid();
}


A questo punto, premendo F5 si avvierà la nostra applicazione e sarà visibile la datagrid contenente i soli dati della tabella Proprietari.
Come si fa a visualizzare anche i dati della tabella Auto in relazione al proprietario???
Semplice...
Vi ricordate che quando abbiamo creato il nostro database Cars.mdb abbiamo anche aggiunto una relazione uno a molti tra Proprietari e Auto?!?!...Bene, ora questa relazione è presente nella base di dati ma non nel nostro dataset e questo dipende da come abbiamo strutturato la SELECT dei datadapter, infatti è stata fatta la scelta di utilizzare due DataAdapter differenti per le due tabelle e non ,invece, uno unico per entrambe; in quest?ultimo caso infatti avremmo avuto la relazione riportata in automatico nel nostro dataset. Personalmente, credo, che sia scomodo utilizzare un unico DataAdapter per tutte le tabelle della base di dati perché questo ci renderebbe solo la vita più difficile (provare per credere!) anche perché tramite il DataSet possiamo ricreare tutte le relazioni che vogliamo, duplicando, quindi, quelle della base di dati fisica.
E? importante dire che la relazione presente nella base di dati non ha molto valore dopo averne inserita una identica nel DataSet, ma noi la creiamo e la manteniamo per una questione di coerenza strutturale del progetto. Questo vi fa capire che lavoriamo essenzialmente ad alto livello rispetto al database e quanto siano potenti l?oggetto DataSet e DataAdapter che, facendo da ponte, permette questo scambio ?intelligente? di informazioni.
Detto ciò, ricreiamo nel nostro dataset la relazione presente all?interno del database tra le tabelle Auto e Proprietari...ecco, allora, la funzione che andrà inserita nel Load del form dopo CaricaDatagrid() :


private void CreaRelazioni()
{
dataSet11.Relations.Add("Possiede",dataSet11.Proprietari.IDProprietarioColumn, dataSet11.Auto.IDProprietarioColumn);
}


Sappiamo che un oggetto DataSet è composto da due Collection, la DataTableCollection e la DataRelationCollection, quest?ultima è, appunto, un insieme di oggetti DataRelation, ognuno dei quali consente di legare due o più tabelle permettendoci di navigare tra esse tramite la relazione padre-figlio. Nel nostro caso, abbiamo la tabella Proprietari che è padre della tabella Auto secondo la relazione ?Possiede?, per creare questa relazione nel nostro dataset si usa il metodo Add() della Collection Relation del dataset. Anche questo metodo, come tanti altri, ha vari overload, nel nostro caso i suoi argomenti sono il nome della relazione, il campo della tabella padre e quello della tabella figlio su cui è basata la relazione. E? ovvio che entrambi i campi devono essere di tipo chiave per il padre e chiave esterna per il figlio e vengono creati ad hoc durante la fase di progettazione del database?ma questo è un altro discorso!
Eseguito il codice della riga 3 ci troveremo il nostro dataset con una relazione che ci permetterà di legare le auto al relativo proprietario, per cui, dopo aver aggiunto la funzione CreaRelazioni() nel Load del form


private void Form1_Load(object sender, System.EventArgs e)
{
CaricaDatagrid();
CreaRelazioni();
}


e premendo F5 otteremo quanto di seguito riportato. Per fare un drill-down sui dati sarà sufficiente premere sui "+" per espanderli. La sequenza delle immagini è più esplicativa di qualsiasi parola:



Allo stato attuale, possiamo vedere tutti i proprietari e le relative auto... ma come si fa per aggiungere, modificare ed eliminare i record?!?
Anche per questa domanda la risposta è... poche righe di codice!
Se provate a modificare, ad esempio, il nome di un proprietario, selezionando la relativa cella, avete la possibilità di scrivere o cancellare, lo stesso vale anche per tutte le altre celle, noterete anche che le vostre modifiche vengono mantenute senza problemi, ma fate attenzione, controllate se il database rispecchia le modifiche o provate a riavviare l?applicazione, vi sarà subito palese che il nuovo nome da voi inserito non è stato salvato!
Per capire cosa è successo bisogna aver ben presente la struttura di ADO.NET e quella dell?oggetto DataAdapter e DataSet in particolare. Sappiamo che il dataset è la rappresentazione in memoria dei dati attuali, quindi, quando proviamo a modificare una cella, ci riusciamo senza grossi problemi, ma la modifica non viene riportata nella DataSource originale, questo perché abbiamo modificato i ?dati attuali? ,cioè il dataset, e non quelli permanenti, ovvero la sorgente dati fisica.
Occorre, quindi, replicare la modifica anche nel database, per farlo aggiungiamo una funzione nell?evento Click del bottone Aggiorna :


private void button1_Click(object sender, System.EventArgs e)
{
try
{
AggiornaDataSource();
}
catch(Exception ex)
{
MessageBox.Show(ex.ToString(),"Errore",MessageBoxButtons.OK);
}

}

private void AggiornaDataSource()
{
oleDbDataAdapter2.Update(dataSet11.Proprietari);
oleDbDataAdapter1.Update(dataSet11.Auto);
}


Per rendere le modifiche permanenti utilizziamo il metodo Update() dell?Adapter, tanto per cambiare, anche questo metodo ha svariati overload e può scaturire varie eccezioni.
Nel nostro caso, gli argomenti passati al metodo sono le tabelle del DataSet che contengono i valori aggiornati e che devono essere salvati. E? importante capire cosa accade quando si chiama il metodo Update() che, a mio avviso, è il più importante e potente dell?oggetto Adapter in generale.
Dopo ogni chiamata a questo metodo, il DataAdapter esamina la proprietà ?RowState? (mantiene lo stato di ogni riga) di ogni singola riga, controllando se è stata inserita, modificata o eliminata, quindi, in base all?azione subita dalla row, il DataAdapter chiama il comando relativo (INSERT,UPDATE,DELETE) con gli opportuni valori per ogni singola riga (secondo l?ordine del loro indice nel dataset) e , seguendo la trafila della SELECT (apre connessione,esegue,chiude connessione), aggiorna la sorgente dati fisica sfruttando anche l?oggetto Connection.

Tramite il datagrid possiamo modificare o aggiungere un record ma non possiamo eliminarlo, almeno in maniera diretta. Prima di vedere l?eliminazione però, vi dico come ordinare le colonne, per farlo basta impostare la proprietà AllowSorting della DataGrid a True e successivamente, in fase di runtime, cliccare sull?intestazione della colonna che si vuole ordinare e il gioco è fatto.
Torniamo ora all?eliminazione dei record che avverrà sfrutteremo una piccola funzione che andrà richiamata nell?evento Click del bottone Elimina :


private void button2_Click(object sender, System.EventArgs e)
{
try
{
EliminaRiga();
}
catch(Exception ex)
{
MessageBox.Show(ex.ToString(),"Errore",MessageBoxButtons.OK);
}
}


private void EliminaRiga()
{
System.Data.DataRow dr;
CurrencyManager cm = (CurrencyManager)this.BindingContext[dataGrid1.DataSource,dataGrid1.DataMember];
DataView dv = (DataView)cm.List;
dr =dv[cm.Position].Row;
dr.Delete();
AggiornaDataSource();
}


Il codice di questa funzione è fortemente determinato dal fatto che se prima di eliminare una riga effettuiamo un ordinamento per colonna cambia la vista del DataSet e quindi rischiamo di eliminare una riga che in realtà non era quella voluta. Per ovviare a questo problema, che ha causato e causa ancora tanti dubbi e incertezze (basta vedere le domande nei forum riguardo questo argomento!), utilizziamo la classe CurrencyManager che gestisce un insieme di oggetti Binding (che associano il valore di una proprietà di un oggetto con il valore di una proprietà di un controllo) e la cui istanza ci viene restituita dall?oggetto BindingContext (che mantiene sincronizzati due oggetti del fom che sono legati ad una stessa fonte dati ) del controllo form.
Sia CurrencyManager che BindingContext non sono oggetti facili da spiegare in poche righe, per cui, per ora, accontentatevi di sapere che grazie all?interazione tra questi due oggetti riusciamo sempre a sapere qual è lo stato attuale ed effettivo dei controlli del form che vengono passati come argomento a BindingContext.
Dichiariamo quindi una variabile di tipo DataRow senza però inizializzarla, nell'istruzione successiva recuperiamo lo stato attuale del DataMember del DataGrid (tabella Proprietari o Auto del dataset), successivamente nelle riga 5 dichiariamo una variabile di tipo DataView che viene istanziata tramite il cast dell?oggetto List restitutito dal metodo List() di CurrencyManager, tale oggetto non è altro che un insieme di record che rappresenta lo stato attuale del DataView che si crea automaticamente dopo ogni ordinamento di colonna; purtroppo, anche questo, non è un concetto facile da capire se non si hanno delle buone basi di teoria su ADO.NET?ma abbiamo tempo per recuperare ;) .
Assegniamo a dr la riga che è stata selezionata nel datagrid (riga 5) e chiamiamo il metodo Delete() dell?oggetto DataRow per eliminare la riga stessa dal dataset ma non dalla fonte dati fisica. L?eliminazione permanente avviene solo chiamando la funzione AggiornaDataSource() descritta in precedenza.

A questo punto il quadro è completo, credo che non vi sia più nulla da spiegare riguardante questo facile esempio. Ora avete le basi per costruire applicazioni elementari che accedono ad una fonte dati e che potete personalizzare sia dal punto di vista grafico che delle funzioni, senza scrivere troppe righe di codice. Il consiglio che vi do è di iniziare usando i controlli già pronti nella barra degli strumenti ma in seguito di creare tutto da codice, perché solo così potrete sfruttare al massimo ADO.NET che vi assicuro è davvero eccezionale.
Voto medio articolo: 4.0 Numero Voti: 3
Stefano Passatordi

Stefano Passatordi

Laureato in Tecnologie Informatiche presso l'Università di Pisa.Amante della programmazione in generale,ha iniziato da autodidatta con VB6 e poi tramite l'università e studi personali ha approfondito le sue conoscenze in vari ambiti del mondo della programmazione. Profilo completo

Articoli collegati

Utilizzare NUnit per testare codice .NET
Scopriamo in questo articolo come utilizzare il popolare framework Open Source NUnit per effettuare Unit Testing del codice .NET aumentandone la qualità e riducendo il numero di eventuali bugs.
Autore: Michela Zangarelli | Difficoltà:
SQL Injection, che cosa è e come difendersi
I malintenzionati sono sempre dietro l'angolo. Il SQL Injection è una delle pratiche più semplici da utilizzare per attaccare un'applicazione poco sicura e violarla o arrecare danni al database. Vediamo che cos'è e come garantire la sicurezza delle applicazioni in modo adeguato.
Autore: Alessandro Alpi | Difficoltà: | Commenti: 4
Visual Studio 2005 Team System
Scopriamo il nuovo IDE di sviluppo che copre interamente l'intero ciclo di vita del software integrando in un solo ambiente più prodotti e studiato apposta per i ruoli di Architect, Developer e Tester.
Autore: Marco Caruso | Difficoltà: | Voto:
Introduzione ad ADO.NET - Parte 1
ADO.NET è uno dei componenti chiave del .NET Framework. Eredita il nome dal vecchio ADO ma è praticamente quasi tutto cambiato. Una nuova architettura, nuovi concetti, nuove funzionalità e nuovi oggetti. Vediamo quali sono e a cosa servono.
Autore: Stefano Passatordi | Difficoltà: | Commenti: 6
Colonne calcolate e parola "Child" con ADO.NET
Una cosa utile di ADO.NET e' la possibilita di aggiungere ad una colonna da codice il cui valore sia il risultato di operazioni sui dati dei record (o tra valori di altre colonne). Quando si utilizza un dataset con piu tabelle collegate mediante relazioni invece, usando la parola chiave "child" si possono avere delle informazioni riguardanti le tabelle figlio, direttamente nei record della tabella
Autore: Matteo Raumer | Difficoltà: | Voto:
Usare degli indicatori di progresso con Query SQL
Nell'articolo vediamo come sia possibile con un l'aiuto delle classi DataReader e Command di ADO.NET, mostrare un indicatore di progresso che indichi lo stato di avanzamento di una query SQL durante il fetching dei dati.
Autore: Matteo Raumer | Difficoltà: | Voto:
La crittografia e la classe Rijndael
Vediamo come utilizzare una delle tante classi messe a disposizione dal .NET Framework per la crittografia dei dati. In particolare la classe Rijndael implementa un algoritmo di crittografia molto forte diventato famoso negli ultimi anni.
Autore: David De Giacomi | Difficoltà:
DES il famoso standard creato da IBM per la crittografia dei dati
In questo articolo dimostreremo come crittografare dei file usando il .NET Framework e in particolare la classe DESCryptoServiceProvider che implementa l'ormai noto algoritmo di cifratura inventato da IBM negli anni 70 chiamato DES (Data Encryption Standard)
Autore: David De Giacomi | Difficoltà: | Commenti: 2 | Voto:
Cosa sono e come funzionano le funzioni ricorsive?
Ecco tre esempi efficaci che vi spiegano come usare correttamente le funzioni ricorsive, per effettuare ricerche di file e cartelle all'interno del disco fisso, per ricostruire la struttura di un file XML oppure per svuotare determinati controlli in una Form.
Autore: Matteo Raumer | Difficoltà: | Commenti: 2
Costruire Console Applications con Visual Studio .NET
Spiegheremo in questo articolo i passi base fondamentali per costruire Console Applications utilizzando Visual Studio .NET.
Autore: David De Giacomi | Difficoltà: | Voto:
TextReader e TextWriter
Una panoramica su come utilizzare queste due classi che ci permettono di leggere e scrivere file di testo.
Autore: David De Giacomi | Difficoltà: | Commenti: 1 | Voto:
Giochiamo un po' con il registro di Windows!
Scopriamo uno degli elementi fondamentali del sistema operativo Windows e vediamo come è possibile accedervi tramite le classi offerte dal .NET Framework: Microsoft.Win32.Registry e Microsoft.Win32.RegistryKey
Autore: David De Giacomi | Difficoltà: | Commenti: 1 | Voto:
.NET Framework 1.1 Beta
Una prima panormaica sulle novità offerte dal Framework 1.1 che sarà integrato nella prossima versione di Visual Studio .NET 2003.
Autore: David De Giacomi | Difficoltà:
Cosa posso costruire con Visual Studio .NET ?
Un' introduzione sui vari tipi di progetto disponibili in Visual Studio dalle tipiche applicazioni Windows fino alle recenti applicazioni Web e agli innovativi Web Services.
Autore: David De Giacomi | Difficoltà: | Commenti: 1
Copyright © dotNetHell.it 2002-2024
Running on Windows Server 2008 R2 Standard, SQL Server 2012 & ASP.NET 3.5