2010. október havi bejegyzések

E-mail küldés kódból – sablonnal

.NET-ből e-mailt küldeni nem nagy durranás, három évvel ez előtt már egyszer megírtam, hogy milyen egyszerűen megy ez a MailMessage és az SmtpClient osztályok segítségével. Akkor szó esett arról, hogyan kérhetünk kézbesítési nyugtát, hogyan küldhetünk egy levélen belül többféle formátumban vagy éppen aszinkron módon, és hogy hogyan trace-elhetjük a levélküldést. Egy gyakori feladat azonban kimaradt: hogyan küldhetünk úgy levelet, hogy az e-mail sablonja egy külső fájlból jöjjön, amit a kódunktól függetlenül tudunk karbantartani?

Itt jön a képbe a System.Web.UI.WebControls.MailDefinition osztály. Elsőre igen szokatlan, hogy ezt az osztályt használjuk, hiszen a többi levélküldős osztály a System.Net.Mail névtérben van, ez pedig a WebControlsban, de az a lényeg, hogy működik. A MailDefinition osztály BodyFileName tulajdonságában kell megadnunk a sablont:

  MailDefinition mail = new MailDefinition();
  mail.BodyFileName = @"~/EmailTemplate.txt";

Megadhatjuk továbbá a levél feladóját (From), hogy ki kap másolatot (CC), a levél prioritását (Priority), sőt tárgyát (Subject) is, a legfontosabb azonban az IsBodyHtml tulajdonság beállítása, amivel azt jelezzük, hogy a betöltött sablon plain text vagy HTML fájl.

Az én esetemben plain text e-mailt küldünk, ezért a sablon is sima TXT:

  Tisztelt <%DisplayName%>!
  A <%Acronym%> konferenciára sikeresen regisztrálta dolgozatát.  Köszönjük.

A sablonban természetesen helyőrzők vannak, ahova a behelyettesítendő értékek kerülnek. Fontos, hogy itt a <% %> szintaktikát használjuk, más megoldással voltak rossz tapasztalataink. Egy szótár típusú gyűjteményben (dictionary) kell megadnunk, hogy a helyőrzők helyére milyen konkrét értékek kerüljenek (nálam az emailData objektum tartalmazta a szükséges információkat):

  ListDictionary replacements = new ListDictionary();
  replacements.Add( "<%DisplayName%>", emailData.DisplayName );
  replacements.Add( "<%Acronym%>", emailData.Acronym );

A definícióból a CreateMailMessage függvénnyel állíthatjuk elő az elküldhető MailMessage objektumot, paraméterként a címzettet, a csere szótárt és egy tulajdonos Control objektumot kell átadnunk. Természetesen az így létrejött MailMessage tulajdonságait utána módosíthatjuk és már mehet is a küldés:

  using( MailMessage message =
    mail.CreateMailMessage( emailData.Email, replacements, this ) )
  {
    message.Subject = "Sikeres regisztráció";
    try
    {
        using( SmtpClient client = new SmtpClient() )
        {
            client.Send( message );
        }
    }
    catch( Exception ex )
    {
        // Naplózás
    }
  }

A levelezőszerver és a feladó adatai természetesen a web.config-ból is jöhetnek:

  <system.net>
    <mailSettings>
      <smtp from="no-reply@example.com">
        <network host="mail.example.com" password="" userName=""/>
      </smtp>
    </mailSettings>
  </system.net>

Nagyon testidegennek tűnhet egy ilyen GUI független kódtól egy olyan osztály, ami a WebControls névtérben van, ráadásul a hívott metódusának egy Control példányt is át kell adni. Ez valószínűleg onnan ered, hogy ezt az osztályt a beépített felhasználókezelő vezérlőkhöz készítették, mi csak kihasználjuk, hogy azoktól függetlenül is működik. Természetesen ugyanezt a funkcionalitást néhány perces kódolással bárki elkészítheti és akkor nincs ilyen probléma, én mégis jobban szeretem a standard, nem egyedi megoldásokat. Ráadásul ez készen van, csak használni kell.

Technorati-címkék: ,,,

SQL Server tévhitek

Az SQL Serveren failover után a félbehagyott tranzakciók befejeződnek. Vagy nem? Az online index műveletek nem tesznek tábla szintű zárakat. Vagy mégis? A database mirror azonnal észreveszi, ha hiba van és azonnal megtörténik az átállás. Biztos? (Megjegyzem, erről nagyon jót beszélgettünk kedden Erikkel az SQL Klubon.) A TempDB-hez processzor magonként egy adatfájl tartozzon. Vagy inkább több? Sérült adatfájl meggyógyul, ha újraindítom az SQL Servert. Ha nem, akkor az operációs rendszert. De egy attach/detach biztos segít. És ha mégsem? A resource governor uralkodik az I/O felett is. Vagy talán azért annyira nem mindenható?

Érdekes kérdések ezek, sokan mellélőnek a válaszadáskor. Paul S. Randal az elmúlt időszakban a blogjában ezeknek a gyakori tévhiteknek az eloszlatásával foglalkozott. Szép nagy gyűjtemény jött össze (több, mint 100 közszájon forgó marhaság), melyet most egy 60 oldalas PDF formájában is közzétett. Érdemes elolvasni!

Technorati-címkék: ,

Állítsátok meg a web.configot

.NET platformon a konfigurációs beállítások öröklődnek és ez igaz az ASP.NET-re is. Ha egy felsőbb mappában beállítunk valamit a web.config fájlban, akkor az érvényes lesz az almappákra is, ami általában rendben is van. A gondok akkor jönnek elő, amikor egy alkalmazást egy másik alá telepítünk, például mert az egyik alkalmazás a gyökérben van, a másik pedig egy almappában.

Érdekes hibaüzenetekkel és jelenségekkel találkozhatunk, például ha:

  • A felsőbb alkalmazásnál felveszünk valamelyik szolgáltatáshoz egy providert (pl. Health Monitoringhoz), a gyermek alkalmazás a saját bin vagy App_Code könyvtárában fogja keresni, de persze nem találja.
  • Kiveszünk egy modult a csővezetékből (én például a Session, az AnonymousIdentification, a PassportAuthentication, a WindowsAuthentication, és a Profile modulokat rendszeresen), a gyermek alkalmazásnak pedig szüksége lenne rá.
  • Átállítjuk a defaultDocument értékét és nem jönnek be oldalak.

És akkor még nem beszéltem arról, hogy akár két különböző .NET Framework verziót is igényelhet a két alkalmazás.

Sajnos nem elég, ha a két mappát külön-külön alkalmazássá tesszük az IIS-ben. Szükséges, de nem elég, mert ez csak azt határozza meg, hogy a gyermek alkalmazás gyökér web.config-jában is használhatunk alkalmazás szintű beállításokat.

A beállítások öröklésének megakadályozására a külső alkalmazás web.config-jában használhatjuk a location tag inheritInChildApplications attribútumát:

    <location path="." inheritInChildApplications="false">
        <system.web>
            <!-- Itt jönnek sorban a beállítások -->            
        </system.web>
    </location>

Ne várakoztasd a felhasználót: jQuery Templates és ASP.NET PageMethods

A webalkalmazásokkal szemben az egyik legfontosabb követelmény, hogy gyorsan jelenjen meg az eredmény a böngészőben, ami nem is gond, ha az oldalon kevés adat van. Probléma akkor van, ha az oldalra többféle adatforrásból jönnek az adatok, ekkor ugyanis előfordulhat, hogy egyetlen sokáig tartó adatbázis lekérdezés feltartja a teljes oldal renderelését, hiába jött meg a válasz a többi helyről villámgyorsan. Ekkor a legcélszerűbb megoldás, ha az oldal hamar elkészült részét gyorsan leküldjük a böngészőnek, a lassan előálló részt viszont aszinkron módon rendereljük ki. Hasonló megoldásra van szükség akkor is, amikor az oldal jelentős része output cache-ből jön, de egy kis részből mindig a legfrissebbet kell láttatnunk.

A problémát igazán nem is az aszinkronitás okozza, hiszen manapság már gyerekjáték egy Ajaxos hívást indítani a böngészőből. A gányolás ott kezdődik, amikor a nyers adatszerkezetből formázott HTML-t kell előállítani. Hol tegyük ezt meg: kliens oldalon vagy szerver oldalon? Egyáltalán hogyan lehet ezt kulturáltan megoldani, van jobb megoldás a sztring összefűzésnél?

Szerencsére van jobb megoldás: adatkötés. Jelentős mennyiségű ügyeskedéssel eddig is meg lehetett oldani, hogy szerver oldalon egy ASCX modult készítettünk, abban a szokásos módon szétválasztottuk a kódot és a megjelenést, majd a megszokott adatkötéssel HTML-lé alakítottuk és ezt a kész HTML-t küldtük le a böngészőnek. Ez így elsőre jónak is tűnik, de hamar elkezd több sebből vérezni.

A jó hír az, hogy most már lehet kliens oldalon is adatot kötni! Így nem kell szerver oldalon az adatok formázásával foglalkozni, gyorsan át lehet küldeni a nyers adatszerkezetet a kliensnek, a szép megjelenítésről pedig a böngésző fog gondoskodni. Nagyon vártam már ezt a pillanatot, ugyanis így jelentősen egyszerűsödni fognak az alkalmazásaink, sokkal karbantarthatóbb lesz a kódunk.

Van egy kis dezsavű érzésem, ugyanis ez már a harmadik alkalom, hogy ennek örülök. A Microsoft először 2007-ben próbálkozott a kliens oldali adatkötéssel, akkor XML Script formájában. Aztán azt mind elvetették, jött a Sys.UI.DataView, ami szintén ígéretesnek látszott, de végül az Ajax Library-vel az is ment a kukába. A mostani megoldást szintén a Microsoft kezdte el fejleszteni, de végül a jQuery csapat közreműködésével készült el és jQuery pluginként lett elérhető. Pontosabban két pluginről van szó, a jQuery Templates és a jQuery Data Link pluginekről, az előbbi az adatok megjelenítésére jó, az utóbbi pedig a kétirányú adatkötés megvalósítása. Bár nem ez az első deklaratív megvalósítás a sablon alapú renderelés és az adatkötés megoldására, a jQuery csapat végül úgy döntött, hogy ez lesz a hivatalos, és várhatóan az 1.5 verziótól kezdve az alap jQuery könyvtár részét képezik majd.

Mint a faék

Íme a jQuery Templates használata 4 egyszerű lépésben:

1. Legyen egy JavaScript objektumod az adatokkal:

  var movies = [
    { Name: "The Red Violin", ReleaseYear: "1998" },
    { Name: "Eyes Wide Shut", ReleaseYear: "1999" },
    { Name: "The Inheritance", ReleaseYear: "1976" }
  ];

2. Készíts hozzá egy sablont, ami fontos, hogy text/x-jquery-tmpl típusú script blokk legyen, így a böngésző figyelmen kívül fogja hagyni:

  <script id="myTemplate" type="text/x-jquery-tmpl">
    <li>
        <b>${Name}</b> (${ReleaseYear})
    </li>
  </script>

3. Keress egy placeholdert, ahol az eredményt meg akarod jeleníteni:

  <ul id="movieResults"></ul>

4. Renderelj:

  $("#myTemplate").tmpl(movies).appendTo("#movieResults");

Mondtam én, hogy nem bonyolult. Akárhogy is nézem, egyáltalán nem baj, hogy az előző próbálkozások mentek a levesbe, ez sokkal egyszerűbb és szebb. Az egyszerűségén kívül az tetszik benne nagyon, hogy a ${ } helyekre automatikusan HTML encoded értékek kerülnek. Ha nyers HTML-t, vagy JavaScriptet tartalmazó adataink vannak, akkor a {{html}} placeholdert kell használni. Ezen kívül lehet még a sablonba feltételeket tenni, sablonokat elnevezni és névvel hivatkozni rá és még pár dolgot, akit érdekel, nézze meg a teljes API leírást.

Szerver oldali kóddal

A fenti példa elég sok helyen megtalálható az interneten, de valljuk be, nem túl életszerű, hiszen az adatok többnyire inkább a szerverről jönnek. Ha ilyet szeretnénk, akkor legegyszerűbb az adatokat ASP.NET PageMethod segítségével publikálni:

  public class Movie
  {
    public string Name { get; set; }
    public int ReleaseYear { get; set; }
  }

  public partial class Step2 : System.Web.UI.Page
  {
    [WebMethod]
    public static Movie[] GetMovies()
    {
        return new Movie[]{
            new Movie(){ Name = "The Red Violin", ReleaseYear = 1998 },
            new Movie(){ Name = "Eyes Wide Shut", ReleaseYear = 1999 },
            new Movie(){ Name = "The Inheritance", ReleaseYear = 1976 }
        };
    }
  }

A gyönyörű ebben az, hogy a [WebMethod] attribútum hatására az ASP.NET automatikusan gondoskodik arról, hogy a válasz Movie[] JSON formátumra alakuljon (ha mégis manuálisan szeretnénk, akkor használhatjuk a JavaScriptSerializer osztályt).

Az így elkészült szerver oldali metódust nagyon egyszerű meghívni, elég csak feldobnunk az oldalra egy ScriptManagert és máris lesz egy JavaScriptből hívható PageMethods.GetMovies függvényünk. Persze ha úgyis jQuery-zni fogunk később, akkor logikusabb, ha ezt a PageMethodot is jQuery-ből hívjuk ScriptManager nélkül:

  $(function () {
    $.ajax({
        type: "POST",
        url: "Step2.aspx/GetMovies",
        dataType: "json",
        contentType: "application/json; charset=utf-8",
        success: function (result) {
            $("#myTemplate").tmpl(result.d).appendTo("#movieResults");
        }
    });
  });

Barátságos, nem? Sehol semmi extra kód a sorosításhoz vagy az alacsonyszintű HTTP kommunikációhoz. Egyetlen apró részletre kell figyelni, hogy a válaszban egy d objektumon belül lesznek az értékek. Ennek egyébként az a magyarázata, hogy ha az adatok között JavaScript kód is van, akkor az így nem tud véletlenül végrehajtódni.

Sokat egyszerre

Az Ajax lehetőségeinek bemutatására az egyik kedvenc példám a Pageflakes, mert gyönyörűen látszik, ahogy az oldal egyes részei önállóan betöltődnek. Kipróbáltam, hogy a fenti megoldás működik-e több párhuzamos kérésre is, vagy esetleg a browser connection limit miatt elhal valahol. Átírtam a kódot adatbázisosra és jó sok késleltetést tettem a szerver oldali kódba (így egyúttal azt is kipróbáltam, hogy egy List<T> sorosítása is megy magától), majd megnéztem, hogy mi látszik Firebugban (katt a képre a nagyobb változatért):

jQuery-PageMethods-Firebug

Látszik, hogy a böngészőnek valóban van párhuzamosítási korlátja, nálam a Firefox hatosával küldte el a kéréseket a szerverre. Szerencsére mindez ment teljesen automatikusan, a placeholderekbe tett animáló gifek pörögtek, ahogy kell, így még a felhasználó is kapott visszajelzést.

Ha ehhez a gördülékeny implementációhoz még az OData-t is hozzávesszük, akkor látszik, hogy végre kezdenek összeérni a technológiák, vége a trükközésnek.

Technorati-címkék: ,,,

iWiW-et az AddThis-be

Egyre több weboldalon láthatók megosztás funkciók (ezen a blogon is), amivel a felhasználók az érdekesnek talált tartalmat el kattintással beküldhetik a Facebookra, az iWiW-re, a Deliciousra, a MySpace-re, vagy éppen tíz másik helyre. Természetesen több tucat megosztás gombot nem tehetünk ki mindenhova, de sajnos azt sem tudjuk pontosan, hogy a felhasználóknak pontosan mire van szükségük. Egy másik jó kérdés, hogy megéri-e ezzel egyáltalán foglalkozni? Social marketing ide vagy oda, ezt az apró funkciót is valakinek implementálnia kell az oldalon, ami nyilván pénz és idő, nem mindegy tehát, hogy a felhasználók használják-e. Ha pedig még mérni is akarjuk, az újabb implementálandó funkció.

Ezekre a problémákra nyújtanak megoldást a hivatalos megosztás szolgáltatók, például az AddToAny, a ShareThis és az AddThis. Mindenki találkozott már velük, ezek a kék, a zöld és a narancssárga Megosztás gombok.

AddThis

Az egyik projektünkben az AddThis-re esett a választás, ismer közel 300 cél portált, szolgáltat statisztikákat és nincs túlbonyolítva. Viszont persze az iWiW nem volt benne, így az AddThis Client API-hoz kellett fordulni. Kis kísérletezés után mindössze ennyi lett a megoldás:

  var addthis_config = {
    username: "AddThisFelhasználónevem",
    ui_language: "hu",
    services_compact: 'email, iwiw.hu, facebook, twitter',
    services_expanded: 'email, iwiw.hu, facebook, twitter, startlap, google',
    services_exclude: 'print',
    services_custom: {
        name: "iWiW",
        url: "http://iwiw.hu/pages/share/share.jsp?u={{URL}}&t={{TITLE}}",
        icon: "http://static.iwiw.net/common/image/favicon.ico"
    }
  }
Technorati-címkék: ,,

Silverlight Firestarter

A Silverlight Firestarter egy egész napos rendezvény, amely december 2-án kerül megrendezésre Redmondban a nagy nevek felsorakoztatásával: ott lesz Scott Guthrie, Jesse Liberty, Dan Wahlin, John Papa, Time Heuer, Jaime Rodriguez és még sokan mások. A téma természetesen a Silverlight, de persze az ilyenkor elmaradhatatlan Windows Phone 7 is terítékre kerül.

Mindez nem lenne érdekes, ha nem lehetne az egész eseményt élőben online ingyenesen követni. Na jó, az after party-ról valószínűleg lemaradunk, de az előzetes program alapján úgy tűnik, hogy így is érdemes lesz virtuálisan résztvenni az eseményen, profi Silverlightosoknak és a témával ismerkedőknek egyaránt.

Program és regisztráció: http://www.silverlight.net/news/events/firestarter/

Silverlight-Firestarter-banner

Technorati-címkék: ,

ASP.NET Routing IIS 7-en

Bevallom, hogy eleinte vonakodtam az ASP.NET Routingtól, mert nem tetszett, hogy a routing szabályok ennyire bedrótozódnak az alkalmazásba, de amikor rájöttem, hogy az URL-ek megépítésére is van benne beépített támogatás, akkor elkezdett jobban tetszeni, mint az IIS UrlRewrite modulja. Persze ezzel is sikerült olyan problémába belefutni, ami csak az éles környezetbe történő telepítéskor jött elő, fejlesztői szerveren nem (bár az ASP.NET Routing legalább megy a fejlesztői webszerveren is, nem úgy, mint az UrlRewrite, amihez legalább IIS Express kell).

A jelenség nagyon egyszerű: nem működnek a barátságos URL-ek IIS 7 vagy 7.5 alatt. Ez előjöhet ASP.NET WebForms, MVC vagy WebMatrixos alkalmazással is, ha a Routingot használja. A magyarázat is nagyon egyszerű: az IIS nem kapja el a kiterjesztés nélküli URL-ekre érkező kéréseket.

Két lehetséges megoldás van:

  1. Lehet telepíteni a KB980368-ban leírt frissítést. Ez akkor járható út, ha ismerjük a szerveren futó összes alkalmazást és van jogunk frissítést telepíteni.
  2. Ha csak a saját alkalmazásunkat szeretnénk működésre bírni, akkor az is elég, ha ezt beírjuk a saját web.config fájlunkba:
    <system.webServer>
      <modules runAllManagedModulesForAllRequests="true"/>
    </system.webServer>