MVC 4 Single Page Application and DateTime

2019-04-23 18:42发布

问题:

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?

回答1:

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();
              }
         });
    }
}    


回答2:

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.



回答3:

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".



回答4:

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))