I know I will be marked down for giving you an alternate answer but that is ok because it is worth it to share a different way of solving your problem, that is simpler and more powerful.
If you do not mind a suggestion, stay away from ASP.NET MVC Razor templating, it is not powerful, it is tightly coupling your backend to your front in not much of a different way than ASP.NET Web Forms does.
Here as example of the power of KnockoutJs with ASP.NET MVC, but no Razor for binding Model data to View elements, knockoutjs is used entirely instead. A key take away is that the C# Model is built dynamically in KnockoutJs, using knockout.mapping.js. So when you add a new property to your C# model it is automatically available in your Knockout ViewModel.
Here is a snippet of a .cshtml View using no Razor syntax for the form/input model binding:
@using System.Activities.Statements
@using System.Web.Optimization
@model Mvc.Models.ProjectModel
@{
ViewBag.Title = "Start New Project";
Layout = "~/Views/Shared/_BuildLayout.cshtml";
}
@section sideMenuCustomContent{
@Html.Partial("_PartialSidebarPricing")
}
@using (Html.BeginForm("StartSubmit", "Start", null, FormMethod.Post, new { @role = "form", id = "formStart", enctype = "multipart/form-data" }))
{
<input type="hidden" value="@Url.RouteUrl("UploadStep")" data-bind="valueWithInit: 'UrlWithIdUploadStep'" />
<input type="hidden" value="@Url.Action("GetProjectModel", "Start")" data-bind="valueWithInit: 'UrlRootGetProjectModel'" />
<input type="hidden" value="@Url.Action("StyleInfo", "Start")" data-bind="valueWithInit: 'UrlRootStyleInfo'" />
<input type="hidden" value="@Url.Action("GetCoverColorSwatchUrl", "Asset")" data-bind="valueWithInit: 'UrlRootGetCoverColorImage'" />
<div class="row">
<div class="col-lg-6 col-md-6 featuredTextContainer">
<div class="style-hero" >
<h1 class="style-title" data-bind="text: Style.StyleName"></h1>
<div class="style-start-price" >Starting at: <span data-bind="text: Application.FormatCurrency(BookBasePrice())"></span></div>
<div class="style-caption" >
<p data-bind="text: Style.DisplayText" ></p>
</div>
</div>
</div>
<div id="book-options-right-column" class="col-lg-4 col-md-6 build-right-column well-black">
<div class="form-group">
<input id="tbxPrjName" type="text" placeholder="Project Name" data-bind="value: Name" class="couture-input-underline couture-text-white placeholder-center text-center" />
</div>
<div class="form-group">
<input style="width: 80%;" type="text" class="couture-input-underline couture-text-white placeholder-center text-center" placeholder="Estimated Page Count" data-bind="value: PageCountEstimated, valueUpdate: 'afterkeydown'" />
<a data-toggle="popover"
class="popover-250w"
data-placement="bottom"
data-content="Estimate just to help you with pricing, exact page count will be tracked for you. Click the Hamburger Menu in left corner to see current pricing."
data-title="Estimated Page Count">
<i class="glyphicon glyphicon-question-sign"></i>
</a>
</div>
<input type="button" id="stampYes" data-bind="click: OnTextStampingYes, css: { 'selected': TextStamping }" class="btn-couture btn-couture-transparent btn-round" value="Y" />
<input type="button" id="stampNo" data-bind="click: OnTextStampingNo, css: { 'selected': !TextStamping() }" class="btn-couture btn-couture-transparent btn-round" value="N">
</div>
<div class="form-group" data-bind="visible: Style.HasBackCoverOption">
<label>Back Cover Image</label>
<a data-toggle="popover"
class="popover-250w"
data-placement="bottom"
data-content="You’ll be able to upload your back cover image on the next page."
data-title="Back Cover Image">
<i class="glyphicon glyphicon-question-sign"></i>
</a>
<br />
<span>
<input type="button" id="BackCoverYes" data-bind="click: onBackCoverChange, css: { 'selected': CoverImageBack }" class="btn-couture btn-couture-transparent btn-round" value="Y" />
<input type="button" id="BackCoverNo" data-bind="click: onBackCoverChangeNo, css: { 'selected': !CoverImageBack() }" class="btn-couture btn-couture-transparent btn-round" value="N" />
</span>
</div>
</div>
</div>
<div class="form-group">
<div data-bind="visible: !PricingPartialShowFinalTotals()">
<button type="button" class="btn-couture btn-couture-transparent btn-couture-white btn-couture-large" data-bind="text: PricingPartialSubmitBtnText, disable: PricingPartialSubmitLock, click: formSubmit"></button>
</div>
</div>
</div>
</div>
<div class="form-group">
<div data-bind="visible: !PricingPartialShowFinalTotals()">
<button type="button" class="btn-couture btn-couture-transparent btn-couture-white btn-couture-large" data-bind="text: PricingPartialSubmitBtnText, disable: PricingPartialSubmitLock, click: formSubmit"></button>
</div>
</div>
}@*using (Html.BeginForm())*@
@section scripts {
@Scripts.Render("~/bundles/knockout")
@*<script src="/Scripts/knockout.mapping.updateData.js"></script>*@
@Scripts.Render("~/bundles/knockout/build/start")
<script id="document-ready">
var viewModel = {};
$(function() {
pageInit();
}); //document.ready
</script>
<script id="ko-bind-page-init">
function pageInit() {
viewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)), BuildStartViewModelMapping);
ko.applyBindings(viewModel);
viewModel.errors.showAllMessages(false); //ko.validation init
}
</script>
}
And here is part of knockout.mapping.js (~/bundles/knockout/build/start):
var viewModel = {};
var BuildStartViewModel = function (data) {
ko.mapping.fromJS(data, BuildStartViewModelMapping, this);
};
var BuildStartViewModelMapping = {
//'ignore': ['CoverColorName', 'PaperName', 'CoverColorId'],
create: function (options) {
var self = new BuildStartViewModel(options.data);
ko.BaseViewModel.call(self);
ko.BuildCommonViewModel.call(self);
self.IsStartView(true);
/*=================================================*/
//Custom observables not in data
/*=================================================*/
self.AdditionalTextStampingYes = ko.observable(false);
self.OriginalProjectName = ko.observable(self.Name());
/*=================================================*/
self.BuildStartBgClassGet = ko.computed(function () {
var styleName = self.Style.StyleName().toLowerCase();
return 'build-start-bg-' + styleName;
});
self.PageCountEstimated.subscribe(updatePageCount);
function updatePageCount() {
if (validatePageCountEstimated(self.PageCountEstimated())) {
self.PageCount(self.PageCountEstimated());
}
}
/*=================================================*/
//New Style Selected
/*=================================================*/
self.Style.StyleId.subscribe(onStyleIdNew);
function onStyleIdNew() {
if (!self.Style.StyleId())
return;
Application.LoadingDisplayToggle();
var url = $.concatUrl(viewModel.UrlRootGetProjectModel(), viewModel.Style.StyleId());
$.ajax({
url: url,
type: 'POST',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
async: true,
success: function (data) {
if (!data)
return;
ko.mapping.fromJS(data, BuildStartViewModelMapping, viewModel);
viewModel.errors.showAllMessages(false); //ko.validation init
//Refresh/Update Style Image
var $styleImage = $('#imgDisplayImage');
$styleImage.attr('src', '');
$styleImage.attr('src', data.Style.DisplayImage);
$('#hdnimage').val(data.Style.DisplayImage);
var $body = $('body');
console.log('TODO: UnComment When we have style images');
//$body.removeClass();
//$body.addClass('cover ' + self.BuildStartBgClassGet());
Application.LoadingDisplayToggle();
},
error: function () {
Application.LoadingDisplayToggle();
}
});
}
/*=================================================*/
self.formSubmit = saveData;
return self;
/*=================================================*/
//saveData/submit
/*=================================================*/
function saveData(data, event) {
//self.Name.isValidating.subscribe(function (isValidating) {
// if (!isValidating && self.errors().length > 0) {
// self.errors.showAllMessages(true);
// return;
// }
//});
//if (self.isValidating()) {
// setTimeout(function () {
// saveData(data, event);
// }, 50);
// return false;
//}
// ko.validation check if valid
if (self.errors().length > 0) {
self.errors.showAllMessages(true);
return;
}
var $form = $(event.currentTarget).closest('form');
if ($form.valid() && self.errors().length === 0) {
var copy = ko.toJS(this);
delete copy.errors;
delete copy.__ko_mapping__;
$.ajax({
url: $form.attr('action'),
type: 'POST',
dataType: 'json',
data: ko.toJSON(copy),
contentType: 'application/json; charset=utf-8',
success: function (response) {
var url = self.UrlWithIdUploadStep();
url = url.substr(0, url.lastIndexOf("/")); //remove id (could be StyleId and not ProjectId if new project)
url = $.concatUrl(url, response); //add project id to url, could be existing or new ProjectId
window.location.href = url;
},
beforeSend: function () {
self.PricingPartialSubmitLock(true);
Application.LoadingDisplayToggle();
},
error: function (result, error, errorThrown) {
self.PricingPartialSubmitLock(false);
Application.LoadingDisplayToggle();
Application.ToastrError(result.responseText);
}
});
}
return false;
};
}
};
And here is Scripts/knockout.mapping.updateData.js, I do not think you will need it to get started with this pattern but I included just in case:
/*
* Extension to the knockoutjs mapping plugin
* http://github.com/janhartigan/knockout-mapping-updatedata
* Requires KnockoutJS and the mapping plugin
*
* Dual licensed under the MIT or GPL Version 2 licenses.
* Jan Hartigan
*/
(function () {
/**
* A function that lets you "update from js" without overriding all the view model properties and methods. You just need to supply
* the viewModel, the original JS model on which you based your data (typically what you'd use in the mapping fromJS method), and the new JS
* object that has the updated information.
*
* @param Object viewModel
* @param Object dataModel
* @param Object jsObject
*
* @return Object (returns the viewModel)
*/
ko.mapping.updateData = function(viewModel, dataModel, jsObject) {
if (arguments.length < 3) throw new Error("When calling ko.updateData, pass: the view model, the data model, and the updated data.");
if (!viewModel) throw new Error("The view model is undefined.");
for (var i in dataModel) {
if (i in jsObject && i in viewModel && typeof dataModel[i] != 'function') {
viewModel[i](jsObject[i]);
}
}
return viewModel;
}
ko.exportSymbol('mapping.updateData', ko.mapping.updateData);
})();