2013. április havi bejegyzések

Ajaxos fájl feltöltés

Időnként felmerül, hogy jó lenne úgy feltölteni egy fájlt, hogy közben az oldal többi része nem változik, magyarul Ajaxosan. Rossz hírem van, az XMLHttpRequest objektum Level 1 változata ezt nem tudja, tehát ha régebbi böngészőkre is tekintettel kell lennünk, akkor nincs mese trükközni kell.

A legelterjedtebb trükközési módszer az iframe alkalmazása, azt ugyanis bátran lehet postbackelni, az egyetlen szépséghiba, hogy a válasz, például validációs hibák is az iframe-be fognak megérkezni. Tehát ha rejtett iframe-mel dolgozunk, akkor a választ onnan ki kell venni, és az oldal megfelelő részén meg kell jeleníteni. Van tehát feladat bőven, szerencsére a jQuery Form Plugin sokat tud segíteni a megvalósításban.

Először is készítsünk egy view-modelt szerver oldalon, amiben egy HttpPostedFileBase típusú tulajdonság fogja képviselni a feltöltött fájlt. Mellécsaptam még egy kötelező Name tulajdonságot, csak úgy a demonstráció kedvéért:

public class UploadVM
{
    [Required( ErrorMessage = "Please enter a name!" )]
    public string Name { get; set; }

    [Attachment]
    public HttpPostedFileBase Attachment { get; set; }
}

Az [Attachment] a korábbi cikkben bemutatott, fájl validálásra használt attribútum. Ellenőrzi, hogy van-e feltöltve fájl, illetve hogy helyes a kiterjesztése és nem túl nagy-e a mérete.

Ehhez a modellhez már készíthetünk egy űrlapot, ami fel fogja küldeni a fájlt és a megadott nevet:

@using( Html.BeginForm( "Index", "Home", FormMethod.Post, 
        new { id = "myForm", enctype = "multipart/form-data" } ) )
{
    <p>
        <label for="txtName">Name:</label>
        <input type="text" id="txtName" name="Name" />
    </p>

    <p>
        <label for="fupAttachment">File:</label>
        <input type="file" id="fupAttachment" name="Attachment" />
    </p>

    <p>
        <input type="submit" value="Upload" />
    </p>    

    <div id="errors"></div>
}

Fontos, hogy a generált formnak multipart/form-data értékű enctype attribútuma legyen, mert azzal tud csak fájl utazni, illetve létrehoztam még egy errors azonosítójú div-et is, ahol majd a hibaüzeneteket fogjuk megjeleníteni.

Ez az űrlap a HomeController Index nevű actionjére lő, amit így implementálhatunk:

[HttpPost]
public ActionResult Index( UploadVM model )
{
  if( !this.ModelState.IsValid )
  {
    string firstError = ModelState.First( m => m.Value.Errors.Any() )
                                  .Value.Errors[ 0 ].ErrorMessage;
    return this.FileUploadFailure( firstError );
  }

  // Process the file here

  string message = String.Format( "The file '{0}' is successfully uploaded.", 
                                  model.Name );
  return this.FileUploadSuccess( message );
}

Ha a view-model valamelyik tulajdonsága hibás, akkor az attribútumoknak köszönhetően a hibák bekerülnek a ModelStatebe, amit a metódus elején szokás szerint ellenőrzünk. Ha van hiba, akkor az első hibaüzenettel térünk vissza, ha nincs, akkor pedig feldolgozzuk a fájlt és egy siker üzenettel térünk vissza.

A visszatérési érték egy JSON objektum, mert ezt tudjuk kliens oldalon barátságosan feldolgozni. Csakhogy ne feledjük, hogy a válasz egy iframe-be fog beíródni, és nem minden böngésző szeret iframe-be application/json típusú tartalmat kapni. Szerencsére a jQuery Form Plugin támogatja azt a trükköt, hogy a JSON tartalmat egy <textarea> elembe ágyazva küldjük vissza a szerverre text/html típusú válaszként, onnan ő majd kiveszi a JSON tartalmat.

Hogy ez a csomagolás egyszerű legyen, készítettem egy saját result típust:

public class FileUploadJsonResult : JsonResult
{
  public override void ExecuteResult( ControllerContext context )
  {
    this.ContentType = "text/html";
    context.HttpContext.Response.Write( "<textarea>" );
    base.ExecuteResult( context );
    context.HttpContext.Response.Write( "</textarea>" );
  }
}

Az egyszerű példányosításhoz pedig két bővítő metódust, melyek közül az egyikkel sikert, a másikkal hibát lehet jelezni:

public static FileUploadJsonResult FileUploadSuccess( 
  this Controller controller, string successMessage = null )
{
  return new FileUploadJsonResult
  {
    Data = new { Success = true, Message = successMessage }
  };
}

public static FileUploadJsonResult FileUploadFailure( 
  this Controller controller, string errorMessage )
{
  return new FileUploadJsonResult
  {
    Data = new { Success = false, Message = errorMessage }
  };
}

Itt a Data tulajdonságban bármilyen szerkezetű objektumot összerakhatunk, az fog megérkezni válaszként a kliensre JSON formátumban. Itt most az egyszerűség kedvéért egy Success tulajdonságban jelzem, hogy a feltöltés hibátlanul megtörtént-e, és egy Message tulajdonságban hibaüzenetet vagy sikeres feltöltésre utaló üzenetet küldök vissza, amit a böngésző megjeleníthet.

Készen vagyunk tehát a szerver oldallal: van egy formunk, ami elPOSTolható egy actionnek, ami validálja a feltöltött tartalmat, és ha minden mező érvényes adatot tartalmaz, akkor feldolgozza őket, és az eredményt JSON válaszban jelzi.

Már csak a kliens oldal van hátra, amit természetesen a jQuery Form Plugin segítségével valósítunk meg. Ahogy a neve is mutatja, ez egy jQuery plugin, amit a form elemet burkoló jQuery objektumra (itt épp $form) kell ráhúznunk:

$form.ajaxForm({
    iframe: true,
    dataType: "json",
    beforeSubmit: function () {
      // TODO     },
    success: function (result) {
      // TODO 
    },
    error: function () {
      // TODO 
    }
});

Az alábbi eseménykezelőket célszerű megvalósítanunk:

  • beforeSubmit: ez a POST elküldése előtt fut le, itt írhatjuk ki például, hogy a feltöltés folyamatban van, vagy például a jQuery BlockUI plugin segítségével letilthatjuk az űrlapon lévő vezérlőket.
  • success: ez akkor hívódik meg, ha látszólag hibátlan volt a feltöltés, bár tapasztalatom szerint akkor is meg tud hívódni, ha a szerver valamilyen hibát jelez. Ha nem volt hiba, akkor a paraméterül kapott változóban a szerverről visszaküldött JSON objektum jelenik meg, de számítsunk rá, hogy hiba esetén ez lehet undefined is!
  • error: hiba esetén ez fut le.

Íme egy példa a success callback implementálására:

if (!result) {
$errors.html('<div class="validation-summary-errors"><ul><li>Oooops....
                </li></ul></div>');
}
else {
  $form.resetForm();

  if (result.Success === true) {
    var message = result.Message;
    if (message && message.length > 0) {
      $errors.html( message );
    }
  }
  else {
    $errors.html('<div class="validation-summary-errors"><ul><li>{0}
                  </li></ul></div>'.format(result.Message));
  }
}

Ebben a kódban a hibák megjelenítését egy kicsit összetett HTML darabkával végezzük. Ennek az a jelentősége, hogy a klasszikus ASP.NET MVC-s validálás is pont ilyen HTML-t generál magából, tehát a mi Ajaxos hibáink is pont ugyanolyan stílussal fognak megjelenni.

A format egy a szerver oldali String.Formathoz hasonló függvény, amit például így implementálhatunk JavaScriptben:

String.prototype.format = function () {
  var args = arguments;
  return this.replace(/\{\{|\}\}|\{(\d+)\}/g, function (m, n) {
    if (m === "{{") { return "{"; }
    if (m === "}}") { return "}"; }
    return args[n];
  });
};

Ha a szerverről nem csak az első, hanem összes ModelState hibát visszaadnánk, akkor egy ciklusban renderelhetnénk ki őket.

A cikkhez tartozó forráskód rengeteg kommenttel letölthető innen: http://sdrv.ms/10QQLSp

 

Technorati-címkék: ,
Reklámok

Feltöltött fájl validálása ASP.NET MVC-ben

Az ASP.NET MVC egyik szépségét a kód tisztasága adja, ami részben a model validálásnak köszönhető. Az ember fogja a System.ComponentModel.DataAnnotations névtérben lévő attribútumokat, teleaggatja velük a modellt vagy a view-modelt, és máris működik a validálás, ráadásul a kód egyéb részeit sem zavarja össze.

Mindez szép és jó, ha egy string értékét vagy hasonlóan egyszerű inputot szeretnénk vizsgálni, de például egy feltöltött fájl ellenőrzésére nem sok lehetőségünk van. .NET 4.5-től kezdve ugyan már van FileExtensionAttribute, de gyakran ennél többre van szükségünk.

Amire szinte mindig szükség van:

  • Töltött-e fel fájlt a felhasználó?
  • A feltöltött fájl kiterjesztése megfelel-e?
  • A feltöltött fájl mérete a meghatározott maximumon belül van-e?

De lehet néhány extrább igény is:

Készítsünk magunknak saját attribútumot! Arra kell csak figyelnünk, hogy a ValidationAttribute ősosztályból származzon, és hogy a feltöltésnél használatos HttpPostedFileBase típusú változókat validáljuk vele.

Íme egy megoldás:

[AttributeUsage( AttributeTargets.Property )]
public sealed class AttachmentAttribute : ValidationAttribute
{
  protected override ValidationResult IsValid( object value, 
ValidationContext validationContext ) { HttpPostedFileBase file = value as HttpPostedFileBase; // A fájl kötelező. if( file == null ) { return new ValidationResult( "Tölts fel fájlt!" ); } // A fájl max. 10MB-os lehet. if( file.ContentLength > 10 * 1024 * 1024 ) { return new ValidationResult( "A fájl túl nagy!" ); } // A fájl csak PDF lehet. string ext = Path.GetExtension( file.FileName ); if( String.IsNullOrEmpty( ext ) || !ext.Equals( ".pdf", StringComparison.OrdinalIgnoreCase ) ) { return new ValidationResult( "A fájl nem PDF!" ); } // Minden rendben. return ValidationResult.Success; } }

Ha ezzel megvagyunk, akkor már csak rá kell aggatnunk az [Attachment] attribútumot a modell megfelelő tulajdonságára, és a hibák máris be fognak kerülni a ModelState-be.

Ha a webalkalmazásunkban több helyen, többféle fájlt is fel lehet tölteni, akkor bevezethetünk rájuk egy enumot, amit átadunk az AttachmentAttribute konstruktorának. Így minden fájl ellenőrzése egy helyen, központosítva történhet.

 

Technorati-címkék: ,,

Sok helyet foglalnak a Windows Store alkalmazások

A Windows Store alkalmazások egyik szépsége, hogy viszonylag egyszerűen tudnak frissülni: szinte automatikusan, rendszergazdai jog nélkül. Az egyszerűséget szeretik is kihasználni a fejlesztők, és nem múlik el hét úgy, hogy ne frissíthetnék pár alkalmazást a gépemen.

Mindennek persze van némi hátránya is, konkrétan a diszkigény. A Store alkalmazások a C:\Program Files\WindowsApps rejtett mappába települnek, melyet alapértelmezés szerint csak a Trusted Installer felhasználó érhet el. Ha adunk magunknak jogosultságokat, természetesen belekukkanthatunk ebbe a mappába, és kideríthetjük, hogy mi foglal nálam például közel 3GB-ot.

Valami hasonlóval fogunk találkozni:

metro-apps-versions

Ha belenézünk a mappákba, akkor megtaláljuk az alkalmazásokhoz tartozó fájlokat: HTML5+JavaScript alkalmazások esetén ott a teljes forráskód, .NET-ben készült alkalmazások esetén pedig a lefordított DLL-eket és az egyéb fájlokat találjuk mappákban.

Feltűnhet, hogy minden alkalmazásnak minden verziója külön mappában lakik, így azok teljesen elkülönülnek egymástól. Így már könnyen megmérhetjük, hogy melyik alkalmazás mennyi helyet foglal. Ha ezt összevetjük a PC Settings –> General –> View app sizes listával, akkor azt fogjuk tapasztalni, hogy az a lista csak az adott felhasználó által telepített alkalmazások legfrissebb verzióinak méreteit mutatja.

De miért van egy alkalmazásból több verzió?

Egyrészt azért, hogy az egy gépen dolgozó felhasználók ne legyenek hatással egymásra. Amíg két felhasználó egy alkalmazásnak ugyanazt a verzióját telepíti, addig természetesen a program fájlok csak egy példányban lesznek a gépen. Azonban ha ezek után az egyik felhasználó frissíti a saját profiljában az appot, akkor már a gépen megtalálható lesz a régi és az új verzió is. Ily módon a felhasználók egyáltalán nincsenek hatással egymásra.

Ezen kívül az a verzió is rendelkezésre áll, ami az operációs rendszerrel együtt települt, hogy ha új felhasználót hozunk létre a rendszerben, akkor ő azt a verziót fogja megkapni.

Felmerülhet persze a kérdés, hogy mindez szép és jó, de hogy szabadulhatunk meg a felesleges verzióktól?

Bevallom, erre nem találtam hivatalos megoldást. Ha valaki tud ilyet, kérem írja meg nekem. Találtam viszont egy utalást arra, hogy a Windows takarítja ezt a mappát, “when needed”. Tudja valaki, hogy ez pontosan mit jelent?

 

Technorati-címkék:

Meglepő kapcsolat bontások SQL Azure-ban

Gyakran lehet találkozni azzal a ténnyel, hogy mivel az SQL Azure ugyanazt a tabular data stream (TDS) protokollt támogatja, mint a teljes SQL Server, a programozási modell teljesen azonos a két esetben. Ebből viszont – a közhiedelemmel ellentétben – nem következik az, hogy az alkalmazásunk felhőbe költöztetése mindössze annyiból áll, hogy átállítjuk a connection stringet!

Az SQL Azure ugyanis számos olyan esetben is hajlamos bontani a kapcsolatot, amihez nem vagyunk hozzászokva a saját szervereinken. Íme két példa:

  • 40197 – The service has encountered an error processing your request. Please try again.
  • 40501 – The service is currently busy. Retry the request after 10 seconds.

Ennek az alapvető okai a hibatűrésben és a rendelkezésre állásban keresendőek. Ha a kérés kiszolgálásában részt vevő valamelyik szereplő hibás lesz vagy épp frissül, akkor a felhőnek köszönhetően nem lesz tartós leállásunk, de szolgáltatás visszaállásáig (ami rövid idő) a kapcsolatokat bontani fogja a szerver. A másik ok, hogy mivel az adatbázisok osztoznak bizonyos erőforrásokon, az SQL Azure query és connection throttling szolgáltatásai biztosítják, hogy egyik ügyfél se akaszthassa le teljesen a kiszolgálást. Ha túllépjük a limitet, hibát fogunk kapni.

Mindennek az az eredménye, hogy időnként elszállnak az adatbázis lekérdezéseink, de mivel ezek okai alapvetően átmeneti jelenségek, bátran megismételhetjük a lekérdezést kicsit később. Ez sajnos nagyon kellemetlen, mert általában arra számítva írjuk meg az adatelérési rétegünket, hogy ha az adatbázis nem elérhető, akkor nagy baj van, és nem szoktunk arra számítani, hogy a hiba másodperceken belül megszűnik magától.

Magyarul: SQL Azure-on futó alkalmazások esetén gondoskodnunk kell ezeknek a hibáknak a kezeléséről, és a hiba jellegétől függően meg kell ismételnünk a lekérdezést. Ha korábban nem volt az alkalmazásunkban ilyen intelligens retry-logika, akkor a felhőbe költöztetéskor ezt bele kell tennünk.

Néhány segítség:

 

Technorati-címkék: ,,,

Virtuális gépek védelme a felhőben

Mikor a rendszereinket a felhőbe költöztetjük, annyira meg szoktunk örülni annak, hogy minden “megy magától”, hogy gyakran megfeledkezünk az alapvető biztonsági teendőkről (amiket pedig a saját gépeinken amúgy meg szoktunk tenni). Az első öröm általában akkor ér minket, amikor a varázslóval pikk-pakk létrehozunk egy új virtuális gépet az Azure-ban:

create-vm

A problémák – legalábbis részben – ebből az egyszerűségből fakadnak:

  • A rendszergazda felhasználónevet nem kell megadnunk, sőt nem is lehet (lásd fent, disabled), hiszen az minden esetben Administrator.
  • A gép IP címét nem kell megadnunk, hiszen az Azure a saját tartományából ad majd neki egyet.
  • A RDP-t nem kell beállítanunk, hiszen az minden esetben engedélyezve lesz az alapértelmezett 3389-es porton.

Sajnos ez a kényelem jelentősen megkönnyíti a támadók dolgát: csak végig kell nézniük az Azure IP tartományát és be kell próbálkozniuk a 3389-es porton az Administrator felhasználóval és valamilyen jelszóval. Ilyen RDP brute force támadásra számos eszköz létezik már, és sajnos van már hír olyan esetről is, amikor a támadók sikerrel jártak.

Amit mindenképp érdemes megtenni:

  • Az RDP ne az alapértelmezett porton fusson.
  • Célszerű beállítani, hogy milyen kliens IP címekről lehet RDP-zni a gépre.
  • A rendszergazda felhasználóneve ne az alapértelmezett legyen.
  • Az admin jelszó legyen erős.

Sandrino Di Mattia cikke részletesen leírja, hogy mindezt hogyan tehetjük meg.

 

Technorati-címkék: ,,

UX tippek és olvasnivalók

Mióta nagyobb arányban használok kisebb kijelzős és érintőképernyős eszközöket, gyakrabban villan belém a felismerés, hogy a user experience, és a felhasználói felületek valóban barátságos, használható formára tervezése mennyire fontos – lenne. Sajnos azt tapasztalom, hogy számos olyan szoftver van, ahol az ötlet kiváló, de a felhasználói felület teljesen elrontja az élményt, és megy az alkalmazás vagy a website a süllyesztőbe.

Sokakkal beszélgetve arra jutottam, hogy ennek egyik oka természetesen a projektek költségérzékenysége (lásd még unit testelés), a másik, hogy egyszerűen nincs UX designer a fejlesztőcsapatokban. (Ez a két dolog persze összefügg, de nem csak a pénz miatt nincs ilyen szakember a projektekben.) Egyik sem fog egyik napról a másikra megváltozni, így nincs más lehetőség, mint saját erőből megoldani és erre két megoldást látok:

  1. Másolás. Az ötletek másolását általában rossz szemmel nézzük, de a felhasználói élmény terén szerintem ez nem egészen van így. Sőt, ha egy megoldás beválik, elterjed és az ismertsége még jobban megkönnyíti a használhatóságát. Persze az is lehet, hogy nem válik be, vagy még nem bizonyított, de kényszerből elterjed és ezért lesz közismert. Ezt a mintát követik a standardek és az ajánlások. Például az érintőkijelzős eszközöknél a “behúzom az ujjam a széléről” gesztus egyáltalán nem logikus, csak azért próbálkozunk vele, mert arra többnyire történni szokott valami.
  2. A meglévő szakembereink kiokosítása. Na, ez sem zökkenőmentes, mert nem minden programozót lehet rávenni arra, hogy a GUI-val foglalkozzon, de szerencsére akadnak olyanok, akiknek igenis van érzékük hozzá. Őket már csak idővel és forrásokkal kell ellátni. Ambrose Little (az ő nevével a korábbi bejegyzésben említett Infragistics Indigo Studio kapcsán találkozhattunk) blogjában például találhatunk egy UX Book List cikket, ami jó kiindulás lehet.

És ha már kiindulás, íme Ambrose UX for Devs Manifestoja:

  1. A tervezést kezdd az ember irányából.
  2. Hagyj időt a kutatásra.
  3. Ne írj, rajzolj.
  4. Próbálj ki sok variációt, majd egyesítsd a hasznos részleteket.
  5. Tesztelj a felhasználókkal.
  6. Csak akkor kódolj, ha nincs más mód.
  7. Végül ügyelj a részletekre is.

Ti mit gondoltok?

 

Technorati-címkék: ,,

Content Injector for ASP.NET MVC

Aki készített már újrafelhasználható elemeket ASP.NET MVC-ben, bizonyára találkozott már azzal a problémával, hogy egy partial view vagy egy HTML helper működéséhez szükséges egy CSS stíluslap vagy egy külső JavaScript fájl. Ha odafigyelünk a kódunk minőségére, akkor a CSS-t mindig az oldal tetején, a szkriptet pedig mindig az oldal alján töltjük be, gondosan ügyelve arra, hogy ezek a külső erőforrások akkor is csak egyszer töltődjenek be az oldalra, ha több helyen is szükség van rájuk. Viszont ezt csöppet sem egyszerű megoldani, hiszen a partial view-k és a HTML helperek teljesen önállóan, egymástól függetlenül renderelődnek. Bár a probléma egyáltalán nem újkeletű, még ASP.NET WebFormsban sem egyszerű megoldani, MVC-ben viszont kifejezetten nehéz.

Talán nem is meglepő, hogy a készre sütött megoldást Peter Blum tálalja nekünk a Content Injector for ASP.NET MVC formájában. Az öreg motoros ASP.NET fejlesztők már sokszor találkozhattak Peter nevével, aki egyszemélyes cégében leginkább ASP.NET web controlokat készít. Ezek közül a legismertebb a Peter’s Data Entry Suite, amely több, mint 100 WebForms vezérlőt tartalmaz, melyek elsősorban az adatbevitelt és a validációt teszik egyszerűbbé. Igen hasznos csomagról van szó, nem véletlenül kapott már értük számos pozitív értékelést.

Így csöppet sem csodálkoztam, amikor pár héttel ezelőtt Peter bukkant fel egy ötlettel az ASPInsiders listán, ami nagyon kényelmesen megoldja a fenti problémát. Sokan kipróbáltuk, kapott is pár visszajelzést, amit villámgyorsan át is vezetett a kódon, így most már bátran merem ajánlani a Content Injector for ASP.NET MVC projektet, leginkább NuGet csomag formájában.

A használata pofonegyszerű. Először is jelöljük meg az oldalunkon, tipikusan a Layout.cshtml fájlban, azokat a helyeket, ahova majd beszúródnak a tartalmak:

@Injector.InjectionPoint("ScriptFiles")

Ezek után ha például egy view-nak szüksége van a jQuery Validate szkript fájljaira, akkor a view-ból szúrjuk be őket a korábban megjelölt helyekre:

@Injector.ScriptFile("~/Scripts/jquery.validate.min.js");
@Injector.ScriptFile("~/Scripts/jquery.validate.unobtrusive.min.js");

Ez persze csak a legegyszerűbb használati eset, meg lehet adni még további paramétereket (például sorrend), valamint beszúrhatunk stíluslapot, szkriptet, meta tag-et, rejtett mezőt, szkript blokkot, vagy bármi mást, hiszen a rendszer jól kiterjeszthető és konfigurálható (például tracing). Minderről elég részletesen tájékoztat a 23 oldalas User’s Guide.

További érdekesség, hogy a visszajelzések alapján Peter összekapcsolta a Content Injectort a Microsoft Web Optimization Frameworkkel, így a ContentInjector.WOF NuGet csomag telepítése után már StyleBundle is ScriptBundle is beszúrható.

Letöltések:

 

Technorati-címkék: ,,