Custom client side validaties in ASP.NET MVC4

Seeing anything you like? Google translate does not work out? Drop me a note and I will translate this post.

Kijk voor een vervolg op deze blog naar: Unittests voor MVC4 model validaties

Eén van de meest onderschatte aspecten van gebruikersvriendelijke websites is het toepassen van validaties. Validaties kunnen teruggevonden worden op drie locaties:

  1. Validaties diep de businesslaag (of zelfs in de datalaag)
  2. Validaties op het ASP.NET MVC (view-)model welke op de server gevalideerd worden door de controller
  3. Validaties welke op de client uitgevoerd worden via JavaScript.

De validaties uit 1 zijn lastig om te valideren zonder een daadwerkelijke aanroep van de onderliggende lagen van de website.

Voor de twee andere varianten geldt dat niet. De validaties uit 2 zijn geschreven in .Net code, de validaties uit 3 zijn geschreven in JavaScript. ASP.NET MVC maakt het mogelijk om eenzelfde soort validatie aan elkaar te laten refereren. Hierdoor kan via de Razor view engine de HTML/Javascript voor de client side validatie aangemaakt worden als een eigenschap op het (view-)model een server side validatie implementeert…

Hier volgt een stappenplan voor het schrijven van een Custom Client Side Validatie.

Stap 1

Eigenlijk overbodig, deze opties staan tegenwoordig standaard aan,  maar in de web.config moet het toepassen van client validaties aan staan. Controleer dus of de volgende twee opties op true staan:


<appSettings>

…

<add key="ClientValidationEnabled" value="true" />

<add key="UnobtrusiveJavaScriptEnabled" value="true" />

</appSettings>


Stap 2

Laten we eerst de server side validatie aanmaken. Ik heb als voorbeeld bij gebrek aan inspiratie een validatie geschreven die de minimale lengte van een string property controleert. De server side validatie is volgt:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ValidateMinimalLengthAttribute : ValidationAttribute
{
    private const string DefaultErrorMessage = "{0} must be at least {1} characters long.";

    public ValidateMinimalLengthAttribute()
        : base(DefaultErrorMessage)
    {
    }

    public int MinimalLength { get; set; }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString, name, MinimalLength);
    }

    public override bool IsValid(object value)
    {
        var valueAsString = value as string;

        return (string.IsNullOrEmpty(valueAsSString)) || valueAsString.Length >= MinimalLength;
    }
}

Stap 3

Deze validatie is direct toe te passen op ons model:

public class CustomerModel
{
    public int Id { get; set; }

    public string Name { get; set; }

    [Display(Name = "City")]
    [ValidateMinimalLength(MinimalLength = 10)]
    public string Address1City { get; set; }
}

Stap 4

En deze validatie kan op de controller gecontroleerd worden:

public class CustomerController : Controller
{
    public ActionResult Create()
    {
        return View(new CustomerModel());
    }

    [HttpPost]
    public ActionResult Create(CustomerModel model)
    {
        if (ModelState.IsValid)
        {
            // model is valid
        }
        else
        {
            // model is invalid
        }

        return View();
    }
}

Het toevoegen van server side validaties is dus tamelijk eenvoudig. Laten we nu aan de client side validatie gaan werken…

Stap 5

Om te beginnen moeten we aan de bestaande validatie kennis meegeven dat er een client side validatie beschikbaar komt. We laten de ValidateMinimalLengthAttribute de interface IClientValidatable implementeren:


[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class ValidateMinimalLengthAttribute : ValidationAttribute, IClientValidatable
{
  …

  public override string FormatErrorMessage(string name)
  {
    …
  }

  public override bool IsValid(object value)
  {
    …
  }

  public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
    ModelMetadata metadata,  ControllerContext context)
  {
    //// from metadata: ShortDisplayName > DisplayName > PropertyName
    var messageNameDisplayName = (!string.IsNullOrWhiteSpace(metadata.ShortDisplayName))
               ? metadata.ShortDisplayName
               : metadata.DisplayName;

    var messageName = (!string.IsNullOrWhiteSpace(messageNameDisplayName))
               ? messageNameDisplayName
               : metadata.PropertyName;

    var rule = new ModelClientValidationRule()
    {
      ErrorMessage = FormatErrorMessage(messageName),
      ValidationType = "minimallength",
    };

    rule.ValidationParameters.Add("minlength", MinimalLength);

    yield return rule;
  }
}

Deze implementeert de methode GetClientValidationRules welke een opsomming van uit te voeren validaties opgeeft. In ons geval geven we aan dat er op de client een validatie genaamd “minimallength” gerefereerd moet worden en we laten de eventueel te tonen errormessage doorgeven. Ook laten we MinimalLength integer doorgeven. Deze informatie zal door de Razor viewengine gebruikt worden om de HTML aan te maken voor deze unobtrusive validatie. We definiëren in de view:

<div>
  @Html.LabelFor(model => model.Address1City)
</div>
<div>
  @Html.EditorFor(model => model.Address1City)
  @Html.ValidationMessageFor(model => model.Address1City)
</div>

De uiteindelijk gegenereerd HTML ziet er dan zo uit:

<div>
  <label for="Address1City">City</label>
</div>
<div>
  <input data-val="true"
         data-val-minimallength="City must be at least 10 characters long."
         data-val-minimallength-minlength="10"
         id="Address1City"
         name="Address1City"
         type="text"
         value="" />
  <span data-valmsg-for="Address1City"
        data-valmsg-replace="true"></span>
</div>

Let op! Wat we hier zien zijn slechts enkele extra HTML attributen op een input veld. Dit is nog niet voldoende voor de validatie want we hebben nog niet eens de javascript functie geschreven!

Laten we dus snel verder gaan.

Stap 6

We moeten dus een functie schrijven die de lengte valideert en deze functie moet geregistreerd worden onder de naam ‘minimallength’. De volgende javascript voldoet hieraan:

// validation name must be equal to the ValidationType in the ModelClientValidationRule

$(function () {
    jQuery.validator.addMethod('minimalLength', function (value, element, param) {
        var minlength = param.minlength;

        if (value != null && value.length > 0 && value.length < minlength) {
            return false;
        }
        return true;
    });

    jQuery.validator.unobtrusive.adapters.add('minimallength', ['minlength'], function (options) {
        var params = {
            minlength: options.params.minlength
        };

        options.rules['minimalLength'] = params;

        if (options.message) {
            options.messages['minimalLength'] = options.message;
        }
    });
} (jQuery));

Wat we hier zien is de functie (met de hoofdletter in de naam) en de registratie ervan. Interessant is om te zien hoe de parameter ‘minlength’ doorgegeven wordt tijdens de registratie, tesamen met de message.

Stap 7

Maar deze code moet wel op de client beschikbaar gesteld zijn, en wel na het laden van bibliotheken van de jQuery validaties en de unobtrusive code.

Hiervoor kan handig meegelift worden met de ‘~/bundles/jqueryval’ bundle. In de bundle configuratie vullen we de validatie bundle aan met ons eigen script (deze heb ik in een aparte Validations map gestopt):

bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
            "~/Scripts/jquery.unobtrusive*",
            "~/Scripts/jquery.validate*",
            "~/Validations/MinimalLength.js"
            ));

En, heel prettig, laat deze bundle nu net al standaard gerefereerd en dus geladen worden door views welke met de standaard Create en Edit templates aangemaakt zijn:

@model MvcApplication1.Models.CustomerModel
@{
    ViewBag.Title = "Create";
}
…
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Stap 8

Zo, nu kan alles getest worden. Start de website en ga naar de pagina waarin de validatie actief moet zijn. Controleer eerst even of de data-val attributen geplaatst zijn. Probeer vervolgens eens de client side validatie uit:

clientsideval1

Bonus

Ok, het werkt prima, maar hoe valideer je nu of de server side variant ook nog werkt? Schakel hiervoor in de web.config de ClientValidationEnabled uit. Hierdoor blijft de attributen voor unobtrusive script dus achterwege:

<div class=”editor-label”>
  <label for="Address1City">City</label></div>
<div>
<input id="Address1City"
       name="Address1City"
       type="text"
       value=""   />
<span>City must be at least 10 characters long.</span>
</div>

Conclusie

Client side validatie geeft een hele goede web-ervaring voor gebruikers van jouw site. Met de komst van MVC 3 is het toepassen van unobtrusive validaties een eenvoudige en mooie aanvulling van toe te passen technieken geworden voor de moderne webontwikkelaar. En deze validaties zijn keer op keer her te gebruiken over meerdere projecten.

Integratie van Knockout in Asp.Net MVC3


In need of an English translation? Please drop me a note!

Wie door mijn blog heen bladert ziet dat ik < understatement >redelijk enthausiast</understatement> over Asp.Net MVC ben. Ik vind het een framework wat echt zijn nut bewijst wat betreft snelheid van ontwikkelen, onderhoudbaarheid en flexibiliteit. Natuurlijk is het nog een redelijk jonge implementatie en kan het altijd beter. En wat ik eigelijk het meest ‘irritante’ vind, dat is de JavaScript.

Nu heb ik altijd geroepen dat JavaScript de plakband is die een controls op een webpagina bij elkaar moeten houden en had er ook een haat-liefde verhouding mee. Met de komst van jQuery en nog enkele andere (open souce) bibliotheken is het toch nog goed gekomen tussen mij en JavaScript.

Maar met de komst van jQuery en het gemak waarmee Ajax calls gemaakt worden, komt er wel een gevaar om de hoek: Al die Javascript, verdeelt over meerdere bestanden en over nog meer functies, kan echt spaghetti worden. Vooral als in de functies weer doorverwezen wordt naar de controls op het scherm…

Maar het kan anders! Opeens kwam KnockoutJs  om de hoek.

KnockoutJs is gebouwd op vier peilers:

  1. Declarative Bindings
  2. Automatic UI Refresh
  3. Dependency Tracking
  4. Templating

Dit maakt het mogelijk om MVVM toe te passen in JavaScript. Klinkt moeilijk, is het ook best wel maar ik heb onderstaand voorbeeld uitgewerkt en dit moet je flink op weg helpen.

En hou in je hoofd: we veranderen niks aan de manier waarop je Asp.Net MVC toepast. Het enige wat we willen voorkomen is spaghetti in JavaScript.

Start dus een nieuw Asp.Net MVC3 project:

We gebruiken de lege template:

Vervolgens werken we even de Nuget packages bij met de laatste versies.

Voor mij was het voldoende om de laatste jQuery te installeren. De gelieerde packages worden gewoon bijgewerkt. De jQuery Visual Studio IntelliSense kan daarna gedeinstalleerd worden, dit is in de originele jQuery package opgenomen. En vergeet niet om ook de Knockoutjs package en de Knockout.Mapping package toe te voegen. Nu moeten de volgende packages geïnstalleerd zijn:

De EntityFramework package en Modernizr package zijn out-of-scope voor deze blog.

Vervolgens moet de _Layout.cshtml bijgewerkt worden. (in MVC2 zou dit de master page zijn geweest)

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>@ViewBag.Title</title>
  <link href="@Url.Content("~/Content/Site.css")"
        rel="stylesheet"
        type="text/css" />
  <script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")"
          type="text/javascript"></script>
   <script src="@Url.Content("~/Scripts/knockout-1.3.0beta.js")"
           type="text/javascript"></script>
   <script src="@Url.Content("~/Scripts/knockout.mapping-latest.js")"
           type="text/javascript"></script>
   @RenderSection("Scripts", false)
</head>
<body>
  @RenderBody()
</body>
</html>

Tip: Controleer nog even of hier de juiste versies van de bestanden genoemd zijn. Er komen regelmatig nieuwe versies uit, tenslotte.

Wat hier opvalt is dat we een extra Scripts sectie toegevoegd hebben. Deze zal straks toegepast gaan worden binnen de View.

Maar eerst gaan we de controller en de ViewModel op de server creeeren.

Als ViewModel heb ik object genomen die een gezin moet voorstellen. Dit gezin woont op een adres:

public class HomeEditViewModel
{
  public int Id { get; set; }
  public string FamilyName { get; set; }
  public bool IsScary { get; set; }
  public Address Address { get; set; }
}

public class Address
{
  public string Street { get; set; }
  public int Number { get; set; }
}

Omdat ik een scherm wil demonstreren waarin een ViewModel wordt onderhouden, is dit een redelijk normale situatie die wel vaker voorkomt.

Voeg nu  een lege controller toe:

De controller ziet er (ingevuld) als volgt uit:

public class HomeController : Controller
{
  public ActionResult Edit()
  {
    var model = new HomeEditViewModel();
    model.Id = 42;
    model.FamilyName = "The Adams family";
    model.IsScary = true;
    model.Address = new Address {
            Street = "Cemetery Lane", Number = 1313 };
    return View(model);
  }

  [HttpPost]
  public ActionResult Edit(HomeEditViewModel model)
  {
    if (!ModelState.IsValid)
    {
      // do something with the data provided
    }

    return View(model);
  }
}

De essentie is hier dat er data verzameld wordt en dat de View hiermee gevuld gaat worden door de ViewEngine. Ik heb de PostBack methode toegevoegd zodat te controleren is of de Submit van een gewijzigde View ook correct verwerkt wordt.

Nu wordt het spannend , we gaan de View aanmaken. Voeg deze toe aan de Edit Methode:

Tot zover is alles nog een redelijk normale MVC3 applicatie.

Laten we nu eens naar de gegenereerde View gaan kijken. Ik heb de standaard gegeneerde view vervangen door onderstaande code:

@model KnockoutMvcApplication.Models.HomeEditViewModel
@{
 ViewBag.Title = "Demo of MVC edit view combined with KnockoutJs (MVVM implemention)";
}
@section Scripts{
 <script src="@Url.Content("~/Scripts/home.edit.js")"
         type="text/javascript"></script>
 <script type="text/javascript">
   var serverViewModel = @Html.Raw(Json.Encode(Model));
 </script>
}

<h2>MVC edit view combined with KnockoutJs (MVVM implemention)</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"
        type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")"
        type="text/javascript"></script>

@using (Html.BeginForm()) {
  @Html.ValidationSummary(true)
  <fieldset>
  <legend>KnockoutJS</legend>
  @Html.HiddenFor(model => model.Id)
  @Html.LabelFor(model => model.FamilyName)
  @Html.TextBoxFor(model =>
    model.FamilyName, new { data_bind = "value: FamilyName" })
  <label>@Html.CheckBoxFor(model =>
    model.IsScary, new { data_bind = "checked: IsScary" })Is scary</label>
  <hr/>
  @Html.LabelFor(model => model.Address.Number)
  @Html.TextBoxFor(model =>
    model.Address.Number, new { data_bind = "value: Address.Number" })
  <br/>
  @Html.LabelFor(model => model.Address.Street)
  @Html.TextBoxFor(model =>
    model.Address.Street, new { data_bind = "value: Address.Street" })
  <p>
  <input type="submit" value="Save" />
  <button data-bind="click : doSomethingNifty">Do something nifty here!</button>
  </p>
  </fieldset>
}

Family info: <span data-bind="text: FamilyInfo"></span>
<br/>
Full address: <span data-bind="text: FullAddress"></span>
<br/>

Wat we zien is dat het scherm netjes het ViewModel onderhoudt. De ID is hidden maar de familienaam en adres kunnen gemanipuleerd worden (validatie velden zijn hier weggelaten). We gebruiken hier GEEN gebruik van EditorFor maar van TextBoxFor. Hier is een reden voor! De magie van KnockOut is afhankelijk van de (hier aan de TextBoxFor toegevoegde) HTML5 attribuut DATA-BIND. De ViewEngine liet bij de EditorFor de hier toegevoegde DATA-BIND achterwege. TextBoxFor kent dit probleem niet.

Ook is er met die data-bind iets aan de hand. Zoals je ziet, schijf is die DATA-BIND met een underscore. Dit is een limitatie van Visual Studio, die accepteert geen mintekens in attributen maar een underscore _ wordt door de viewengine automatisch naar een minteken – omgezet.

Vanaf hier begint de knockout implentatie. In dit scherm heb ik drie schermelementen opgenomen die geinteresseerd zijn in wijzigingen op het scherm:

  1. Er is een span opgenomen die geïnteresseerd is in “FamilyInfo”. Die wilt dus bijgewerkt worden als de Familinaam aangepast wordt.
  2. Er is een span opgenomen die geïnteresseerd is in “FullAddress”. Die wilt dus bijgewerkt worden als het adres aangepast wordt.
  3. Er is een button op het scherm (naast de submit button) die op de clickactie de functie “doSomethingNifty” uitvoert

Normaal zouden hiervoor dus drie functies geschreven moeten worden die aan de ‘click’ of ‘change’ van bepaalde controls gehangen zouden worden. Dit zijn dus de controls die de gebruiker aanpast. Welke controls worden er dan gewijzigd? Dat wordt dan in de code bijgehouden…

Enter KnockoutJS. Zoals hier zichtbaar is, hebben we alleen data-bind attributen aan enkele controls toegevoegd.

Dan volgt nu de laatste stap.

We hebben bovenaan in een Scripts sectie (zie de _Layout.cshtml) een verwijzing naar een javascript bestand opgenomen: /Scripts/home.edit.js.

Hier kom ik zo op terug. En we laten de viewengine het totale model omzetten naar een javascript object. Dit is een heel belangrijke stap. De data op van het ViewModel is dus geplaatst in de controls (textboxes, etc.) en het is straks in het browser-geheugen beschikbaar.

Voordat we hier verder op door gaan, wat staat er in de JavaScript:

/// <reference path="jquery-1.6.4-vsdoc.js" />
/// <reference path="knockout-1.3.0beta.debug.js" />
/// <reference path="knockout.mapping-latest.debug.js" />

$(function () {
  var clientViewModel = ko.mapping.fromJS(serverViewModel);

  clientViewModel.FullAddress = ko.dependentObservable(function () {
    return this.Address.Number() + ' ' + this.Address.Street();
  }, clientViewModel);

  clientViewModel.FamilyInfo = ko.dependentObservable(function () {
    return this.Id() + ' ' + this.FamilyName();
  }, clientViewModel);

  clientViewModel.doSomethingNifty = function () {
    if (this.IsScary()) {
      alert("Booh!");
    }
    else {
      alert("I could do some Ajax stuff here with Id "
                  + this.Id() + ' and family ' + this.FamilyName());
    }
  };

  ko.applyBindings(clientViewModel);
});

De referenties bovenaan zijn puur om betere intellisence op de code te krijgen

Wat we hier zien is dat als de webpagina geheel in de browser geladen is, via Knockout Mapping het javascript object met ons server model omgezet wordt naar een Knockout Observable object. Dat is handig en scheelt een hoop handmatig werk.

Aan deze clientViewModel worden twee extra ‘dependent observable’ properties toegevoegd en er wordt een extra functie doSomethingNifty toegevoegd. De functie kan via de ‘this’ bij alle andere properties. Deze drie zagen we ook al genoemd worden bij de bindings op de controls.

En uiteindelijk zien we dat onze clientViewModel dus voor Knockout beschikbaar wordt gesteld. Hierdoor ontstaat binnen de browser een twee-weg binding tussen de inhoud van bv. de textboxen en een verbinding tussen de functie en de button-click.

Alle javascript logica is dus nu gecentraliseerd binnen dit clientviewmodel.

Laten we nu eens kijken of het allemaal werkt. We starten de pagina in de browser

Wat we al gelijk zien is dat de Family info en de Full Address onderaan gesynchroniseerd zijn.

Als we enkele velden wijzigen dan worden de twee spans (en ook alleen die spans) netjes bijgewerkt.

Een click op de extra knop leest ook netjes het Knockout viewmodel uit:

Alles bijbehorende javascript voor deze pagina is dus netjes in één JavaScript bestand beschikbaar met een correcte scheiding tussen de logica (het knockout viewmodel) en de representatie (de controls op de pagina). Geen spaghetti meer!

Hoe nu verder?

Kijk nu nog eens naar de filmpjes

http://channel9.msdn.com/posts/ASPNET-MVC-With-Community-Tools-Part-11-KnockoutJS

http://channel9.msdn.com/Events/MIX/MIX11/FRM08/

* Helaas kan ik Channel9 filmpjes niet direct integreren 😦

Dus laat je inspireren hoe jij dit framework gaat toepassen.

Er is natuurlijk wel die overhead van een extra object en de mapping. Persoonlijk gebruik ik Knockout vooral op pagina’s waar flink interactief gewerkt wordt, en dit zijn meestal maar een handvol pagina’s binnen een website. De overige pagina’s hebben geen interactie of slechts minimaal, in dat geval is Knockout wat overkill. Maar maak steeds de afweging: als ik nu nog geen Knockout op deze pagina toepas, kan ik het dan straks nog makkelijk integreren?

Succes met KnockOut!

English: Liked the examples but could not understand the Dutch gibberish? Want to know more? Please contact me!

Liever lui dan moe: combineer MVC2 met Dynamic Data Web App

Bij mijn huidige MVC2 project zijn we in de testfase aanbeland. En we zitten
nu met een dilemma.

Hoewel MVC2 vooral voor CRUD applicaties toegepast zal worden, hoeft dit nog niet te betekenen dat ook alle vier de acties: create, read, update en delete geïmplementeerd gaan worden. De daadwerkelijke functionaliteit van de applicatie bepaalt namelijk wat er met de data gebeurt.

Lui

Dit maakt het functioneel testen dus niet eenvoudiger. Een applicatie welke alleen invoegen, wijzigen en tonen ondersteunt is heel erg cru (..) voor de tester. Hoe kan de functionaliteit getest worden rond het representeren van een lege selectie of tabel? Wat zal een gridview tonen: “geen data aanwezig”? Of zal er een foutmelding getoond worden? Dit is lastig te testen zonder dat de mogelijkheid bestaat om ook eens een record te verwijderen. Hetzelfde geldt voor het wijzigen van records waarvoor alleen invoeg-functionaliteit is geboden.

Daarom heb ik even een onderzoekje gedaan of ik een Dynamic Data Web
Application (DDWA) hiervoor kom toepassen. Ik heb hier in het verleden al eens over geblogd en dit leek mij wel weer een poging waard.

Nou, om kort te gaan, het kan en het is bijzonder eenvoudig.

Open dus eerst de VS2010 solution met daarin jouw MVC2 applicatie.
Voeg dan een DDWA project toe, in dit geval eentje voor het Entity
framework.

Voeg DDWA toe

In de meeste MVC2 applicaties is de Entity Framework context binnen
die applicatie gecreëerd. Voeg dus een referentie toe van het MVC2 project aan
het DDWA project.

Onthoudt nog even wat de naam is van de context. In mijn geval was
dat “DatabaseEntities”.

In de global.asax van moet vervolgens die regel uit het commentaar
gehaald worden en voorzien worden van de context naam:


DefaultModel.RegisterContext(typeof(DatabaseEntities), new
ContextConfiguration() { ScaffoldAllTables = true });

Standaard staat ScaffoldAllTables op false. Zet deze op true. Dit
forceert dat alle tabellen standaard beschikbaar zijn voor onderhoud via deze
DDWA applicatie.

Als laatste moet wel dezelfde database aangeroepen worden. Kopieer
dus de Entity Framework connectionstring uit de MVC2 applicatie en plaats deze
in de DDWA web.config:

<connectionStrings>

<add    

name=”DatabaseEntities”

connectionString=”metadata=res://*/Models.Model1.csdl|res://*/Models.Model1.ssdl|

res://*/Models.Model1.msl;provider=System.Data.SqlClient;

provider connection string=&quot;Integrated Security=SSPI;Persist
Security Info=False;

Initial Catalog=DATABASE.MDF;Data
Source=.\sqlexpress&quot;”

providerName=”System.Data.EntityClient”/>

</connectionStrings>

Let op, de database moet via een server gedeeld worden. Het
dynamisch koppelen van een Sql Server Express database (b.v. “Data
Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database1.mdf” ) werkt niet en zal bij de DDWA tot een vage foutmelding leiden.

En dat is alles. Makkelijk.

Nu kan via de DDWA applicatie snel onderhoud gepleegd worden op de (test) database. En nu kan ook snel inzichtelijk gemaakt worden hoe de ingevoerde informatie in de database opgeslagen wordt. Handig toch?