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: ,

2 thoughts on “Ajaxos fájl feltöltés

  1. szelpe

    Nagyon király példa, több új ötletet is adtál.

    Következő lépés a progress bar kirajzolása aszinkron fájlfeltöltéshez, ezt nem tudom meg lehet-e egyáltalán oldani.

  2. Gábor Fazekas

    fineuploader.com
    Én ezt használom , szuper szimpla, pár matatás a kliensen , szerveren a fogadó method kitöltése, és már érkezik is az anyag akár multiupload és progressbar üzemmódban is.
    A másik közkedvelt játékos az swfupload(nekem egy kicsit heavy-nek tűnik).

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s