Bounty
It's been awhile and I still have a couple outstanding questions. I hope by adding a bounty maybe these questions will get answered.
How do you use html helpers with knockout.js
Why was document ready needed to make it work(see first edit for more information)
How do I do something like this if I am using the knockout mapping with my view models? As I do not have a function due to the mapping.
function AppViewModel() {
// ... leave firstName, lastName, and fullName unchanged here ...
this.capitalizeLastName = function() {
var currentVal = this.lastName(); // Read the current value
this.lastName(currentVal.toUpperCase()); // Write back a modified value
};
I want to use plugins for instance I want to be able to rollback observables as if a user cancels a request I want to be able to go back to the last value. From my research this seems to be achieved by people making plugins like editables
How do I use something like that if I am using mapping? I really don’t want to go to a method where I have in my view manual mapping were I map each MVC viewMode field to a KO model field as I want as little inline javascript as possible and that just seems like double the work and that’s why I like that mapping.
I am concerned that to make this work easy (by using mapping) I will lose a lot of KO power but on the other hand I am concerned that manual mapping will just be a lot of work and will make my views contain too much information and might become in the future harder to maintain(say if I remove a property in the MVC model I have to move it also in the KO viewmodel)
Original Post
I am using asp.net mvc 3 and I looking into knockout as it looks pretty cool but I am having a hard time figuring out how it works with asp.net mvc especially view models.
For me right now I do something like this
public class CourseVM
{
public int CourseId { get; set; }
[Required(ErrorMessage = "Course name is required")]
[StringLength(40, ErrorMessage = "Course name cannot be this long.")]
public string CourseName{ get; set; }
public List<StudentVm> StudentViewModels { get; set; }
}
I would have a Vm that has some basic properties like CourseName and it will have some simple validation on top of it. The Vm model might contain other view models in it as well if needed.
I would then pass this Vm to the View were I would use html helpers to help me display it to the user.
#Html.TextBoxFor(x => x.CourseName)
I might have some foreach loops or something to get the data out of the collection of Student View Models.
Then when I would submit the form I would use jquery and serialize array and send it to a controller action method that would bind it back to the viewmodel.
With knockout.js it is all different as you now got viewmodels for it and from all the examples I seen they don't use html helpers.
How do you use these 2 features of MVC with knockout.js?
I found this video and it briefly(last few minutes of the video # 18:48) goes into a way to use viewmodels by basically having an inline script that has the knockout.js viewmodel that gets assigned the values in the ViewModel.
Is this the only way to do it? How about in my example with having a collection of viewmodels in it? Do I have to have a foreach loop or something to extract all the values out and assign it into knockout?
As for html helpers the video says nothing about them.
These are the 2 areas that confuses the heck out of me as not many people seem to talk about it and it leaves me confused of how the initial values and everything is getting to the view when ever example is just some hard-coded value example.
Edit
I am trying what Darin Dimitrov has suggested and this seems to work(I had to make some changes to his code though). Not sure why I had to use document ready but somehow everything was not ready without it.
#model MvcApplication1.Models.Test
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
<script src="../../Scripts/jquery-1.5.1.js" type="text/javascript"></script>
<script src="../../Scripts/knockout-2.1.0.js" type="text/javascript"></script>
<script src="../../Scripts/knockout.mapping-latest.js" type="text/javascript"></script>
<script type="text/javascript">
$(function()
{
var model = #Html.Raw(Json.Encode(Model));
// Activates knockout.js
ko.applyBindings(model);
});
</script>
</head>
<body>
<div>
<p>First name: <strong data-bind="text: FirstName"></strong></p>
<p>Last name: <strong data-bind="text: LastName"></strong></p>
#Model.FirstName , #Model.LastName
</div>
</body>
</html>
I had to wrap it around a jquery document ready to make it work.
I also get this warning. Not sure what it is all about.
Warning 1 Conditional compilation is turned off -> #Html.Raw
So I have a starting point I guess at least will update when I done some more playing around and how this works.
I am trying to go through the interactive tutorials but use the a ViewModel instead.
Not sure how to tackle these parts yet
function AppViewModel() {
this.firstName = ko.observable("Bert");
this.lastName = ko.observable("Bertington");
}
or
function AppViewModel() {
// ... leave firstName, lastName, and fullName unchanged here ...
this.capitalizeLastName = function() {
var currentVal = this.lastName(); // Read the current value
this.lastName(currentVal.toUpperCase()); // Write back a modified value
};
Edit 2
I been able to figure out the first problem. No clue about the second problem. Yet though. Anyone got any ideas?
#model MvcApplication1.Models.Test
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
<script src="../../Scripts/jquery-1.5.1.js" type="text/javascript"></script>
<script src="../../Scripts/knockout-2.1.0.js" type="text/javascript"></script>
<script src="../../Scripts/knockout.mapping-latest.js" type="text/javascript"></script>
<script type="text/javascript">
$(function()
{
var model = #Html.Raw(Json.Encode(Model));
var viewModel = ko.mapping.fromJS(model);
ko.applyBindings(viewModel);
});
</script>
</head>
<body>
<div>
#*grab values from the view model directly*#
<p>First name: <strong data-bind="text: FirstName"></strong></p>
<p>Last name: <strong data-bind="text: LastName"></strong></p>
#*grab values from my second view model that I made*#
<p>SomeOtherValue <strong data-bind="text: Test2.SomeOtherValue"></strong></p>
<p>Another <strong data-bind="text: Test2.Another"></strong></p>
#*allow changes to all the values that should be then sync the above values.*#
<p>First name: <input data-bind="value: FirstName" /></p>
<p>Last name: <input data-bind="value: LastName" /></p>
<p>SomeOtherValue <input data-bind="value: Test2.SomeOtherValue" /></p>
<p>Another <input data-bind="value: Test2.Another" /></p>
#* seeing if I can do it with p tags and see if they all update.*#
<p data-bind="foreach: Test3">
<strong data-bind="text: Test3Value"></strong>
</p>
#*took my 3rd view model that is in a collection and output all values as a textbox*#
<table>
<thead><tr>
<th>Test3</th>
</tr></thead>
<tbody data-bind="foreach: Test3">
<tr>
<td>
<strong data-bind="text: Test3Value"></strong>
<input type="text" data-bind="value: Test3Value"/>
</td>
</tr>
</tbody>
</table>
Controller
public ActionResult Index()
{
Test2 test2 = new Test2
{
Another = "test",
SomeOtherValue = "test2"
};
Test vm = new Test
{
FirstName = "Bob",
LastName = "N/A",
Test2 = test2,
};
for (int i = 0; i < 10; i++)
{
Test3 test3 = new Test3
{
Test3Value = i.ToString()
};
vm.Test3.Add(test3);
}
return View(vm);
}
I think I have summarized all your questions, if I missed something please let me know (If you could summarize up all your questions in one place would be nice =))
Note. Compatibility with the ko.editable plug-in added
Download the full code
How do you use html helpers with knockout.js
This is easy:
#Html.TextBoxFor(model => model.CourseId, new { data_bind = "value: CourseId" })
Where:
value: CourseId indicates that you are binding the value property of the input control with the CourseId property from your model and your script model
The result is:
<input data-bind="value: CourseId" data-val="true" data-val-number="The field CourseId must be a number." data-val-required="The CourseId field is required." id="CourseId" name="CourseId" type="text" value="12" />
Why was document ready needed to make it work(see first edit for more information)
I do not understand yet why you need to use the ready event to serialize the model, but it seems that it is simply required (Not to worry about it though)
How do I do something like this if I am using the knockout mapping with my view models? As I do not have a function due to the mapping.
If I understand correctly you need to append a new method to the KO model, well that's easy merging models
For more info, in the section -Mapping from different sources-
function viewModel() {
this.addStudent = function () {
alert("de");
};
};
$(function () {
var jsonModel = '#Html.Raw(JsonConvert.SerializeObject(this.Model))';
var mvcModel = ko.mapping.fromJSON(jsonModel);
var myViewModel = new viewModel();
var g = ko.mapping.fromJS(myViewModel, mvcModel);
ko.applyBindings(g);
});
About the warning you were receiveing
Warning 1 Conditional compilation is turned off -> #Html.Raw
You need to use quotes
Compatibility with the ko.editable plug-in
I thought it was going to be more complex, but it turns out that the integration is really easy, in order to make your model editable just add the following line: (remember that in this case I am using a mixed model, from server and adding extension in client and the editable simply works... it's great):
ko.editable(g);
ko.applyBindings(g);
From here you just need to play with your bindings using the extensions added by the plug-in, for example, I have a button to start editing my fields like this and in this button I start the edit process:
this.editMode = function () {
this.isInEditMode(!this.isInEditMode());
this.beginEdit();
};
Then I have commit and cancel buttons with the following code:
this.executeCommit = function () {
this.commit();
this.isInEditMode(false);
};
this.executeRollback = function () {
if (this.hasChanges()) {
if (confirm("Are you sure you want to discard the changes?")) {
this.rollback();
this.isInEditMode(false);
}
}
else {
this.rollback();
this.isInEditMode(false);
}
};
And finally, I have one field to indicate whether the fields are in edit mode or not, this is just to bind the enable property.
this.isInEditMode = ko.observable(false);
About your array question
I might have some foreach loops or something to get the data out of the collection of Student View Models.
Then when I would submit the form I would use jquery and serialize array and send it to a controller action method that would bind it back to the viewmodel.
You can do the same with KO, in the following example, I will create the following output:
Basically here, you have two lists, created using Helpers and binded with KO, they have a dblClick event binded that when fired, remove the selected item from the current list and add it to the other list, when you post to the Controller, the content of each list is sent as JSON data and re-attached to the server model
Nuggets:
Newtonsoft
jQuery
knockoutjs
Knockout.Mapping
External scripts.
Controller code
[HttpGet]
public ActionResult Index()
{
var m = new CourseVM { CourseId = 12, CourseName = ".Net" };
m.StudentViewModels.Add(new StudentVm { ID = 545, Name = "Name from server", Lastname = "last name from server" });
return View(m);
}
[HttpPost]
public ActionResult Index(CourseVM model)
{
if (!string.IsNullOrWhiteSpace(model.StudentsSerialized))
{
model.StudentViewModels = JsonConvert.DeserializeObject<List<StudentVm>>(model.StudentsSerialized);
model.StudentsSerialized = string.Empty;
}
if (!string.IsNullOrWhiteSpace(model.SelectedStudentsSerialized))
{
model.SelectedStudents = JsonConvert.DeserializeObject<List<StudentVm>>(model.SelectedStudentsSerialized);
model.SelectedStudentsSerialized = string.Empty;
}
return View(model);
}
Model
public class CourseVM
{
public CourseVM()
{
this.StudentViewModels = new List<StudentVm>();
this.SelectedStudents = new List<StudentVm>();
}
public int CourseId { get; set; }
[Required(ErrorMessage = "Course name is required")]
[StringLength(100, ErrorMessage = "Course name cannot be this long.")]
public string CourseName { get; set; }
public List<StudentVm> StudentViewModels { get; set; }
public List<StudentVm> SelectedStudents { get; set; }
public string StudentsSerialized { get; set; }
public string SelectedStudentsSerialized { get; set; }
}
public class StudentVm
{
public int ID { get; set; }
public string Name { get; set; }
public string Lastname { get; set; }
}
CSHTML page
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>CourseVM</legend>
<div>
<div class="editor-label">
#Html.LabelFor(model => model.CourseId)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.CourseId, new { data_bind = "enable: isInEditMode, value: CourseId" })
#Html.ValidationMessageFor(model => model.CourseId)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.CourseName)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.CourseName, new { data_bind = "enable: isInEditMode, value: CourseName" })
#Html.ValidationMessageFor(model => model.CourseName)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.StudentViewModels);
</div>
<div class="editor-field">
#Html.ListBoxFor(
model => model.StudentViewModels,
new SelectList(this.Model.StudentViewModels, "ID", "Name"),
new
{
style = "width: 37%;",
data_bind = "enable: isInEditMode, options: StudentViewModels, optionsText: 'Name', value: leftStudentSelected, event: { dblclick: moveFromLeftToRight }"
}
)
#Html.ListBoxFor(
model => model.SelectedStudents,
new SelectList(this.Model.SelectedStudents, "ID", "Name"),
new
{
style = "width: 37%;",
data_bind = "enable: isInEditMode, options: SelectedStudents, optionsText: 'Name', value: rightStudentSelected, event: { dblclick: moveFromRightToLeft }"
}
)
</div>
#Html.HiddenFor(model => model.CourseId, new { data_bind="value: CourseId" })
#Html.HiddenFor(model => model.CourseName, new { data_bind="value: CourseName" })
#Html.HiddenFor(model => model.StudentsSerialized, new { data_bind = "value: StudentsSerialized" })
#Html.HiddenFor(model => model.SelectedStudentsSerialized, new { data_bind = "value: SelectedStudentsSerialized" })
</div>
<p>
<input type="submit" value="Save" data-bind="enable: !isInEditMode()" />
<button data-bind="enable: !isInEditMode(), click: editMode">Edit mode</button><br />
<div>
<button data-bind="enable: isInEditMode, click: addStudent">Add Student</button>
<button data-bind="enable: hasChanges, click: executeCommit">Commit</button>
<button data-bind="enable: isInEditMode, click: executeRollback">Cancel</button>
</div>
</p>
</fieldset>
}
Scripts
<script src="#Url.Content("~/Scripts/jquery-1.7.2.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/knockout-2.1.0.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/knockout.mapping-latest.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/ko.editables.js")" type="text/javascript"></script>
<script type="text/javascript">
var g = null;
function ViewModel() {
this.addStudent = function () {
this.StudentViewModels.push(new Student(25, "my name" + new Date(), "my last name"));
this.serializeLists();
};
this.serializeLists = function () {
this.StudentsSerialized(ko.toJSON(this.StudentViewModels));
this.SelectedStudentsSerialized(ko.toJSON(this.SelectedStudents));
};
this.leftStudentSelected = ko.observable();
this.rightStudentSelected = ko.observable();
this.moveFromLeftToRight = function () {
this.SelectedStudents.push(this.leftStudentSelected());
this.StudentViewModels.remove(this.leftStudentSelected());
this.serializeLists();
};
this.moveFromRightToLeft = function () {
this.StudentViewModels.push(this.rightStudentSelected());
this.SelectedStudents.remove(this.rightStudentSelected());
this.serializeLists();
};
this.isInEditMode = ko.observable(false);
this.executeCommit = function () {
this.commit();
this.isInEditMode(false);
};
this.executeRollback = function () {
if (this.hasChanges()) {
if (confirm("Are you sure you want to discard the changes?")) {
this.rollback();
this.isInEditMode(false);
}
}
else {
this.rollback();
this.isInEditMode(false);
}
};
this.editMode = function () {
this.isInEditMode(!this.isInEditMode());
this.beginEdit();
};
}
function Student(id, name, lastName) {
this.ID = id;
this.Name = name;
this.LastName = lastName;
}
$(function () {
var jsonModel = '#Html.Raw(JsonConvert.SerializeObject(this.Model))';
var mvcModel = ko.mapping.fromJSON(jsonModel);
var myViewModel = new ViewModel();
g = ko.mapping.fromJS(myViewModel, mvcModel);
g.StudentsSerialized(ko.toJSON(g.StudentViewModels));
g.SelectedStudentsSerialized(ko.toJSON(g.SelectedStudents));
ko.editable(g);
ko.applyBindings(g);
});
</script>
Note: I just added these lines:
#Html.HiddenFor(model => model.CourseId, new { data_bind="value: CourseId" })
#Html.HiddenFor(model => model.CourseName, new { data_bind="value: CourseName" })
Because when I submit the form my fields are disabled, so the values were not transmitted to the server, that's why I added a couple of hidden fields to do the trick
You could serialize your ASP.NET MVC view model into a javascript variable:
#model CourseVM
<script type="text/javascript">
var model = #Html.Raw(Json.Encode(Model));
// go ahead and use the model javascript variable to bind with ko
</script>
There are lots of examples in the knockout documentation that you could go through.
To achieve the additional computed properties after server mapping you will need to further enhance your viewmodels on the client side.
For example:
var viewModel = ko.mapping.fromJS(model);
viewModel.capitalizedName = ko.computed(function() {...}, viewModel);
So everytime you map from raw JSON you would need to reapply the computed properties.
Additionally the mapping plugin provides the ability to incrementally update a viewmodel as opposed to recreating it every time you go back and forth (use an additional parameter in fromJS):
// Every time data is received from the server:
ko.mapping.fromJS(data, viewModel);
And that executes an incremental data update on your model of just properties that are mapped. You can read more about that in the mapping documentation
You mentioned in the comments on Darin's answer the FluentJSON package. I'm the author of that, but its use case is more specific than ko.mapping. I would generally only use it if your viewmodels are one way (ie. server -> client) and then data is posted back in some different format (or not at all). Or if your javascript viewmodel needs to be in a substantially different format from your server model.
Related
I have a requirement to have different forms for different clients which can all be configured in the background (in the end in a database)
My initial idea is to create an object for "Form" which has a "Dictionary of FormItem" to describe the form fields.
I can then new up a dynamic form by doing the following (this would come from the database / service):
private Form GetFormData()
{
var dict = new Dictionary<string, FormItem>();
dict.Add("FirstName", new FormItem()
{
FieldType = Core.Web.FieldType.TextBox,
FieldName = "FirstName",
Label = "FieldFirstNameLabel",
Value = "FName"
});
dict.Add("LastName", new FormItem()
{
FieldType = Core.Web.FieldType.TextBox,
FieldName = "LastName",
Label = "FieldLastNameLabel",
Value = "LName"
});
dict.Add("Submit", new FormItem()
{
FieldType = Core.Web.FieldType.Submit,
FieldName = "Submit",
Label = null,
Value = "Submit"
});
var form = new Form()
{
Method = "Post",
Action = "Index",
FormItems = dict
};
return form;
}
Inside my Controller I can get the form data and pass that into the view
public IActionResult Index()
{
var formSetup = GetFormData(); // This will call into the service and get the form and the values
return View(formSetup);
}
Inside the view I call out to a HtmlHelper for each of the FormItems
#model Form
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#using FormsSpike.Core.Web
#{
ViewData["Title"] = "Home Page";
}
#using (Html.BeginForm(Model.Action, "Home", FormMethod.Post))
{
foreach (var item in Model.FormItems)
{
#Html.FieldFor(item);
}
}
Then when posting back I have to loop through the form variables and match them up again. This feels very old school I would expect would be done in a model binder of some sort.
[HttpPost]
public IActionResult Index(IFormCollection form)
{
var formSetup = GetFormData();
foreach (var formitem in form)
{
var submittedformItem = formitem;
if (formSetup.FormItems.Any(w => w.Key == submittedformItem.Key))
{
FormItem formItemTemp = formSetup.FormItems.Single(w => w.Key == submittedformItem.Key).Value;
formItemTemp.Value = submittedformItem.Value;
}
}
return View("Index", formSetup);
}
This I can then run through some mapping which would update the database in the background.
My problem is that this just feels wrong :o{
Also I have used a very simple HtmlHelper but I can't really use the standard htmlHelpers (such as LabelFor) to create the forms as there is no model to bind to..
public static HtmlString FieldFor(this IHtmlHelper html, KeyValuePair<string, FormItem> item)
{
string stringformat = "";
switch (item.Value.FieldType)
{
case FieldType.TextBox:
stringformat = $"<div class='formItem'><label for='item.Key'>{item.Value.Label}</label><input type='text' id='{item.Key}' name='{item.Key}' value='{item.Value.Value}' /></ div >";
break;
case FieldType.Number:
stringformat = $"<div class='formItem'><label for='item.Key'>{item.Value.Label}</label><input type='number' id='{item.Key}' name='{item.Key}' value='{item.Value.Value}' /></ div >";
break;
case FieldType.Submit:
stringformat = $"<input type='submit' name='{item.Key}' value='{item.Value.Value}'>";
break;
default:
break;
}
return new HtmlString(stringformat);
}
Also the validation will not work as the attributes (for example RequiredAttribute for RegExAttribute) are not there.
Am I having the wrong approach to this or is there a more defined way to complete forms like this?
Is there a way to create a dynamic ViewModel which could be created from the origional setup and still keep all the MVC richness?
You can do this using my FormFactory library.
By default it reflects against a view model to produce a PropertyVm[] array:
```
var vm = new MyFormViewModel
{
OperatingSystem = "IOS",
OperatingSystem_choices = new[]{"IOS", "Android",};
};
Html.PropertiesFor(vm).Render(Html);
```
but you can also create the properties programatically, so you could load settings from a database then create PropertyVm.
This is a snippet from a Linqpad script.
```
//import-package FormFactory
//import-package FormFactory.RazorGenerator
void Main()
{
var properties = new[]{
new PropertyVm(typeof(string), "username"){
DisplayName = "Username",
NotOptional = true,
},
new PropertyVm(typeof(string), "password"){
DisplayName = "Password",
NotOptional = true,
GetCustomAttributes = () => new object[]{ new DataTypeAttribute(DataType.Password) }
}
};
var html = FormFactory.RazorEngine.PropertyRenderExtension.Render(properties, new FormFactory.RazorEngine.RazorTemplateHtmlHelper());
Util.RawHtml(html.ToEncodedString()).Dump(); //Renders html for a username and password field.
}
```
Theres a demo site with examples of the various features you can set up (e.g. nested collections, autocomplete, datepickers etc.)
I'm going to put my solution here since I found this searching 'how to create a dynamic form in mvc core.' I did not want to use a 3rd party library.
Model:
public class IndexViewModel
{
public Dictionary<int, DetailTemplateItem> FormBody { get; set; }
public string EmailAddress { get; set; }
public string templateName { get; set; }
}
cshtml
<form asp-action="ProcessResultsDetails" asp-controller="home" method="post">
<div class="form-group">
<label asp-for=#Model.EmailAddress class="control-label"></label>
<input asp-for=#Model.EmailAddress class="form-control" />
</div>
#foreach (var key in Model.FormBody.Keys)
{
<div class="form-group">
<label asp-for="#Model.FormBody[key].Name" class="control-label">#Model.FormBody[key].Name</label>
<input asp-for="#Model.FormBody[key].Value" class="form-control" value="#Model.FormBody[key].Value"/>
<input type="hidden" asp-for="#Model.FormBody[key].Name"/>
</div>
}
<input type="hidden" asp-for="templateName" />
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
You can use JJMasterData, it can create dynamic forms from your tables at runtime or compile time. Supports both .NET 6 and .NET Framework 4.8.
After setting up the package, access /en-us/DataDictionary in your browser
Create a Data Dictionary adding your table name
Click on More, Get Scripts, Execute Stored Procedures and then click on Preview and check it out
To use your CRUD at runtime, go to en-us/MasterData/Form/Render/{YOUR_DICTIONARY}
To use your CRUD at a specific page or customize at compile time, follow the example below:
At your Controller:
public IActionResult Index(string dictionaryName)
{
var form = new JJFormView("YourDataDictionary");
form.FormElement.Title = "Example of compile time customization"
var runtimeField = new FormElementField();
runtimeField.Label = "Field Label";
runtimeField.Name = "FieldName";
runtimeField.DataType = FieldType.Text;
runtimeField.VisibleExpression = "exp:{pagestate}='INSERT'";
runtimeField.Component = FormComponent.Text;
runtimeField.DataBehavior = FieldBehavior.Virtual; //Virtual means the field does not exist in the database.
runtimeField.CssClass = "col-sm-4";
form.FormElement.Fields.Add(runtimeField);
return View(form);
}
At your View:
#using JJMasterData.Web.Extensions
#model JJFormView
#using (Html.BeginForm())
{
#Model.GetHtmlString()
}
I'm having a problem with my model losing one of its values when submitted in a http post form.
The controller methods:
[HttpGet]
public ActionResult AddTeam(int id)
{
return PartialView("_AddTeam", ViewModelGenerator.TeamFormModel(id));
}
[HttpPost]
public ActionResult AddTeam(TeamFormModel model)
{
TournamentTeamService.CreateTeam(model);
return RedirectToAction("Index", "Tournament", null);
}
The form model:
public class TeamFormModel
{
public TeamViewModel Team { get; set; }
public TournamentViewModel Tournament { get; set; }
public List<PlayerViewModel> Players { get; set; }
}
This function is called from the controller, it creates a new instance of the form model and gets the Tournament from the database and puts it into a view model
public static TeamFormModel TeamFormModel(int id)
{
var _db = new DbContext();
var model = new TeamFormModel();
var tempModel = new TournamentViewModel();
var temp = (from t in _db.tournament
where t.Id == id
select t).SingleOrDefault();
tempModel.Id = temp.Id;
tempModel.Name = temp.Name;
tempModel.SignupStartDate = temp.SignupStartDate;
tempModel.SignupEndDate = temp.SignupEndDate;
tempModel.StartDate = temp.StartDate;
tempModel.EndDate = temp.EndDate;
tempModel.Description = temp.description;
tempModel.TournamentType = temp.TournamentType;
model.Players = new List<PlayerViewModel>();
model.Tournament = tempModel;
return model;
}
Here's the view, the code with the inputs for the player list is missing, it's because it is appendedTo the player-forms div via jquery and it works perfectly so i didn't consider it relevant in this case.
#model Context.ViewModels.TeamFormModel
<script src="~/Scripts/jquery-1.8.2.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
#using (Html.BeginForm("AddTeam", "Tournament", FormMethod.Post, new { enctype = "multipart/form-data", #class = "form-horizontal" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<h2>Registering a team to #Model.Tournament.Name </h2>
<div class="control-group">
#Html.LabelFor(model => model.Team.Name)
<div class="control">
#Html.EditorFor(model => model.Team.Name)
</div>
<a class="add-player-form" href="javascript:void(0)">Add a player</a>
<div id="player-forms">
</div>
</div>
<div class="control-group">
<div class="control" style="clear: both;">
<input type="submit" value="Register" />
</div>
</div>
}
Before i submit this form, the TeamFormModel is perfectly set up, all values are inserted and i only need to save everything to DB. However when it goes back to the controller, the tournament variable in TeamFormModel is null. So when i send it to a function that changes it to a Db model and submits it to db fails everytime.
I know there are ways around the problem like only keeping the id of the tournament instead of the whole model and then get it from db after the form is submitted but it really bugs me that it behaves this way.
I wasn't able to find anything on this specific issue here on stackoverflow, there were a few similar questions but nothing that presented it self the same way.
you have to set something like this
#Html.HiddenFor(x => x.TournmentID);
something has to retain the value of the model that was passed into the view while the view is rendered, this will pass the value back to the controller on post
My simpel test Model:
public class MovieModel {
public string SelectedCategorieID { get; set; }
public List<CategorieModel> Categories { get; set; }
public MovieModel() {
this.SelectedCategorieID = "0";
this.Categories = new List<CategorieModel>() {new CategorieModel {ID = 1,
Name = "Drama"},
new CategorieModel {ID = 2,
Name = "Scifi"}};
}
}
public class CategorieModel {
public int ID { get; set; }
public string Name { get; set; }
}
My Home controller action Index:
public ActionResult Index() {
Models.MovieModel mm = new Models.MovieModel();
return View(mm);
}
My strongly typed View:
#model MvcDropDownList.Models.MovieModel
#{
ViewBag.Title = "Home Page";
}
<script type="text/javascript">
function categoryChosen(selectedCatID) {
// debugger;
var url = "Home/CategoryChosen?SelectedCategorieID=" + selectedCatID;
$.post(url, function (data) {
$("#minicart").html(data);
});
}
</script>
#using (Html.BeginForm("CategoryChosen", "Home", FormMethod.Get)) {
<fieldset>
Movie Type
#Html.DropDownListFor(m => m.SelectedCategorieID, new SelectList(Model.Categories, "ID", "Name", Model.SelectedCategorieID), "---Select categorie---")
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
}
<input type="button" value="Minicart test" onclick="categoryChosen('#Model.SelectedCategorieID');" />
<div id="minicart">
#Html.Partial("Information")
</div>
Please ignore the first input, because I'm using the second input with 'Minicart test' on it (the HTML.Beginform is there to learn something else later). The mini cart stuff is from another tutorial, I apologize. Don't let it distract you please.
When the button is clicked categoryChosen jQuery is called, which calls the action:
[AcceptVerbs("POST")]
public ActionResult CategoryChosen(string SelectedCategorieID) {
ViewBag.messageString = SelectedCategorieID;
return PartialView("Information");
}
The partial view Information looks like this:
#{
ViewBag.Title = "Information";
}
<h2>Information</h2>
<h2>You selected: #ViewBag.messageString</h2>
My question is why is Model.SelectCategorieID zero (Model.SelectCategorieID = 0) even after I changed the value in the dropdownlist? What am I doing wrong? Thank you very much in advance for answering. If you need any information or anything in unclear, please let me know.
My question is why is Model.SelectCategorieID zero
(Model.SelectCategorieID = 0) even after I changed the value in the
dropdownlist?
That's because you have hardcoded that value in your onclick handler:
onclick="categoryChosen('#Model.SelectedCategorieID');"
If you want to do that properly you should read the value from the dropdown list:
onclick="categoryChosen(this);"
and then modify your categoryChosen function:
<script type="text/javascript">
function categoryChosen(ddl) {
// debugger;
var url = 'Home/CategoryChosen';
$.post(url, { selectedCategorieID: $(ddl).val() }, function (data) {
$('#minicart').html(data);
});
}
</script>
Also I would recommend you using an URL helper to generate the url to invoke instead of hardcoding it in your javascript function. And last but not least, I would recommend you doing this unobtrusively, so that you could put this in a separate javascript file and stop mixing markup and script.
So here's how your code will look like after taking into consideration my remarks:
#model MvcDropDownList.Models.MovieModel
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm("CategoryChosen", "Home", FormMethod.Get))
{
<fieldset>
Movie Type
#Html.DropDownListFor(
m => m.SelectedCategorieID,
new SelectList(Model.Categories, "ID", "Name"),
"---Select categorie---",
new {
id = "categoryDdl"
data_url = Url.Action("CategoryChoosen", "Home")
}
)
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
}
<input type="button" value="Minicart test" id="minicart-button" />
<div id="minicart">
#Html.Partial("Information")
</div>
and then in your separate javascript file unobtrusively subscribe to the click handler of your button and send the AJAX request:
$(function() {
$('#minicart-button').click(function() {
// debugger;
var $categoryDdl = $('#categoryDdl');
var selectedCategorieID = $categoryDdl.val();
var url = $categoryDdl.data('url');
$.post(url, { selectedCategorieID: selectedCategorieID }, function (data) {
$('#minicart').html(data);
});
});
});
Provide an id for your dropdownlist:
#Html.DropDownListFor(m => m.SelectedCategorieID, new SelectList(Model.Categories, "ID",
"Name", Model.SelectedCategorieID), new {id = "myDropDownList"})
And your javascript function as follows:
<script type="text/javascript">
function categoryChosen() {
var cat = $("#myDropDownList").val();
var url = "Home/CategoryChosen?SelectedCategorieID=" + cat;
$.post(url, function (data) {
$("#minicart").html(data);
});
}
</script>
Why your code did not work?
onclick="categoryChosen('#Model.SelectedCategorieID')
is generated as
onclick="categoryChosen('0')
because the value of SelectedCategorieID is 0 when it is generated.
Bounty
It's been awhile and I still have a couple outstanding questions. I hope by adding a bounty maybe these questions will get answered.
How do you use html helpers with knockout.js
Why was document ready needed to make it work(see first edit for more information)
How do I do something like this if I am using the knockout mapping with my view models? As I do not have a function due to the mapping.
function AppViewModel() {
// ... leave firstName, lastName, and fullName unchanged here ...
this.capitalizeLastName = function() {
var currentVal = this.lastName(); // Read the current value
this.lastName(currentVal.toUpperCase()); // Write back a modified value
};
I want to use plugins for instance I want to be able to rollback observables as if a user cancels a request I want to be able to go back to the last value. From my research this seems to be achieved by people making plugins like editables
How do I use something like that if I am using mapping? I really don’t want to go to a method where I have in my view manual mapping were I map each MVC viewMode field to a KO model field as I want as little inline javascript as possible and that just seems like double the work and that’s why I like that mapping.
I am concerned that to make this work easy (by using mapping) I will lose a lot of KO power but on the other hand I am concerned that manual mapping will just be a lot of work and will make my views contain too much information and might become in the future harder to maintain(say if I remove a property in the MVC model I have to move it also in the KO viewmodel)
Original Post
I am using asp.net mvc 3 and I looking into knockout as it looks pretty cool but I am having a hard time figuring out how it works with asp.net mvc especially view models.
For me right now I do something like this
public class CourseVM
{
public int CourseId { get; set; }
[Required(ErrorMessage = "Course name is required")]
[StringLength(40, ErrorMessage = "Course name cannot be this long.")]
public string CourseName{ get; set; }
public List<StudentVm> StudentViewModels { get; set; }
}
I would have a Vm that has some basic properties like CourseName and it will have some simple validation on top of it. The Vm model might contain other view models in it as well if needed.
I would then pass this Vm to the View were I would use html helpers to help me display it to the user.
#Html.TextBoxFor(x => x.CourseName)
I might have some foreach loops or something to get the data out of the collection of Student View Models.
Then when I would submit the form I would use jquery and serialize array and send it to a controller action method that would bind it back to the viewmodel.
With knockout.js it is all different as you now got viewmodels for it and from all the examples I seen they don't use html helpers.
How do you use these 2 features of MVC with knockout.js?
I found this video and it briefly(last few minutes of the video # 18:48) goes into a way to use viewmodels by basically having an inline script that has the knockout.js viewmodel that gets assigned the values in the ViewModel.
Is this the only way to do it? How about in my example with having a collection of viewmodels in it? Do I have to have a foreach loop or something to extract all the values out and assign it into knockout?
As for html helpers the video says nothing about them.
These are the 2 areas that confuses the heck out of me as not many people seem to talk about it and it leaves me confused of how the initial values and everything is getting to the view when ever example is just some hard-coded value example.
Edit
I am trying what Darin Dimitrov has suggested and this seems to work(I had to make some changes to his code though). Not sure why I had to use document ready but somehow everything was not ready without it.
#model MvcApplication1.Models.Test
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
<script src="../../Scripts/jquery-1.5.1.js" type="text/javascript"></script>
<script src="../../Scripts/knockout-2.1.0.js" type="text/javascript"></script>
<script src="../../Scripts/knockout.mapping-latest.js" type="text/javascript"></script>
<script type="text/javascript">
$(function()
{
var model = #Html.Raw(Json.Encode(Model));
// Activates knockout.js
ko.applyBindings(model);
});
</script>
</head>
<body>
<div>
<p>First name: <strong data-bind="text: FirstName"></strong></p>
<p>Last name: <strong data-bind="text: LastName"></strong></p>
#Model.FirstName , #Model.LastName
</div>
</body>
</html>
I had to wrap it around a jquery document ready to make it work.
I also get this warning. Not sure what it is all about.
Warning 1 Conditional compilation is turned off -> #Html.Raw
So I have a starting point I guess at least will update when I done some more playing around and how this works.
I am trying to go through the interactive tutorials but use the a ViewModel instead.
Not sure how to tackle these parts yet
function AppViewModel() {
this.firstName = ko.observable("Bert");
this.lastName = ko.observable("Bertington");
}
or
function AppViewModel() {
// ... leave firstName, lastName, and fullName unchanged here ...
this.capitalizeLastName = function() {
var currentVal = this.lastName(); // Read the current value
this.lastName(currentVal.toUpperCase()); // Write back a modified value
};
Edit 2
I been able to figure out the first problem. No clue about the second problem. Yet though. Anyone got any ideas?
#model MvcApplication1.Models.Test
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
<script src="../../Scripts/jquery-1.5.1.js" type="text/javascript"></script>
<script src="../../Scripts/knockout-2.1.0.js" type="text/javascript"></script>
<script src="../../Scripts/knockout.mapping-latest.js" type="text/javascript"></script>
<script type="text/javascript">
$(function()
{
var model = #Html.Raw(Json.Encode(Model));
var viewModel = ko.mapping.fromJS(model);
ko.applyBindings(viewModel);
});
</script>
</head>
<body>
<div>
#*grab values from the view model directly*#
<p>First name: <strong data-bind="text: FirstName"></strong></p>
<p>Last name: <strong data-bind="text: LastName"></strong></p>
#*grab values from my second view model that I made*#
<p>SomeOtherValue <strong data-bind="text: Test2.SomeOtherValue"></strong></p>
<p>Another <strong data-bind="text: Test2.Another"></strong></p>
#*allow changes to all the values that should be then sync the above values.*#
<p>First name: <input data-bind="value: FirstName" /></p>
<p>Last name: <input data-bind="value: LastName" /></p>
<p>SomeOtherValue <input data-bind="value: Test2.SomeOtherValue" /></p>
<p>Another <input data-bind="value: Test2.Another" /></p>
#* seeing if I can do it with p tags and see if they all update.*#
<p data-bind="foreach: Test3">
<strong data-bind="text: Test3Value"></strong>
</p>
#*took my 3rd view model that is in a collection and output all values as a textbox*#
<table>
<thead><tr>
<th>Test3</th>
</tr></thead>
<tbody data-bind="foreach: Test3">
<tr>
<td>
<strong data-bind="text: Test3Value"></strong>
<input type="text" data-bind="value: Test3Value"/>
</td>
</tr>
</tbody>
</table>
Controller
public ActionResult Index()
{
Test2 test2 = new Test2
{
Another = "test",
SomeOtherValue = "test2"
};
Test vm = new Test
{
FirstName = "Bob",
LastName = "N/A",
Test2 = test2,
};
for (int i = 0; i < 10; i++)
{
Test3 test3 = new Test3
{
Test3Value = i.ToString()
};
vm.Test3.Add(test3);
}
return View(vm);
}
I think I have summarized all your questions, if I missed something please let me know (If you could summarize up all your questions in one place would be nice =))
Note. Compatibility with the ko.editable plug-in added
Download the full code
How do you use html helpers with knockout.js
This is easy:
#Html.TextBoxFor(model => model.CourseId, new { data_bind = "value: CourseId" })
Where:
value: CourseId indicates that you are binding the value property of the input control with the CourseId property from your model and your script model
The result is:
<input data-bind="value: CourseId" data-val="true" data-val-number="The field CourseId must be a number." data-val-required="The CourseId field is required." id="CourseId" name="CourseId" type="text" value="12" />
Why was document ready needed to make it work(see first edit for more information)
I do not understand yet why you need to use the ready event to serialize the model, but it seems that it is simply required (Not to worry about it though)
How do I do something like this if I am using the knockout mapping with my view models? As I do not have a function due to the mapping.
If I understand correctly you need to append a new method to the KO model, well that's easy merging models
For more info, in the section -Mapping from different sources-
function viewModel() {
this.addStudent = function () {
alert("de");
};
};
$(function () {
var jsonModel = '#Html.Raw(JsonConvert.SerializeObject(this.Model))';
var mvcModel = ko.mapping.fromJSON(jsonModel);
var myViewModel = new viewModel();
var g = ko.mapping.fromJS(myViewModel, mvcModel);
ko.applyBindings(g);
});
About the warning you were receiveing
Warning 1 Conditional compilation is turned off -> #Html.Raw
You need to use quotes
Compatibility with the ko.editable plug-in
I thought it was going to be more complex, but it turns out that the integration is really easy, in order to make your model editable just add the following line: (remember that in this case I am using a mixed model, from server and adding extension in client and the editable simply works... it's great):
ko.editable(g);
ko.applyBindings(g);
From here you just need to play with your bindings using the extensions added by the plug-in, for example, I have a button to start editing my fields like this and in this button I start the edit process:
this.editMode = function () {
this.isInEditMode(!this.isInEditMode());
this.beginEdit();
};
Then I have commit and cancel buttons with the following code:
this.executeCommit = function () {
this.commit();
this.isInEditMode(false);
};
this.executeRollback = function () {
if (this.hasChanges()) {
if (confirm("Are you sure you want to discard the changes?")) {
this.rollback();
this.isInEditMode(false);
}
}
else {
this.rollback();
this.isInEditMode(false);
}
};
And finally, I have one field to indicate whether the fields are in edit mode or not, this is just to bind the enable property.
this.isInEditMode = ko.observable(false);
About your array question
I might have some foreach loops or something to get the data out of the collection of Student View Models.
Then when I would submit the form I would use jquery and serialize array and send it to a controller action method that would bind it back to the viewmodel.
You can do the same with KO, in the following example, I will create the following output:
Basically here, you have two lists, created using Helpers and binded with KO, they have a dblClick event binded that when fired, remove the selected item from the current list and add it to the other list, when you post to the Controller, the content of each list is sent as JSON data and re-attached to the server model
Nuggets:
Newtonsoft
jQuery
knockoutjs
Knockout.Mapping
External scripts.
Controller code
[HttpGet]
public ActionResult Index()
{
var m = new CourseVM { CourseId = 12, CourseName = ".Net" };
m.StudentViewModels.Add(new StudentVm { ID = 545, Name = "Name from server", Lastname = "last name from server" });
return View(m);
}
[HttpPost]
public ActionResult Index(CourseVM model)
{
if (!string.IsNullOrWhiteSpace(model.StudentsSerialized))
{
model.StudentViewModels = JsonConvert.DeserializeObject<List<StudentVm>>(model.StudentsSerialized);
model.StudentsSerialized = string.Empty;
}
if (!string.IsNullOrWhiteSpace(model.SelectedStudentsSerialized))
{
model.SelectedStudents = JsonConvert.DeserializeObject<List<StudentVm>>(model.SelectedStudentsSerialized);
model.SelectedStudentsSerialized = string.Empty;
}
return View(model);
}
Model
public class CourseVM
{
public CourseVM()
{
this.StudentViewModels = new List<StudentVm>();
this.SelectedStudents = new List<StudentVm>();
}
public int CourseId { get; set; }
[Required(ErrorMessage = "Course name is required")]
[StringLength(100, ErrorMessage = "Course name cannot be this long.")]
public string CourseName { get; set; }
public List<StudentVm> StudentViewModels { get; set; }
public List<StudentVm> SelectedStudents { get; set; }
public string StudentsSerialized { get; set; }
public string SelectedStudentsSerialized { get; set; }
}
public class StudentVm
{
public int ID { get; set; }
public string Name { get; set; }
public string Lastname { get; set; }
}
CSHTML page
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>CourseVM</legend>
<div>
<div class="editor-label">
#Html.LabelFor(model => model.CourseId)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.CourseId, new { data_bind = "enable: isInEditMode, value: CourseId" })
#Html.ValidationMessageFor(model => model.CourseId)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.CourseName)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.CourseName, new { data_bind = "enable: isInEditMode, value: CourseName" })
#Html.ValidationMessageFor(model => model.CourseName)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.StudentViewModels);
</div>
<div class="editor-field">
#Html.ListBoxFor(
model => model.StudentViewModels,
new SelectList(this.Model.StudentViewModels, "ID", "Name"),
new
{
style = "width: 37%;",
data_bind = "enable: isInEditMode, options: StudentViewModels, optionsText: 'Name', value: leftStudentSelected, event: { dblclick: moveFromLeftToRight }"
}
)
#Html.ListBoxFor(
model => model.SelectedStudents,
new SelectList(this.Model.SelectedStudents, "ID", "Name"),
new
{
style = "width: 37%;",
data_bind = "enable: isInEditMode, options: SelectedStudents, optionsText: 'Name', value: rightStudentSelected, event: { dblclick: moveFromRightToLeft }"
}
)
</div>
#Html.HiddenFor(model => model.CourseId, new { data_bind="value: CourseId" })
#Html.HiddenFor(model => model.CourseName, new { data_bind="value: CourseName" })
#Html.HiddenFor(model => model.StudentsSerialized, new { data_bind = "value: StudentsSerialized" })
#Html.HiddenFor(model => model.SelectedStudentsSerialized, new { data_bind = "value: SelectedStudentsSerialized" })
</div>
<p>
<input type="submit" value="Save" data-bind="enable: !isInEditMode()" />
<button data-bind="enable: !isInEditMode(), click: editMode">Edit mode</button><br />
<div>
<button data-bind="enable: isInEditMode, click: addStudent">Add Student</button>
<button data-bind="enable: hasChanges, click: executeCommit">Commit</button>
<button data-bind="enable: isInEditMode, click: executeRollback">Cancel</button>
</div>
</p>
</fieldset>
}
Scripts
<script src="#Url.Content("~/Scripts/jquery-1.7.2.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/knockout-2.1.0.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/knockout.mapping-latest.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/ko.editables.js")" type="text/javascript"></script>
<script type="text/javascript">
var g = null;
function ViewModel() {
this.addStudent = function () {
this.StudentViewModels.push(new Student(25, "my name" + new Date(), "my last name"));
this.serializeLists();
};
this.serializeLists = function () {
this.StudentsSerialized(ko.toJSON(this.StudentViewModels));
this.SelectedStudentsSerialized(ko.toJSON(this.SelectedStudents));
};
this.leftStudentSelected = ko.observable();
this.rightStudentSelected = ko.observable();
this.moveFromLeftToRight = function () {
this.SelectedStudents.push(this.leftStudentSelected());
this.StudentViewModels.remove(this.leftStudentSelected());
this.serializeLists();
};
this.moveFromRightToLeft = function () {
this.StudentViewModels.push(this.rightStudentSelected());
this.SelectedStudents.remove(this.rightStudentSelected());
this.serializeLists();
};
this.isInEditMode = ko.observable(false);
this.executeCommit = function () {
this.commit();
this.isInEditMode(false);
};
this.executeRollback = function () {
if (this.hasChanges()) {
if (confirm("Are you sure you want to discard the changes?")) {
this.rollback();
this.isInEditMode(false);
}
}
else {
this.rollback();
this.isInEditMode(false);
}
};
this.editMode = function () {
this.isInEditMode(!this.isInEditMode());
this.beginEdit();
};
}
function Student(id, name, lastName) {
this.ID = id;
this.Name = name;
this.LastName = lastName;
}
$(function () {
var jsonModel = '#Html.Raw(JsonConvert.SerializeObject(this.Model))';
var mvcModel = ko.mapping.fromJSON(jsonModel);
var myViewModel = new ViewModel();
g = ko.mapping.fromJS(myViewModel, mvcModel);
g.StudentsSerialized(ko.toJSON(g.StudentViewModels));
g.SelectedStudentsSerialized(ko.toJSON(g.SelectedStudents));
ko.editable(g);
ko.applyBindings(g);
});
</script>
Note: I just added these lines:
#Html.HiddenFor(model => model.CourseId, new { data_bind="value: CourseId" })
#Html.HiddenFor(model => model.CourseName, new { data_bind="value: CourseName" })
Because when I submit the form my fields are disabled, so the values were not transmitted to the server, that's why I added a couple of hidden fields to do the trick
You could serialize your ASP.NET MVC view model into a javascript variable:
#model CourseVM
<script type="text/javascript">
var model = #Html.Raw(Json.Encode(Model));
// go ahead and use the model javascript variable to bind with ko
</script>
There are lots of examples in the knockout documentation that you could go through.
To achieve the additional computed properties after server mapping you will need to further enhance your viewmodels on the client side.
For example:
var viewModel = ko.mapping.fromJS(model);
viewModel.capitalizedName = ko.computed(function() {...}, viewModel);
So everytime you map from raw JSON you would need to reapply the computed properties.
Additionally the mapping plugin provides the ability to incrementally update a viewmodel as opposed to recreating it every time you go back and forth (use an additional parameter in fromJS):
// Every time data is received from the server:
ko.mapping.fromJS(data, viewModel);
And that executes an incremental data update on your model of just properties that are mapped. You can read more about that in the mapping documentation
You mentioned in the comments on Darin's answer the FluentJSON package. I'm the author of that, but its use case is more specific than ko.mapping. I would generally only use it if your viewmodels are one way (ie. server -> client) and then data is posted back in some different format (or not at all). Or if your javascript viewmodel needs to be in a substantially different format from your server model.
I am currently facing a problem with validation after dynamically adding content.
I have a view strongly typed to a model (Order). This Order can have many items. The model looks something like the following:
public class Order
{
[Key]
[HiddenInput]
public int id { get; set; }
[Display(Name = "Order Number")]
public string number { get; set; }
[Display(Name = "Order Date")]
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MM/dd/yyyy}")]
public DateTime date { get; set; }
[Required(ErrorMessage = "Beneficiary is required.")]
[Display(Name = "Beneficiary")]
public int beneficiary_id { get; set; }
[Display(Name = "Beneficiary")]
public Beneficiary beneficiary { get; set; }
[Display(Name = "Items")]
public List<Item> items { get; set; }
[Display(Name = "Payment Method")]
public List<PaymentMethod> payment_methods { get; set; }
}
I enter the order information and also the items for that specific order. I tried a couple of ways to add content dynamically and finally went with Steven Sanderson's way.
In my view, I have the regular Order information and then the items, where my model looks something like this:
#model trackmeMvc.Models.Model.Order
#{
ViewBag.Title = "Create";
Html.EnableClientValidation();
Html.EnableUnobtrusiveJavaScript();
}
<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 src="#Url.Content("~/Scripts/MicrosoftAjax.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/MicrosoftMvcAjax.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/MicrosoftMvcValidation.js")" type="text/javascript"></script>
#using (Html.BeginForm("Create", "Order", FormMethod.Post, new { #id = "create_order" }))
{
#Html.ValidationSummary(true, "Order creation was unsuccessful. Please correct the errors and try again.")
<div class="editor-label">
#Html.LabelFor(m => m.date)<req>*</req>
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.date, new { #id = "order_date" })<br />
#Html.ValidationMessageFor(m => m.date)
</div>
...
<script type="text/javascript">
$(document).ready(function () {
$("#addItem").click(function () {
var formData = $("#main_div").closest("form").serializeArray();
$.ajax({
url: "/IPO/BlankItemRow",
type: "POST",
//data: formData,
cache: false,
success: function (html) {
$("#editorRows").append(html);
//$.validator.uobtrusive.parseDynamicContent("form *");
//$("#editorRows").removeData("validator");
//$("#editorRows").removeData("unobtrusiveValidation");
//$.validator.unobtrusive.parse("#editorRows");
//$.validator.unobtrusive.parse("#create_ipo");
//$.validator.unobtrusive.parseDynamicContent($(this).first().closest("form"));
//$.validator.unobtrusive.parse($("#new_ipo_item"));
//$.validator.unobtrusive.parseElement($("#editorRows").find(".editRow:last").children().find("select"));
//$("#editorRows").find(".editRow:last").find("select").each(function () {
//alert($(this).attr("id"));
//$.validator.unobtrusive.parseElement($(this));
//$.validator.unobtrusive.parseDynamicContent($(this));
//$.validator.unobtrusive.parseDynamicContent($(this).attr("name"));
//});
//$("#editorRows").children().find(".editRows:last").find("*").each(function () {
// alert($(this).attr('id'));
//$.validator.unobtrusive.parseDynamicContent('input');
//});
//var form = $(this).closest("form").attr("id");
//$(form).removeData("validator");
//$(form).removeData("unobtrusiveValidation");
//$.validator.unobtrusive.parse(form);
}
});
return false;
});
});
</script>
Those are some of the things I tried, and nothing works.
I got the parseDynamicContent from Applying unobtrusive jquery validation to dynamic content in ASP.Net MVC. I tried it in every scenario I could think of, but still no luck.
I also tried the regular parse, removing validation from the form then applying it again, but still the newly added elements are not validated:
<div id="editorRows">
#foreach (var item in Model.items)
{
#Html.Partial("_NewItem", item)
}
</div>
... and my partial view would look something like this:
#model trackmeMvc.Models.Model.Item
#{
Layout = "";
Html.EnableClientValidation(true);
if (this.ViewContext.FormContext == null)
{
this.ViewContext.FormContext = new FormContext();
}
}
<div class="editRow">
#using (Html.BeginCollectionItem("order_items"))
{
#Html.DropDownListFor(m => m.item_id, #items, "None", new { #style = "width:205px;", #id = "ddlItems", #class="ddlItem", #name="ddlItemList" })
#Html.ValidationMessageFor(m => m.item_id)
...
}
</div>
So what's happening is, I have one empty item sent from the controller to the view by default, to show one empty row. That item is validated, but whatever comes after when I click add item, another row appears, from that partial, but I can't get it to validate. I tried to put the validation in the partial view, (before the document ready in the main form), and everything I read I applied, and it always ends up the same: validating the first row, and not the others. I tried the validation of Steven Sanderson done for that purpose - still no luck - even the validation for partials, found at this link
and the page that follows which is specific to partial validation...
What should I do to get this validation working?
Ok, I am going to start over with a new answer here.
Before you call $.validator.unobtrusive.parse, remove the original validator and unobtrusive validation from the form like so:
var form = $("#main_div").closest("form");
form.removeData('validator');
form.removeData('unobtrusiveValidation');
$.validator.unobtrusive.parse(form);
This same answer is documented here.
What worked for me was to re-apply the validator after the call to load the partial view. In my case, I'm using $.post().then() but you could do something similar with a .always() callback of an AJAX call.
$.post(url, model, function (data) {
//load the partial view
$("#Partial").html(data);
}).then(function () {
$("form").each(function () { $.data($(this)[0], 'validator', false); });
$.validator.unobtrusive.parse("form");
});