8

While playing around with MVC 4's new single page application tooling, I noticed that none of the examples I have found contains an example of a DateTime being updated back through the WebApi. I soon found out why.

I started by generating the standard SPA from the template provided. I then opened up TodoItem.cs and added a DateTime field. Then I generated the controller as instructed by the comments. (Without the datetime field, everything works just fine).

After everything generated, I started the app and navigated to the controller index (I called the controller "tasks"). I got the grid page with 0 records as expected and clicked on the add button. I was taken to the edit page as expected and entered some data including a date in my shiny new datetime field. Then clicked save.

An error was produced that said:

Server error: HTTP status code: 500, message: There was an error deserializing the object of type System.Web.Http.Data.ChangeSetEntry[]. DateTime content '01/01/2012' does not start with '/Date(' and end with ')/' as required for JSON.

It would appear that the tooling doesn't support DateTime yet. I'm sure I could go through and spend a bit of time figuring it out and getting it to work, but I thought I may find a bit of luck here with someone who has already fixed this problem and can provide insight.

Anyone already battled this?

Update: I am adding more information I have found since asking this. I tried using JSON.Net as my Formatter as suggested below. I think that will be the eventual solution, however, just doing as the poster below recommended is not enough.

When using the JSON.Net serializer, I get the following error:

This DataController does not support operation 'Update' for entity 'JObject'.

The reason is that JSON.Net doesn't fully populate the object that the formatter is trying to deserailize to (System.Web.Http.Data.ChangeSet).

The json that is sent in is:

[{"Id":"0",
  "Operation":2,
  "Entity":
    {"__type":"TodoItem:#SPADateProblem.Models",
     "CreatedDate":"/Date(1325397600000-0600)/",
     "IsDone":false,
     "Title":"Blah",
     "TodoItemId":1},
  "OriginalEntity":
    {"__type":"TodoItem:#SPADateProblem.Models",
     "CreatedDate":"/Date(1325397600000-0600)/",
     "IsDone":false,
     "Title":"Blah",
     "TodoItemId":1}
}]

The built in Json Formatter is able to reconstitute this Json into a ChangeSet object with embeded TodoItem objects in the Entity and OriginalEntity fields.

Has anyone gotten JSON.Net to deserialize this properly?

tpeczek
  • 23,867
  • 3
  • 74
  • 77
Brian McCord
  • 4,943
  • 7
  • 32
  • 44
  • Showing some code allowing to reproduce your problem would really be helpful. – Darin Dimitrov Feb 27 '12 at 22:59
  • Unfortunately, I would need to provide the entire solution. The Single Page Application template provided generates a lot of code. To reproduce the error, just create a new MVC 4 Single Page Application. Once the tooling finishes, open up the TodoItem.cs and add a datetime field. Then right click "Controllers" and add a controller. Choose a name for it, select TodoItem as your model, the SPA template and let it create a new context. Once that is done, run the application and navigate to the new controller. Click on add and enter data then click save. – Brian McCord Feb 27 '12 at 23:04

4 Answers4

3

The problem is that in the current beta, ASP.NET Web API uses DataContractJsonSerializer, which has well-known problems with serialization of DateTime. Here is a quiet recently raised bug on Microsoft Connect for the issue; MS responds that they already have a bug tracking the issue but it won't be fixed in the .Net 4.5/VS11 timeframe.

Fortunately you can substitute an alternative JSON serializer, such as James Newton-King's excellent JSON.Net.

Henrik Nielsen on the ASP.NET team has an excellent blog post showing how you can use JSON.Net with ASP.NET Web API. Here is his implementation of a MediaTypeFormatter that uses JSON.Net (it would also need to be wired up to the ASP.NET Web API configuration, Henrik's blog demonstrates that as well).

public class JsonNetFormatter : MediaTypeFormatter
{
    private readonly JsonSerializerSettings settings;

    public JsonNetFormatter(JsonSerializerSettings settings = null)
    {
        this.settings = settings ?? new JsonSerializerSettings();

        SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
        Encoding = new UTF8Encoding(false, true);
    }

    protected override bool CanReadType(Type type)
    {
        return type != typeof(IKeyValueModel);
    }

    protected override bool CanWriteType(Type type)
    {
        return true;
    }

    protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext)
    {
        var ser = JsonSerializer.Create(settings);

        return Task.Factory.StartNew(() => {
            using (var strdr = new StreamReader(stream))
            using (var jtr = new JsonTextReader(strdr))
            {
                var deserialized = ser.Deserialize(jtr, type);
                return deserialized;
            }
        });
    }

    protected override Task OnWriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext, TransportContext transportContext)
    {
         JsonSerializer ser = JsonSerializer.Create(settings);

         return Task.Factory.StartNew(() =>
         {
              using (JsonTextWriter w = new JsonTextWriter(new StreamWriter(stream, Encoding)) { CloseOutput = false})
              {
                   ser.Serialize(w, value);
                   w.Flush();
              }
         });
    }
}    
Community
  • 1
  • 1
James Webster
  • 4,046
  • 29
  • 41
  • I think this may be close to the correct answer, but it doesn't seem to work with the scenario I laid out above. After using this code and plugging it into the webapi, the following error is triggered when trying to save: Server error: HTTP status code: 500, message: This DataController does not support operation 'Update' for entity 'JObject'. This may be a problem with upshot.js, but I haven't been able to figure out where this error is coming from yet. – Brian McCord Feb 28 '12 at 14:38
  • I have had to pretty much abandon this whole path. Json.Net seemed to be the answer, but getting it to respect the Json format that upshot.js wanted in the Single Page Application example code was a major headache that I just couldn't get to work quite right. At the end of the day, I can do it faster in MVC3 and wait for MVC4 to hopefully add support for these things. I'm going to mark this as the answer since the poster demonstrated it as a bug. – Brian McCord Feb 29 '12 at 19:33
  • Thanks for accepting the answer; might be worth reaching out to JSON.Net and raising a bug to see if JSON.Net can be fixed (or submit a patch!) – James Webster Feb 29 '12 at 23:40
1

I was having the exact same problem. I spent too much time trying to get json.net to work. I finally came up with this workaround that you would stick in TodoItemsViewModel.js in the example project:

    self.IsDone = ko.observable(data.IsDone);
    self.EnterDate = ko.observable(data.EnterDate);
    self.DateJson = ko.computed({
        read: function () {
            if (self.EnterDate() != undefined) {
                 var DateObj = new Date(parseInt(self.EnterDate().replace("/Date(", "").replace(")/", ""), 10));    //.toDateString();
                var ret = DateObj.getMonth() + 1 + "/" + DateObj.getDate() + "/" + DateObj.getFullYear();
                return ret;
            }
            else {
                return self.EnterDate();
            }
        },
        write: function (value) {
            var formattedDate = "\/Date(" + Date.parse(value) + ")\/"
            self.EnterDate(formattedDate);
        }
     });
    upshot.addEntityProperties(self, entityType);

The surrounding lines of code were included for context. I found this in the comments at: http://blog.stevensanderson.com/2012/03/06/single-page-application-packages-and-samples/

You also want to change the html in _Editor.cshtml to bind to "DateJson", not "EnterDate"

This is certainly a kludge but it has the virtue of working which is no small feat.

SteveK
  • 21
  • 3
1

You can also get the JQuery calendar popup to work by adding the following code.

Add this at the bottom of TodoItemsViewModel.js in the example MVC 4 SPA project:

    ko.bindingHandlers.datepicker = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        //initialize datepicker with some optional options
        var options = allBindingsAccessor().datepickerOptions || {};
        $(element).datepicker(options);

        //handle the field changing
        ko.utils.registerEventHandler(element, "change", function () {
            var observable = valueAccessor();
            observable($(element).datepicker("getDate"));
        });

        //handle disposal (if KO removes by the template binding)
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            $(element).datepicker("destroy");
        });

    },
    update: function (element, valueAccessor) {
        var value = ko.utils.unwrapObservable(valueAccessor()),
        current = $(element).datepicker("getDate");

        if (value - current !== 0) {
            //$(element).datepicker("setDate", value);
            $(element).val(value.toString());
        }
    }
}

ko.bindingHandlers.date = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var jsonDate = "/Date(12567120000-1000)/";
        var value = new Date(parseInt(jsonDate.substr(6)));
        var ret = value.getMonth() + 1 + "/" + value.getDate() + "/" + value.getFullYear();
        element.innerHTML = ret;
    },

    update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
    }
};

This is what your _Editor.cshtml code would look like to bind to the datepicker

    <p>
    EnterDate:
    @*<input name="EnterDate" data-bind="value: EnterDate, autovalidate: true" />
    <span class="error" data-bind="text: EnterDate.ValidationError"></span>*@
    <input name="DateJson" data-bind="datepicker: DateJson, datepickerOptions: { minDate: new Date() }" />
    <span class="error" data-bind="text: DateJson.ValidationError"></span>

</p>

Also, you want to change the variable displayed on the _Grid.cshtml page from "EnterDate" to "DateJson".

SteveK
  • 21
  • 3
0

JSON.NET expects $type whereas you have __type to sopecify the entity type so it converts it to a JObject.

I got round it with the following klunk

first make sure that the JsonSerializerSettings has.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects;

then write your own ````JsonTextReader

public class MyJsonTextReader : JsonTextReader
{
    public MyJsonTextReader(TextReader reader)
        : base(reader)
    { }

    public override object Value
    {
        get
        {
            var o = new ActivityManager.Models.Sched_ProposedActivities();

            if (TokenType == JsonToken.PropertyName && base.Value.ToString() == "__type")
                return "$type";
            if (TokenType == JsonToken.String && Path.ToString().EndsWith(".__type"))
            {
                string s = base.Value.ToString();
                var typeName = Regex.Match(s, ":#.*").ToString().Substring(2) + "." + Regex.Match(s, "^.*:#").ToString().Replace(":#", "");

                return
                    typeName + ", ActivityManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
            }

            return base.Value;
        }
    }
}

and use it to deserialise the Json with ````using (MyJsonTextReader jsonTextReader = new MyJsonTextReader(streamReader))