Aplicatia este www.scurt.ro si o gasiti la adresa www.scurt.ro. Este o aplicatie de shortening service ( tinyurl si bit.ly fiind unele cunoscute deja).
Documentatia aplicatiei o gasiti la adresa http://www.scurt.ro/Home/About .
Mai intii , ce vreau sa fac cu aceasta aplicatie: Vreau sa arat ca programarea este doar baza piramidei. Si ca munca pentru o aplicatie simpla este destul de mare, in plus mai trebuie tot felul de persoane - testare,raportare, administrator de BD, SEO specialist, marketing, vinzari si altii…
Aplicatia mai are citeva chestii de facut ( de ex., paginile pentru utilizatorii inregistrati, logare,erori , add-on de IE si Firefox, SEO, etc- le gasiti in documentul http://www.scurt.ro/Docs/aplicatia%20scurt.docx )
Pentru cei care ma ajuta nu pot sa le promit nimic – decit ca vor fi mentionati printre autori –si vor avea link pus pe o pagina care trebuie definita.
Cine vrea sa ma ajute, va rog sa cititi documentul, downloadati sursele – si vorbim pe email!
Multumesc,
Andrei
Daca va place ASP.NET MVC, as fi interesat sa imi dati feedback la aceasta mini-e-carte
Am pus-o in format Word si PDF – word daca vreti sa faceti modificari la ea, pdf daca nu aveti Word
Multumiri celor care mi-au dat feedback :
Bogdan Maxim,http://www.bogdanmaxim.ro/ , pentru ca mi-a aratat greseli legate de poze/formatare si mi-a facut sugestii legate substanta cartii .
Mecu Sorin AKA Yoda, http://ronua.ro/CS/members/yoda/default.aspx - pentru aditionale sugestii legate de subiect.
Stefan Pirvu AKA Strofo , http://sharp-monkey.blogspot.com/ - pentru sugestii legate de formatare si de substanta cartii.
Alin Berce, http://alinberce.wordpress.com/ - pentru sugestii legate de formularea problematicii.
Catalin Gheorghiu AKA Mr.Smersh, http://itboard.ro/blogs/catalins_blog/, pentru poze, cod si resurse.Precum si pentru o sugestie ( nefacuta) de a utiliza FxCop si a imi corecta codul ( rusine mie!)
Andrei Rinea, http://blog.andrei.rinea.ro/ , pentru detalii legate de carte.
Gabriel Enea , http://gabrielenea.blogspot.com/ , Senior Software Developer, fondator al serviciului joobs.ro, pentru suport, review si sugestii.
Aurelian Popa, http://aurelian.ro/dasBlogCE/ , pentru continut, formatare/ si sugestii si pentru Postfata .
Timotei Dolean, http://timoteidolean.wordpress.com/, pentru sugestii legate de continut si formatare.
Tudor Turcu, http://www.turcu.name/ , pentru sugestii legate de continut.
Sau, daca vreti tiparita(si vreti sa am si eu un ce profit), atunci puteti cumpara de la Amazon :
Romana : http://www.amazon.com/Asp-Net-Tips-Tricks-Romanian/dp/1449563562/ref=sr_1_1?ie=UTF8&qid=1260851152&sr=8-1
Engleza : http://www.amazon.com/Asp-NET-MVC-Tips-Tricks-programmer/dp/144992123X/ref=sr_1_1?ie=UTF8&qid=1260851053&sr=8-1
Stiu ca ma chinuisem pina sa gasesc un control de calendar pentru ASP.NET (RJS.POPCALENDAR) -si apoi deodata am data de ASP.NET MVC – si deodata nu functiona.
Asa ca solutia a fost sa caut cu jquery – si bineinteles exista www.jqueryui.com .
Ce aveti de facut daca aveti un textbox , de ex
<input type="text" id="FromDate" name="FromDate" value="<% =ViewData.Model.data.ToString("yyyy-MM-dd") %>" />
- si vreti un mic buton linga el care sa afiseze calendarul ? Nimic mai simplu . Downloadati de la www.jqueryui.com/download jquery-ui, adaugati la proiect in folderul scripts,puneti imaginea de buton (sa ii zicem calndar.jpg) si urmatorul cod :
$(document).ready(function() { //seteaza date picker var hid = ($("#FromDate").attr("type") == "hidden"); if (!hid)//daca nu e input type = hidden
{
$("#FromDate").datepicker({ showOn: 'button', buttonImage: '/content/images/calendar.jpg’, buttonImageOnly: true , changeMonth: true, changeYear: true , dateFormat: 'yy-mm-dd'//, numberOfMonths: 2 }); }
Am avut o problema aparent minora cu ASP.NET MVC . Aveam un path de forma /client/view//client/view/http://localhost/<numevirtualdir>/client/view/<nume client>
Foarte bine si frumos – dar stiati ca nu accepta caractere ciudate in path ( de exemplu, ampersant :, A&D Servicii SRL) . Eroarea este :
This error (HTTP 400 Bad Request) means that Internet Explorer was able to connect to the web server, but the webpage could not be found because of a problem with the address.
For more information about HTTP errors, see Help.
Am cochetat cu ideea sa schimb denumirea – si sa pun codul lor – dar supriza : codul era non-numeric : A&DS …
Am inlocuit, fara sa ma gindesc prea mult, & cu & - aceeasi eroare, normal!
In cele din urma, dupa cautari amarnice( 2 ore…) pe internet, am dat de un fisier .reg cu 2 rinduri – sper sa va fie de folos :
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET] "VerificationCompatibility"=dword:00000001
Pentru infovalutar (mai exact, pentru mine …) am vrut sa preiau licitatiile de la banci.
Pentru rezultatul final, vezi http://infovalutar.ro/licitatie
Dar sa vedem care a fost povestea :am inceput cu preluarea paginilor HTML . Incepusem cu HttpWebRequest - dar am descoperit la timp HtmlAgilityPack si am ramas credincios lui.
Acum, dupa preluarea paginilor HTML( de ex., http://www.banca-romaneasca.ro/main.php?did=527&code=executare+silita) a fost de ajuns un XPath + expresie regulata de parsare a text-ului din interior.
Ce mi-a produs batai de cap a fost http://vanzari.leumi.ro/bunuri_imobile.html – aveau bunurile in document Word! Ori, ca sa ii ceri celui de la Hosting sa instaleze Word-ul ca sa il instantiezi tu in ASP.NET e aproape imposibil!
Solutia : ASPOSE.WORDS - citeste documente dintre cele mai diverse si scoate un TXT superb – si asta, fara sa aiba nevoie de WORD instalat(se prea poate sa fi omorit muste cu tunul …)
Ca folosire, trebuia sa ii dau un Stream – dar cind am incercat sa ii dau stream-ul de document, mi-a zis ca nu suporta Seek. Asa incit am rezolvat cu un MemoryStream :
public string LeumiData(string URL) { byte[] buffer = new byte[1024*1024*4];
HttpWebRequest hwr = WebRequest.Create(URL) as HttpWebRequest; using (WebResponse response = hwr.GetResponse()) { using (Stream responseStream = response.GetResponseStream()) { using (MemoryStream memoryStream = new MemoryStream()) { int count = 0; do { count = responseStream.Read(buffer, 0, buffer.Length); memoryStream.Write(buffer, 0, count);
} while (count != 0);
//ASPOSE Document d = new Document(memoryStream); return d.ToTxt();
} } }
}
Pot sa spun ca ASPOSE, daca vreti manipulare de documente, face toti banii!
Destul de tirziu mi-am dat seama de beneficiile aduse de un Mock – dar mai bine mai tirziu decit niciodata …
Ma gindeam ca niciodata nu o sa il folosesc – ca ajunge sa verific BLL cu unit test(NUNIT/VS Test) , site-ul Web cu NUnit ASP/WATI(N|R) / Selenium , Windows Forms cu NUnitForms si nu o sa am nevoie de Mock.
Adevarul este ca da, nu as avea nevoie de Mock … decit daca as vrea sa verific mai repede unele date, fara sa ating BD.De exemplu, pot sa verific controller-ele fara sa am nevoie sa instantiez HttpContext si BD. Sa zicem ca am un controller care are o actiune ce doar exporta un fisier Word– bazat pe un template. Codul din fisier arata cam asa :
public ActionResult ExportDate(string id) {
IDateExport fe = ObjectFactory.GetInstance<IDateExport>(); fe.id = id;
export exp = new export(Server.MapPath("~/bin/Templates"));
FileContentResult fcr = new FileContentResult(exp.Export(fe), "application/ms-word"); fcr.FileDownloadName = fe.Number + ".doc";
return fcr;
Daca as vrea sa verific rapid metoda aceasta ar trebui sa nu ating baza de date si sa am pun un rezultat in loc de Server.MapPath ?
Se vede clar ca deja folosesc StructureMap, deci nu ar fi o problema cu gasitul a niste date fake. Dar pentru Server.MapPath intervine stralucit Mock.
Am preluat de la Hanselmann MVC Mock Helpers ,iar codul de test arata cam asa :
exportController i = new exportController(); MockRepository mocks = new MockRepository(); using (mocks.Record()) { //MvcMockHelpers.SetFakeControllerContext(mocks, i); mocks.SetFakeControllerContext(i); SetupResult.For(i.ControllerContext.HttpContext.Server.MapPath(null)).IgnoreArguments().Return(@"c:\programs\templates"); // cod pentru chemarea Server.MapPath mocks.ReplayAll(); } using (mocks.Playback()) { ObjectFactory.Initialize(x => { x.ForRequestedType<IExport>().TheDefaultIsConcreteType<FactFind>();// FactFind nu atinge BD }); FileContentResult fcr = i.exportdate("865", "A") as FileContentResult; fcr.ShouldNotBeNull(); fcr.FileContents.Length.ShouldBeGreaterThan(0);//TODO : Verifica si continutul }
Voi ce alte frameworkuri/tools-uri folositi ?
Am avut de facut o aplicatie cu ASP.NET MVC - si bineinteles, cu un grid ce lista niste vinzari si cu total.
Asta e ceea ce mi-a pus probleme : totalul … Asa incit, dupa ce am studiat grid-ul de la ASP.NET MVC Contrib , a trebuit sa il extind. Si nu a fost foarte greu. Mai intii , practica - sintaxa e asemanatoare, doar ca adaug o functie pentru footer :
<% =Html.HtmlGridFooter<Lines>(ViewData.Model.Lines, (x) => { decimal total = 0; foreach (var s in x) { total += s.Total; }; return "<tr><td colspan=1 align=right>Total</td><td colspan=1>"+ total.ToString("#.00")+"</td></tr>"; }
) si de aici incepe grid-ul obisnuit.
Teoria : a trebuit sa extind HtmlTableGridRenderer si sa il atasez de grid …Probabil ca o sa fie nevoie sa am , odata, doi grid renderer?
Cod :
public class HtmlGridFooter<T> : Grid<T> where T : class { public HtmlGridFooter(IEnumerable<T> dataSource, TextWriter writer, ViewContext context) : base(dataSource, writer, context) { } public Func<IEnumerable<T>, string> footer { set { this.RenderUsing(new FooterHtmlGrid<T>() { actions = base.DataSource, footer = value }); } }
} public class FooterHtmlGrid<T> : HtmlTableGridRenderer<T> where T : class { public IEnumerable<T> actions; public Func<IEnumerable<T>,string > footer; protected override void RenderGridEnd(bool isEmpty) { if (footer != null) { base.RenderText(footer(actions)); } base.RenderGridEnd(isEmpty); } }
si intr-o clasa statica ar trebui pus:
public static IGrid<T> HtmlGridFooter<T>(this HtmlHelper helper, IEnumerable<T> dataSource, Func<IEnumerable<T>, string> footer) where T : class { HtmlGridFooter<T> g = new HtmlGridFooter<T>(dataSource, helper.ViewContext.HttpContext.Response.Output, helper.ViewContext); g.footer = footer; return g; }
Ok, titlul e cam naspa . Sa incerc sa o iau altfel : Orice site de informatii are un script js prin care alte site-uri preiau informatia(bineinteles, este o chestie de reclama)
Si iar revin la marota mea,www.infovalutar.ro, care avea preluare de curs prin PHP,Java, .NET, Python – dar nu avea prin Jscript (ceea ce majoritatea competitorilor aveau ) . Asa ca am fost fortat sa ma gindesc
OK, imi trebuie un js care sa fie interpretat O chestie simpla era sa fac un js care sa se interpreteze pe server – dar nu aveam chef:
E clar ca problema ar trebui inversata – si anume, avut “ceva” care se interpreteaza pe server care sa intoarca rezultatul . Clara solutia acum : un generic handler, de tipul ashx, care intoarce document.write(“text”);
Doua sfaturi:
Referitor la 2, eu am obtinut 2 clase:
clsTable – care preia niste stari de tipul cellpadding,backgroundcolor, tdbgcolor etc – astfel incit fiecare sa poa sa isi preia informatia avind culorile/fontul personalizat
clsConnect :ConfigurationSection : care intoarce datele din BD . E deriata din ConfigurationSection astfel incit sa o pun in web.config(app.config) si sa uit de ea.
Demo gasiti la http://infovalutar.ro/webmaster.aspx . Daca cineva poate crede ca este util, voi posta si sursele
Disclaimer : Acest post se refera la un website obisnuit, la care nu ai acces la configurarea de pe server(de ex., nu ai acces la IIS Compression) si trebuie sa ti le faci singur
Marota mea favorita, www.infovalutar.ro, se incarca destul de greu prima oara. Asa incit a trebuit sa ii fac o optimizare. Site-ul fiind facut cu ASP.NET MVC, prima optimizare chioara am facut-o cu [OutputCache] pe controller. Asta cacheuia si pagina, si datele …tot. Ca sa vad cum cachuieste, am gasit un Html.Substitution cu care pot sa afisez chestii ne=cache-uite (modificat un pic, ca poate am nevoie sa scriu stringul EXACT asa cum este …):
public static object Substitute(this HtmlHelper html, MvcCacheCallback cb, bool Encode) { if (Encode) { html.ViewContext.HttpContext.Response.WriteSubstitution( c => HttpUtility.HtmlEncode( cb(new HttpContextWrapper(c)) ));
} else { html.ViewContext.HttpContext.Response.WriteSubstitution( c => ( cb(new HttpContextWrapper(c)) ));
} return null; }
OK, apoi am inceput sa ma gindesc la optimizari … Mai intii imaginile, apoi css-urile + jscript, apoi redirect-urile . Asa ca am zis ca cel mai bine este sa incerc cu un tool – si primul ales este Fiddler. Asa ca am vazut redirecturi (301)la greu .De ce le aveam ? pentru ca aveam probleme la controlere pentru uppercase/lowercase la argumente si m-am decis ca toate sa fie cu lowercase , asa incit pusesem in global.asax
protected void Application_BeginRequest(Object sender, EventArgs e) { // If upper case letters are found in the URL, redirect to lower case URL. if (Regex.IsMatch(HttpContext.Current.Request.Url.ToString(), @"[A-Z]") == true) { string LowercaseURL = HttpContext.Current.Request.Url.ToString().ToLower();
Response.Clear(); Response.Status = "301 Moved Permanently"; Response.AddHeader("Location", LowercaseURL); Response.End(); } }
Asa ca am intervenit in site si am schimbat ca toate sa fie cu litere mici (un .ToLower() la linkuri a ajuns)
Dar am revenit la problema initiala: mai aveam nevoie sa fie cache-uite imaginile,css-urile si jscripturile. E adevarat, ma puteam uita in Fiddler(click pe fisier=>inspectors )
sa vad care e cache-uita, care e zip-uita – dar aveam nevoie de un tool general. Asa ca , pentru mine , combinatia ideala a ajuns : Firefox + Firebug + Yahoo Slow + Google Page Speed. OK,daca vi le-ati instalat pe toate atunci puteti incepe cu yahoo Slow – si, ca sa ma dau bun iata cum arata la mine:
Va rog sa observati ca am Grade A pentru “Small Site or Blog” . OK, ceea ce recomanda Yahoo Slow si nu faceam era:
1 si 2 sunt relativ usor – mai modifica site master-ul si gata.
3.Cum facem Compress components with Gzip ? Am cautat rapid – si am gasit, in final, asta GZip and Deflate Compression Filter for ASP.Net MVC . Am pus-o, mergea bine pe masina mea, dar cum am pus-o in productie , cum a dat eroarea asta . Asa ca a trebuit sa renunt la OutputCache – si sa fac caching pe ASP.NET Cache – dar asta doar la datele din BD .Acum mergea perfect! Dovada : nu a mai aparut pagina la Yahoo Slow - iar , inspectata cu fiddler => inspector , a aparut Gzip.
4.OK, acum era problema de optimizare a imaginilor – mai exact, vroiam ca imaginile sa ramine in cache-ul browser-ului, astfel incit sa nu se mai downloadeze inca o data. Pentru asta trebuia sa setez expiration la image -dar cum, daca nu am acces la server ? Ca de obicei, se rezolva cu o indirectare: Sa zicem ca imaginile sunt in folder-ul flags. Atunci il pus sa treaca printr-un controller:
public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute("flags", "flags/{id}.ashx", new { controller = "imagini", action = "flags" });
//alte date
Iar fiecare imagine se va prelungi cu un ashx ( adica .gif.ashx) :
<img width="30" height="20" src="/<% ="flags/" + curLoop.IDMoneda.ToLower() + ".gif.ashx"%>" />
In controller-ul respectiv pe actiunea flags, redam imaginea :
Response.Cache.SetExpires(DateTime.Now.AddDays(300));// Response.Cache.SetCacheability(HttpCacheability.Public); Response.Cache.SetValidUntilExpires(false); Response.AddHeader("content-disposition", "inline; filename=" + filename);
Response.WriteFile(file); OK, acum iarasi Fiddler si presto : imaginile erau cache-uite aproximativ 1 an. Dar cum sa fac cu Jscriptul ca sa fie si compresat si cache-uit ? Pai – acelasi lucru – sa treaca printr-un Controller si sa ii aplic Gzip prin clasa CompressFilter. Aici am avut cea mai mare problema : de acasa imi dadea ca js e compresat – la servici ca nu. Innebunisem – si totul s-a rezolvat cu //proxy servers in between may cache Response.AppendHeader("Vary", "Accept-Encoding"); Bun, deja ma miscam mai usor in sensul ca pagina, de la citiva bun kb ajunsese la a doua iteratie la < 10 (ma rog, cu reclame < 15) Daca apasati pe Statistics o sa vedeti 2 grafice : una de la prima incarcare si una de la a doua incarcare (in care nu se mai incarca cache-ul de la prima)– Daca numarul de Http Requests si numarul de Kb este acelasi – atunci aveti o problema. La mine este 48 / 6 Requests – cu 84 / 13 Kb. Daca ati facut pina aici - e super! Acum sa mai aplicam doar Google Page Speed – acesta este doar cireasa de pe tort Cu el am mai optimizat din imagini , m-am uitat ca puteam optimiza prin minify “There is 173.8kB worth of JavaScript. Minifying could save 4.7kB (2.7% reduction).” – ma rog, doar pentru primii veniti – ceilalti au oricum cache-ul… ca ar trebui sa fac “This page makes 43 parallelizable requests to infovalutar.ro. Increase download parallelization by distributing these requests across multiple hostnames:” – mda, si de unde bani ? Si yahoo vorbeste de CDN-uri
Response.WriteFile(file);
OK, acum iarasi Fiddler si presto : imaginile erau cache-uite aproximativ 1 an.
Dar cum sa fac cu Jscriptul ca sa fie si compresat si cache-uit ? Pai – acelasi lucru – sa treaca printr-un Controller si sa ii aplic Gzip prin clasa CompressFilter.
Aici am avut cea mai mare problema : de acasa imi dadea ca js e compresat – la servici ca nu. Innebunisem – si totul s-a rezolvat cu
//proxy servers in between may cache Response.AppendHeader("Vary", "Accept-Encoding");
Bun, deja ma miscam mai usor in sensul ca pagina, de la citiva bun kb ajunsese la a doua iteratie la < 10 (ma rog, cu reclame < 15)
Daca apasati pe Statistics o sa vedeti 2 grafice : una de la prima incarcare si una de la a doua incarcare (in care nu se mai incarca cache-ul de la prima)– Daca numarul de Http Requests si numarul de Kb este acelasi – atunci aveti o problema. La mine este 48 / 6 Requests – cu 84 / 13 Kb.
Daca ati facut pina aici - e super! Acum sa mai aplicam doar Google Page Speed – acesta este doar cireasa de pe tort
Cu el am mai optimizat din imagini , m-am uitat ca
OK, ultimul tool de care voiam sa va vorbesc este Search Engine Optimization Toolkit. Este o scula extraordinara pentru incepatorii ca mine, se integreaza de minune in VISTA . Il rulati odata pe site-ul vostru(din IIS,vedeti Scott http://weblogs.asp.net/scottgu/archive/2009/06/03/iis-search-engine-optimization-toolkit.aspx ) , puteti creea robots.txt, sitemap si vedea multe altele. Oricum, postul lui Scott http://weblogs.asp.net/scottgu/archive/2009/06/03/iis-search-engine-optimization-toolkit.aspx spune tot – si face analiza chiar pe site-ul propriu! Nu spun pe www.infovalutar.ro cit mi-a gasit!
Oricum, daca faceti site-uri, e bine de avut in trusa de dezvoltator!
Nu eram multumit de vechiul sitemap de la ASP.NET – din singurul motiv ca nu e dinamic - adica nu tine seama ca poate exista Categorie\Produs\{id}\edit.
Ca omul puturos , am cautat pe google si am gasit pe codeplex http://mvcsitemap.codeplex.com/
Citeva observatii :
Succes!
Facusem un site care incarca , dintr-o BD , prin EntityFramework si Linq2Sql date dintr-o BD.Problema era ca dura cel putin 30 de secunde.
Ma uit in pagina – nu era de acolo – era aproape goala.
Ma uit la Sql-urile generate de EF si L2S -se executau in milisecunde.
Ma uit cu NUnit sa vad in cit timp se incarca ToList<> – milisecunde.
In disperare , pun Fiddler si browsez http://<numemasina>/aplicatie ( nu folositi localhost, ci numele masinii) – si - surpriza : imi arata , cu rosu, ca declaram un serviciu Ajax in ScriptManager –serviciu care nu exista , codul fiind copiat din alta aplicatie de a mea…
Iar ScriptManager chiar incerca sa vada daca serviciul exista – chiar daca NU il foloseam!
Si , pina sa isi dea seama, dura alea 30 de secunde…
Oh si apropo :in Application_OnError aveam logata eroarea cu log4net - daca ma uitam in loguri eram destept
Concluzie : nu puneti in ScriptManager servicii decit daca e necesar…
Zilele(noptile?) acestea, cum am spus de atitea ori, am trecut pet-project, www.infovalutar.ro la ASP.NET MVC.
Si ma gindeam cum sa ii imbunatatesc timpul de afisare. OK, yahoo slow pentru inceput imi spunea ca nu am expires header la imagini. Stiam ca exista in IIS -dar cum le setez la hosting? Dau telefon, amabil, ii spun unde sa faca – dar … ceva nu a mers. Si au ramas ca la inceput.
Asa incit le-am mapat o extensie proprie, ashx, si le-am pus sa fie interceptate de un handler si le-am setat cam ca aici
OK –teoretic asta cam ar trebui sa o faca oricine.
Ce parere aveti de un mini-site de best practices , gen wiki, pe care sa il tinem updatat la zi cu ce intilnim noi ca developeri ?
A fost primul live meeting al meu - am vorbit despre ASP.NET MVC. A fost destul de ciudat sa vorbesc la pereti si sa nu aud pe nimeni … dar sper ca a fost ok dupa citeva aaa-uri. Prezentarea a durat 50 de minute si am vorbit despre:
Multumesc lui Petru pentru oportunitate si sprijin tehnic/material/anunt
Inregistrarea o gasiti la http://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032408740&EventCategory=5&culture=ro-RO&CountryCode=RO iar prezentarea o gasiti aici http://serviciipeweb.ro/iafblog/content/binary/ASPNETMVC.pdf.zip
Va astept probleme / sugestii / reclamatii / comentarii …
(Oh – si asta e un motiv pentru care tutorialul de .NET 3.5 nu a mai continuat … dar promit sa continui - dupa ce imi trag rasuflarea…)
Dupa cum a scris Petru aici si aici , o sa tin o prezentare LiveMeeting ( trebuie sa il instalati!) despre marota mea – www.infovalutar.ro portat la ASP.NET MVC
O sa aiba tips and tricks si problemele uzuale de care m-am lovit.
Va astept aici http://msevents.microsoft.com/CUI/WebCastEventDetails.aspx?EventID=1032408739&EventCategory=4&culture=ro-RO&CountryCode=RO
Aseara am inceput sa trec infovalutar pe Asp.NET MVC.Si aveam 2 probleme :
1. degeaba ii dadeam Home/Index/3 ca in controller-ul
public class HomeController : Controller
public ActionResult Index(string Banca)
ViewData.Model = new CurrencyList();
ViewData["Bank"] = Banca;
return View("Index");
Nu vroia sa imi ia id-ul(parametrul Banca era null) si gata, indiferent cum ii dadeam eu Home/Index/BNR sau Home sau orice altceva –desi trecea prin procedura.
M-am gindit sa dau vina pe Asp.NET MVC, dar , fiind un framework folosit de atitia, nu ma gindeam ca tocmai eu am un caz deosebit …Si citisem ca pe IIS integrated NU trebuie sa ii faci modificari …
2. Cind incerca sa se conecteze la Sql Server, imi dadea “login failed for user …”
Am incercat sa refac login-ul , sa schimb parola, ce nu am incercat …
Asa ca m-am dus la culcare si am revenit cu sentimente mai bune de dimineata.
Rezolvari :
1. M-am dus sa ma uit cum e inregistrat routing-ul in global.asax.Cum sa fie , obisnuit:
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = "BNR" } // Parameter defaults );
Si mi-a cazut fisa : parametrul de la functia index nu se cheama cum vrea el, ci cum vrea MVC – adica id
public ActionResult Index(string id) fata de public ActionResult Index(string Banca) Naspa! Mi-aduce aminte de Java, cind numele clasei = numele fisierului !
2. Am incercat , in disperare , sa ma conectez si de pe Sql Server Management Console – acelasi mesaj : “Login Failed” . In disperare, ma conectez cu credentialele de Windows si ma uit in log-urile de la SQL Server – Management/Sql Server Logs - si acolo mi-a zis ca nu se poate ca nu este configurat in mixed mode …
Ce sa mai zic ? Schimb, dau restart la Sql, merge!
Sfat catre mine : Daca nu iti iese , du-te si te culca sau apuca-te de altceva. Revii cu mintea odihnita dupa aceea!
Dintotdeauna mi-am dorit asa ceva - multiple site-uri, nu surogatul de Virtual Directory . Si iata ca se poate usor pe vista cu iis 7.
Cum ? simplu
Start=> Control Panel, Clasic View, Administrative Tools=> Internet Information Services (IIS) Manager . Expandati, ajungeti la site, puneti sitename si nu uitati Host Name( sa zicem test)
Acum porniti Notepad cu drepturi de administrator, ajungeti in %SystemRoot%\system32\drivers\etc , editati fisierul host si mai adaugati o inregistrare:
127.0.0.1 test
Si acum va merge http://test …In sfirsit!
Pentru ca la nu imi mergea Reporting Services( un IIS pe o masina cu 64 de biti, cu Sql pe 32 si cu Web Extensions de aspnet exe pe 32 de biti disabled, ca altfel nu merge alt site) a trebuit sa fac un raport cu numere. Si, pentru ca un grafic spune cit 100 de cuvinte, am zis sa le fac si citeva grafice. Si am fost bucuros sa dau o sansa la noile controale de chart. In 15 minute am reusit sa fac ceva ok –sunt super
Citeva caveats, totusi :
Download the free Microsoft Chart Controls
Download the VS 2008 Tool Support for the Chart Controls
Download the Microsoft Chart Controls Samples
Download the Microsoft Chart Controls Documentation
SI mai ales Download the Microsoft Chart Controls Samples – si rasfoiti exemplele.
<add key="ChartImageHandler" value="storage=file;timeout=20;dir=x:\inetpub\etc\log;" />
Acolo x:\inetpub\etc\log trebuie sa fie calea catre un director in care IIS sa aiba dreptul de scriere….
System.Web.DataVisualization.dll System.Web.DataVisualization.Design.dll
( folositi subst X %windir%\assembly – si copiati-le din X)
Daca vreti poze frumoase , gasiti la Scott . Oricum, sunt foarte bune – si mai bune decit vechiul Chart Control din VB6…Folositi cu incredere!
Pe scurt : foloseste 2 browsere.
Pe lung : e adevarat ca ar trebui facute teste separate pentru interfata – dar , de obicei, eu le fac in BL iar interfata o testez "manual". Si , avind in vedere drepturile, exista cel putin 2 user-i: (l)user-ul simplu si admin-ul.
Daca e vorba de un site cu autentificare prin usernmae si parola, e destul de simplu – poti face log-off si logon din nou ca celalalt user.
Dar pentru un site cu autentifcare Windows(Active Directory) e un pic mai complicat. Internet Explorer te autentifica automat ca user-ul care esti logat. Doar browser-ul de Firefox te ajuta si iti cere Username+parola. Asa incit , daca le folosesti pe amindoua, si ai si un alt cont de user, poti vedea cum se comporta site-ul tau pe 2 user-i diferiti. Si, avind in vedere ca cookie-urile sunt diferite, e bun si pentru autentificarea forms.
Ca pont: chiar daca esti ispitit sa "ascunzi" niste optiuni in interfata grafica pentru un user mic, nu o fa de la inceput. Lasa-le acolo, pune cod in BL in care arunci eroare ca nu are dreptul, fa un unit test si verifica ca prinzi eroarea. Verifica si in GUI si apoi ascunde-le(visible sau enabled=false)
Am avut de facut , de multe ori, site-uri care cuprindeau pagini de genul : Lista, Editare, Editare proprietati ( de ex., Lista User ( cu cautare + sortare), Editare User, Editare Proprietati UserCurent).Problema intervine cind vrei sa ii lasi si un site map – si ai urmatoarea structura : Lista User-i => Editare User => Editare proprietati user si , dupa ce user-ul este pe pagina de editare proprietati, revine la Editare User - de unde stii ce user ai editat ? De obicei pasezi id-ul in query string ...dar cum il pui in sitemap ?
Solutia rapida pe care o gasisem(nu foarte corecta, dar rapida ) a fost sa tin in Session ultimul user care este editat.Nu prea mi-a placut – mult de scris, de repetat.
Ma uitasem si la pagina aceasta, de modificat nodurile in memorie, dar iar nu mi-a placut.
Simteam ca trebuie sa fie ceva in XMLProvider ... dar nu reuseam sa ii dau de cap.
Dupa mult timp, am dat de pagina asta
http://www.csharper.net/blog/custom_sitemapprovider_incorporates_querystring_reliance.aspx
care mi-a rezolvat problema, prin mostenire de la XMLSiteMapProvider.
Ce trebuie sa faceti :
Sau de aici
public class SmartSiteMapProvider : XmlSiteMapProvider
public override void Initialize(string name, NameValueCollection attributes)
base.Initialize(name, attributes);
this.SiteMapResolve += new SiteMapResolveEventHandler(SmartSiteMapProvider_SiteMapResolve);
SiteMapNode SmartSiteMapProvider_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
if(SiteMap.CurrentNode == null)
return null;
SiteMapNode temp;
temp = SiteMap.CurrentNode.Clone(true);
Uri u = new Uri(e.Context.Request.Url.ToString());
SiteMapNode tempNode = temp;
while(tempNode != null)
string qs = GetReliance(tempNode, e.Context);
if(qs != null)
if(tempNode != null)
tempNode.Url += qs;
tempNode = tempNode.ParentNode;
return temp;
private string GetReliance(SiteMapNode node, HttpContext context)
//Check to see if the node supports reliance
if(node["reliantOn"] == null)
NameValueCollection values = new NameValueCollection();
string[] vars = node["reliantOn"].Split(",".ToCharArray());
foreach(string s in vars)
string var = s.Trim();
//Make sure the var exists in the querystring
if(context.Request.QueryString[var] == null)
continue;
values.Add(var, context.Request.QueryString[var]);
if(values.Count == 0)
return NameValueCollectionToString(values);
private string NameValueCollectionToString(NameValueCollection col)
string[] parts = new string[col.Count];
string[] keys = col.AllKeys;
for(int i = 0; i < keys.Length; i++)
parts[i] = keys[i] + "=" + col[keys[i]];
string url = "?" + String.Join("&", parts);
return url;
(inca o data: nu e codul meu, ci al lui C# Shiznit,http://www.csharper.net/blog/custom_sitemapprovider_incorporates_querystring_reliance.aspx)
<siteMap defaultProvider="SmartSiteMapProvider" enabled="true"> <providers> <clear /> <add name="SmartSiteMapProvider" type="SmartSiteMapProvider" siteMapFile="web.sitemap" securityTrimmingEnabled="true" /> </providers> </siteMap>
<siteMapNode url="~/EditUser.aspx" title="Editare utilizator" reliantOn="UserID" />
Ce mai poate fi facut ? Poate sa ia nu doar din QueryString, ci si din Form.
In sfirsit am avut ocazia sa ma joc cu LINQ de adevaratelea(cam tirziu, avind in vedere ca a aparut Visual Studio 2008 SP1 si The .NET Framework 3.5 SP1 cu DataEntity si Astoria). Si prima chestie mare pe care a trebuit sa o fac a fost un gridview cu paginare, sortare si filtrare.
Imi generasem clasele Linq intr-un dll – si am zis ca pun un LinqDataSource. Cind citesc documentatia , gasesc TableName – si mi-am zis: "UPS... nu vreau sa ma conectez la tabela, ci la datele mele"
Asa incit imi zic – ObjectDataSource e ceea ce vreau.
Il configurez – punindu-i metodele pe care le vroia – cea cu selectcount si cea cu paginare - si mergea OK. In SQL Server Profiler aparea Ok... Si acum filtrarea pe server . Aici au inceput problemele: Nu mai voia sa afiseze paginile (1, 2 ,etc), desi selectcount-ul il facea pe server...
Ca solutie intermediara am zis sa aduca toate datele daca face filtrare... Dar nu mi-a placut prea mult...si in plus nu mai genera evenimente de rowcommand...
Asa ca intreb si eu ca incepatorul pe Ronua si am noroc :sirocco imi raspunde ca TableName e de fapt numele clasei ... Chiar asa este!Merge paginarea si sortarea rapid si fara probleme...
Iar la filtrare e suficient sa puneti un parametru la WhereParameters si sa puneti clauza Where cum trebuie .
Ma rog, am mai dat de o problema : Am filtrat dupa stringuri - merge NumeProprietate.Contains(@numeparametru).
Problema este ca ,daca nume parametru este gol, va da eroarea
No applicable method 'Contains' exists in type 'String'
Nu va speriati – eroare spune o prostie ... puneti la parametru ConvertEmptyStringToNull="false"
Incercasem sa interceptez erorile din Master page – si nu intelegeam de ce nu merge. Pina mi-am adus aminte ca Master deriva din control – si ca, de fapt, este un control instantiat de pagina, nu invers…
Totusi, o functionalitate buna a Master este cea de afisare de erori la nivel central ( ca si cum ai avea un control de afisare erori).In App_Code se adauga un fisier .cs de forma
public class ErrorAdd : IValidator
public ErrorAdd(string Message)
ErrorMessage = Message;
public string ErrorMessage { get; set; }
public bool IsValid
get
return false;
set
public void Validate()
(clar invalid...)
Apoi in Master adaugam un control ValidationSummary si urmatorul cod :
public void AddTheError(string Message)
ErrorAdd e = new ErrorAdd(Message);
this.Page.Validators.Add(e);
Cam de atit e nevoie … La orice cod de pe pagina – de pilda, pe evenimente, scrieti
try
{//cod
catch (Exception ex)
this.Master. AddTheError(ex.Message);
Singura problema pe care o vad este sa folosesti acelasi master pentru mai multe functionalitati ...Nu vrei un God Object, nu-i asa?
Imi place din ce mai mult XP - si incep sa ma rog pentru verde...Iar VS e o adevarat bomboana pentru asta ...Cind genereaza method stub( scriind , de ex., instanta.Metoda ... si geenrind codul pentru metoda inexistenta ... apasati pe coltul de la cuvintul Metoda ) scrie codul punind in corp
throw new NotImplementedException();
Este super ... apare rosu ... si pe urma apare faimosul verde ...
The site map is relatively easy:
Add a new item – find “Site Map” and accept the default name (Web.sitemap)
And put the following
<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode url="default.aspx" title="Main" description="First Page">
<siteMapNode url="frmPublisherList.aspx" title="All publishers" description="Publishers list" >
<siteMapNode url="frmPublisher_Insert.aspx" title="New Publisher" description="Add new"></siteMapNode>
<siteMapNode url="frmPublisher_Edit.aspx" title="Edit Publisher" description="Edit"></siteMapNode>
<siteMapNode url="frmPublisher_Delete.aspx" title="Delete Publisher" description="Delete"></siteMapNode>
</siteMapNode>
<siteMapNode url="frmBookList.aspx" title="All books" description="Book list" >
</siteMap>
( the names are pretty suggestive – url, title and description)
Now it’s time to put to work :
Open Book.master , and put a site map control ( find into the navigation tab on toolbox) before content place holder:
<asp:SiteMapPath ID="SiteMapPath1" runat="server" Font-Names="Verdana" Font-Size="0.8em" PathSeparator=" : ">
<PathSeparatorStyle Font-Bold="True" ForeColor="#990000" />
<CurrentNodeStyle ForeColor="#333333" />
<NodeStyle Font-Bold="True" ForeColor="#990000" />
<RootNodeStyle Font-Bold="True" ForeColor="#FF8000" />
</asp:SiteMapPath>
And put a tree view instead of right menu:
<asp:TreeView ID="TreeView1" runat="server" DataSourceID="SiteMapDataSource1" MaxDataBindDepth="1">
</asp:TreeView>
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" />
Now, if you run the project, and press new button, you will see the following
Sure that all books it is not implemented yet – but it is your task to do it.
Now we will proceed to the localization part. We want to be able that people see the content in English and French.
We will localize just one form, and we left the others as an exercise to the reader.
The setting of language will be set in a cookie on the user’s PC and will be read each time.
Add a drop down list to the master page, near Book application with the following code:
<asp:DropDownList runat="server" id="ddlLanguage" OnSelectedIndexChanged="ddlLanguage_SelectedIndexChanged" AutoPostBack="true">
<asp:ListItem Text="English" Value="en">
</asp:ListItem>
<asp:ListItem Text="French" Value="fr">
</asp:DropDownList>
On the .cs page, let’s store the actual configuration :
protected void ddlLanguage_SelectedIndexChanged(object sender, EventArgs e)
HttpCookie cookie = Request.Cookies["Language"];
cookie.Value = ddlLanguage.SelectedValue;
Response.AppendCookie(cookie);
cookie.Expires = System.DateTime.Now.AddYears(1);
Response.Redirect(Request.Url.LocalPath);
So we have saved the value ... now, let’s retrieve it:
protected void Page_Load(object sender, EventArgs e)
if (!IsPostBack)
ChangeLanguage();
private void ChangeLanguage()
if (cookie == null)
//set default the cookie in web.config
string s = Thread.CurrentThread.CurrentUICulture.Name;
cookie = new HttpCookie("Language");
cookie.Value = s;
foreach (ListItem li in ddlLanguage.Items)
if (li.Value == cookie.Value)
li.Selected = true;
break;
Now we must change the language : We can put this on each page, overriding InitializeCulture , or put in a global.asax file( that retains the application events) on Application_BeginRequest: ( new item => Global Application Class)
protected void Application_BeginRequest(object sender, EventArgs e)
string lang = System.Threading.Thread.CurrentThread.CurrentUICulture.Name;
if (cookie != null && cookie.Value != null)
lang = cookie.Value;
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(lang);
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture(lang);
It is time now to proceed to the localization
Add an Asp.NET folder, named “App_LocalResources”
And in this folder, add three resource files, named :
frmPublisherList.aspx.en.resx
frmPublisherList.aspx.fr.resx
frmPublisherList.aspx.resx
(The file name is compose by the name of the aspx file + (optional) language + .resx )
In these files we will add just one string for the Text property of the button that is new, like in the figure:
Now, we put meta:resourcekey="btnNew" on the button:
<asp:Button ID="btnNew" runat="server" Text="New" OnClick="btnNew_Click" meta:resourcekey="btnNew"/>
And we will see in this mode the translation by changing from English to French in the combo.
Attention: if you do not have the invariant culture file ( the one without language in the name) it does not work!
If you have several items that are invariant ( like the “save” button) you can add resources to the special folder App_GlobalResources and add there resx files ( that now can be named as you want to ) As example suppose we have now in the App_GlobalResources the files
Buttons.en.resx
Buttons.fr.resx
Buttons.resx
And one resource named
btnSaveText
We can acces as so :
<%$ Resources:Buttons,btnSaveText%>
<asp:Button ID="btnSave" Text="<%$ Resources:Buttons,btnSaveText%>" runat="server" OnClick="btnSave_Click" />
Or , programatically, by writing :
Resources.Buttons.btnSaveText
Next time we will look at making a DOS project for the same application
Items to read:
Localization: http://quickstarts.asp.net/QuickStartv20/aspnet/doc/localization/localization.aspx
Master Pages:
http://quickstarts.asp.net/QuickStartv20/aspnet/doc/masterpages/default.aspx
Now we will edit the Publisher objects .
Add a new WebForm , name it frmPublisher_Insert.aspx and make sure the “Place code in separate file” and “Select master page” are both selected by default.
Change in source view the title from “Untitled Page” to “Insert Publisher”
Now we must put controls in place to insert publishers
There must be the name and site of the publisher.
I prefer enter a table for this and the page will look like this:
<%@ Page Language="C#" MasterPageFile="~/Book.master" AutoEventWireup="true" CodeFile="frmPublisher_Insert.aspx.cs" Inherits="frmPublisher_Insert" Title="Insert Publisher" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<table>
<tr>
<td colspan="2">Enter values
</td>
</tr>
<td>
Name
<asp:TextBox ID="txtName" runat="server">
</asp:TextBox>
Site
<asp:TextBox ID="txtSite" runat="server">
<td><asp:Button ID="btnSave" Text="Insert" runat="server" />
<td><asp:Button ID="btnCancel" Text="Cancel" runat="server" />
</table>
</asp:Content>
Now switch to design view and double click on Insert button in order to generate Click event. Double click in solution explorer the frmPublisher_Insert.aspx and , in Design view, double click on Cancel button in order to generate Click event.
For cancel it is clear what we must do – redirect to the frmPublisherList.aspx
Response.Redirect("frmPublisherList.aspx", false);
For save button, we must create a new publisher and save
Publisher p = new Publisher();
p.Name = txtName.Text;
p.Site = txtSite.Text;
p.Insert();
Please try it by setting the frmPublisher_Insert.aspx as start page and run the project (F5)
If all works well (please ensure that Insert has a call to Save()) you will see in the frmPublisherList.aspx the item you just selected.
It is clear that frmPublisherList.aspx has a need for a new button . Let’s put it
<asp:Button ID="btnNew" runat="server" Text="New" OnClick="btnNew_Click" />
And on code:
protected void btnNew_Click(object sender, EventArgs e)
Response.Redirect("frmPublisher_Insert.aspx", false);
That will be ok for adding a new publisher.
Now for editing and deleting we can make on list… but I prefer having 2 new pages.
So modify a little bit the code on the grid, in order to have the edit and delete operations : the edit will be a link , and the delete will be a button to see how different is the model on the two implementations.
The list page now looks like this:
<%@ Page Language="C#" MasterPageFile="~/Book.master" AutoEventWireup="true" CodeFile="frmPublisherList.aspx.cs" Inherits="frmPublisherList" Title="Publisher Lists" %>
<asp:GridView ID="grdPublisher" runat="server" AutoGenerateColumns="false">
<Columns>
<asp:BoundField DataField="Site" HeaderText="Site" />
<asp:BoundField DataField="Name" HeaderText="Name" />
<asp:TemplateField HeaderText="Operations">
<ItemTemplate>
<asp:Button runat="server" ID="btnDelete" CommandName="deletepub" CommandArgument='<%# Eval("IDPublisher") %>' Text="Delete" />
<asp:HyperLink runat="server" ID="hkEdit" NavigateUrl='<%# Eval("IDPublisher","~/frmPublisher_Edit.aspx?ID={0}") %>' Text="Edit"></asp:HyperLink>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<br />
The link hkEdit is self explanatory – it goes to the frmPublisher_Edit.aspx with the ID of publisher in the row.
For the button we must create the event – and the event is on the grid itself – is the RowCommand
On the .cs file:
protected void grdPublisher_RowCommand(object sender, GridViewCommandEventArgs e)
switch(e.CommandName)
case "deletepub":
int idPublisher;
if(int.TryParse(e.CommandArgument.ToString(),out idPublisher))
Response.Redirect("frmPublisher_Delete.aspx?ID="+ idPublisher, false);
return;
Response.Write("Can not find id:" + idPublisher);
default:
Response.Write("Do not know command : " + e.CommandName);
Now create the two pages frmPublisher_Delete and frmPublisher_Edit
On both we will copy the table from the new page and the source – without the class declaration. One more thing is to retrieve from ID the editing publisher:
if(!int.TryParse(Request.QueryString["ID"],out idPublisher))
//we have id of the publisher
How can we retrieve from the ID of the publisher the object ? Remember that in Windows forms application we did pass from one form to another the publisher object. Here we have just the Id. For this, we will open again the Book.sln solution and add the method to load one single object.
I like to put the method on ColPublisher and make it static … to not apparently create a new object.
public static Publisher sLoadFromID(int ID)
DbConnection db = Settings.TheConnection;
using (db)
db.Open();
IDataReader ir = Settings.Load("select IDPublisher, NamePublisher, SitePublisher from Publisher where IDPublisher="+ ID, db);
while (ir.Read())
p.FillObject(ir);
return p;
Compile and go to Web project.We can have the publisher:
Publisher p = ColPublisher.sLoadFromID(idPublisher);
if (p == null)//maybe someone deleted
//now fill the text boxes
txtName.Text = p.Name;
txtSite.Text = p.Site;
Why we have put (!IsPostBack ) ? Simply because the textboxes must be filled only once – the first time. When the user enter new name and/or new site and after clicks on save, we must preserve his data .Other problem is that when we have to save the data, we must have the same code to load the publisher – so we put this into a function into the page:
private Publisher pub
if (!int.TryParse(Request.QueryString["ID"], out idPublisher))
return ColPublisher.sLoadFromID(idPublisher); ;
The code on PageLoad will be now shorter :
Publisher p = pub;
if (p == null)
And we must modify the code on saving also :
protected void btnSave_Click(object sender, EventArgs e)
//TODO : throw an exception that someone deleted the publisher
p.Update();
You can modify also the text of btnsave from “Insert” to “Save”
On the delete page we will put the same code to retrieve the Publisher .Here is the code:
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using BookObjects;
public partial class frmPublisher_Delete : System.Web.UI.Page
if(p != null)
p.Delete();
protected void btnCancel_Click(object sender, EventArgs e)
Do not forget to change the text “Insert” of btnSave to “Delete”. You also can put readonly property to true on the textboxes
<%@ Page Language="C#" MasterPageFile="~/Book.master" AutoEventWireup="true" CodeFile="frmPublisher_Delete.aspx.cs" Inherits="frmPublisher_Delete" Title="Untitled Page" %>
<asp:TextBox ID="txtName" runat="server" ReadOnly="true">
<asp:TextBox ID="txtSite" runat="server" ReadOnly="true">
<td><asp:Button ID="btnSave" Text="Delete" runat="server" OnClick="btnSave_Click" />
<td><asp:Button ID="btnCancel" Text="Cancel" runat="server" OnClick="btnCancel_Click" />
Next time we will put some modifications to the site: a site map(in order to can have indications for the user where he is) and code to change and load resources in English and French languages at run time.
Theme design by Jelle Druyts