I’ve seen this question asked by less experienced MVC3/Knockout programmers like myself but I haven’t come across anyone answering the question. The question might be off base but it would be nice to know how things should be handled. If you are not working with MVC3 then you may not understand where this question originates from.
In MVC3 I have a complex view model that I pass from my controller to the view. When creating the view I specify Razor (CSHTML) as the view engine, a strongly typed view, and the model class. This places a “@model MyApp.ViewModels.MyComplexModelClass” statement in the view.
Within my HTML form a typical mapping to a MVC3 view model field would then be like the following.
<div class="editor-field">
@Html.EditorFor(model => model.Country)
@Html.ValidationMessageFor(model => model.Country)
</div>
Normally when everything is filled out on my HTML form I simply call “submit” and Razor passes my MVC3 view model back to the controller where I can process the user’s responses.
Everything is cool until I want to do a Knockout/JavaScript lookup for a select list. To do this I create a JavaScript view model, bind the appropriate variables, and call a lookup action on the controller. I can see my value coming back in debugger but I am unable to assign it to the appropriate MVC3 view model variable.
In examples I have seen others approach this problem by converting the whole MVC3 view model to a JavaScript view model, manually bind the fields, and then pass the JavaScript/JSON view model back to the controller when the form is complete. The problem with this is that it seems like a lot of manual work, it may not work with my complex MVC3 view model, my data annotations would cease to function [@Html.ValidationMessageFor(model => model.Country)], etc.
Can a JavaScript variable from a knockout call be mapped or assigned to a MVC3 view model variable, i.e., MVC3 view model variable = javascript knockout value? Can Knockout be implemented with MVC3 view models?
The Sample code below is from a project I pulled together to isolate this problem in my application. Note that in the code section starting with "this.muncipalityCd.subscribe(function (municipalityCd) {" is where I am trying to set the MVC3 view model variable. Also, at the end of the sample there is another possible approach that I have listed.
Thank you for any guidance you can give while approaching this scenario.
Sample
@model KiltR.ViewModels.Residency
@{
ViewBag.Title = "MVC Knockout Example";
}
@*<h2>Index</h2>*@
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
<script type="text/html" id="noInfoTemplate">
No Information Available (Select State, County, and then Municipality)
</script>
<script type='text/javascript'>
$(document).ready(function () {
// var initialData = @Html.Raw( new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(Model));
var residencyItems = function () {
this.residencyStateCd = ko.observable();
this.countyCd = ko.observable();
this.muncipalityCd = ko.observable();
this.schoolDistictCd = ko.observable();
this.states = ko.observableArray();
this.counties = ko.observableArray();
this.municipalities = ko.observableArray();
this.schoolDistricts = ko.observableArray();
this.residencyServerModel = ko.observable(initialData);
var selectedStateCd = undefined;
var selectedCountyCd = undefined;
var selectedMunicipalityCd = undefined;
var selectedSchoolDistrictCd = undefined;
// Whenever the state changes, reset the county selection
this.residencyStateCd.subscribe(function (stateCd) {
selectedStateCd = stateCd;
initialData.ResidencyStateCd = stateCd;
this.countyCd(undefined);
this.counties(undefined);
if (stateCd != null) {
$.ajax({
url: '@Url.Action( "GetCounties", "MVCKnockout")',
data: { stateCd: stateCd },
type: 'GET',
success: function (data) {
residencyViewModel.counties(data);
}
});
}
} .bind(this));
// Whenever the county changes, reset the municipality selection
this.countyCd.subscribe(function (countyCd) {
selectedCountyCd = countyCd;
initialData.CountyCd = countyCd;
this.muncipalityCd(undefined);
this.municipalities(undefined);
if (countyCd != null) {
$.ajax({
url: '@Url.Action( "GetMunicipalities", "MVCKnockout")',
data: { stateCd: selectedStateCd, countyCd: countyCd },
type: 'GET',
success: function (data) {
residencyViewModel.municipalities(data);
}
});
}
} .bind(this));
// Whenever the municipality changes, reset the school district selection
this.muncipalityCd.subscribe(function (municipalityCd) {
selectedMunicipalityCd = municipalityCd;
initialData.MunicipalityCd = municipalityCd;
this.schoolDistictCd(undefined);
this.schoolDistricts(undefined);
if (municipalityCd != null) {
$.ajax({
url: '@Url.Action( "GetSchoolDistricts", "MVCKnockout")',
data: { stateCd: selectedStateCd, countyCd: selectedCountyCd, municipalityCd: municipalityCd },
type: 'GET',
success: function (data) {
if(data) {
residencyViewModel.schoolDistricts(data);
// This is where I am attempting to set my MVC3 view model i.e., replace the JS var fields with the MVC view model fields
var tempCd = data[0].SchoolDistrictCd; //This works...
var tempDescr = data[0].SchoolDistrictDescr; //This works...
@("Residency.SchoolDistrictCd") = tempCd; //This does not work....
}
}
});
}
} .bind(this));
};
var residencyViewModel = new residencyItems();
ko.applyBindings(residencyViewModel);
//Load the states
$.ajax({
url: '@Url.Action( "GetResidencyStates", "MVCKnockout" )',
type: 'GET',
success: function (data) {
residencyViewModel.states(data);
}
});
});
</script>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Residency</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Country)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Country)
@Html.ValidationMessageFor(model => model.Country)
</div>
<div class="editor-label">
<span class="error">*</span>@Html.LabelFor(m => m.ResidencyStateCd)
</div>
<div class="editor-field">
<select id='states'
data-bind='
options: states,
optionsValue : "ResidencyStateCd",
optionsText: "ResidencyStateDescr",
optionsCaption: "[Please select a state]",
value: residencyStateCd'>
</select>
</div>
<div class="editor-label">
<span class="error">*</span>@Html.LabelFor(m => m.CountyCd)
</div>
<div class="editor-field">
<select id='counties'
data-bind='
options: residencyStateCd() ? counties : null,
optionsValue : "CountyCd",
optionsText: "CountyDescr",
optionsCaption: "[Please select a county]",
value: countyCd,
visible: (counties() && counties().length > 0 )'>
</select>
<span data-bind='
template: {name: "noInfoTemplate"},
visible: !(counties() && counties().length > 0)'>
</span>
</div>
<div class="editor-label">
<span class="error">*</span>@Html.LabelFor(m => m.MuncipalityCd)
</div>
<div class="editor-field">
<select id='municipalities'
data-bind='
options: countyCd() ? municipalities : null,
optionsValue : "MunicipalityCd",
optionsText: "MunicipalityDescr",
optionsCaption: "[Please select a municipality]",
value: muncipalityCd,
visible: (municipalities() && municipalities().length > 0 )'>
</select>
<span data-bind='
template: {name: "noInfoTemplate"},
visible: !(municipalities() && municipalities().length > 0)'>
</span>
</div>
<div class="editor-label">
<span class="error">*</span>@Html.LabelFor(m => m.SchoolDistrictCd)
</div>
<div class="editor-field">
<select id='schoolDistricts'
data-bind='
options: muncipalityCd() ? schoolDistricts : null,
optionsValue : "SchoolDistrictCd", @*ScSclDistCd", *@
optionsText: "SchoolDistrictDescr",
optionsCaption: "[Please select a school district]",
value: schoolDistictCd,
visible: (schoolDistricts() && schoolDistricts().length > 0 )'>
</select>
<span data-bind='
template: {name: "noInfoTemplate"},
visible: !(schoolDistricts() && schoolDistricts().length > 0)'>
</span>
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
Maybe I am trying to set the value in the wrong place. Perhaps I should be trying to use the Razor syntax to call the Knockout/ JavaScript code something like the following:
@Html.DropDownListFor(model => model.ResidencyStateCd, new SelectList(Enumerable.Empty<SelectListItem>(), "ResidencyStateCd", "Residency"),
"Please select a state", new { id = "states" })
Rather than this from the sample above:
<div class="editor-field">
<select id='states'
data-bind='
options: states,
optionsValue : "ResidencyStateCd",
optionsText: "ResidencyStateDescr",
optionsCaption: "[Please select a state]",
value: residencyStateCd'>
</select>
</div>
Note that I found the above syntax in another example and I have not tried to wire it up to my Knockout /Ajax logic.