In questo articolo, ci occuperemo di capire perché costruire lo strato dei servizi in una applicazione Multi Tier e ovviamente di costruire quello della nostra applicazione per quanto semplice esso sia.
Prima di tutto, perché costruire un ulteriore strato e una serie di classi fra la User Interface e i Dati, quando abbiamo visto nell’articolo precedente che possiamo semplicemente referenziare la libreria dati e fare quello che ci serve per inserire dati nel nostro database leggerli e modificarli.
Per un motivo molto semplice, costruendo una User Interface direttamente collegata a un database, legheremo la nostra interfaccia in modo molto stretto al database che gli sta alle spalle, se invece spostiamo tutto il codice che si occupa di parlare con il database in una specifica libreria non sarà difficile astrarre l’interfaccia utente dallo strato dati che gli sta alle spalle in modo tale da poter lavorare con qualsiasi database. Se limitiamo l’interazione fra la UI e i dati alla necessità di indicare alla libreria business che database vogliamo usare e qual’è la stringa di connessione, saremo in grado di costruire un’interfaccia utente robusta, che, se volessimo cambiare completamente la sorgente dati (ad esempio usando un database su cloud oppure se volessimo mettere il database in remoto e chiamarlo tramite servizi web) non servirà toccare la UI ma dovremo modificare unicamente la classe dei servizi, creando un manager per la nuova sorgente dati che potremo semplicemente indicare aggiungendolo ai possibili manager dati nella configurazione dell’applicazione ed avremmo fatto.
Separare completamente la gestione dati ed il database dalla User Interface ha anche un ulteriore beneficio, quello di non farci cadere in tentazione, se così possiamo dire, perché non vedremo direttamente le classi dati, pertanto non succederà neppure per errore che ci troviamo ad avere qualche porzione del programma che in qualche modo è collegata direttamente ad una fonte dati.
La libreria dei servizi, nel caso di questa applicazione specifica, ci permetterà di avere un unica serie di metodi che interagiscono con i dati, dal lato della user interface, mentre la libreria servizi stessa, avrà al suo interno tutto il necessario a parlare con i 3 diversi database, oggi faremo il collegamento a SQL Server, nelle prossime puntate creeremo le librerie per gli altri due database.
Iniziamo con l’aggiungere una nuova Class Library alla nostra soluzione, quindi, ci posizioniamo sulla Solution e usiamo il comando Add > New Project… del menu contestuale.
Selezioniamo una Class Library dalla sezione Classic Desktop e chiamiamo questa libreria UsersServices.
Andiamo ora nelle property a fare il solito “Make up” della libreria, oltre a cancellare subito Class1.cs.
- Modifichiamo il nome dell’assembly inserendovi il prefisso Dnw. come per tutte le nostre DLL (ribadisco che è una mia scelta non un obbligo).
- Modifichiamo il default namespace in Dnw.UsersServices.
- Modifichiamo l’Assembly Info inserendo delle informazioni pertinenti sulla nuova libreria.
- Modifichiamo l’icona in quella personalizzata usata per il resto.
- Firmiamo l’assembly con la chiave dotnetwork come tutti gli altri.
Prima di creare la classe UsersDataManager, che fornirà i servizi dati alla User Interface, dobbiamo generare qualcosa che permetta di indicare a tale classe qual’è il tipo di fonte dati che vogliamo utilizzare. Considerato che questo dato dovrà essere usato dalla User Interface, ma i tipi di fonti dati gestiti li saprà la classe dei servizi, il posto corretto ove inserire questo strumento è proprio la classe servizi. L’oggetto adatto a questo compito è un enumerazione, che potrà in seguito essere aggiornata per ognuna delle fonti dati che decideremo di supportare.
DataSourceType.cs
namespace Dnw.UsersServices
{
public enum DataSourceType
{
NotSet,
SqlServer
}
}
Visto che abbiamo creato solo la prima DataSource, per ora inseriremo nell’enumerazione due soli valori, quello impostato automaticamente che indicherà che la sorgente dati non è ancora stata definita, il secondo che indica l’uso di SqlServer.
UsersDataManager.cs
namespace Dnw.UsersServices
{
public class UsersDataManager
{
public UsersDataManager(string cnString,DataSourceType dataType )
{
}
}
}
Iniziamo a costruire la nostra classe “data manager” per la tabella TbUsers con il costruttore, che obbliga chi istanzia la classe a fornire una stringa di connessione ed un tipo di data source da utilizzare.
Per prima cosa andiamo a referenziare le librerie che ci servono, quindi la libreria delle Entity e quella del Data Provider SqlServer.
Andiamo quindi su References, Menu contestuale Add Reference, e selezioniamo Projects, Solutions e mettiamo la spunta sulle due librerie.
public UsersDataManager(string cnString, DataSourceType dataType)
{
DataType = dataType;
CnString = cnString;
switch (DataType)
{
case DataSourceType.SqlServer:
mSqlDp = new Data.SqlServer.UsersDp(CnString);
break;
}
}
Nel costruttore della classe manager, per prima cosa inizializziamo le variabili che contengono il tipo di Database utilizzato e la sua stringa di connessione. Definiremo le property fra poco.
Generiamo anche il corretto tipo di classe Data Provider in base al tipo di database selezionato.
public string CnString
{
get
{
return mCnString;
}
private set
{
mCnString = value;
}
}
public DataSourceType DataType
{
get
{
return mDataType;
}
private set
{
mDataType = value;
}
}
Le property in cui vengono inseriti i nostri parametri di configurazione, non possono essere modificate dopo la generazione della classe, per questo hanno il metodo set privato.
public Result<List<int>> Delete(List<User> usersToDelete)
{
switch (DataType)
{
case DataSourceType.SqlServer:
return mSqlDp.Delete(usersToDelete);
default:
Result<List<int>> ret = Result.Get<List<int>>();
ret.SetError(ERR_NoDatasource);
return (ret);
}
}
Il metodo Delete, che mappa il corrispondente metodo del Data Provider e restituisce un errore se non è stato indicato il tipo di dati corretto. Il messaggio di errore è inserito in una costante perché lo riutilizzeremo in tutti i metodi.
public Result<List<User>> Insert(List<User> usersToInsert)
{
switch (DataType)
{
case DataSourceType.SqlServer:
return mSqlDp.Insert(usersToInsert);
default:
Result<List<User>> ret = Result.Get<List<User>>();
ret.SetError(ERR_NoDatasource);
return (ret);
}
}
Il metodo Insert, anche in questo caso mappa il metodo del data provider e gestisce l’errore.
public Result<List<User>> SelectAll()
{
switch (DataType)
{
case DataSourceType.SqlServer:
return mSqlDp.SelectAll();
default:
Result<List<User>> ret = Result.Get<List<User>>();
ret.SetError(ERR_NoDatasource);
return (ret);
}
}
Il metodo SelectAll, che restituisce la lista di tutti gli User al chiamante e gestisce l’errore.
public Result<List<User>> Update(List<User> usersToUpdate)
{
switch (DataType)
{
case DataSourceType.SqlServer:
return mSqlDp.Update(usersToUpdate);
default:
Result<List<User>> ret = Result.Get<List<User>>();
ret.SetError(ERR_NoDatasource);
return (ret);
}
}
Il metodo Update, che aggiorna i dati e gestisce l’errore.
Come presumo abbiate notato, non ho inserito alcun tipo di controllo sui dati in questa classe, ma solo la parte di mappatura e controllo del Database, in questo caso, la parte che controlla il modo in cui le liste vengono preparate, ho deciso di metterla nella User Interface perché potrebbe essere più utile decidere lato UI come far comportare la gestione della manipolazione delle classi User. Però, quella specifica parte della gestione dati, potrebbe anche essere ospitata nella parte servizi. Dipende sempre dal tipo di applicazione che state realizzando e dal fatto che vogliate implementare uno o più Client. Per esempio, se voleste avere un client Desktop ed un Client semplificato Web, potrebbe valere la pena di inserire parte delle classi collegati alle “Viste” concetto connesso alla UI più che ai servizi, di essere ospitate nella parte servizi stessa. Nel mio caso, la gestione logica delle collezioni dati inserite, modificate, cancellate, preferisco inserirla nel viewmodel a livello di UI WPF.
Modifichiamo UsersWindow.xaml.cs
Per testare la nostra classe dei servizi, aggiungiamo il Reference alla dll appena generata e sostituiamo le chiamate dirette alla classe dati con l’uso della nostra classe di servizi.
Sostituiamo la descrizione del pulsante Test Data Provider, con Test Service Provider Class in UsersWindows.xaml.cs.
Nell’event handler del click del pulsante modifichiamo il codice per utilizzare la nostra classe service provider al posto della classe data provider:
StringBuilder sb = new StringBuilder();
UsersDataManager udme = new UsersDataManager(AppContext.Instance.CnString, DataSourceType.NotSet);
Result<List<User>> retError = udme.SelectAll();
if (retError.HasError)
{
sb.AppendLine("Test ERROR Select");
sb.AppendLine( retError.Error);
}
retError = udme.Insert(new List<User>());
if (retError.HasError)
{
sb.AppendLine("Test ERROR Insert");
sb.AppendLine(retError.Error);
}
retError = udme.Update(new List<User>());
if (retError.HasError)
{
sb.AppendLine("Test ERROR Update");
sb.AppendLine(retError.Error);
}
Result<List<int>> retIntError = udme.Delete(new List<User>());
if (retError.HasError)
{
sb.AppendLine("Test ERROR Delete");
sb.AppendLine(retError.Error);
}
In questa prima parte, testiamo i quattro metodi quando non viene selezionata la sorgente dati.
UsersDataManager udm = new UsersDataManager(AppContext.Instance.CnString, DataSourceType.SqlServer);
Result<List<User>> retSelect = udm.SelectAll();
if (retSelect.HasError)
{
MessageBox.Show(retSelect.Error, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
Users.Clear();
foreach (User x in retSelect.Data)
{
Users.Add(x);
}
sb.AppendLine("Selected users prior modification are:");
foreach (User u in Users)
{
sb.AppendLine(u.ToString());
}
Il codice per il test della funzione Select.
User usr = new User();
usr.ID = -1;
usr.Computer = "PC_999";
usr.LoginType = TypeOfLogin.Windows;
usr.UserName = "Giovanni Dalle Bande Nere";
usr.WinDomain = "Domain_999";
usr.WinUser = "GDBNere";
Users.Add(usr);
usr = new User();
usr.ID = -2;
usr.Computer = "PC_888";
usr.LoginType = TypeOfLogin.Manual;
usr.UserName = "Bartolomeo Moranni";
usr.WinDomain = "Domain_999";
usr.WinUser = "BMoranni";
Users.Add(usr);
usr = new User();
usr.ID = -3;
usr.Computer = "PC_777";
usr.LoginType = TypeOfLogin.Windows;
usr.UserName = "Luigino Fantoni";
usr.WinDomain = "Domain_999";
usr.WinUser = "LFantoni";
Users.Add(usr);
Result<List<User>> retInsert = udm.Insert(Users.Where(x => x.ID < 0).ToList());
if (retInsert.HasError)
{
MessageBox.Show(retInsert.Error, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
sb.AppendLine(new string('-', 80));
sb.AppendLine("The Added users are:");
foreach (User u in retInsert.Data)
{
sb.AppendLine(u.ToString());
}
Il codice di test della funzione Insert.
usr = Users.FirstOrDefault(x => x.ID >= 1);
usr.Computer = usr.Computer + " MOD";
usr.Password = "W0rkF0ll0wsMe";
usr.UserName = usr.UserName + " MOD";
List<User> changedUsers = new List<User>();
changedUsers.Add(usr);
usr = Users.FirstOrDefault(x => x.ID >= (usr.ID + 1));
usr.Computer = usr.Computer + " MOD";
usr.Password = "But1RunF@ster";
usr.UserName = usr.UserName + " MOD";
changedUsers.Add(usr);
Result<List<User>> retUpdate = udm.Update(changedUsers);
if (retUpdate.HasError)
{
MessageBox.Show(retUpdate.Error, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
sb.AppendLine(new string('-', 80));
sb.AppendLine("The Edited users are:");
foreach (User u in retUpdate.Data)
{
sb.AppendLine(u.ToString());
}
Il codice di test della funzione Update.
Result<List<int>> retDelete = udm.Delete(Users.Where(x => x.WinDomain == "Domain_999").ToList());
if (retDelete.HasError)
{
MessageBox.Show(retDelete.Error, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
sb.AppendLine(new string('-', 80));
sb.AppendLine("The Deleted users are:");
foreach (int i in retDelete.Data)
{
usr = Users.FirstOrDefault(x => x.ID == i);
if (usr != null)
{
sb.AppendLine(usr.ToString());
Users.Remove(usr);
}
}
sb.AppendLine(new string('-', 80));
sb.AppendLine("The Remaining users are:");
foreach (User u in Users)
{
sb.AppendLine(u.ToString());
}
Il codice di test della funzione Delete.
retSelect = udm.SelectAll();
if (retSelect.HasError)
{
MessageBox.Show(retSelect.Error, "ERROR", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
Users.Clear();
foreach (User x in retSelect.Data)
{
Users.Add(x);
}
sb.AppendLine("Check if true re-reading database:");
foreach (User u in Users)
{
sb.AppendLine(u.ToString());
}
ResultText = sb.ToString();
Ed infine la rilettura del database per verificare cosa vi si trova dentro e l’aggiornamento della TextBox per visualizzare il risultato.
Per oggi ci fermiamo qui, nel prossimo articolo creeremo le classi per gli altri 2 database, per poi sviluppare la User Interface ed esplorare le tecniche possibili per gestire in modo efficiente il controllo di quali dati sono stati inseriti, modificati o cancellati.
Riepilogo
In questo articolo abbiamo visto le seguenti cose:
- Il motivo per cui creare uno strato di Servizi fra il database e la User Interface.
- Come creare la libreria dei servizi.
- Creare una enumerazione per gestire i tipi di database
- Creare la classe UsersDataManager, che gestisce la comunicazione fra User Interface e Database.
- Testare la classe appena generata e la sua gestione degli errori.
Potete scaricare il progetto di esempio al link qui indicato:
Per qualsiasi domanda, curiosità, commento o per segnalare un errore potete utilizzare il link alla form di contatto in cima alla pagina.