Als je een ASP.NET MVC toepassing maakt moet je meestal gegevens van een controller action doorgeven aan de view. Er zijn verschillende manieren waarop je dat kunt doen. Je kunt een eigen ViewModel-klasse maken, of gebruik maken van de reeds beschikbare objecten TempData, ViewData, en ViewBag. In dit artikel bekijken we de verschillen en overeenkomsten tussen deze opties.

Met een ViewModel-object hebben we een standaard, nette, manier voor het doorgeven van gegevens aan de view. Dat werkt als volgt:

public ActionResult Index()
{
    List<Artikel> model = GetArtikelen();
    return View(model);
}

In deze bovenstaande methode wordt een lijst van artikelen opgehaald, bijvoorbeeld uit een database. Deze lijst wordt vervolgens doorgegeven aan de view in de methode View(). De view Index kan dit model vervolgens weer benaderen via de eigenschap Model.

@model List<Artikel>
<ul>
@foreach(var artikel in Model)
{
  <li>@artikel.Titel</li>
}
</ul>

Alhoewel dit de aanbevolen methode is om gegeven naar de view te sturen is het soms nodig om wat eenvoudige gegevens door te geven. Je moet bijvoorbeeld een goed- of foutmelding doorgeven vanuit de controller zodat deze zichtbaar wordt in de browser. Of je hebt berekende, tijdelijke, of gebruikersspecifieke gegevens die in de view moeten staan. Dit is een reden om uit te wijken naar ViewData, TempData en ViewBag. Deze objecten zijn standaard beschikbaar in controllers en views en hebben hun eigen bijzonderheden en eigenschappen. We bekijken hieronder elk object in detail.

ViewData

Het belangrijkste doel van ViewData is om gegevens van de controller naar de view te sturen. ViewData is een Dictionary-object van het type ViewDataDictionary. Net als ieder ander Dictionary-object kan je het gebruiken om naam-waarde paren in te bewaren. De data in ViewData wordt bij iedere response geleegd. Je moet deze dus op ieder request opnieuw vullen. De volgende code toont een voorbeeld van een bericht dat met behulp van ViewData in de view gebruikt kan worden.

public ActionResult NieuwArtikel()
{
   Artikel model = new Artikel();
   model.Titel = "[nieuw artikel]";
   model.Datum = DateTime.Now;
   ViewData["status"] = "Dit is een nieuw artikel. Vul alle velden in.";
   return View(model);
}

De methode NieuwArtikel() maakt een nieuwe instantie van een Artikel. Deze instantie geldt als het model voor de view. Er wordt daarna een bericht in ViewData opgeslagen met de sleutel status. De methode sluit af door de methode View() aan te spreken met een Artikel-object als parameter.

In de view is het nu mogelijk om zowel het artikel als het extra bericht op te vragen.

@model Artikel

...
<h1>@Model.Titel</h1>
<strong>@ViewData["status"]</strong>
<form>
...
</form>
<p>Datum: @ViewData.Model.Datum</p>
...

Let op de tekst die vetgedrukt is in bovenstaande code. Met behulp van de instructie @model Artikel maken we duidelijk dat het datamodel voor de view een object is van het type Artikel. Dit model kunnen we in de view benaderen via de eigenschap Model, zoals je ziet in de regel met @Model.Titel. Daaronder tonen we de inhoud van het bericht dat we hebben opgeslagen in ViewData. Dit bericht kunnen we ophalen met de standaard syntax voor Dictionary-objecten. De onderste regel is misschien verrassend. Het ViewData-object bevat ook een referentie naar het model via de eigenschap ViewData.Model. Maar in de praktijk zul je dat weinig gebruiken want het model is ook beschikbaar via de Model-eigenschap in de view.

Zoals gezegd is het ViewData-object weer leeg zodra de request-response routine is afgerond. Bij een volgend request moet je dus ViewData opnieuw vullen.

Je bent niet beperkt tot het bewaren van string-waarden in een ViewData-object. Ieder type object kan gebruikt worden. Het is wel van belang om het object in de view te casten naar het type dat je erin gestopt hebt. We kunnen dus het volgende doen in de controller action

Auteur auteur = new Auteur();
auteur.Naam = "Douglas Adams";
auteur.Code = 42;
ViewData["schrijver"] = auteur;

ViewData is een Dictionary-object met een string-waarde als sleutel en een System.Object als waarde. We kunnen er dus van alles in stoppen, maar om het eruit te halen voor gebruik in een view moeten we dit object casten naar het specifieke type dat erin zit.

@{ 
  Auteur auteur = (Auteur)ViewData["schrijver"];
}
<h3> 
 @auteur.Naam (@auteur.Code)
</h3>

Zoals je ziet casten we het object in ViewData eerst naar een Auteur-object, om er vervolgens de eigenschappen Naam en Code van te kunnen gebruiken. Het is ook mogelijk om de typecast te doen via  de instructie ViewData[“schrijver”] as Auteur. Door vervolgens op null te controleren kan je voorkomen dat je eigenschappen probeert te tonen van een object dat niet in ViewData is opgenomen.

ViewBag

ViewBag is een wrapper voor ViewData en geeft je de mogelijkheid om waarden op te slaan en op te halen via een object.eigenschap notatie in plaats van naam-waarde. Hiervoor gebruikt ViewBag het dynamic datatype dat onderdeel is van .NET 4.0. In oudere versies van ASP.NET MVC die gebruik maken van .NET 3.5 is ViewBag dus niet beschikbaar. ViewBag bespaart je de moeite om objecten die zijn opgeslagen te casten naar het gewenste type.

Een ViewBag kan dus als volgt gebruikt worden.

public ActionResult NieuwArtikel()
{
   Artikel model = new Artikel();...   
   Auteur auteur = new Auteur(); ...
   ViewBag.Schrijver = auteur;
   return View(model);
}

Zonder dat we expliciet opgeven dat ViewBag een eigenschap Schrijver heeft kunnen we deze eigenschap wel een waarde geven. De eigenschap wordt in runtime toegevoegd en gevuld met het Auteur-object. Je kunt in een debug-sessie ook het ViewData object bekijken en dan constateren dat ViewData in feite een sleutel “Schrijver” krijgt met het betreffende object als waarde. Alhoewel je dus ViewBag kunt gebruiken om data in ViewData te krijgen is het verstandig om vast te houden aan een enkele syntax, in dit geval dus ViewBag. Het weergeven van de inhoud van een in de ViewBag opgeslagen object gaat als volgt.

<h3>
  @ViewBag.Schrijver.Naam (@ViewBag.Schrijver.Code)
</h3>

Het is niet nodig om het object in een ViewBag te casten naar een Auteur-object om de eigenschappen Naam en Code te benaderen.

Omdat ViewBag in feite een wrapper is voor ViewData geldt ook hier weer dat de data slechts beschikbaar is voor 1 request.

TempData

Net als ViewData biedt ook TempData een Dictionary-gebaseerde opslag van gegevens. Maar het doel van TempData is om gegevens te delen tussen controller actions. De gegevens in TempData worden verwijderd op het moment dat ze gelezen worden of wanneer de sessie van de gebruiker afloopt. Het opslaan van gegevens in TempData is handig bij een redirect, zoals een http 302 response. Dit kan bijvoorbeeld gebeuren bij het verwijderen van een record uit een database. Wanneer je een door middel van een controller action een record uit een database verwijdert kan je niet via deze action een View teruggeven, het model is immers leeg. Bekijk het onderstaande voorbeeld.

public ActionResult VerwijderArtikel(int id)
{
   // verwijder het artikel uit de database. 
   VerwijderArtikel(id);
   return ...;
}

We kunnen het artikel wel verwijderen, maar wat moet de methode nu eigen tot slot doen? De instructie return View() heeft niet zo veel zin, want we hebben geen model. Daarom wordt meestal terug gegaan naar een overzichtsscherm. Maar dat levert een HTTP 302 (redirect) response op, en dan is ViewData (en ViewBag dus ook) niet beschikbaar. We kunnen daarmee dus geen status informatie doorgeven aan de view. TempData levert hiervoor een oplossing, en wel zo.

public ActionResult VerwijderArtikel(int id)
{
   // verwijder het artikel uit de database. 
   VerwijderArtikel(id);
   TempData["status"] = "Artikel " + id + " is verwijderd.";
   return RedirectToAction("Index");
}

Het eerste request is die voor VerwijderArtikel(). In de afhandeling van dit request wordt in TempData een waarde opgeslagen met een bericht. De methode RedirectToAction() levert een 302 response op zodat de browser een tweede request doet. Pas in de response op tweede request willen we de in TempData opslagen waarde gebruiken. Zolang de waarde in TempData niet is gelezen blijft deze beschikbaar. In de view Index kunnen we de waarde ophalen via de volgende code.

@model List<Artikel>
<strong>@TempData["status"]</strong>
<ul>
@foreach(var artikel in Model)
{
  <li>@artikel.Titel</li>
}
</ul>

Zoals gezegd moet je goed opletten waar je TempData gebruikt. Stel je de volgende situatie voor.

public ActionResult BewerkArtikelStap1(int id)
{
  TempData["status"] = "concept";
  Return View();
}
public ActionResult BewerkArtikelStap2(int id)
{
  Return View();
}
public ActionResult BewerkArtikelStap3(int id)
{
  Return View();
}

De view in BewerkArtikelStap1 heeft een hyperlink heeft naar de controller action BewerkArtikelStap2 en deze heeft heeft weer een link naar BewerkArtikelStap3. Wanneer de views van de eerste twee geen gebruik maken van TempData is deze nog steeds beschikbaar in de view voor BewerkArtikelStap3. Maar als een van de eerste twee views wel TempData benaderen is deze niet meer te lezen voor BewerkArtikelStap3.

Er is een mogelijkheid om het object in TempData wel te blijven gebruiken, en daarvoor beschikt deze over de methode Keep(). Door na gebruik de instructie TempData.Keep() of TempData.Keep(sleutel) aan te roepen blijven de gegevens beschikbaar in het volgende request.

Het is belangrijk om te realiseren dat TempData gebruik maakt van het ASP.NET Session-object. Dit heeft consequenties in load balanced omgevingen waarbij je er niet vanuit kunt gaan dat ieder request naar dezelfde server gaan. In dergelijke gevallen moet je ofwel afzien van het gebruik van TempData, sessionstate centraal opslaan of de load balancer configureren voor zgn. sticky sessions.

Net als ViewData is het belangrijk om bij gebruik van de opgeslagen objecten een controle te doen op het type object dat je benadert.

Conclusie

We hebben gezien dat er naast het standaard ViewModel concept ook andere manieren zijn om gegevens naar een view te sturen. Het ViewBag-object is hierbij verleidelijk omdat deze vanwege het dynamische karakter eenvoudig voorzien kan worden van allerhande eigenschappen gevuld met een veelheid aan objecten van verschillende typen.

Het komt de overzichtelijkheid en leesbaarheid van de code niet ten goede wanneer een ViewBag object gebruikt ter vervanging van een ViewModel-klasse. Met een specifieke ViewModel-klasse beschik je over IntelliSense en typechecking zodat deze niet tijdens runtime hoeft plaats te vinden.

Niettemin zijn er voldoende scenarios denkbaar waarbij ViewData, TempData en ViewBag objecten van wezenlijk nut kunnen zijn. We zetten de eigenschappen nog eens op een rij:

  • Met ViewData, TempData en ViewBag kan je gegevens van een controller action naar een view sturen.
  • ViewData en TempData gebruiken een Dictionary object
  • ViewBag profiteert van de dynamic feature van .NET 4.0 waardoor je objecten als eigenschap kunt benaderen.
  • ViewData en ViewBag zijn alleen beschikbaar voor een enkele request/response cyclus
  • TempData kan gebruikt worden in opvolgende requests en wordt geleegd zodat het gelezen is.
  • TempData gebruik het Session-object.