MVC címkéhez tartozó bejegyzések

MVC nézetek fordítása hibamentesen

A Visual Studio és az ASP.NET alapértelmezés szerint nem fordítási időben, hanem futási időben dolgozza fel a MVC nézetekben lévő kódot. Ennek természetesen az az eredménye, hogy ha ott valamilyen hibát vétünk, akkor az csak futási időben fog kiderülni.

Szerencsére van arra lehetőség, hogy rávegyük a Studiot, fordítsa le a nézeteket is a build folyamat részeként. Ehhez nyissuk meg az MVC projekthez tartozó projekt fájlt (.csproj), és írjuk át az MvcBuildViews elem értékét false-ról true-ra:

<MvcBuildViews>true</MvcBuildViews>

Ez meg is oldja a kezdeti problémát, ám gyakran bevezet egy újabbat. Időnként fordításkor vagy a webalkalmazás publikálásakor az alábbi hibaüzenetet kaphatjuk:

It is an error to use a section registered as allowDefinition=’MachineToApplication’ beyond application level.  This error can be caused by a virtual directory not being configured as an application in IIS.

A legborzasztóbb ebben az üzenetben, hogy nem csak az nem derül ki belőle, hogy hol a hiba, hanem az sem, hogy hogyan lehet kijavítani.

A megoldás az obj mappa törlése fordítás előtt, és a hibaüzenet máris megszűnik. Gondoltad volna a hibaüzenet alapján?

Aki nem szeretné mindig kézzel törölgetni ezt a mappát, az persze megteheti a build folyamat első lépéseként is. Szerencsére az MSBuild ad nekünk egy BaseIntermediateOutputPath nevű változót, ami pont az obj mappára mutat, sőt még egy RemoveDir nevű task is van hozzá, így már csak össze kell pakolni őket. Megint nyissuk meg a .csproj fájlt, és vegyünk fel egy RemoveDir elemet a BeforeBuild target-hez:

<Target Name="BeforeBuild">
    <RemoveDir Directories="$(BaseIntermediateOutputPath)" />
</Target>

Akinek esetleg a bin mappával van személyes nézeteltérése, az esetleg használhatja a BaseOutputPath változót.

 

Technorati-címkék: ,,

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

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

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

Optimista konkurenciakezelés ASP.NET MVC-ben

Webes rendszerek esetén nem ritka, hogy ugyanahhoz az adathoz egyszerre több felhasználó fér hozzá és egyszerre módosítják azt. Ha nem teszünk semmit ellene, akkor a későbbi módosítás felülírja a korábbi módosítást, ami az esetek nagy részében nem egyezik a kívánatos eredménnyel. Alapvetően a következő lehetőségeink vannak:

  • Optimista konkurenciakezelés: nem zároljuk az adatot, mert bízunk abban, hogy nem lesz ütközés. Ha mégis előfordul, hogy két felhasználó egyszerre ugyanazt az adatot módosítja, akkor a második módosítás adatbázisba írása előtt figyelmeztetjük a felhasználót, hogy már elavult adatot módosít.
  • Pesszimista konkurenciakezelés: arra számítunk, hogy több felhasználó ugyanazt az adatot akarja módosítani, ezért amint az egyik felhasználó hozzáfér az adathoz zároljuk azt, és addig nem engedünk másik felhasználót hozzá, amíg az első módosítás be nem fejeződik.

Az optimista megoldás hátránya, hogy a felhasználó tipikusan későn értesül arról, hogy feleslegesen dolgozott, mégis sok adat és kevés felhasználó (például admin oldalak) esetén ez a célszerűbb megoldás, ugyanis nem kell zárakkal és azok feloldásával (pl. timeout) vesződnünk.

Az optimista megoldáshoz először is egy olyan mezőre van szükségünk az adatbázisban, ami a rekord módosításakor mindig frissül, azaz egy verzió vagy időbélyeg típusú mezőre. SQL Server esetén pont erre szolgál a timestamp típus, aminek a frissítéséről az SQL Server automatikusan gondoskodik. Tehát csak létre kell hoznunk az oszlopot, a többi megy magától:

VersionNumber timestamp NOT NULL

ORM használata esetén a modell frissítése után a VersionNumbernek byte[] típusú oszlopként kell megjelennie.

Ha egységesen akarunk eljárni és minden olyan táblában, ahol szükség van konkurenciakezelésre ugyanígy nevezzük az oszlopot, akkor célszerű kitalálnunk egy módszert, amivel hivatkozhatunk a “verziózott” táblákra. Ehhez készíthetünk öröklést az ORM modellben, vagy ha a Studio által generált modellt nem akarjuk bántani, akkor interfészt is bevezethetünk:

public interface IVersionedEntity
{
    byte[] VersionNumber { get; set; }
}

Az interfészt a partial osztályoknak köszönhetően külön fájlban alkalmazhatjuk a modellben lévő entitásokra, nem kell belenyúlni a modellbe. Például ha a modellünkben van egy Partner nevű osztály, akkor így “származtathatjuk” az interfészünkből:

public partial class Partner : IVersionedEntity
{
}

Ezzel készen vagyunk az adatbázissal és az adatelérési réteggel.

Következő lépésként fogjuk meg a dolgot a másik végéről, induljunk el a GUI irányából és okosítsuk fel a ViewModelt. Itt a módosítással kapcsolatos VM-ekbe kell tennünk egy VersionNumber tulajdonságot, amit a módosítás előtt feltöltünk az adatbázisból, és az update előtt összevetünk az adatbázisban tárolt értékkel. Itt is választhatjuk az interfészes megoldást, de ha spórolni akarunk a kóddal, akkor alkothatunk ősosztályt is:

public class VersionedObject
{
    [HiddenInput( DisplayValue = false )]
    public byte[] VersionNumber { get; set; }

    public string VersionNumberEncoded
    {
        get
        {
            return Convert.ToBase64String( this.VersionNumber );
        }
    }

}

A VersionNumberEncoded akkor lesz hasznos, ha az értéket például query stringben kell átadnunk, mert egyébként az MVC automatikusan gondoskodik a byte[] típusú érték Base64 kódolásáról, mielőtt a HiddenInput attribútum szerint kiírja egy rejtett mezőbe.

Ezek után a szerkesztéssel kapcsolatos viewmodeleket egyszerűen származtassuk ebből az ősosztályból:

public class EditPartnerVM : VersionedObject
{
    [HiddenInput(DisplayValue = false)]
    public int ID { get; set; }

    public string DisplayName { get; set; }
}

A modellbe így bekerült értékünket ne felejtsük el megutaztatni a kliensre:

@Html.EditorFor( m => m.VersionNumber )

Miután a böngészőből visszajön az adat, a mentés előtt össze kell vetnünk az adatbázisban lévő értékkel. Ehhez készíthetünk például egy CheckConcurrency( VersionedObject viewModel, IVersionedEntity entity ) függvényt, amely összehasonlítja a két paraméter VersionNumber tulajdonságát, és eltérés esetén kivételt dob. Én készítettem egy saját ObjectMissingException osztályt, amit akkor használok, ha az adatbázis rekord null (azaz már törölték a rekordot), és egy ObjectChangedException osztályt, amit akkor, ha a két verziószám eltér egymástól.

Természetesen a felhasználóval valahogy illik tudatnunk, hogy a művelet kudarcba fulladt. Ezt tehetjük például egy saját exception filterrel, ami ezeknél a kivételeknél egy hibaüzenetet tartalmazó view-t jelenít meg:

[AttributeUsage( AttributeTargets.Class | AttributeTargets.Method, 
Inherited =
true, AllowMultiple = false )] public sealed class HandleConcurrencyErrorAttribute :
FilterAttribute, IExceptionFilter { public void OnException( ExceptionContext filterContext ) { if( filterContext.Exception.GetType() == typeof( ObjectChangedException ) ) { filterContext.ExceptionHandled = true; filterContext.Result = new ViewResult { ViewName = "_ObjectChanged" }; } if( filterContext.Exception.GetType() == typeof( ObjectMissingException ) ) { filterContext.ExceptionHandled = true; filterContext.Result = new ViewResult { ViewName = "_ObjectMissing" }; } } }

Legvégül ne felejtsük el ráaggatni ezt a filtert azokra az actionökre, amik a módosítás POST-ját kezelik, hogy a kivétel biztosan ne szálljon egészen a felhasználóig:

[HttpPost]
[HandleConcurrencyError]
public virtual ActionResult EditPartner( EditPartnerVM model )
{ ...

Ha kevés paraméterünk van (például Ajaxos hívások esetén), ne felejtsük el átadni a VersionNumberEncoded értékét az actionnek.

Ti hogyan oldjátok meg ezt a problémát?

 

Technorati-címkék: ,,

T4MVC: ASP.NET MVC stringmentesen

Az ASP.NET MVC egyik kellemetlensége, hogy alapértelmezés szerint sok helyen stringekkel hivatkozunk a controllerekre, actionökre és view-kra. Aki valaha készített már ilyen projektet, az biztosan írt már az alábbihoz hasonló sort egy actionbe:

  return View( "Index" );

vagy épp egy view-ba:

  @Html.ActionLink( "Bejelentkezés", "Login" )

A szépséghibát a stringek jelentik, ugyanis ha ott hibázunk, az csak futási időben derül ki.

Ezen segít a T4MVC projekt, amit legegyszerűbben NuGet csomagként illeszthetünk be az alkalmazásunkba:

t4mvc-nuget

Ha már vannak a projektünkben controllerek, akkor ne lepődjünk meg, hogy a NuGet telepítő egy kicsit átírja őket:

t4mvc-warnings

Sőt kapunk néhány extra fájlt is a projektünkhöz:

t4mvc-files

Cserébe ezek után már típusosan hivatkozhatunk a nézetekre:

  return View( Views.Index );

az actionökre:

  @Html.ActionLink( "Bejelentkezés", MVC.Home.Login() )

és még sok minden másra, amiről bővebben a dokumentációból tájékozódhatunk.

Mivel a háttérben T4 template-ek alapján történik kódgenerálás, felmerülhet a kérdés, hogy mi történik, ha például felveszünk egy új actiont? Azt fogjuk tapasztalni, hogy simán hivatkozhatunk rá view-ból, még az IntelliSense is fel fogja kínálni, csak éppen futási időben fog elszállni ezzel a hibával (itt azért egy pillanatra elgondolkozhatunk, hogy vajon nem épp ezt akartuk-e elkerülni):

T4MVC was called incorrectly. You may need to force it to regenerate by right clicking on T4MVC.tt and choosing Run Custom Tool

Szerencsére nagyon beszédes hibaüzenet, és ha követjük, valóban meg is oldódik a probléma. Ha még ezzel sem szeretnénk foglalkozni, akkor használhatjuk az AutoT4MVC bővítményt, amely figyeli, hogy módosulnak-e a fájljaink, és szükség esetén automatikusan újrafuttatja a kód generátort.

 

Technorati-címkék: ,

Phil Haack az IT Businessben

Záróvizsgára várva kezembe akadt az IT Business egy régebbi száma:

it-business-1

Szoktak lenni benne érdekes cikkek, el is kezdtem olvasgatni, de aztán megakadt a szemem ezen az oldalon, pontosabban a képen (a cikk megtalálható online is):

it-business-2

ASP.NET terén régi motorosok felismerhetik, ő Phil Haack az ASP.NET MVC és a NuGet egyik megteremtője a testvérével. A kép eredetije pedig itt található: http://haacked.com/archive/2008/11/06/usability-vs-discoverability.aspx

Érdekesség, hogy Philnek volt korábban egy Twitter bejegyzése arról, hogy hányan másolják ezt a képet az engedélye nélkül és hány autóvezetős oldalon lehet megtalálni.

 

Technorati-címkék: