Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC 4 Single Page Application and DateTime

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?

like image 344
Brian McCord Avatar asked Feb 27 '12 22:02

Brian McCord


2 Answers

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();
              }
         });
    }
}    
like image 154
James Webster Avatar answered Oct 25 '22 20:10

James Webster


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.

like image 23
SteveK Avatar answered Oct 25 '22 21:10

SteveK