Archiv der Kategorie: knockoutjs

MVC4 WebaAPI und knockoutjs – Teil 3 – Datum und Zeitangaben (moment.js)


Die größten Probleme hatte ich mit knockoutjs inkl. mapping Plugin bisher mit dem Zusammenspiel von MVC Datums und Zeitangaben. Hier habe ich kein Beispiel für meine “Problemstellung” im Netz finden können. Daher versuche ich im Folgenden zu erläutern welche Plugins ich verwendet habe um das Problem zu lösen.

1. Problemstellung

Verwendung von Modeltype “Date” und “Time” im C# Model welches im Formular abgebildet werden soll.

[DataType(DataType.Date)]
[Display(Name = "Geburtsdatum")]
public DateTime Birthdate { get; set; }

[DataType(DataType.Time)]
[Display(Name = "Startzeit")]
public DateTime Starttime { get; set; }

Das Erste Problem ist die Übertragung der Werte im JSON Format. Es muss darauf geachtet werden, das die Datumswerte im ISO-8601 Format umgewandelt werden. Die C# Lib von “Newtonsoft.Json” hilft hier weiter, denn diese verwendet Standardmäßig ISO-8601 bei Datumswerten.

//Umwandeln des Models in ein JSON Objekt mit JsonConvert - ISO Format für DateTime
var mod12 = ko.mapping.fromJS(@(Html.Raw(JsonConvert.SerializeObject(Model))));
ko.applyBindings(mod12);

Damit die Eingabefelder für “Date” auch als Valides Datum erkannt werden habe ich bereits einen Blogpost erstellt, der hier ebenfalls beachtet werden muss.

Aktuell wird im Eingabefeld aber nur das Komplette ISO-8601 Datumsformat angezeigt

“2013-01-10T19:40:19.2353852+01:00”

Damit hier das richtige Datum dargestellt wird muss ein Bindinghandler für “Date” und “Time” erstellt werden. Hier ist es wichtig, das die Werte nicht nur im Browser richtig dargestellt werden, sondern das im knockoutjs Model weiterhin das Datum im ISO-8601 Format vorliegt, denn sonst kann das JSON-Objekt nicht automatisch in unser C# Model Objekt umgewandelt werden.

2. Verwendete JavaScript Addons

Alle Javascript Addons sind per NuGet verfügbar.

  • moment.js für das umwandeln und Berechnen sowie die Formatierte Ausgabe von Datumsangaben in JavaScript. Wie ich finde eine Sehr coole und nützliche Library.
  • knockoutjs für das Modelbinding
  • jQuery als Abhängigkeit und weil wir es auch verwenden.
  • jQuery-Globalize (siehe Blogpost von mir)

3. Umsetzung

Umgesetzt werden soll ein einfaches Formular mit Daten in denen auch die Datentypen (DataType) “Date” und “Time” mit dargestellt werden sollen in einer einfachen Textbox. Dieses Formular wird per Submit Button in einem AJAX Request an eine WebAPI Funktion übergeben werden die als Parameter den Modeltype enthält den wir im Formular dargestellt haben.

Erstellen eines passenden Models in C# welches den DataType “Date” und “Time” besitzt und erstellen eines Passenden Formulars (Views). Im View verwenden wir nicht “value” sondern die Namen unserer CustomModelBinder für knockoutjs.

</pre>
<div class="control-group">@Html.LabelFor(model => model.Birthdate, new { @class = "control-label" })
<div class="controls">@Html.TextBoxFor(model => model.Birthdate, new { data_bind = "dateValue: Birthdate" }) @Html.ValidationMessageFor(model => model.Birthdate)</div>
</div>
<div class="control-group">@Html.LabelFor(model => model.Starttime, new { @class = "control-label" })
<div class="controls">@Html.TextBoxFor(model => model.Starttime, new { data_bind = "timeValue: Starttime" }) @Html.ValidationMessageFor(model => model.Starttime)</div>
</div>
<pre>

Wie bereits weiter oben beschrieben wandeln wir unser C# Model im View in ein JSON Objekt um und binden es an unser knockoutjs Model.

//Umwandeln des Models in ein JSON Objekt mit JsonConvert - ISO Format für DateTime
var mod12 = ko.mapping.fromJS(@(Html.Raw(JsonConvert.SerializeObject(Model))));
ko.applyBindings(mod12);

Erstellen unseres bindingHandlers für den DataType “Date”. Wichtig beim Erstellen der Handler ist zum einen die Verwendung von “ko.bindingHandlers.value.init(…)” und “ko.bindingHandlers.value.update(…)” durch das Aufrufen dieser Funktion werden wohl die Standardevents für den “value” type auf auf meinen Handler angewendet und der Handler wird bei einer Wertänderung automatisch aufgerufen.

Außerdem wird mit Hilfe der Funktion “bindingContext.$data[element.id](date.format());” für unser aktuelles Element das ISO-8601 Datum im Model hinterlegt.

Beim Zurückschreiben des Datums zum Server wird hier die Lokale Client Uhrzeit übergeben, wenn die “alte” Originaluhrzeit wichtig ist, muss das Datum auf Serverseite demensprechend verarbeitet werden.

//Eigene bindinghandler für das Datumsformat für knockoutjs erstellen
ko.bindingHandlers.dateValue = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        //die ValueBindings Handler von Knockout verwenden.
        ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor);

        //Das Passende Ausgabeformat festgelegen.
        var value = valueAccessor(), strDate = ko.utils.unwrapObservable(value);
        if (strDate) {
            var date = moment(strDate);
            if (date.isValid()) {
                $(element).val(date.format(dateformatStr));
            }
        }
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var value = valueAccessor(), strDate = ko.utils.unwrapObservable(value);
        if (strDate) {
            var date = moment(strDate);
            if (!date.isValid()) {
                date = moment(strDate, dateformatStr);
            }

            if (date.isValid()) {
                //Hier kann das Datum direkt als ISO Format String abgelegt werden.
                //das passende Property wird dabei anhand der ElementId ermittelt.
                bindingContext.$data[element.id](date.format());
                //Value binding einstellen - wird benötigt, da sonst unsere custom
                //UpdateFunktion nicht aufgerufen wird, wenn wir unseren Wert ändern.
                ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor);
                //Den Formatierten Wert ausgeben
                $(element).val(date.format(dateformatStr));
                return;
            }
        }

        //Den Standardwert wieder herstellen, der nicht Valide war.
        $(element).val(strDate);
        //Value binding einstellen
        ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor);
    }
};

Erstellen unseres bindingHandlers für den DataType “Time”. Der Time Handler speichert im knockoutjs Model immer das Aktuelle Datum und die in der Oberfläche angegebene Uhrzeit, sollte also auch das Datum wichtig sein, dann muss später beim Auswerten der Daten auf der Serverseite aufgepasst werden und hier z.B. nur die Uhrzeit extrahiert werden.

//Bindinghandler für die Uhrzeit erstellen.
ko.bindingHandlers.timeValue = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        ko.bindingHandlers.value.init(element, valueAccessor, allBindingsAccessor);
        var value = valueAccessor(), strTime = ko.utils.unwrapObservable(value);
        if (strTime) {
            var date = moment(strTime);
            if (date.isValid()) {
                $(element).val(date.format(timeformatStr));
            }
        }
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var value = valueAccessor(), strDate = ko.utils.unwrapObservable(value);

        if (strDate) {
            var date = moment(strDate);
            if (!date.isValid()) {
                date = moment(strDate, timeformatStr);
            }

            if (date.isValid()) {
                //Neues "Datum" anlegen - damit hier die Passende Urhzeit gesetzt werden kann
                //denn wir benötigen ein gültiges ISO Datum für einen erfolgreichen Postback.
                var orgDate = moment();
                //die passenden Stunden und Minuten aus der Textbox setzten und dann Serverseitig die
                //passende Uhrzeit zu "extrahieren"
                orgDate.hours(date.hours());
                orgDate.minutes(date.minutes());
                bindingContext.$data[element.id](orgDate.format());
                ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor);

                $(element).val(date.format(timeformatStr));
                return;
            }
        }

        $(element).val(strDate);
        //Value binding einstellen
        ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor);
    }
};

Der “Rest” bleibt wie gewohnt, d.h. ein einfaches Submit was einen WebAPI Aufruf startet und dann unser Model mit den Daten an unsere C# Funktion übergibt.

//Funktion die aufgerufen wird, wenn das Objekt erfolgreich angelegt werden konnte
function CreatedSuccess(data) {
    //Das Model neu "binden" um die Daten auf der Oberfläche zu aktualisieren
    ko.applyBindings(data);
}

//Submit der Formulars und der Daten aus dem Model
$("form").submit(function (event) {
    event.preventDefault();
    var model = JSON.stringify(ko.toJS(mod12));

    $.ajax({
        //url: "/api/personEditApi/SaveWithParams?take=10&skip=20",
        url: "/api/personEditApi/SavePerson",
        //url: "/api/personEditApi/Save",
        type: "POST",
        data: model,
        contentType: 'application/json; charset=utf-8',
        statusCode: {
            200 /* OK */: function (data) {
                CreatedSuccess(data);
            },
            201 /* Created */: function (data) {
                CreatedSuccess(data);
            },
            //400 /* BadRequest*/: function () {
            //    //Die ModelRevalidation wird automatisch aufgerufen, 
            //    //die Untersuchung auf Bad Request ist nicht notwendig.
            //    //Es muss nur die Klasse "ValidateAttribute" für die 
            //    //WebApi Registriert werden.
            //}
        }
    });
});

4. Quelle und Codeplex Beispiel

Ich bin mir nicht Sicher ob es sich bei meiner Lösung um die Beste Möglichkeit handelt das Problem zu lösen, daher freue ich mich über Kommentare die mir evtl. sagen können ob ich richtig liege oder mir Verbesserungsvorschläge liefern können.

Quelle:

http://denverdeveloper.wordpress.com/2012/10/02/knockout-helpful-date-time-tips/

Codeplex:

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier dann die Wichtigsten Dateien im Webprojekt unter “Views/Home/Person.cshtml”, “App_Start/WebApiConfig.cs” und “Controllsers/PersonEditApiController.cs”

MVC4 WebAPI und knockoutjs – Teil 2 – Validation


Auch wenn man die WebApi benutzt und AJAX Requests ausführt ist es problemlos möglich die Modelvalidierung von MVC zu benutzen. Dafür müssen wir den View wie bisher erstellen und z.B. auf Attribute wie “Required” in unserem Model zurückgreifen.

Im View muss weiterhin “ValidationMessageFor” für das jeweilige Eingabefeld verwendet werden.

</pre>
<div class="control-group">@Html.LabelFor(model => model.Vorname, new { @class = "control-label" })
<div class="controls">@Html.TextBoxFor(model => model.Vorname, new { data_bind = "value: Vorname" }) @Html.ValidationMessageFor(model => model.Vorname)</div>
</div>
<pre>

Um die native Model Validierung von MVC auszuführen/anzuzeigen, gibt es zwei Möglichkeiten.

1. Validierung für alle Requests automatisch einstellen.

Für diese Variante muss ein FilterAttribut erstellt werden und global für alle WebApi Aufrufe registriert werden.

 /// 
 /// ValidationAttribute für WebApi Zugriffe die nicht valide sind.
 /// 
 public class ValidateAttribute : ActionFilterAttribute
 {
     public override void OnActionExecuting(HttpActionContext actionContext)
     {
         if (!actionContext.ModelState.IsValid)
         {
             actionContext.Response = actionContext.Request.CreateErrorResponse(
                 HttpStatusCode.BadRequest,
                 actionContext.ModelState);
         }
     }
 } 

Registrieren des ValidateAttribute, wird bei mir vor dem Registrieren der Routen gemacht

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Registrieren unseres Filters für die Modelvalidation für AJAX Requests
        config.Filters.Add(new ValidateAttribute());

        ......

Wenn jetzt ein AJAX Request mit mit einem “Aktuellen” Model stattfindet z.b. mit “Required” Attribute werden die Fehlermeldungen im Webformular angezeigt und es werden keine Daten am Controller auf dem Server “ankommen”. Es wird vom Server ein StatusCode 400 zurückgegeben, der bei Bedarf auch noch ausgewertet werden kann.

2. Validierung nur für bestimmte Controllerfunktionen durchführen.

Hier wird kein ValidateAttribute angelegt, sondern direkt in der jeweiligen Controllerfunktion eine HttpResponseException ausgelöst, wenn das Model nicht valide ist. Anhand dieser Exception, weiß der Client auch das er einen Fehler anzeigen soll.

public PersonModel Save(PersonModel value)
{
    if (ModelState.IsValid)
    {
        if (value == null)
        {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        //Gibt den Statuscode 200 für OK zurück!
        value.Id = 5;
        return value;
    }

    throw new HttpResponseException(HttpStatusCode.BadRequest);
}

Codeplex:

https://squadwuschel.codeplex.com/

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier dann die Wichtigsten Dateien im Webprojekt unter “Views/Home/Person.cshtml”, “App_Start/WebApiConfig.cs”, “Controllsers/PersonEditApiController.cs” und “Helpers/Filters/ValidateAttribute.cs”

MVC4 WebAPI und knockoutjs – Teil 1 – Modelbinding


Um dynamische und flexible Oberflächen zu erstellen, lohnt es sich einen Blick auf die WebAPI in Verbindung mit knockoutjs zu werfen. Die Webseite von knockoutjs bietet hier bereits einen sehr guten Überblick und eine gute Einführung wie genau knockoutjs funktioniert, daher werde ich darauf nicht im Detail eingehen.

Mit Hilfe von MVC4, WebAPI und knockoutjs lassen sich MVC Oberflächen sehr einfach aktualisieren. Ich versuche es hier mit dem normalen knockoutjs + mapping Plugin, damit ist es möglich direkt das MVC Model in ein knockoutjs Model umzuwandeln.

Das folgende Beispiel zeigt, wie man ein Model erstellt und dann z.B. beim Speichern keinen Postback auslöst, sondern das ganze per Ajax Call abspeichern kann und die Oberfläche aktualisiert.

1. Erstellen eines passenden Models in C#, welches dann in ein JSON Objekt umgewandelt wird.

public class PersonModel
{
        [Display(Name = "ID")]
        public int Id { get; set; }

        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Vorname")]
        public string Vorname { get; set; }

        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Nachname")]
        public string Nachname { get; set; }
}

2. Wir nutzen einen normalen Controller und einen normalen View um unsere Daten in der Bearbeiten Ansicht darzustellen.

Wir passen den View aber bereits an unser knockoutjs Model an, d.h. wir Nutzen TextBoxFor, da wir hier zusätzliche HTML Attribute erstellen können. Wir erstellen hier “data_bind” welches dann im HTML als “data-bind” gerendert wird und setzten den passenden Value der aus unserem Model per knockoutjs angezeigt bzw. aktualisiert werden soll.

<div class="control-group">
     @Html.LabelFor(model => model.Vorname, new { @class = "control-label" })
     <div class="controls">
         @Html.TextBoxFor(model => model.Vorname, new { data_bind = "value: Vorname" })
         @Html.ValidationMessageFor(model => model.Vorname)
     </div>
</div>

3. Anlegen unseres API Controllers.

Hier gibt es zwei Funktionen, die beide das “gleiche” machen. Die Daten die wir mit dem Postback erhalten, werden in unser PersonModel “umgewandelt” und wir können können die Daten jetzt Speichern, …. Wir passen aber nur die ID an und geben unser Aktualisiertes Model wieder zurück, damit wir auch auf der Webseite sehen, das sich die ID geändert hat.

public class PersonEditApiController : ApiController
{
    /// <summary>
    /// POST api/PersonEditApi/SavePerson
    /// </summary>
    public HttpResponseMessage SavePerson(PersonModel value)
    {
        if (ModelState.IsValid)
        {
            if (value == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            value.Id = 5;
            //Wird automatisch in JSON Objekt umgewandelt und gibt den StatusCode 201 für Created zurück!
            HttpResponseMessage response = Request.CreateResponse<PersonModel>(HttpStatusCode.Created, value);
            response.Headers.Location = new Uri(Request.RequestUri, "/api/PersonEditApi/SavePerson/" + value.Id.ToString());
            return response;     
        }

        throw  new HttpResponseException(HttpStatusCode.BadRequest);
    }

    /// <summary>
    /// POST api/PersonEditApi/Save
    /// </summary>
    public PersonModel Save(PersonModel value)
    {
        if (ModelState.IsValid)
        {
            if (value == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            //Gibt den Statuscode 200 für OK zurück!
            value.Id = 5;
            return value;
        }

        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }
}

4. Hinzufügen einer WebApi Route “AlternateWebApi”

Damit wir den PersonEditApiController auch so nutzen können wie wir es bisher von den MVC Controllern gewohnt sind, müssen wir noch eine weitere Route zu unserer WebApi hinzufügen. Denn mit der Standardroute für API aufrufe sind nur GET/POST/DELETE Abfragen möglich. Aber meist hat man mehr wie nur diese “paar” Abfragen die man für einen View benötigt, daher fügen wir noch eine neue Route vor der “DefaultApi” Route hinzu.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        //Alternatives Routing, hier ist es jetzt wieder möglich gezielte Actionen und Controller abzufragen
        //In der DefaultApi kann man nur Get/Post/Delete abfragen und ist damit sehr begrenzt was die Abfragen angeht.
        config.Routes.MapHttpRoute(
            name: "AlternateWebApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        //Standardroute  - hier lässt sich nur GET/POST/DELETE abfragen.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }

5. Anpassen unseres JavaScript Codes im View ‘cshtml’, wo die Daten angezeigt werden sollen.

Das Aktuelle C# Model in ein JSON Model Serealisieren und mit dem knockoutjs Modelmapping direkt ein Model erstellen lassen und dieses an unser Formular binden.

Wenn der User den Submit Button klickt, wird unser knockoutjs Model wieder in JSON umgewandelt und vom JSON Format dann in einen String umgewandelt, welcher per API Aufruf übergeben wird. Aktuell werden hier zwei StatusCodes ausgewertet, da wir den Statuscode 200 bei der “Save” Abfrage zurückbekommen und die 201 bei “SavePerson”. Beide Aufrufe führen aber zum gleichen Ergebnis.

(Achtung es gibt auch eine ko.toJSON() Funktion, diese darf hier NICHT verwendet werden, da sonst ein Model erstellt wird welches nicht Valide mit unserem C# Objekt ist und unsere Funktion in der C# API als Model “null” übergeben bekommt.)

var mod12 = ko.mapping.fromJS(@(Html.Raw(JsonConvert.SerializeObject(Model))));
ko.applyBindings(mod12);

function CreatedSuccess(data) {
    ko.applyBindings(data);
}

$("form").submit(function (event) {
    event.preventDefault();
    var model = JSON.stringify(ko.toJS(mod12));

    $.ajax({
        url: "/api/personEditApi/Save",
        type: "POST",
        data: model,
        contentType: 'application/json; charset=utf-8',
        statusCode: {
            200 /* OK */: function (data) {
                CreatedSuccess(data);
            },
            201 /* Created */: function (data) {
                CreatedSuccess(data);
            }
        }
    });
});

 

Aktuell habe ich noch Probleme beim anzeigen und Konvertieren von Datumsangaben beim Modelbinding, daher verwende ich auch “JsonConvert” und nicht die Standard Json.Serialize Funktion von MS, denn dann werden nur Ticks angezeigt und mit JsonConvert wird das ISO Format für das Datum verwendet. Wenn ich diese Problematik in den Griff bekommen habe, wird es ebenfalls einen eigenen Post geben. Bzw. wenn jemand bereits Erfahrungen damit gesammelt hat freue ich mich gern über einen Kommentar :-).

Codeplex:

https://squadwuschel.codeplex.com/

Dann unter “Source Code” –> “Browse” –> “Testprojekte” –> “Mvc4WebApiKoTb” hier dann die Wichtigsten Dateien im Webprojekt unter “Views/Home/Person.cshtml”, “App_Start/WebApiConfig.cs” und “Controllsers/PersonEditApiController.cs”

Quellen:

http://www.west-wind.com/weblog/posts/2012/Aug/21/An-Introduction-to-ASPNET-Web-API

http://www.asp.net/web-api/overview

http://code.msdn.microsoft.com/ASPNET-Web-API-JavaScript-d0d64dd7/sourcecode?fileId=63598&pathId=212749278

http://knockoutjs.com/index.html