jsTreeview in ASP.NET MVC gevuld met Json action

Op dit moment heeft jQuery UI nog geen treeview control. Als alternatief zijn er enkele andere aanbieders van Treeview controls. In het verleden heb ik al eens bericht over de Telerik controls. Deze kan ik zonder meer aanbevelen maar helaas zijn de gratis extensies alleen voor open source projecten toe te passen.

Een andere treeview is de jsTreeview. Deze is heel aardig (ondersteunt ajax, de jQuery themes en bv. ook checkboxes). Helaas is de deocumentatie nogal fragmentarisch. Daarom geef ik hier een opstap hoe jsTreeview is toe te passen in ASP.NET MVC. We maken hierbij gebruik van Json actions om de kinderen op te vragen vanuit de controller.

Stap 1: Bestanden plaatsen

Haal eerst de laatste versie op vanaf http://www.jstree.com/ . Dit is de versie pre 1.0 fix 2.

Plaats in het ASP.NET MVC4 project de jquery.jstree.js in de \Scripts map.

Plaats d.gif (voor ie6 gebruik), d.png, styles.css en throbber.gif in \Scripts\themes\treeview map.

Stap 2: bundles aanpassen

Vul de bundles aan in de BundleConfig.cs:

bundles.Add(new StyleBundle("~/Scripts/themes/treeview").Include(
                         "~/Scripts/themes/treeview/style.css"));
bundles.Add(new ScriptBundle("~/bundles/jqueryplugins").Include(
                        [skip other plugins],
                        "~/Scripts/jquery.jstree.js"));

En deze bundles moeten op de _Layout.cshtml aangeroepen worden:

…
 @Styles.Render("~/Scripts/themes/treeview")
…
</head>
<body>
…
 @Scripts.Render("~/bundles/jquery", "~/bundles/jqueryui", "~/bundles/jqueryplugins")
…

Stap 3: HTML placeholder

Plaats een placeholder op de _Layout.cshtml

<div id="treeviewEdit"> Tree view placeholder</div>

Stap 4: JavaScript

Plaats daarna het volgende  script onder aan de _Layout.cshtml pagina  om te worden uitgevoerd als de pagina laadt (dit moet dus in een javascript blok):

$(function () {
    $("#treeviewEdit").jstree({
        "json_data": {
            "ajax": {
                "url": "/Home/FillEditTree",
                "data": function (n) {
                    return { id: n.attr ? n.attr("id") : 0 };
                }
            }
        },
        "themes": {
            dots: false,
            icons: true
        },
        "plugins": ["themes", "json_data"]

    });
});

Deze jQuery functie zal op de treeview div losgelaten worden om het treeview tekenen te forceren.

Stap 5: de (view) modellen

We praten straks in Json formaat met de treeview, maar we willen dit in typed classes samenstellen. Voeg dus de volgende viewmodellen toe:

jstreeview1

De code wordt dan:

public class JsTreeModel
{
    public JsTreeData data { get; set; }

    public JsTreeAttribute attr { get; set; }

    // "open" or "closed" or null (not available for ajax)
    public string state { get; set; }

    public List<JsTreeModel> children { get; set; }
}
public class JsTreeAttribute
{
    public string id { get; set; }
}
public class JsTreeData
{
    public string title { get; set; }

    public JsTreeDataAttribute attr { get; set; }

    // "folder"  or "/" for no icon thus file
    public string icon { get; set; }
}
public class JsTreeDataAttribute
{
    public string href { get; set; }
}

Laatste stap 6: Controller action

Voeg de volgende action toe aan de controller. Deze geeft ter demonstratie als antwoord altijd de opgevraagde node met daaronder twee kinderen. De eerste keer wordt node 0 opgevraagd. het eerste kind heeft zelf ook kinderen (niet meegestuurd in deze aanroep) en de tweede heeft geen kinderen:

public ActionResult FillEditTree(int id)
{
    // just to show some nodes
    var rootId = id + 2;

    var children = new List<JsTreeModel>();

    // first child containing one or more children
    children.Add(
        new JsTreeModel
            {
                attr = new JsTreeAttribute { id = (rootId + 1).ToString() },
                data = new JsTreeData
                        {
                            title = "Child " + (rootId + 1).ToString(),
                            attr =
                                new JsTreeDataAttribute
                                    {
                                        href =
                                            @"\home\edit\"
                                            + (rootId + 1).ToString()
                                    }
                        },
                state = "closed",
            });

    // second child containing NO children
    children.Add(
        new JsTreeModel
            {
                attr = new JsTreeAttribute { id = (rootId + 2).ToString() },
                data = new JsTreeData { icon = "/", title = "Child " + (rootId + 2).ToString(), },
            });

    // de ‘root’
    var requestedSubTree = new List<JsTreeModel>();

    requestedSubTree.Add(
            new JsTreeModel()
            {
                attr = new JsTreeAttribute { id = rootId.ToString() },
                state = "open",
                data =
                    new JsTreeData
                        {
                            title = "Title " + rootId.ToString(),
                            attr =
                                new JsTreeDataAttribute { href = @"\home\edit\23" }
                        },
                children = children
            });

    return Json(requestedSubTree, JsonRequestBehavior.AllowGet);
}

Dit geeft het volgende plaatje (na enkele keren kinderen opvragen):

jstreeview2

Er zijn een aantal zaken die opvallen:

  • De ‘root’ moet standaard de state geopend (“open”) krijgen
  • Je moet dus onderzoeken of de getoonde kinderen op hun beurt ook kinderen hebben
    • De kinderen moeten standaard de state te openen (“closed”) krijgen als deze op hun beurt kinderen hebben.
    • Een kind zonder kinderen moet geen state krijgen (dus null). Dan komt er ook geen ajax call mogelijkheid.

Bonus stap 7: jQuery Thema

Als laatste in het rijtej plugins kan “themeroller” toevoegen worden.  Deze kan je toepassen als alternatief voor thema:

$(function () {
    $("#treeviewEdit").jstree({
        [skip],
        "themes": {
            dots: false,
            icons: true
        },
        "plugins": ["json_data", "themeroller"]
    });
});

Dit geeft een aardige uitbreiding op het standaard aanwezige thema:

jstreeview3

Bonus stap 8: cookies

Je kunt ook plugin “cookies” toepassen. Dan wordt bij een F5 de selectie bewaard en niet opnieuw opgevraagd. Maar de laatste kan ook op de server in de sessie onthouden worden en bij de initiële aanroep (node id is 0) toegepast kunnen worden.

Conclusie 

Deze plugin is heel aardig om voor weinig geld en snel een treeview toe te passen in ASP.NET MVC.

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

Advertenties

Unittests voor MVC4 model validaties

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

Dit is een vervolg op mijn eerdere blog: Custom client side validaties in ASP.NET MVC4

Als men vroeger (..) een MVC2 voorbeeld applicatie liet genereren dan kreeg men daar ook enkele custom validaties bij. Zo was er een validaties op het invoeren van tweemaal eenzelfde wachtwoord bij registratie. Dit voorbeeld werkte op class niveau om beide properties te vergelijken.

Een subtiele verbetering met de komst van MVC3 was dat dit opeens op property niveau ingesteld kon worden. Hierdoor was het mogelijk om de tekst van de validatie naast het tweede wachwoord veld te tonen.

Helaas wordt deze custom validatie niet meer gegenereerd maar is deze in het ASP.NET MVC framework als de CompareAttribute opgenomen. Gelukkig is ASP.NET MVC tegenwoordig open source en kan de broncode gedownload worden van http://aspnetwebstack.codeplex.com/SourceControl/changeset/view/3007f73f8cb2 (adres kan veranderen).

Door hier eens in te duiken heb ik een vergelijkbare validatie gebouwd. Deze controleert ook twee velden en wel of deze in de juiste volgorde gevuld zijn. Het tweede veld mag niet gevuld worden als de eerste nog niet gevuld is.

valunit1

De client side code is als volgt:


[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class CompareSequenceAttribute : ValidationAttribute, IClientValidatable
{
  private const string DefaultErrorMessage = "Fields {0} and {1} must be filled in the right sequence.";

  public CompareSequenceAttribute(string otherProperty)
    : base(DefaultErrorMessage)
  {
    if (otherProperty == null)
    {
      throw new ArgumentNullException("otherProperty");
    }

    OtherProperty = otherProperty;
  }

  public string OtherProperty { get; private set; }

  public string OtherPropertyDisplayName { get; internal set; }

  public override string FormatErrorMessage(string name)
  {
    return String.Format(CultureInfo.CurrentCulture, ErrorMessageString,
              name, OtherPropertyDisplayName ?? OtherProperty);
  }

  protected override ValidationResult IsValid(object value,
                         ValidationContext validationContext)
  {
    PropertyInfo otherPropertyInfo =
              validationContext.ObjectType.GetProperty(OtherProperty);

    if (otherPropertyInfo == null)
    {
      return new ValidationResult(String.Format(CultureInfo.CurrentCulture,
                     "Unknown {0} to compare with", OtherProperty));
    }

    object otherPropertyValue = otherPropertyInfo.GetValue(
                            validationContext.ObjectInstance, null);

    if (!string.IsNullOrWhiteSpace(value as string)
         && (string.IsNullOrWhiteSpace(otherPropertyValue as string)))
    {
      if (OtherPropertyDisplayName == null)
      {
        OtherPropertyDisplayName = ModelMetadataProviders.Current.
             GetMetadataForProperty(() => validationContext.ObjectInstance,
                  validationContext.ObjectType, OtherProperty).GetDisplayName();
      }

      return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
    }

    return null;
  }

  public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
                    ModelMetadata metadata, ControllerContext context)
  {
    if (metadata.ContainerType != null)
    {
      if (OtherPropertyDisplayName == null)
      {
        OtherPropertyDisplayName = ModelMetadataProviders.Current.
                 GetMetadataForProperty(() => metadata.Model,
                         metadata.ContainerType, OtherProperty).GetDisplayName();
      }
    }

    var rule = new ModelClientValidationRule()
    {
      ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
      ValidationType = "comparesequence",
    };

    rule.ValidationParameters.Add("other", OtherProperty);

    yield return rule;
  }
}

De bijbehorende JavaScript zier er dan zo uit:

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

$(function () {
  jQuery.validator.addMethod('compareSequence', function (value, element, param) {
    var other = param.other;
    var otherInput = $(element.form).find(":input[id='" + other + "']")[0];

    if (value != null
      && value.length > 0
      && (otherInput.value == null || otherInput.value == '')) {
        return false;
    }

    return true;
  });

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

    options.rules['compareSequence'] = params;

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

Maar kunnen we deze code ook unittesten? Ja, en best eenvoudig!

De client side script unittesten is hier dan niet mee ondersteund maar de server side validatie test zal ik tonen. Overigens, heeft iemand ervaring met JavaScript unittest frameworks?

Nu is het zo dat een Validatie class een publieke IsValid methode heeft.  Helaas is het simpel aanroepen van die IsValid() methode niet voldoende, Deze validatie heeft namelijk een ValidationContext nodig om bij de waarde van de andere property te komen en die context is lastig te injecteren.

En een viewmodel met een attribuut er op mocken is ook lastig. Dus ik heb maar voor stubs gekozen.De stub ziet er dan zo uit:



public class ViewModelToValidateCompareSequenceStub
{
  public string FirstPropertyName { get; set; }

  [CompareSequence("FirstPropertyName")]
  public string SecondPropertyName { get; set; }
}

En hiermee kan ik prima unittesten. Hier is een test waarbij de validatie goed gaat:

[TestMethod]
public void CompareSequenceFirstFilledSecondFilledReturnsTrue()
{
  //// ARRANGE

  var model = new ViewModelToValidateCompareSequenceStub {
        FirstPropertyName = "Bla", SecondPropertyName = "Bla" };

  //// ACT

  var actual = Validator.TryValidateObject(model,
            new ValidationContext(model, null, null), null, true);

  //// ASSERT

  Assert.IsTrue(actual);
}

En hier is een test waarbij het valideren fout gaat en dus een false teruggeeft:

[TestMethod]
public void CompareSequenceFirstNullSecondFilledReturnsFalse()
{
  //// ARRANGE

  var model = new ViewModelToValidateCompareSequenceStub {
                                   SecondPropertyName = "Bla" };
  var results = new List<ValidationResult>();

  //// ACT

  var actual = Validator.TryValidateObject(model,
         new ValidationContext(model, null, null), results, true);

  //// ASSERT

  Assert.IsFalse(actual);
  Assert.IsNotNull(results);
  Assert.AreEqual(1, results.Count);
  Assert.AreEqual(
    "Fields SecondPropertyName and FirstPropertyName must be filled in the right sequence.",
                             results[0].ErrorMessage);
}

Zoals te zien is, test ik ook op de specifieke foutmeldingen. Ik kan bv. ook een melding krijgen dat de opgegeven property niet bestaat. En er kunnen zelfs meerdere validaties gefaald hebben.

Conclusie

Het unittesten van validaties op viewmodellen is eenvoudig als men gebruik maakt van stubs. Enige nadeel is dat er geen controle is over de waarden die op de attributen ingesteld worden. Er moet dus voor andere varianten aparte stubs ingevuld worden.

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.

Schrijf je in voor events in KnockoutJs

In mijn vorige blog heb ik laten zien hoe KnockoutJs fraai in Asp.Net MVC3 integreert. Zonder de werking van MVC3 te ondermijnen, kan nu voorkomen worden dat je JavaScript een spaghetti wordt.

KnockoutJs is namelijk een MVVM implementatie waardoor de opmaak (de DOM elementen) en de achterliggende JavaScript code van elkaar gescheiden worden. Wijzigingen in de DOM (invoer van tekst, een checkbox aanvinken, een button click, etc.) worden opgepakt door het ViewModel van KnockoutJs en andere controls reageren op die ViewModel wijzigingen door bindings.

Prima. Voorbeeldje:

Je kan met het omzetten van een checkbox een boolean waarde in het ViewModel manipuleren (via een binding) en die wordt dan weer ‘geobserveerd’ door een div. Die div wordt dan namelijk (afhankelijk van de boolean waarde) zichtbaar of onzichtbaar via de Visible binding.

(de onderstaande codevoorbeelden zijn aanpassingen op het voorbeeld uit de andere blog)

Op de Edit.cshtml zetten we een div met de binding:

<div data-bind="visible: IsScary">   
  Did you know? Addams's original cartoons were one-panel gags. 
  <br/>
  The characters were undeveloped and unnamed until later versions.
</div>

Dit werkt prima, maar wat als je die visible/invisible had willen oplossen met jQuery? Dan had je in code een .toggle() willen uitvoeren, of het willen oplossen via een .show() en een .hide() met eventueel een vertraging.

Een FOUT alternatief zou dan zijn om gebruik te maken van een extra dependentObservable…

Op de Edit.cshtml staat nu gewoon een div zonder binding:

<div id="didYouKnow">   
  Did you know? Addams's original cartoons were one-panel gags.
  <br/>
  The characters were undeveloped and unnamed until later versions.
</div>

En in de home.edit.js is het KnockoutJs ViewModel uitgebreid:

$(function () {
    var clientViewModel = ko.mapping.fromJS(serverViewModel);
 
    //skip some code
 
    clientViewModel.DoSomethingWhenTheScarybooleanChanges 
                      = ko.dependentObservable(function () {
        if (this.IsScary()) {
            $('#didYouKnow').show(500);
        }
        else {
            $('#didYouKnow').hide(250);
        }
        return 'dummy';
    }, clientViewModel);
 
    ko.applyBindings(clientViewModel);
});

Dit werkt prima. De div verdwijnt sneller dan dat deze weer getoond wordt als het vinkje aangezet wordt.

Pas dit niet toe!

Er is nu een directe koppeling tussen het ViewModel en een control op de pagina. Als het control verdwijnt of hernoemd wordt, dan moet het VIEWMODEL aangepast worden.

Het zelfde geldt voor code in de lees en schrijf ‘methodes’ op de property (read en write callbacks; zie example 1 in de KnockOut documentatie).

Er is een simpele en elegante oplossing!

Kijk, uiteindelijk moeten we ergens de code schrijven voor de show en de hide; daar is geen ontkomen aan. Maar we willen die code loskoppelen van het ViewModel. Het ViewModel mag hoogstens een seintje geven dat er iets veranderd mag worden. Maar wat er gaat veranderen, dat speelt zicht BUITEN het ViewModel af.

Wat we willen is een Event. En KnockoutJS kent events (zie Explicit Subscribing to Observables (onderaan de pagina))

Herschrijf de vorige code  naar de nieuwe ViewModel invulling:

$(function () {
    var clientViewModel = ko.mapping.fromJS(serverViewModel);
 
    // skip some code
 
    var isScarySubscription = 
        clientViewModel.IsScary.subscribe(OnIsScareChanged);
 
    ko.applyBindings(clientViewModel);
});
 

function OnIsScareChanged(isScary) {
    if (isScary) {
        $('#didYouKnow').show(500);
    }
    else {
        $('#didYouKnow').hide(250);
    }
}

Wat we hier zien is dat op het boolean veld IsScary een ‘event’ OnIsScaryChanged wordt geabonneerd. Dit abonnement wordt vertegenwoordigd door de variabele isScarySubscription. En we zien dat aan de functie heel netjes een boolean isScary wordt meegegeven die de nieuwe waarde van de checkbox bevat.

De functie met daarin directe referenties naar controls is nu dus netjes losgekoppeld van het ViewModel. Sterker nog, we kunnen de functie ook ‘fysiek’ loskoppelen van het ViewModel via code. Voer maar eens net achter de applyBindings de volgende regel uit:

isScarySubscription.dispose();

Als we nu de checkbox van waarde veranderen, dan gebeurt er met de div… helemaal niks. Het event is namelijk net zo snel ontkoppeld als weer gekoppeld aan de IsScary ‘property’.

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!

Een iPhone WebApp ‘in a snap’

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

Het bouwen van een app voor Apple devices hoeft niet moeilijk te zijn. Als je een native App wilt bouwen voor de iPhone/iPad/iPod dan komt daar al snel ‘kosten’ bij om de hoek kijken:

          Apple hardware om op te bouwen ($$$ + $$$)

          Keuze voor Apple development tools of Mono Touch ($$$)

          Licence om op de apple store te mogen uitrollen ($$$)

          Eventuel een nieuwe programmeertaal + Api leren ($$$)

Maar het kan ook eenvoudiger. Apple devices laten het toe om webpagina’s direct te openen vanaf het hoofdscherm, fullscreen dus zonder zoekbalk. Als zo’n pagina een Apple skin voert (via CSS) en leuke overgangen naar andere pagina’s (transities) simuleert, dan lijkt het een native app is. Pas als je echte integratie met het device wilt (zoals toegang tot de map met plaatjes) dan ga je nat J

En het mooie is dat het gewoon kan met wat HTML, Javascript en een onderliggende server laag zoals PHP, Java of .Net. En dat voor het prijsje van 0$.

Dus je wilt een webapp bouwen. Er zijn tientallen gelijkwaardige bibliotheken (ik heb ook nog eens naar IUI gekeken) maar hier toon ik hoe het kan met jQTouch en Asp.Net MVC3.

jQTouch is vooral voor de (Apple) webkit browsers en dan nog voor de iphone. Voor de ipad is jqMobile wellicht een betere keuze maar daar is (nog) weinig documentatie voor. Heb je geen iPhone om je creaties op te testen? Gebruik dan een lokale Safari browser of gebruik een andere browser die html 5 aankan. Het zou overigens ook op Android kunnen werken met uitzondering van enkele transities die de grafische processor aanspreken.

Leuk, maar waar moet je beginnen als je echt een applicatie wil bouwen? In onderstaande POC wordt een MVC3 site tot een web app omgetoverd met een homescherm, schermovergangen (transities) enkele invoervelden, en een ajax call om eea. uit te voeren.

We beginnen met een standaard, lege, MVC 3 webproject:

 

Voeg vervolgens een HomeController toe, die een eenvoudige Index actie heeft.

Voeg ook voor die Index methode een view toe. Zie dat we een masterpage aanroepen.

Voordat we naar de code kijken, moeten we eerst jQTouch toevoegen aan het project. Download deze vanaf https://github.com/senchalabs/jQTouch/zipball/b3.1 en pak het even uit. Plaats vervolgens:

In de Context map Vul deze aan met de jQTouch themes mappen (hele structuurtje even overnemen) en jqtouch.css .Verwijder de aanwezige .css
In de Scripts map Vul deze aan met jquery-1.4.2 (standaard meegeleverd bij jQTouch, de huidige 1.6.2 versie werkte bij mij niet correct) en de jqtouch.js

Ok, alles staat klaar om aangeroepen te gaan worden. Dus pas daarna de _Layout.cshtml in de View|Shared map aan:

<!DOCTYPE html>
<html>
  <head>
    <link href="@Url.Content("~/Content/jqtouch.css")"
       rel="stylesheet" type="text/css" media="screen" />
    <link href="@Url.Content("~/Content/themes/apple/theme.css")"
       rel="stylesheet" type="text/css" media="screen" />
    <script src="@Url.Content("~/Scripts/jquery-1.4.2.min.js")"
       language="JavaScript" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jqtouch.js")"
       language="JavaScript" type="text/javascript"></script>
    <script type="text/javascript" charset='utf-8'>
      jqt = $.jQTouch({
        icon: '@Url.Content("~/Content/themes/icon.png")',
        fullScreen: true,
        preloadImages: [
    '@ Url.Content("~/Content/themes/apple/img/chevron.png")',
    '@ Url.Content("~/Content/themes/apple/img/whiteButton.png")',
    '@ Url.Content("~/Content/themes/apple/img/backButton.png")',
    '@ Url.Content("~/Content/themes/apple/img/activeButton.png")',
    '@ Url.Content("~/Content/themes/apple/img/loading.gif")'
        ]
      });
      // Determine if iPhone, Android or Desktop OS and
      // setup the right click-event ("tap" vs "click").
      var userAgent = navigator.userAgent.toLowerCase();
      var isiPhone = (userAgent.indexOf('iphone') != -1 ||
        userAgent.indexOf('ipod') != -1) ? true : false;
      clickEvent = isiPhone ? 'tap' : 'click';
    </script>
  </head>
  <body>
    <div id="jqt">
      @RenderBody()
    </div>
  </body>
</html>

Hier worden de links naar CSS en Javascript gezet. Zie ook dat er een div met als naam jqt in staat. Deze is later nodig als de invulling van de body (in index.cshtml) geplaatst wordt. Dit is een standaard dingetje voor jqTouch, vergeet deze div niet!

De tap vs. Click code is overgenomen uit http://www.timo-ernst.net/2010/08/tutorial-a-simple-twitter-client-with-jqtouch/. Handig als je ook Android wilt ondersteunen.

Vervolgens gaan we de index.cshtml, de View aanpassen:

@ {Layout = "~/Views/Shared/_Layout.cshtml";}
<div id="home">
  <div>
    <h1>Welcome</h1>
  </div>
  <h2>Welcome here</h2>
  Please go to the next page to enter some information.<br />
  <ul>
    <li><a href="#input">Start entering some info</a></li>
  </ul>
</div>
<div id="input" >
  <div>
    <a id="backFromInput" href="#home">back</a>
    <h1>Start</h1>
  </div>
  <h2>Start entering</h2>
  Please enter the requested information:
  <br />
  <ul>
    <li>Name:<br /><input type="text" name="name"
          placeholder="name" id="name" /></li>
    <li>Phone number:<br /><input type="tel" name="phone"
          placeholder="Phone" id="phone" /></li>
    <li>Zipcode:<br /><input type="number" name="zip"
          placeholder="Numbers" id="zip" /></li>
    <li>Email address:<br /><input type="email" name="Email"
      placeholder="Please enter your email address" id="email" /></li>
    <li><input type="checkbox" name="Agree" title="I accept" id="agree" /></li>
  </ul>
  <a href="#" style="margin: 10px;" id="btnStartCallback">Start callback</a>
</div>
<div id="waiting" >
  <div>
    <h1>Waiting</h1>
  </div>
  <h2><center>Please wait while we do something...</center></h2>
  <center>
    <img src="@ Url.Content("~/Content/themes/apple/img/loading.gif")"
         alt="wait" />
  </center>
</div>
<div id="success" >
  <div>
    <h1>Succeeded</h1>
  </div>
  <h2><center>Entering succeeded<center/></h2>
  <center>Thank you for entering the info.</center>
  <center>You can now close this app.</center>
</div>
<div id="error" >
  <div>
    <h1>Oops</h1>
  </div>
  <h2><center>Oops, an error...</center></h2>
  <center>An error occurred. Please contact the administrator...</center>
  <center><label id="errorInfo"></label></center>
</div>
<script type="text/javascript" charset='utf-8'>
  // first hide the submit button
  $("#btnStartCallback").hide();
  // enable or disable the callback button
  $('#agree').click(function () {
    if ($('#agree').attr("checked")) {
      $("#btnStartCallback").show();
    }
    else {
      $("#btnStartCallback").hide();
    }
  });
  // Attach an event when starting screen
  $('#btnStartCallback').bind(clickEvent, function (e) {
    var name = $('#name').val() + "";
    var phone = $('#phone').val() + "";
    var zip = $('#zip').val() + 0;
    var email = $('#email').val() + "";
    var dataToSend = { 'name': name, 'phone': phone,
                            'zip': zip, 'email': email };
    var url = "@Url.Action("DoSomething", "Home")";
    $.ajax({
      url: url,
      data: dataToSend,
      dataType: 'json',
      type: 'POST',
      beforeSend: function () {
        // before callback, show the waiting animation
        jqt.goTo('#waiting', 'dissolve');
      },
      success: function (result) {
        if (result.Succeeded) {
          // after a successfull callback
          jqt.goTo('#success', 'slideleft');
        }
        else {
          // after an unsuccessful callback
          $('#errorInfo').text("Invalid input");
          jqt.goTo('#error', 'slideleft');
        }
      },
      error: function (xhr, err) {
        // after an error
        $('#errorInfo').text("Status: " + xhr.status);
        jqt.goTo('#error', 'slideleft');
      }
    });
  });
</script>

Hier zien we de standaard opzetje die veel van dit soort webapp bibliotheken gemeen hebben. Dit zie je ook terug in b.v. jQMobile en IUI. Alles schermen die straks getoond moeten gaan worden, de verschillende pagina’s, worden in één index.cshtml opgenomen als verschillende divs:

We hebben hier vijf pagina’s:

1.       Home; De openingspagina; Bevat alleen een lijstje met navigatie buttons (ééntje)

2.       Input; Invoer van gegevens en een verstuurknop

Dit is overigens geen form oplossing en er is dus ook geen echte submit-button maar we gebruiken een url gestyled als een button. Deze traps dan een ajax call af. Eventueel kan men terugnavigeren naar de Home pagina. De invoer controls zijn ook zo ingesteld dat ze hun eigen specifieke toetsenbord opvragen. De button is pas zichtbaar als de gebruiker de ‘voorwaarden’ geaccepteerd heeft.

3.       Waiting; Dit scherm wordt getoond tijdens de callback van de ajax call.

Een animated gif doet het altijd leuk hier. jQTouch levert er al eentje bij.

4.       Success; De actie is geslaagd. De gebruiker mag de applicatie sluiten.

5.       Error; Deze pagina wordt getoond als de ajax call fout gaat.

De overgangen zijn via jqtouch functies opgelost. Ik mag zelfs de transities aangeven J.

Blijft nog over de controller. Deze is erg kaal:

public class HomeController : Controller
{
  public ActionResult Index()
  {
    return View();
  }
  [AcceptVerbs(HttpVerbs.Post)]
  public ActionResult DoSomething(string name, string phone,
                                    int zip, string email)
  {
    var myJsonResult = new MyJsonResult();
    try
    {
      // Do something
      Thread.Sleep(2000);
      // throw new ArgumentException("boo");
      myJsonResult.Succeeded = true;
    }
    catch (Exception)
    {
      myJsonResult.Succeeded = false;
    }

    return new JsonResult { Data = myJsonResult };
  }
}
public class MyJsonResult
{
  public bool Succeeded { get; set; }
}

Hier zien we de aanroep van het Index scherm. Er is geen tweede
Index action gedefineerd voor een eventuele submit van het Index scherm, want
die is er niet. Als de ‘submit’ knop ingedrukt wordt dan wordt de ajax call
uitgevoerd die vervolgens de DoSomething aanroept. Hieracher kan code
uitgevoerd worden en eventueel informatie teruggeven aan de callback. Hier geef
ik alleen een seintje terug of de denkbeeldige functionaliteit werkt. Dit wordt
dan vertaald in de juiste transitie in door de javascript code achter de callback.

Conclusie

Voordat ik er uit was hoe ik deze structuur moest opzetten was ik enkele uurtjes verder. Doe er dus je voordeel mee. Diezelfde blog die me op de ‘tab’ wees vertelde dat een transitie via code niet mogelijk was. Dus wel. Kijk maar in de jqtouch.js.

Natuurlijk kan dit nog veel fraaier gebouwd worden. Het is met jQTouch mogelijk om de ‘GPS’ locatie op te vragen die de browser beschikbaar kan stellen. jQTouch kan ook reageren op het kantelen van toestellen. Het is geen volledige integratie met het toestel maar het is een goede start.

Dus hoe ziet het resultaat er uiteindelijk uit? Hieronder toon ik hoe het er in de Safari browser uit ziet (links) en daarnaast de printscreen van een iPad met het uiteindelijke resultaat (rechts).

Safari
Browser
Ipad
 
 

Voor jQMobile zou de code er bijna hetzelfde uit moeten zien. Heb je dit uitgeprobeerd? Dan zie ik jou bevindingen graag terug in de comments hieronder.

Beter uitzicht op het Builden van views

Zo, de zomervakantie is weer over. Iedereen is weer aan het werk of zit nog in een ontkennende fase.

Dit keer wil ik jullie deelgenoot maken van de grootste prestatie-verbetering binnen ons project. Het stelt op zich niks voor (een boolean omzetten) maar deze fix scheelt ons veel (her-)testwerk.

Brekende wijzigingen in MVC Viewmodellen tov. Views worden namelijk standaard niet door de compiler herkent. Als een (kapotte) aanroep van een property uit een viewmodel niet meer aansluit op de werkelijkheid (de property is weggehaald of hernoemd) dan kom je daar pas bij het testen op de testserver of productie achter. De compiler negeert deze afwijkingen volkomen.

Een demo…

Maak een MVC3 (internet applicatie template). Het mag ook een MVC2 applicatie zijn.

Creeer daarna een viewmodel:

public class MyViewModel
{
    public int MyFirstProperty { get; set; }
    public int MySecondProperty { get; set; }
}

Deze bevat nu twee properties. Hercompileer het project om het model beschikbaar te maken voor de Create View wizard.

Nu gaan we een View op de home controller aanmaken (Create view). Eerst wordt de create.cshtml gegeneerd in de \views\Home map. Let er op dat deze de Create scaffold template toepast:

En nu moeten we op de Home controller de Create methode toevoegen:

public class HomeController : Controller
{
    ...
    public ActionResult Create()
    {
        MyViewModel myViewModel = new MyViewModel();
        myViewModel.MyFirstProperty = 42;
        myViewModel.MySecondProperty = 43;
        return View(myViewModel);
    }
}

Test nu of alles aan elkaar gekoppeld is. Navigeer naar de /home/Create actie:

Ok. De pagina werkt, de twee waarden zijn al vooraf ingevuld.

Sluit nu de applicatie om terug naar VS2010 te gaan. Nu gaan we in het model een property hernoemen.

public class MyViewModel
{
    public int MyFirstProperty { get; set; }
    // renamed only here in the model
    public int MyThirdProperty { get; set; }
}

Hercompileer eens. Dan volgt er deze foutmelding:

Error 1 ‘MvcApplication2.Models.MyViewModel’ does not contain a definition for ‘MySecondProperty’ and no extension method ‘MySecondProperty’ accepting a first argument of type ‘MvcApplication2.Models.MyViewModel’ could be found (are you missing a using directive or an assembly reference?) d:\Projects\testpath\MvcApplication2\MvcApplication2\Controllers\HomeController.cs

Maar helaas is dit alleen een melding over de aanroep in de Home controller! Maar in de view staat nog enkele verkeerde aanroepen, die worden genegeerd. Want als de view nogmaals aangeroepen wordt:

We krijgen dus een vette foutmelding, runtime! Dus de viewengine code wordt niet gecontroleerd door de compiler.

Dit is wel op te lossen! Hiervoor is een eenvoudige oplossing…

Open de project properties (Edit ProjectFile):

Pas hier de optie MvcBuildViews aan. Pas de waarde aan door deze op ‘true’ te zetten.

Herlaadt het project…

En hercompileer het project…

En nu komen de fouten in de view wel naar voren:

Error 1 ‘MvcApplication2.Models.MyViewModeldoes not contain a definition for ‘MySecondProperty’ and no extension method ‘MySecondProperty’ accepting a first argument of type ‘MvcApplication2.Models.MyViewModel’ could be found (are you missing a using directive or an assembly reference?)

d:\Projects\testpath\MvcApplication2\MvcApplication2\Views\Home\Create.cshtml

Etc…

De project optie MvcBuildViews forceert dat VS2010 de view al hercompileert. Dit betekent wel dat het compileren iets langer gaat duren maar zorgt tevens voor minder testwerk en een betere uitrol.