Archiv für den Monat Januar 2013

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”