I've got a MVC application that I'm getting along OK with. I'm using knockout.js for binding and using it on an edit page where you can edit child records also. Everything seems to be working OK apart from when I click save, all the Date Pickers reset to todays date. The data that was saved is what is persisted to the DB but the data changes in all the Date Pickers on that page. A refresh of the page shows the correct persisted data.
Before:
After:
Here is my code:
Edit Page:
@model SPMVC.Models.SeasonViewModel
@using Newtonsoft.Json
@{
ViewBag.Title = "Edit Season";
}
@{ string data = JsonConvert.SerializeObject(Model); }
@section scripts
{
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.1/themes/smoothness/jquery-ui.css">
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery-ui-1.11.1.js"></script>
<script src="~/Scripts/knockout-3.2.0.js"></script>
<script src="~/Scripts/knockout.mapping-latest.js"></script>
<script src="~/Scripts/moment.js"></script>
<link href="~/Content/select2.css" rel="stylesheet" />
<script src="~/Scripts/select2.js"></script>
<script>
$(document).ready(function () { $("#e1").select2(); });
</script>
<script src="~/Scripts/seasonsviewmodel.js"></script>
<script type="text/javascript">
var seasonViewModel = new SeasonViewModel(@Html.Raw(data));
ko.applyBindings(seasonViewModel);
</script>
}
@Html.Partial("_EditSeason")
Edit Partial View:
<h2>@ViewBag.Title</h2>
<p data-bind="text: MessageToClient"></p>
<div class="form-group">
<label class="control-label" for="SeasonDescription">Season Description</label>
<input class="form-control" name="SeasonDescription" id="SeasonDescription" data-bind="value: SeasonDescription, event: {change: flagSeasonAsEdited}, hasfocus: true" />
</div>
<div class="form-group">
<label class="control-label" for="StartDate">Start Date</label>
<input class="form-control" type="text" name="StartDate" id="StartDate" data-bind="datepicker: StartDate, datepickerOptions: {dateFormat: 'DD, MM d, yy'}, event: {change: flagSeasonAsEdited}" />
</div>
<div class="form-group">
<label class="control-label" for="Publish">Publish</label>
<input class="checkbox" name="Publish" id="Publish" type="checkbox" data-bind="checked: Publish, event: {change: flagSeasonAsEdited}" />
</div>
<table class="table table-striped">
<tr>
<th>Game Description</th>
<th>Game Type</th>
<th>Game Date</th>
<th>Publish to Schedule</th>
<th>Publish Results</th>
<th><button data-bind="click: addGame" class="btn btn-info btn-xs">Add</button></th>
</tr>
<tbody data-bind="foreach: Games">
<tr>
<td class="form-group"><input class="form-control input-sm" data-bind="value: GameDescription, event: {change: flagGameAsEdited}, hasfocus: true" /></td>
<td class="form-group"><select data-bind="options: $parent.gameTypes, optionsText: 'GameTypeDescription', optionsValue: 'Id', value: GameTypeId, select2: { }, event: {change: flagGameAsEdited}"></select></td>
<td class="form-group"><input class="form-control input-sm" type="text" data-bind="datepicker: GameDate, datepickerOptions: {dateFormat: 'DD, MM d, yy'}, event: {change: flagGameAsEdited}" /></td>
<td class="form-group"><input class="checkbox" type="checkbox" data-bind="checked: PublishToSchedule, event: {change: flagGameAsEdited}"></td>
<td class="form-group"><input class="checkbox" type="checkbox" data-bind="checked: PublishResults, event: {change: flagGameAsEdited}"></td>
<td class="form-group"><button data-bind="click: $parent.deleteGame" class="btn btn-danger btn-xs">Delete</button></td>
</tr>
</tbody>
</table>
<p><button data-bind="click: save" class="btn btn-primary">Save</button></p>
<p><a href="/Admin/Seasons/" class="btn btn-default btn-sm">« Back to List</a></p>
Knockout View Model:
SeasonViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, gameMapping, self);
self.save = function () {
$.ajax({
url: "/Seasons/Save/",
type: "POST",
data: ko.toJSON(self),
contentType: "application/json",
success: function (data) {
// TODO: When re mapping the view model after save, all dates are in date2??
if (data.seasonViewModel != null)
ko.mapping.fromJS(data.seasonViewModel, {}, self);
if (data.newLocation != null)
window.location = data.newLocation;
}
});
},
self.flagSeasonAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
return true;
},
self.addGame = function () {
// Game defaults
var game = new GameViewModel({ Id: 0, GameDescription: "", GameTypeId: 1, GameDate: new Date(), PublishToSchedule: false, PublishResults: false, ObjectState: ObjectState.Added })
self.Games.push(game);
},
self.deleteGame = function (game) {
self.Games.remove(this);
if (game.Id() > 0 && self.GamesToDelete.indexOf(game.Id()) == -1)
self.GamesToDelete.push(game.Id());
};
};
GameViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, gameMapping, self);
self.flagGameAsEdited = function () {
if (self.ObjectState() != ObjectState.Added) {
self.ObjectState(ObjectState.Modified);
}
return true;
};
};
GameTypeViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, gameTypeMapping, self);
ko.applyBindings(new GameTypeViewModel());
};
var gameMapping = {
'Games': {
key: function (game) {
return ko.utils.unwrapObservable(game.Id);
},
create: function (options) {
return new GameViewModel(options.data);
}
}
};
var gameTypeMapping = {
'gameTypes': {
key: function (gameType) {
return ko.utils.unwrapObservable(gameType.Id);
},
create: function (options) {
return new GameTypeViewModel(options.data);
}
}
};
var ObjectState = {
Unchanged: 0,
Added: 1,
Modified: 2,
Deleted: 3
};
// Custom Knockout binding handler for date picker
ko.bindingHandlers.datepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
var options = allBindingsAccessor().datepickerOptions || {};
$(element).datepicker(options);
ko.utils.registerEventHandler(element, "change", function () {
var observable = valueAccessor();
observable($(element).datepicker("getDate"));
});
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).datepicker("destroy");
});
},
update: function (element, valueAccessor) {
var value = new Date(ko.utils.unwrapObservable(valueAccessor()));
var current = $(element).datepicker("getDate");
// Prevents the datepicker from popping up a second time
if (value - current !== 0) {
// For some stange reason, Chrome subtracts a day when first displaying the date
if (navigator.userAgent.indexOf('Chrome') > -1 && value != '') {
var date = value.getDate() + 1;
var month = value.getMonth();
var year = value.getFullYear();
value = new Date(year, month, date);
};
$(element).datepicker("setDate", value);
}
}
};
// Custom knockout binding handler for read-only date display
ko.bindingHandlers.dateString = {
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor();
var allBindings = allBindingsAccessor();
var pattern = allBindings.datePattern || 'dd/MM/yyyy';
var momentObj = moment(ko.utils.unwrapObservable(value), moment.ISO_8601);
$(element).text(momentObj.format(pattern));
}
};
ko.bindingHandlers.select2 = {
init: function (element, valueAccessor, allBindingsAccessor) {
// var options = ko.toJS(valueAccessor()) || {};
// setTimeout(function () {
// $(element).select2(options);
// }, 0);
//}
var obj = valueAccessor(),
allBindings = allBindingsAccessor(),
lookupKey = allBindings.lookupKey;
$(element).select2(obj);
if (lookupKey) {
var value = ko.utils.unwrapObservable(allBindings.value);
$(element).select2('data', ko.utils.arrayFirst(obj.data.results, function (item) {
return item[lookupKey] === value;
}));
}
}
};
Can anyone shine any light as to why this behavior would occur?
Thanks for taking the time to read this post.
Paul.