Wednesday, May 11, 2005

ClickOnce - o treabă făcută pe jumătate ?

ClickOnce versus Msi
În ultima vreme, am urmârit cu interes câteva prezentări despre noua tehnologie de deployment din .Net 2.0 – ClickOnce. Şi pentru prima dată, am senzaţia că am priceput cum stă treaba.
Aveţi ocazia să-mi dovediţi contrariul.

De ce ClickOnce ? Aveam MSI !
Întrebarea asta mă rodea de mai mult timp. Care e relaţia între ClickOnce şi Windows Installer (msi)? Va înlocui ClickOnce Msi-urile pentru aplicaţii .Net ?
MSI e o tehnologie foarte puternică şi matură. MSI poate face aproape orice şi-ar putea dori un developer de kit-uri – şi de cele mai multe ori, aşa cum bine a subliniat Ovidiu Platon, poate face mult mai mult decât se aşteaptă un programator sau un administrator.
Lucrez ca programator în cadrul departamentului de dezvoltare software al unei firme non-IT – clientul meu deci e chiar firma care mă plăteşte. Altfel zis - admin-ul clientului meu lucrează în celălalt birou şi il pot auzi cum trănteşte uşa ori de câte ori se enervează. Apropierea asta de admin m-a obligat să mă pun la curent cu modalităţile de deployment în Enterprise – nu de alta, dar adminul meu e prea urâcios ca să risc să-i spun că ar trebui să instaleze manual o dată la două săptămâni câte un update pe vreo 80 de computere...
Aşa am ajuns să apreciez combinaţia Active Directory/MSI advertising. O chestie super practică, uşor de pus pe picioare, dar din păcate prea puţin cunoscută. Ideea e următoarea : o aplicaţie care cere privilegii speciale la instalare poate fi instalată doar de pe un cont de administrator. Exemple sunt destule – orice aplicaţie care scrie în registry, care înregistrează obiecte COM, pune assembly-uri în GAC, creează o sursă de log in Event Log sau doar vrea să pună un icon pe desktop-ul din All Users.
Există însă o facilitate prea puţin cunoscută – un administrator poate doar să „facă reclamă” unei aplicaţii (advertise). Folosind “msiexec /jm nume.msi” de pe contul de admin, se crează doar iconiţele pe desktop sau în Start -> Programs – restul fişierelor nu sunt instalate. Dacă însă un user fără drepturi speciale va da dublu click pe iconiţa de “reclamă”, va determina pornirea instalării efective a aplicaţiei cu drepturi de admin. (Probabil ăsta e motivul pentru care msiexec este un serviciu – pentru ca procesul de instalare invocat de un user fără privilegii să poată fi executat sub local system account). Nu, nu e o breşă de securitate – pentru ca doar administratorul hotărăşte de la început ce aplicaţii vor fi “advertise” pe un calculator.
Cu toate că e cool, advertise-ul nu pare util în prea multe situaţii la prima vedere. Devine însă foarte interesant în combinaţie cu Group Policy Objects din Active Directory. Pe scurt, se poate crea o politică la nivelul unei întregi reţele care să specifice ce useri pot beneficia de o anumită aplicaţie. Indiferent de computerul pe care se logează, unui user i se vor crea automat, la log-on, prin advertise, iconiţele de care are nevoie pe desktop. Un dublu click pe o astfel de iconiţă determină instalarea imediată a aplicaţiei. Se pot face chiar şi update-uri de aplicaţii în acest mod – e suficient ca adminul să definească o nouă politică prin care să specifice un nou msi care va înlocui aplicaţia anterioară.
Aşadar existau, cel puţin la nivel de Enterprise, soluţii de deployment şi AutoUpdate destul de practice. De ce totuşi ClickOnce ?
Msi-ul e o soluţie super elaborată, creată ca răspuns la DLL hell. Ani de zile, programatorii au fost încurajaţi să dezvolte aplicaţii care îşi pun în comun dll-uri şi resurse; orice kit de instalare putea să aducă o versiune mai nouă sau mai veche al unui astfel de dll – iar Windows Installer-ul avea ca misiune principală gestionarea şi arbitrarea unor potenţiale conflicte între versiuni diferite ale aceluiaşi DLL partajat între aplicaţii.
.Net-ul vine însă cu o abordare revoluţionară: adio dll-uri partajate. Orice aplicaţie va funcţiona doar cu versiunea dll-ului cu care a fost compilată – motiv penstru care se încurajează ca o aplicaţie să conţină toate dll-urile necesare chiar în directorul aplicaţiei. Hard disk-urile sunt suficient de mari – nu putem considera că risipim spaţiu cu dll-uri redundante în fiecare director de aplicaţie. Iar potenţialele beneficii pe care le-ar aduce aplicaţiei X înlocuirea de către aplicaţia Y a unui dll comun cu o versiune mai nouă sunt mult mai mici decăt riscul ca noua versiune să fie incompatibilă cu aplicaţia X. Dacă aplicaţia X ar putea beneficia de o versiune mai nouă a dll-ului în cauză, ar fi de preferat ca aplicaţia X să fie recompilată, retestată şi re-deploy-ată cu noua versiune de dll. Pe calculatorul userului nu vor mai ajunge combinaţii aplicaţie/dll care nu au fost luate în calcul de către developer şi tester.
Altfel zis, nu mai încercăm să gestionăm dll hell-ul – ci pur şi simplu renunţăm la dll-urile comune. Microsoft-ul a tăiat nodul gordian.
Această nouă abordare pe care o aduce .Net-ul, ridică semne de întrebare asupra utilităţii Windows Installer-ului (msi-ului). La drept vorbind, Windows Installer-ul a fost creat pentru a gestiona dll hell-ul – şi tocmai am decis că nu mai vrem să gestionăm nimic – vom copia fiecare dll în directorul fiecărei aplicaţii. .Net-ul ar putea beneficia de pe urma unui sistem simplificat de deployment, care să pună în practică XCopy deployment şi să cuprindă suport pentru AutoUpdate.

Faceţi cunoştinţă cu ClickOnce.
ClickOnce permite copierea unei aplicaţii de pe un server de web spre exemplu în profilul userului care are nevoie de serviciile acelei aplicaţii. ClickOnce nu va face niciodata mai mult decat să aducă fisierele local şi să pună vreun icon pe desktop-ul acelui user. ClickOnce va rula întotdeauna sub credenţialele userului logat – sub nici o formă nu îşi propune să suporte scenarii gen Advertise, menite să dea posibilitatea unui user fără privilegii speciale să instaleze o aplicaţie care a fost aprobată în prealabil de un admin. Aprobarea admin-ului se va face pe viitor nu sub forma definirii unei liste de aplicaţii safe, pe care un user are dreptul să le instaleze (prin publish sau advertise) – ci mai curând prin specificarea unor politici de securitate (~CAS) pe care aplicaţiile instalabile trebuie să le respecte. Spre exemplu, s-ar putea defini ca userul să aibă dreptul să instaleze orice aplicaţie atâta timp căt acea aplicaţie nu necesită accesarea sistemului de fişiere sau nu cere să se conecteze la internet (no more spyware).
Frumoasă idee.
Dar ce lipseşte? Ei bine, m-aş simţi mai liniştit dacă aş avea siguranţa că soluţia e completă.
1. Pentru a motiva introducerea SmartClients-urilor, prezentările Microsoft aduc de fiecare dată în discuţie costurile de deployment pentru soluţiile clasice windows forms în Enterprise (se foloseşte SMS-ul pentru a speria puţin audienţa). Eu interpretez asta ca pe o promisiune din partea Microsoft de a susţine Smart Clients (= ClickOnce) ca alternativă la sistemele clasice de deployment. .Net 2,0 însă e doar jumătate din treabă.
E drept, fiecare user al meu ar avea posibilitatea să-şi instaleze aplicaţia pe profilul personal prin simpla accesare a unui URL. Întrebarea e : care url ? Există vreo modalitate la nivelul Enterprise-ului de a publica URL-urile aplicaţiilor către user ? Mă gândesc la un sistem care să înlocuiască Advertise-ul aplicaţiilor cu Active Directory GPOs, nu la mass mailing sau SharePoint. Userii din enterprise sunt obişnuiţi să-şi găsească pe desktop iconiţele de care au nevoie şi nu au obiceiul ca atunci când iconiţele le lipsesc să-şi verifice mailul în căutarea link-ului salvator. Aş vrea să aud şi vocea celor de la Active Directory cântând la unison cu cei de la .Net Framework.
2. .Net-ul aduce o revoluţie în problema dll hell-ului. Fiecare aplicaţie cu dll-urile sale. Gata cu incompatibilităţie. Însă în acelaşi timp, am renunţam şi la beneficiile dll-urilor partajate. Ei bine, uneori actualizarea unui dll comun putea fi benefic pentru toate aplicaţiile care îl foloseau. Acum însă, va trebui să actualizăm fiecare din aplicaţiile existente independent. Vă aduceţi aminte? Compilare – testare – deployment pentru fiecare aplicaţie, ori de câte ori un dll (fost) partajat a fost modificat. Doar aşa o aplicaţie poate beneficia de bug fix-urile din aceste dll-uri.
Costurile procesului de compilare – testare – deployment vor trebui însă să fie cât mai mici pentru a putea permite redeployment-ul fiecărei aplicaţii la fiecare fixare a unui bug într-o bibliotecă partajată. Pentru developer, asta se traduce în necesitatea unui sistem de automatizare a build-urilor care să cuprindă inclusiv faza de realizare a kitului aplicaţiilor. E absolut necesar ca generarea acelor manifeste (fişiere xml) care descriu kitul aplicaţiei să fie realizabilă nu doar dintr-un wizard (util pentru obţinerea primului kit) – ci şi în mod automatizat (linie de comandă) pentru a suporta build script-urile. Dacă Microsoft lasă in seama programatorului crearea uneltelor necesare regenerării acelor manifeste pe baza dependinţelor aplicaţiei se chemă că nu a făcut treaba decât pe jumătate.

Monday, May 02, 2005

Generarea codului cu CodeSmith

În episodul anteriror, am analizat avantajele folosirii propriilor clase în locul TypedDataSet-urilor şi am amintit de facilităţile la care suntem nevoiţi să renunţăm odată cu abandonarea TypedDataSet-urilor. E momentul să ne ocupăm de câteva din aceste probleme.
Probabil cea mai mare piedică în adoptarea propriilor clase în locul TypedDataSet-urilor este cantitatea de cod care ar trebui scrisă – capabilă să înspăimânte şi să omoare prin plictiseală chiar şi pe cel mai meticulos programator. Pentru a trece mai uşor peste acest impediment, putem folosi generatoare de cod care, pornind de la un tabel din baza de date, să ne ofere un punct de plecare pentru obiectele noastre.
O căutare cu google după “code generator" o să vă dezvăluie cât efort a fost depus de-a lungul timpului în direcţia generatoarelor de cod. Există incredibil de multe soluţii – multe din ele specializate pe generarea de cod pentru DataLayer, altele care promit generarea unor întregi site-uri plecând de la schema unei baze de date. Am ales CodeSmith pentru că este o unealtă nespecializată, care nu ne impune un anumit model de programare sau o anumită arhitectură şi, în plus, are o sintaxă foarte apropiată de Asp.Net – deci familiară multora dintre noi.
Trebuie însă să menţionez că e vorba de un generator de cod gratuit însă nu open-source. Doar binarele pot fi descărcate de la adresa http://www.ericjsmith.net/codesmith/ . Produsul e suficient de matur pentru a putea fi folosit în medii profesionale iar site-ul autorului oferă şi câteva resurse de asistenţă (câteva tutorialuri şi un forum de suport). Pe lângă generatorul propriu zis (care e gratuit) există şi un editor comercial de templateuri – CodeStudio - care oferă facilităţi suplimentare precum sintax highlight-ing.

Template-urile CodeSmith îndeplinesc câteva funcţii diferite :
· declară setul de parametrii de intrare
· definesc un şablon al codului care va fi generat
· suportă adăugarea de cod C#/VB.Net/JScript care să manipuleze parametrii şi sablonul

Pentru început, să încercăm să facem un template de generare a colecţiilor tipizate (typed collections). Rezultatul ar trebui să arate cam aşa pentru o colecţie de obiecte Salariat :

public class SalariatCollection : System.Collections.CollectionBase
{
public Salariat this[int index]
{
get{ return (Salariat) this.List[index];}
set{ this.List[index] = value;}
}

public int Add(Salariat valoareNoua)
{
return this.List.Add(valoareNoua);
}

public void Remove(Salariat valoareDeSters)
{
this.List.Remove(valoareDeSters);
}
}

iar pentru o colecţie de Funcţii, rezultatul trebuie să fie destul de simetric :

public class FunctieCollection : System.Collections.CollectionBase
{
public Functie this[int index]
{
get{ return (Functie) this.List[index];}
set{ this.List[index] = value;}
}

public int Add(Functie valoareNoua)
{
return this.List.Add(valoareNoua);
}

public void Remove(Functie valoareDeSters)
{
this.List.Remove(valoareDeSters);
}
}

Comparând cele două exemple, punctele comune şi diferenţele ies imediat în evidenţă:

public class xxxxCollection : System.Collections.CollectionBase
{
public xxxx this[int index]
{
get{ return (xxxx) this.List[index];}
set{ this.List[index] = value;}
}

public int Add(xxxx valoareNoua)
{
return this.List.Add(valoareNoua);
}

public void Remove(xxxx valoareDeSters)
{
this.List.Remove(valoareDeSters);
}
}

Prin urmare, template-ul nostru va trebui să aibă un parametru prin care să putem furniza tipul-ţintă pentru care se generează colecţia.
Pentru a scrie template-ul, deschideţi CodeSmith Explorer (găsiţi optiunea în menu-ul Tools din Visual Studio); selectaţi ca editor de template-uri Notepad-ul din directorul Windows


apoi creaţi un template nou în care scrieţi codul următor :

<%@ CodeTemplate Language="C#" TargetLanguage="C#" %>
<%@ Property Name="TipDeBaza" Type="System.String" %>
<%@ Property Name="NumeNamespace" Type="System.String" %>

namespace <%=NumeNamespace%>
{
public class <%=TipDeBaza%>Collection : System.Collections.CollectionBase
{
public <%=TipDeBaza%> this[int index]
{
get{ return (<%=TipDeBaza%>) this.List[index];}
set{ this.List[index] = value;}
}

public int Add(<%=TipDeBaza%> valoareNoua)
{
return this.List.Add(valoareNoua);
}

public void Remove(<%=TipDeBaza%> valoareDeSters)
{
this.List.Remove(valoareDeSters);
}
}
}
Salvaţi şi executaţi template-ul din CodeSmith Explorer, şi veţi obţine un property grid în care vi se cere să furnizaţi valori pentru cei doi parametrii declaraţi mai sus:




Un click pe butonul de Generate şi veţi obţine codul aşteptat. Schimbând valoarea parametrului TipDeBază veţi putea obţine codul de TypedCollection pentru orice tip de date de care aţi avea vreodată nevoie. Atenţie însă – codul rezultat este un cod demonstrativ, simplificat în mod special pentru acest articol (nu conţine o implementare completă a unei colecţii typed); între exemplele cu care se distribuie CodeSmith sau pe forumul de asistenţă veţi găsi câteva template-uri de colecţii tipizate mult mai elaborate.
Cu toate că sunt foarte utile astăzi, colecţiile tipizate generate cu CodeSmith îşi vor pierde probabil fanii odată cu introducerea Generics-urilor din C# 2.0 . CodeSmith nu se limitează însă la generarea colecţiilor tipizate ...
Folosirea property grid-ului pentru introducerea valorilor parametrilor este probabil una dintre cele mai inspirate decizii ale autorului, întrucât property grid-ul oferă un suport teribil pentru extensibilitate. Aşa se face că în template-urile CodeSmith putem declara şi edita parametrii de orice tip - fie el tip de bază, tip definit de utilizator sau colecţie. Practic, se utilizează aceeaşi infrastructură care permite invocarea în Visual Studio a unor editoare de proprietăţi precum cele de alegere a culorilor, de Dock şi Anchor sau de editare a colecţiilor. E suficientă decorarea unui tip de date cu atributul Editor pentru a specifica ecranul ce va fi deschis pentru alegerea valorilor proprietăţilor de acel tip.
Acest mecanism a fost folosit de autorul CodeSmith pentru a oferi suport pentru generarea codului pornind de la elemente din baze de date. În câteva assembly-uri auxialiare (SchemaExplorer.dll, SchemaExplorer.ADOXSchemaProvider.dll şi SchemaExplorer.SqlSchemaProvider.dll) sunt declarate clase care expun caracteristicile bazelor de date, ale tabelelelor, view-urilor sau procedurilor stocate. Toate aceste tipuri au asociate editoare care permit o selecţie foarte facilă a sursei lor. Spre exemplu, un template cu o proprietate de tipul SchemaExplorer.TableSchema va determina afişarea în property grid a unui butonaş cu trei puncte,


care permite deschiderea unui ecran de selecţie a bazei de date şi a tabelului sursă :




Aceste facilităţi ne vor fi foarte utile pentru problema noastră: va trebui să punem pe picioare un template care să ceară drept parametru o tabelă din baza de date şi să genereze câte un property get şi un property set pentru fiecare coloană a tabelei sursă. Ceva de genul

public class Salariat
{
private string _nume;
public string Nume
{
get{return _nume;}
set{_nume = value;}
}
private string _prenume;
public string Prenume
{
get{return _prenume;}
set{_prenume = value;}
}
private DateTime _dataNasterii;
public DateTime DataNasterii
{
get{return _dataNasterii;}
set{_dataNasterii = value;}
}
……….
}

Cel mai sensibil aspect e declararea parametrului de tip TableSchema; întrucât tipul SchemaExplorer.TableSchema este definit într-un assembly auxiliar (nu în core-ul generatorului de cod) va trebui să folosim directiva <%@Assembly %> pentru a face o referinţă la acest assemby :

<%@ CodeTemplate Language="C#" TargetLanguage="C#" %>

<%@ Assembly Name="SchemaExplorer" %>

<%@ Property Name="TabelaSursa" Type="SchemaExplorer.TableSchema" %>
<%@ Property Name="NumeNamespace" Type="System.String" %>

namespace <%=NumeNamespace%>
{
public class <%=TabelaSursa.Name%>
{
}
}

După cum se observă, am folosit expresia TabelaSursa.Name pentru a obţine numele tabelei ce a fost selectată ca parametru. Tipul proprietăţii TabelaDeBază - SchemaExplorer.TableSchema - oferă probabil prin proprietăţile sale orice informaţie de care aţi avea vreodată nevoie de la o tabelă, incluzând informaţii complete despre coloanele tabelei, despre cheia primară, despre indecşi, despre ralaţiile cu alte tabele sau despre proprietăţile ei extinse.
Putem folosi deci proprietatea TabelaSursa.Columns, de tipul ColumnSchemaCollection, pentru a enumera coloanele tabelei. Pentru fiecare coloană, proprietatea Type ne va oferi tipul de date nativ bazei de date (spre exemplu nvarchar) în timp ce proprietatea SystemType ne va oferi tipul echivalent in C# (string).

<%@ CodeTemplate Language="C#" TargetLanguage="C#" %>

<%@ Assembly Name="SchemaExplorer" %>

<%@ Property Name="TabelaSursa" Type="SchemaExplorer.TableSchema"%>
<%@ Property Name="NumeNamespace" Type="System.String" %>

namespace <%=NumeNamespace%>
{
public class <%=TabelaSursa.Name%>
{
<%
foreach (SchemaExplorer.ColumnSchema coloana in TabelaSursa.Columns)
{
%>

private <%=coloana.SystemType%> _<%=coloana.Name%>;
public <%=coloana.SystemType%> <%=coloana.Name%>
{
get{ return _<%=coloana.Name%>; }
set{ _<%=coloana.Name%> = value;}
}
<%
}
%>
} //de la numele clasei
} // de la namespace

In template-ul de mai sus, trebuie remarcat codul scris italic bold. Codul scris între tag-urile <% si %> nu va apare în cadrul rezultatului generării, ci va fi rulat la execuţia template-ului – în cazul de faţă determină repetarea secvenţei imbricate pentru fiecare coloană a tabelei.
Template-ul nostru este funcţional – şi generează deja câte o proprietate get/set pentru fiecare coloană din tabela furnizată ca parametru. E un punct de plecare care are încă nevoie de suficiente îmbunătăţiri; spre exemplu, am putea suprima generarea property set-ului pentru coloanele Identity, încadrând codul property set-ului într-un if:
<%
if ( (bool)coloana.ExtendedProperties["CS_IsIdentity"].Value != true)
{
%>
set{ _<%=coloana.Name%> = value;}
<%
}
%>

La fel de uşor, am putea să adăugăm la acest template şi scriptul de generare a colecţiilor tipizate de la începutul acestui articol. Pentru a păstra modularitatea, cel puţin în cazul template-urilor mai complexe, ar fi indicat să nu concatenăm cele două fişiere, ci să apelăm TypedCollectionsTemplate ca pe un subTemplate. În acest scop, vom scrie următorul cod:
<%
Response.WriteLine("// si acum, colectia typed");
CodeTemplateCompiler compiler = new
CodeTemplateCompiler(this.CodeTemplateInfo.DirectoryName +
"TypedCollectionTemplate.cst");
compiler.Compile();
CodeTemplate subtemplate = compiler.CreateInstance();
subtemplate.SetProperty("TipDeBaza", TabelaSursa.Name);
subtemplate.SetProperty("NumeNamespace",NumeNamespace);
subtemplate.Render(Response);
%>
Vom continua în episoadele urmâtoare să completăm acest template, în măsura în care dezvoltarea obiectelor noastre ne va cere noi facilităţi (spre exemplu, vom discuta despre suportul coloanelor ce permit null sau suportul pentru limitarea lungimii maxime a string-urilor în funcţie de dimensiunea specificată în baza de date).
Cu toate că posibilităţile de generare a codului cu CodeSmith sunt impresionante (nu am văzut deocamdată decât vârful iceberg-ului) e important să ţinem cont că nu ne-am propus să generăm integral clasele noastre de business cu CodeSmith. În primul rând, aceste clase nu sunt întotdeauna în relaţie 1-la-1 cu tabelele din baza de date; pe de altă parte, aceste clase vor trebui să implementeze regulile de validare sau de calcul specifice fiecărei entităţi, care nu pot fi deduse din schema bazei de date şi nici nu ar putea fi exprimate sub forma parametrilor. Rolul CodeSmith în generarea obiectelor de business se reduce la generarea unui cod funcţional, care să poată fi compilat şi testat imediat, dar care este doar un punct de plecare, un schelet pe care să-l putem modifica şi peste care să putem adauga manual codul propriu ori de câte ori regulile de business o cer.
Datorită flexibilităţii deosebite, CodeSmith poate fi o unealtă extrem de puternică, cu aplicaţii din cele mai diverse. Putem genera la fel de uşor orice fel de cod, fie el C#, VB.Net, JavaScript sau TSQL. (Spre exemplu, am putea face un template care să primească ca parametru o bază de date şi să genereze un script conţinând toate procedurile stocate de insert/update/delete pentru fiecare tabelă, sau triggere pentru un log al modificărilor asupra datelor din tabele.) Template-urile CodeSmith pot fi invocate şi cu un utilitar command line, din fisiere .bat, sau din IDE cu un CustomTool asemănător celui care generează TypedDataSet-uri din fişiere XSD. Vom reveni asupra integrării în IDE prin CustomTool în unul din episoadele următoare, în care vom genera un pseudo-DataLayer pentru a asigura funcţiile de persistenţă ale obiectelor noastre.