Dins del desenvolupament d'aplicacions web cal una peça que enllaci la capa de BBDD (clsDades i "stored procedures" que estan sota) amb les pàgines web que llencen peticions. Per això faig servir els "manejadors generics" (que malament sonen traduïts), "generic handler" (millor). Són com pàgines web però a les que s'ha tret tota la part HTML de .NET. Tenen l'extensió ASHX i estan en .NET des del primer Visual Studio (bé jo només puc afirmar que estan des del VS 2005, el 2003 no estic segur).
Al no tenir HTML no tenen ViewState, no gestionen events, al no tenir aquests dos components el seu rendiment és millor que el d'una pàgina ASPX (concretament el pipeline (cadena de muntatge) és més curt), són força ràpids. El seu punt d'entrada és una funció:
publicvoid ProcessRequest(HttpContext context)
Dins de l'objecte HttpContext disposem dels objectes HttpResponse per enviar dades a la pàgina web que invoca al ASHX i l'objecte HttpRequest per recuperar informació que ens envien.
Normalment cada taula de la BBDD, o entitat que es vulgui té un ASHX associat. En aquest ASHX es fan les operacions bàsiques (típiques d'un CRUD). A més a més es pot afegir la lògica de negoci (les operacions específiques d'un objecte) que estigui relacionada amb l'objecte. Per exemple si tractem comandes, en el ASHX corresponent a les comandes hi haurà, alta, baixa, actualitzar, obtenir un conjunt de comandes (per muntar un grid), i a més a més també poden existir canvi d'estat de la comanda, tancar comanda, verificar comanda, enviar comanda a algú, autocompletes, etc... (això seria la lògica de negoci). El generador de codi munta un ASHX normal, amb les operacions CRUD ja definides, però no crea la lògica de negoci (no ho pot endevinar). Poso el codi d'un ASHX típic.
publicclass classificacions : IHttpHandler, IReadOnlySessionState
{
privateHttpResponse Resp;
privateHttpRequest Req;
privateHttpSessionState Ses;
string IdPersona = "";
private Connexions Conn = new Connexions();
privatestring ConnStr;
publicvoid ProcessRequest(HttpContext context)
{
ConnStr = Conn.GetConnStr();
Resp = context.Response;
Req = context.Request;
Ses = context.Session;
Resp.Cache.SetCacheability(HttpCacheability.NoCache);
Resp.ContentEncoding = Encoding.Default;
Resp.Charset = "ISO-8859-1";
//Verificació de seguretat, es pot fer d'altres formes //Normalment el login informa aquestes variables a sessió, BBDD...if (Ses["idPersona"] == null)
{
Resp.Write("ERROR: Sessió expirada, torni a validar-se al sistema.");
return;
}
else
{
IdPersona = Ses["idPersona"].ToString();
}
string Action = Req["action"];
switch (Action)
{
case"data":
LoadData();
break;
case"update":
UpdateData();
break;
case"insert":
Insertar();
break;
case"delete":
Delete();
break;
case"loadgrid":
LoadGrid();
break;
}
}
publicbool IsReusable
{
get
{
returnfalse;
}
}
}
En posts posteriors anirem desgranant les funcions que estan al switch:
Algunes d'aquestes funcions envien JSON a la part client, concretament LoadData i LoadGrid. Cal doncs una llibreria que converteixi a JSON el resultat. Jo faig servir aquesta.
#10/01/2014 09:17 Programació C# Autor: Alex Canalda
Dins de la clsDades hi ha el mètode UPD que actualitza un registre, però i si volem actualitzar una DataTable? Cap problema. Fent servir tots els mètodes de la clsDades és possible cridar al que correspon segons l'estat de cada registre de la DataTable. Al final es fa un AcceptChanges per resetejar l'estat dels registres i llestos. Com sempre el codi:
/// <summary>/// Actualitza una taula contra la BBDD, fa els inserts, updates i deletes segons l'estat de la fila./// Quan acaba deixa totes les files en DataRowState.Unchanged fent una crida a AcceptChanges/// </summary>/// <param name="TBL">La taula que s'ha d'actualitzar</param>publicvoid UPD(DataTable TBL)
{
foreach (DataRow DR in TBL.Rows)
{
switch (DR.RowState)
{
caseDataRowState.Added:
INS(DR);
break;
caseDataRowState.Deleted:
DEL(DR);
break;
caseDataRowState.Modified:
UPD(DR);
break;
}
}
TBL.AcceptChanges();
}
#09/01/2014 17:07 Programació C# Autor: Alex Canalda
Igual que el procés que deserialitza, també és important la part que envia les dades. I la part comuna està a la clsDades.
Es a dir recuperar els valors de la BBDD, normalment amb un GET, després empaquetar-los en alguna estructura que després sigui fàcil enviar per JSON al navegador (faig servir un Dictionary). Els Dictionary són objectes que guarden valors (en el cas de la clsDades cadenes de text (strings)) en forma de parells, clau-valor, això és ideal per convertir-los a JSON, ja que el JSON també són claus-valors. Un cop al navegador hi ha una llibreria que fa el procés contrari (passar del JSON als camps), aquesta llibreria es crida en la funció LoadData de Javascript.
El que fa el serialitzar es crear aquest Dictionary, amb les dades adequades. La funció fa servir la llista de camps que està a la variable "Serialitzar" que s'inicialitza amb els camps de la taula que toqui (una d'exemple). Hi ha tipus de dades que admeten un determinat format, per exemple les dates o els decimals, el string que defineix com és aquest format surt de la propietat "format" (valgui la redundància) de la clsCamp (en el post de clsDades està descrita la clsCamp) i s'informa en el programa generador de classes. Per exemple podria ser un "dd-mm-yyyy" per dates o un "D2" per números.
La funció Serialize existeix en dos sabors, una que retorna el Dictionary (a vegades es recuperen dades i es vol afegir algo) o directament el JSON. Com la signatura de les funcions és la mateixa vaig posar un paràmetre dummy per diferenciar-les. El codi a continuació.
En un projecte web, una part molt important i que acostuma a consumir molt temps de programació és recuperar els valors d'un formulari al servidor quan es rep un submit (ja sigui per Ajax o normal) per posar-los en una estructura que permeti guardar-los, normalment a una BBDD.
Per evitar-ho el que es fa és una llista dels camps a recuperar (variable "Deserialitzar" que és un array de clsCamp). Es a dir de tots els camps que pertanyen a una taula es marquen els que es volen deserialitzar (recuperar) i el software que genera la classe els inclou en aquesta llista (variable). Així el que s'acostuma a fer és invocar a la funció deserialitzar i després fer un UPD o un INS. Durant la deseriatlització es verifica el tipus dels valors que es reben des del formulari web.
La funció genèrica que realitza aquest procés està a la clsDades, però la llista de camps a deserialitzar està a cada classe especifica (derivada), la que correspon a cada taula. La funció Deserialitzar rep el Request (per obtenir les dades) i un DataRow o un Dictionary (acostuma a fer-se servir el DataRow per updates i l'altra per inserts).
Com sempre el codi associat, primer la versió Dictionary:
publicDictionary<string, string> Deserialize(HttpRequest Req)
{
Dictionary<string, string> Values = newDictionary<string, string>();
string ValorCamp;
decimal ValorDecimal;
Int64 ValorInt64;
int ValorInt;
DateTime ValorData;
foreach (clsCamp Camp in Deserialitzar)
{
ValorCamp = Req[Camp.NomCamp];
switch (Camp.Tipus)
{
caseTipus.bit:
if (string.IsNullOrEmpty(Req[Camp.NomCamp])) Values.Add(Camp.NomCamp, "False");
else Values.Add(Camp.NomCamp, "True");
break;
caseTipus.bigint:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (Int64.TryParse(ValorCamp, out ValorInt64)) Values.Add(Camp.NomCamp, ValorCamp);
elsethrownewException("El camp "
+ Camp.NomCamp + " no té un valor sencer de 64 bits vàlid");
}
break;
caseTipus.tint:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (int.TryParse(ValorCamp, out ValorInt)) Values.Add(Camp.NomCamp, ValorCamp);
elsethrownewException("El camp "
+ Camp.NomCamp + " no té un valor sencer vàlid");
}
break;
caseTipus.dec:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (decimal.TryParse(ValorCamp, out ValorDecimal)) Values.Add(Camp.NomCamp, ValorCamp);
elsethrownewException("El camp "
+ Camp.NomCamp + " no té un valor numèric vàlid");
}
break;
caseTipus.datetime:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (DateTime.TryParse(ValorCamp, out ValorData)) Values.Add(Camp.NomCamp, ValorCamp);
elsethrownewException("El camp "
+ Camp.NomCamp + " no té una data vàlida");
}
break;
caseTipus.varchar:
caseTipus.chr:
caseTipus.nchar:
caseTipus.nvarchar:
if (!string.IsNullOrEmpty(ValorCamp))
{ //Els varchar(max) i nvarchar(max) tenen longitud -1 i no s'ha de verificar la seva longitud.if (ValorCamp.Length > Camp.Longitud && Camp.Longitud != -1)
thrownewException("Camp " + Camp.NomCamp + " supera longitud permesa");
else Values.Add(Camp.NomCamp, ValorCamp);
}
break;
}
}
return Values;
}
Ara la versió DataRow:
publicvoid Deserialize(DataRow DR, HttpRequest Req)
{
string ValorCamp;
decimal ValorDecimal;
int ValorInt;
Int64 ValorInt64;
DateTime ValorData;
foreach (clsCamp Camp in Deserialitzar)
{
ValorCamp = Req[Camp.NomCamp];
switch (Camp.Tipus)
{
caseTipus.bit:
if (string.IsNullOrEmpty(Req[Camp.NomCamp])) DR[Camp.NomCamp] = false;
else DR[Camp.NomCamp] = true;
break;
caseTipus.bigint:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (Int64.TryParse(ValorCamp, out ValorInt64)) DR[Camp.NomCamp] = ValorCamp;
elsethrownewException("Camp "
+ Camp.NomCamp + " no té un valor sencer de 64 bits vàlid");
}
else
{
if (Camp.Nulable) DR[Camp.NomCamp] = DBNull.Value;
elsethrownewException("Camp "
+ Camp.NomCamp + " no permet valors nulls");
}
break;
caseTipus.tint:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (int.TryParse(ValorCamp, out ValorInt)) DR[Camp.NomCamp] = ValorCamp;
elsethrownewException("Camp "
+ Camp.NomCamp + " no té un valor sencer vàlid");
}
else
{
if (Camp.Nulable) DR[Camp.NomCamp] = DBNull.Value;
elsethrownewException("Camp "
+ Camp.NomCamp + " no permet valors nulls");
}
break;
caseTipus.dec:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (decimal.TryParse(ValorCamp, out ValorDecimal)) DR[Camp.NomCamp] = ValorCamp;
elsethrownewException("Camp "
+ Camp.NomCamp + " no té un valor numèric vàlid");
}
else
{
if (Camp.Nulable) DR[Camp.NomCamp] = DBNull.Value;
elsethrownewException("Camp "
+ Camp.NomCamp + " no permet valors nulls");
}
break;
caseTipus.time:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (string.IsNullOrEmpty(Camp.Format))
{
if (DateTime.TryParseExact(ValorCamp, "HH:mm",
System.Globalization.CultureInfo.CurrentCulture,
System.Globalization.DateTimeStyles.None,
out ValorData)) DR[Camp.NomCamp] = ValorCamp;
elsethrownewException("Camp "
+ Camp.NomCamp + " no té una hora vàlida");
}
else
{
if (DateTime.TryParseExact(ValorCamp,
Camp.Format,
System.Globalization.CultureInfo.CurrentCulture,
System.Globalization.DateTimeStyles.None,
out ValorData)) DR[Camp.NomCamp] = ValorCamp;
elsethrownewException("Camp "
+ Camp.NomCamp + " no té una hora vàlida");
}
}
else
{
if (Camp.Nulable) DR[Camp.NomCamp] = DBNull.Value;
elsethrownewException("Camp "
+ Camp.NomCamp + " no permet valors nulls");
}
break;
caseTipus.datetime:
caseTipus.date:
if (!string.IsNullOrEmpty(ValorCamp))
{
if (DateTime.TryParse(ValorCamp, out ValorData)) DR[Camp.NomCamp] = ValorCamp;
elsethrownewException("Camp "
+ Camp.NomCamp + " no té una data vàlida");
}
else
{
if (Camp.Nulable) DR[Camp.NomCamp] = DBNull.Value;
elsethrownewException("Camp " + Camp.NomCamp + " no permet valors nulls");
}
break;
caseTipus.varchar:
caseTipus.chr:
caseTipus.nchar:
caseTipus.nvarchar:
if (!string.IsNullOrEmpty(ValorCamp))
{ //Els varchar(max) i nvarchar(max) tenen longitud -1 i no s'ha de verificar la seva longitud.if (ValorCamp.Length > Camp.Longitud && Camp.Longitud != -1)
thrownewException("Camp " + Camp.NomCamp + " supera longitud permesa");
else DR[Camp.NomCamp] = ValorCamp;
}
else
{
if (Camp.Nulable) DR[Camp.NomCamp] = DBNull.Value;
elsethrownewException("Camp " + Camp.NomCamp + " no permet valors nulls");
}
break;
}
}
}
#08/01/2014 12:32 Programació C# Autor: Alex Canalda
Seguint amb la sèrie de la clsDades aquesta és la funció que s'encarrega d'obtenir de la BBDD una pàgina de registres, ordenats d'una forma determinada i filtrats si cal.
Fa servir la SP QUERY per fer-ho, en realitat la SP és la que fa tota la feina, la clsDades només té la invocació a aquesta SP. Així que ha de fer servir la variable ParamsQUERY per informar-ne els paràmetres, a més a més hi ha uns paràmetres fixes que són:
SortColumn: Columna per la que s'ordena (haurà de coincidir amb alguna de les que té la SP). En el literal de la columna també està informat el sentit, ascendent o descendent.
PageSize: Mida de la pàgina, en número de registres, 5, 10, 20...
PageNum: Número de la pàgina que es vol, la 1, 2, 3...
A més a més d'aquests paràmetres d'entrada la SP té un valor important de sortida que cal recollir, el número total de registres un cop filtrat (sense paginar). Es podria fer amb un COUNT però seria fer el filtre dues vegades, dins de la SP ja es calcula i llavors només es fa un cop. Aquest valor és important per calcular el número total de pàgines donat un filtre a una taula. En la funció és el paràmetre NumTotRegs, que està declarat de sortida. Com sempre el codi:
Seguint amb la sèrie de la clsDades aquesta és la funció que s'encarrega de contar quants registres hi ha a la BBDD que compleixin un cert criteri.
Fa servir la SP COUNT per fer-ho. Fa servir els mateixos paràmetres que el GET (que són els mateixos que el QUERY), es a dir es poden aplicar els mateixos criteris de cerca que quan es fa un GET però enlloc d'obtenir registres retorna només el número de registres. Això és útil quan hi ha una barra de progres i es vol saber el número total de registres, o quan s'ha de calcular el número de pàgines...
Fa servir ParamsQUERY per informar els seus paràmetres. La funció només admet un Dictionary per informar els seus paràmetres, no cal que siguin tots, només el que volem filtrar. Com sempre el codi:
Seguint amb la sèrie de la clsDades aquesta és la funció que s'encarrega d'esborrar un registre a la BBDD.
Fa servir la SP DEL per fer-ho. Així que ha de fer servir la variable ParamsDEL per informar-ne els paràmetres. Normalment aquests paràmetres són la primary key (PK), de tal manera que només s'esborra un registre. Existeix en 2 sabors diferents, un que rep un DataRow i un altra que rep un objecte Dictionary (on les Keys són els noms dels camps i els Values el valor).
Igual que la funció INS, aquesta també invalida la cache.
Com sempre el codi, primer el que rep el Dictionary:
Seguint amb la sèrie de la clsDades aquesta és la funció que s'encarrega d'actualitzar un registre a la BBDD.
Fa servir la SP UPD per fer-ho. Així que ha de fer servir la variable ParamsUPD per informar-ne els paràmetres. Existeix en 2 sabors diferents, un que rep un DataRow i un altra que rep un objecte Dictionary (on les Keys són els noms dels camps i els Values el valor).
Igual que la funció INS, aquesta també invalida la cache.
Com sempre el codi, primer el que rep el Dictionary:
Seguint amb la sèrie de la clsDades aquesta és la funció que s'encarrega d'insertar un registre a la BBDD.
Fa servir la SP INS per fer-ho. Així que ha de fer servir la variable ParamsINS per informar-ne els paràmetres. Existeix en 2 sabors diferents, un que rep un DataRow i un altra que rep un objecte Dictionary (on les Keys són els noms dels camps i els Values el valor). Ambdues quan hi ha una identitat en la taula on inserten recuperen el valor que s'ha generat.
Ja posats que hi ha informació sobre si un camp admet nulls o no, es fa aquesta verificació. El que no es mira és el tipus de dades del valor que passa per actualitzar.
Resaltar que aquesta funció invalida la cache de dades, aleshores si la clase fa servir la cache i està activada la auto-neteja aleshores buida la cache. Pot ser que no interessi netejar la cache cada cop, per exemple si hi ha molts inserts seguits, aleshores cal posar el "Autonetejar" a fals.
El codi de la versió DataRow:
publicvoid INS(DataRow DR)
{
SqlCommand SqlComm = newSqlCommand();
SqlParameter SqlParam;
Int64 ID = 0;
bool LocalConn;
SqlConnection Conn = null;
if (SQLConn == null)
{
LocalConn = true;
Conn = newSqlConnection(CadConnBBDD);
Conn.Open();
}
else
{
LocalConn = false;
Conn = SQLConn;
}
SqlComm.Connection = Conn;
if (SQLTrans != null) SqlComm.Transaction = SQLTrans;
SqlComm.CommandType = CommandType.StoredProcedure;
SqlComm.CommandText = spINS;
foreach (clsCamp Camp in ParamsINS)
{
SqlParam = CreaParametre(Camp, true);
if (DR.Table.Columns.Contains(Camp.NomCamp))
{
if (!DR.IsNull(Camp.NomCamp))
{
if (Camp.Nulable) SqlParam.Value = DBNull.Value;
else
{
if (!Camp.Identitat)
{
Exception Ex = newException("El camp " + Camp.NomCamp + " no admet valors nulls");
throw Ex;
}
}
}
else SqlParam.Value = DR[Camp.NomCamp];
}
else
{ //Potser la taula té menys columnes que camps hi ha a la BBDDif (Camp.Nulable) SqlParam.Value = DBNull.Value;
else
{
if (!Camp.Identitat)
{
Exception Ex = newException("El camp " + Camp.NomCamp + " no admet valors nulls");
throw Ex;
}
}
}
SqlComm.Parameters.Add(SqlParam);
}
SqlComm.ExecuteNonQuery();
if (TeCache && AutoNetejaCache)
{
ClearCache();
}
if (LocalConn)
{
Conn.Close();
Conn.Dispose();
Conn = null;
}
if (TeIdentitat)
{
//Cal vigilar que la columna identitat no sigui ReadOnly//DR.Table.Columns[Identitat].ReadOnly = false;
ID = Int64.Parse(SqlComm.Parameters[Prefix + Identitat].Value.ToString());
DR[Identitat] = ID;
}
DR.AcceptChanges();
}
La versió que fa servir el Dictionary retorna el ID generat en cas que hagi identitat.
public Int64 INS(Dictionary<string, string> Params)
{
SqlCommand SqlComm = newSqlCommand();
SqlParameter SqlParam;
Int64 ID = 0;
bool LocalConn;
SqlConnection Conn = null;
if (SQLConn == null)
{
LocalConn = true;
Conn = newSqlConnection(CadConnBBDD);
Conn.Open();
}
else
{
LocalConn = false;
Conn = SQLConn;
}
SqlComm.Connection = Conn;
if (SQLTrans != null) SqlComm.Transaction = SQLTrans;
SqlComm.CommandType = CommandType.StoredProcedure;
SqlComm.CommandText = spINS;
foreach (clsCamp Camp in ParamsINS)
{
SqlParam = CreaParametre(Camp, true);
if (Params.Keys.Contains<string>(Camp.NomCamp))
{
if (string.IsNullOrEmpty(Params[Camp.NomCamp]))
{
if (Camp.Nulable) SqlParam.Value = DBNull.Value;
else
{
Exception Ex = newException("El camp " + Camp.NomCamp + " no admet valors nulls");
throw Ex;
}
}
else SqlParam.Value = Params[Camp.NomCamp];
}
else
{
if (Camp.Nulable) SqlParam.Value = DBNull.Value;
else
{
if (!Camp.Identitat)
{
Exception Ex = newException("El camp " + Camp.NomCamp + " no admet valors nulls");
throw Ex;
}
}
}
SqlComm.Parameters.Add(SqlParam);
}
SqlComm.ExecuteNonQuery();
if (TeCache && AutoNetejaCache)
{
ClearCache();
}
if (LocalConn)
{
Conn.Close();
Conn.Dispose();
Conn = null;
}
if (TeIdentitat) ID = Int64.Parse(SqlComm.Parameters[Prefix + Identitat].Value.ToString());
else ID = 0;
return ID;
}
#30/12/2013 16:36 Programació C# Autor: Alex Canalda
Aquesta funció pertany a la clsDades i funciona igual que la GET però limita el nombre de files retornades a les que se li passen per paràmetre.
Perque és necessària? Suposem que tenim un procés que ha de processar (valgui la redundància) 50.000 registres, això ho retorna el GET sense problemes. Però quan porta 10.000 processats en una aplicació web dóna un timeout. Llavors és necessari processar de 5.000 en 5.000 registres. Durant el procés el registres poden canviar d'estat (per exemple) i així evitar tornar a ser seleccionats a la següent crida del GETTOP. Cada cop que es processa un conjunt de registres es pot enviar a la UI un missatge indicant que encara queden per processar, la UI pot actualitzar alguna barra de progrés o similar i llestos (haurà fet un COUNT per saber el total).
La funció té els mateixos paràmetres que el GET amb un afegit que és el número de registres a retornar.