Одностраничное приложение MVC 4 и DateTime
Играя с новым одностраничным инструментарием MVC 4, я заметил, что ни один из найденных мной примеров не содержит пример обновления DateTime через WebApi. Я скоро узнал почему.
Я начал с генерации стандартного SPA из предоставленного шаблона. Затем я открыл TodoItem.cs и добавил поле DateTime. Затем я сгенерировал контроллер, как указано в комментариях. (Без поля datetime все работает просто отлично).
После того, как все сгенерировано, я запустил приложение и перешел к индексу контроллера (я назвал контроллер "задачами"). Я получил страницу сетки с 0 записями, как и ожидалось, и нажал кнопку добавления. Я был доставлен на страницу редактирования, как и ожидалось, и ввел некоторые данные, включая дату, в свое блестящее новое поле даты и времени. Затем нажали сохранить.
Произошла ошибка, в которой говорилось:
Ошибка сервера: код состояния HTTP: 500, сообщение: произошла ошибка десериализации объекта типа System.Web.Http.Data.ChangeSetEntry[]. Содержимое DateTime '01/01/2012'не начинается с'/Date('и заканчивается')/', как требуется для JSON.
Похоже, что инструмент еще не поддерживает DateTime. Я уверен, что смогу пройти и потратить немного времени на то, чтобы разобраться с этим и заставить его работать, но я подумал, что мне может повезти с кем-то, кто уже исправил эту проблему и может дать понимание.
Кто-нибудь уже боролся с этим?
Обновление: я добавляю больше информации, которую я нашел с тех пор, как спросил об этом. Я попытался использовать JSON.Net в качестве моего Formatter, как предложено ниже. Я думаю, что это будет окончательное решение, однако, просто делать то, что рекомендовано ниже, недостаточно.
При использовании сериализатора JSON.Net я получаю следующую ошибку:
Этот DataController не поддерживает операцию "Обновить" для сущности "JObject".
Причина в том, что JSON.Net не полностью заполняет объект, к которому пытается отменить форматирование форматировщик (System.Web.Http.Data.ChangeSet).
JSON, который отправляется в это:
[{"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}
}]
Встроенный Json Formatter может преобразовать этот Json в объект ChangeSet с внедренными объектами TodoItem в полях Entity и OriginalEntity.
Кто-нибудь получил JSON.Net для правильной десериализации?
4 ответа
Проблема в том, что в текущей бета-версии ASP.NET Web API использует DataContractJsonSerializer
, которая имеет известные проблемы с сериализацией DateTime
, Вот небольшая недавно возникшая ошибка в Microsoft Connect для этой проблемы; MS отвечает, что у них уже есть ошибка отслеживания проблемы, но она не будет исправлена в.Net 4.5/VS11.
К счастью, вы можете заменить альтернативный сериализатор JSON, такой как превосходный JSON.Net James Newton-King.
У Хенрика Нильсена из команды ASP.NET есть отличное сообщение в блоге, показывающее, как вы можете использовать JSON.Net с ASP.NET Web API. Вот его реализация MediaTypeFormatter
который использует JSON.Net (он также должен быть подключен к конфигурации ASP.NET Web API, в блоге Хенрика это тоже демонстрируется).
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();
}
});
}
}
У меня была точно такая же проблема. Я потратил слишком много времени, пытаясь заставить работать json.net. Я наконец-то нашел этот обходной путь, который вы должны вставить в TodoItemsViewModel.js в примере проекта:
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);
Окружающие строки кода были включены для контекста. Я нашел это в комментариях по адресу: http://blog.stevensanderson.com/2012/03/06/single-page-application-packages-and-samples/
Вы также хотите изменить HTML в _Editor.cshtml, чтобы привязать к "DateJson", а не "EnterDate"
Это, конечно, клочок, но он обладает достоинством работы, что немаловажно.
Вы также можете получить всплывающее окно с календарем JQuery, добавив следующий код.
Добавьте это внизу TodoItemsViewModel.js в примере проекта SPA MVC 4:
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) {
}
};
Вот как должен выглядеть ваш код _Editor.cshtml для привязки к средству выбора даты
<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>
Кроме того, вы хотите изменить переменную, отображаемую на странице _Grid.cshtml, с "EnterDate" на "DateJson".
JSON.NET ожидает $type, тогда как у вас есть __type для определения типа сущности, поэтому он преобразует его в JObject.
Я обошел его со следующим кланком
сначала убедитесь, что JsonSerializerSettings
has
.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects;
затем напишите свой собственный `` `` 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;
}
}
}
и использовать его для десериализации Json с использованием `` `` (MyJsonTextReader jsonTextReader = new MyJsonTextReader(streamReader))