knockout binding partially work - binding

Here's the HTML code of PartialView returned by the server upon AJAX call:
<tbody data-bind="foreach: CoverQuotesViewModel">
<tr>
<td><input type="checkbox" data-bind="checked: IsSelected" /></td>
<td ><input type="text" data-bind="value: Label, enable: IsSelected" /></td>
</tr>
</tbody>
then the binding is applied:
$.ajax("getSelectedQuote", {
data: ko.toJSON({ model: self.selectedQuote, model1: formData }),
}),
type: "post", contentType: "application/json",
success: function (result) {
$("#custom").html(result);
ko.applyBindings(self.selectedQuote, $("#covers")[0]);
}
});
I can see my table being populated with the checkboxes properly checked or not. However for unchecked checkboxes, the corresponding input is not greyed out (disabled). If I manually uncheck a checkbox though, the input gets disabled.
So why the 'enable' property is not bound at the beginning ?
EDIT
The MVC Model class:
public class CoverQuoteViewModel
{
public CoverQuoteViewModel()
{
Childs = new List<CoverQuoteViewModel>();
}
public string ProductName { get; set; }
public string Label { get; set; }
public bool IsVisible { get; set; }
public bool IsMandatory { get; set; }
public bool IsSelected { get; set; }
public bool IsChoice { get; set; }
public bool IsComposite { get; set; }
public decimal YearPrice { get; set; }
public decimal BiannualPrice { get; set; }
public decimal QuarterPrice { get; set; }
public decimal MonthPrice { get; set; }
public List<CoverQuoteViewModel> Childs { get; private set; }
public CoverQuoteViewModel SelectedCoverQuote { get; set; }
}
EDIT
Returned JSON data
var initialData = { "Quotes":
[{ "ProductName": null, "MonthPrice": 0, "QuarterPrice": 0, "BiannualPrice": 0, "YearPrice": 0, "CoverQuotesViewModel":
[{ "ProductName": null, "Label": "Première Assistance 24h/24 (GRATUITE)", "IsVisible": true, "IsMandatory": true, "IsSelected": true, "IsChoice": false, "IsComposite": false, "YearPrice": 0.97, "BiannualPrice": 0.49, "QuarterPrice": 0.25, "MonthPrice": 0.08, "Childs": [], "SelectedCoverQuote": null },
{ "ProductName": null, "Label": "Assistance PLUS 24h/24", "IsVisible": true, "IsMandatory": false, "IsSelected": false, "IsChoice": false, "IsComposite": false, "YearPrice": 36.06, "BiannualPrice": 18.22, "QuarterPrice": 9.20, "MonthPrice": 3.10, "Childs": [], "SelectedCoverQuote": null },
{ "ProductName": null, "Label": "Supplément Vol Audio", "IsVisible": true, "IsMandatory": false, "IsSelected": false, "IsChoice": false, "IsComposite": false, "YearPrice": 33.36, "BiannualPrice": 16.85, "QuarterPrice": 8.51, "MonthPrice": 2.87, "Childs": [], "SelectedCoverQuote": null }
]}]
};
The data is parsed like that:
<script type="text/javascript">
#{ var jsonData = new HtmlString(new JavaScriptSerializer().Serialize(Model)); }
var initialData = #jsonData;
</script>
and the complete ViewModel
$(function () {
var mvcModel = ko.mapping.fromJS(initialData);
function QuoteViewModel() {
var self = this;
self.customizeQuote = function (quote) {
self.selectedQuote = quote;
//remove the disable attribute on all form controls before serializing data
$(".step").each(function () {
$(this).find('input, select').removeAttr('disabled');
});
//convert form data to an object
var formData = $('#etape').toObject();
$.ajax("getSelectedQuote", {
data: ko.toJSON({ model: self.selectedQuote, model1: formData }),
type: "post", contentType: "application/json",
success: function (result) {
debugger
$("#custom").html(result);
$("#etape").formwizard("show", "customize");
ko.applyBindings(self.selectedQuote, $("#covers")[0]);
}
});
};
self.addRemove = function (cover) {
alert(cover.Label);
};
}
var myViewModel = new QuoteViewModel();
var g = ko.mapping.fromJS(myViewModel, mvcModel);
ko.applyBindings(g);
});

Without being able to see your data or view model, I'd suggest setting the value to IsSelected, so then you will be able to see what the value is. If that looks correct, then there's some other issue, but I wouldn't be surprised if the values are initially incorrect. This could be due to how you are mapping the data to the view model, though I've no way of knowing.
<td><input type="text" data-bind="value: IsSelected, enable: IsSelected" /></td>
I'm confused as to why you are using the mapping plugin twice in your code here, once on your initial data to produce mvcModel, and once to map, well, it looks like you are mapping myViewModel onto mvcModel. Did you mean to have it that way round, because that looks incorrect?
Have you looked at g in the debugger before you applyBindings, and investigated what it looks like?

Related

Submit MVC ListBoxFor values to controller

I have a multiselect ListBoxFor control as shown in mvc view
#using ExampleViewModel
#using (Html.BeginForm("SaveExample", "Example", FormMethod.Post, new { #id
= "exampleForm" }))
{
#Html.ListBoxFor(x => x.AssignedContainers, new
MultiSelectList(Model.RegisteredContainers, "ID", "Description", null),
new { #class = "fieldNoFocusRequiredListBox", #size = 15,
id="RegisteredContainers" })
}
View model code looks like this:
public class ExampleViewModel
{
public int[] AssignedContainers { get; set; }
public List<Container_Sizes> RegisteredContainers { get; set; }
}
Controller code:
public ActionResult SaveExample(ExampleViewModel model)
{
// code
return Json(true, JsonRequestBehavior.AllowGet);
}
I was able to bind the listbox with model property RegisteredContainers. I do some some jQuery validation and then submit the form via ajax like this:
function SaveProduct() {
if (!ValidateProductForm()) {
return;
}
$.ajax({
type: "POST",
data: $('#exampleForm').serialize(),
url: '/Example/SaveExample/',
success: function (result) {
},
error: function (jqXhr, error, errorThrown) {
}
});
}
When the break point hits the controller ExampleViewModel -> AssignedContainers is null. Please note that i used ICollection/IEnumerable/List for this property AssignedContainers but no luck. Also when i see $('#exampleForm').serialize() in debugger watch i see ".....&AssignedContainers=17" as there is one item in the list box but received null on controller.
Do i need any updates to model binder or what is going wrong here?

Common validation for multiple inputs MVC 3 [duplicate]

I have a view model that has year/month/day properties for someone's date of birth. All of these fields are required. Right now, if someone doesn't enter anything for the date of birth they get 3 separate error messages.
What I want to do is somehow group those error messages together into 1 message that just says 'Date of birth is required'. So if 1 or more of those fields are blank, they will always just get the 1 validation message.
I NEED this to work on client-side validation via jquery validate and unobtrusive validate. I know this is possible with the jquery validate plugin by looking at this question. But I don't know how to achieve this with asp.net mvc using validation attributes on my model and unobtrusive validation. Hopefully there's some built in way to group properties for validation purposes, but if not can this be done with a custom validation attribute?
Here's what my existing model and view looks like:
The Model:
public class MyModel {
[Required(ErrorMessage = "Year is required")]
public int Year { get; set; }
[Required(ErrorMessage = "Month is required")]
public int Month { get; set; }
[Required(ErrorMessage = "Day is required")]
public int Day { get; set; }
}
The View:
<div>
<label>Date of birth: <span style="color:red;">*</span></label>
<div>#Html.DropDownListFor(m => m.Year, ApplicationModel.GetSelectListForDateRange(DateTime.Today.Year - 16, DateTime.Today.Year - 10), "", new{data_description="birthDate"})#Html.LabelFor(m => m.StudentBirthYear)</div>
<div>#Html.DropDownListFor(m => m.Month, ApplicationModel.GetSelectListForDateRange(1, 12, true), "", new{data_description="birthDate"})#Html.LabelFor(m => m.StudentBirthMonth)</div>
<div>#Html.DropDownListFor(m => m.Day, ApplicationModel.GetSelectListForDateRange(1, 31), "", new{data_description="birthDate"})#Html.LabelFor(m => m.StudentBirthDay)</div>
</div>
<div class="error-container">#Html.ValidationMessageFor(m => m.Year)</div>
<div class="error-container">#Html.ValidationMessageFor(m => m.Month)</div>
<div class="error-container">#Html.ValidationMessageFor(m => m.Day)</div>
I am somewhat late to the party (only couple of years) still...
Most appropriate solution is indeed creating a CustomAttribute but instead of giving you good advice an leaving to die I will show you how.
Custom Attribute:
public class GroupRequiredAttribute : ValidationAttribute, IClientValidatable
{
private readonly string[] _serverSideProperties;
public GroupRequiredAttribute(params string[] serverSideProperties)
{
_serverSideProperties = serverSideProperties;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (_serverSideProperties == null || _serverSideProperties.Length < 1)
{
return null;
}
foreach (var input in _serverSideProperties)
{
var propertyInfo = validationContext.ObjectType.GetProperty(input);
if (propertyInfo == null)
{
return new ValidationResult(string.Format("unknown property {0}", input));
}
var propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
if (propertyValue is string && !string.IsNullOrEmpty(propertyValue as string))
{
return null;
}
if (propertyValue != null)
{
return null;
}
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = ErrorMessage,
ValidationType = "grouprequired"
};
rule.ValidationParameters["grouprequiredinputs"] = string.Join(",", this._serverSideProperties);
yield return rule;
}
}
ViewModel: Decorate only one field on your viewModel like following:
[GroupRequired("Year", "Month", "Day", ErrorMessage = "Please enter your date of birth")]
public int? Year { get; set; }
public int? Month { get; set; }
public int? Day { get; set; }
Jquery: You will need to add adapters in my case it's jquery.validate.unobtrusive.customadapters.js or wherever you register your adapters (you might put this on the page just do it after unobtrusive validation runs).
(function ($) {
jQuery.validator.unobtrusive.adapters.add('grouprequired', ['grouprequiredinputs'], function (options) {
options.rules['grouprequired'] = options.params;
options.messages['grouprequired'] = options.message;
});
}(jQuery));
jQuery.validator.addMethod('grouprequired', function (value, element, params) {
var inputs = params.grouprequiredinputs.split(',');
var values = $.map(inputs, function (input, index) {
var val = $('#' + input).val();
return val != '' ? input : null;
});
return values.length == inputs.length;
});
and that should do it.
For those who are interested what this does: In C# land it grabs the ids of fields glues them with , and puts into custom attribute on Year field.
HTML should look something like this (If it doesn't debug C# attribute):
<input class="tooltip form-control input dob--input-long" data-val="true" data-val-grouprequired="Please enter your date of birth" data-val-grouprequired-grouprequiredinputs="Year,Month,Day" name="Year" placeholder="YYYY" tabindex="" type="text" value="">
Then Jquery validation splits them back into id's and checks if all of them are not empty and that's pretty much it.
You will want to mark fields as invalid somehow (now it would only mark the field attribute is sitting on) most appropriate solution IMHO is to wrap all fields in container with class field-error-wrapper and then add following to your page after Jquery validation is loaded:
$.validator.setDefaults({
highlight: function (element) {
$(element).closest(".field-error-wrapper").addClass("input-validation-error");
},
unhighlight: function (element) {
$(element).closest(".field-error-wrapper").removeClass("input-validation-error");
}
});
instead of marking field it will mark container and then you can write your css in a way that if container is marked with .input-validation-error then all fields inside turn red. I think my job here is done.
EDIT: Ok so there appears to be one more issue where fields get unmarked because validator thinks that day and month are valid and it needs to remove invalid class from parent, validator first marks invalid fields then unmarks valid which causes validation not to get highlighted, so I changed the sequence in which validation happens, I wouldn't recommend overriding this globally (cause I am not sure on what catastrophic side affects it might have) just paste it on the page where you have birthdate fields.
$(function () {
$.data($('form')[0], 'validator').settings.showErrors = function () {
if (this.settings.unhighlight) {
for (var i = 0, elements = this.validElements() ; elements[i]; i++) {
this.settings.unhighlight.call(this, elements[i], this.settings.errorClass, this.settings.validClass);
}
}
this.hideErrors();
for (var i = 0; this.errorList[i]; i++) {
var error = this.errorList[i];
this.settings.highlight && this.settings.highlight.call(this, error.element, this.settings.errorClass, this.settings.validClass);
this.showLabel(error.element, error.message);
}
if (this.errorList.length) {
this.toShow = this.toShow.add(this.containers);
}
if (this.settings.success) {
for (var i = 0; this.successList[i]; i++) {
this.showLabel(this.successList[i]);
}
}
this.toHide = this.toHide.not(this.toShow);
this.addWrapper(this.toShow).show();
};
});
Hope this saves you some time.
You could do that simply using CustomAttribute.
Just put this attribute on your model
[CustomValidation(typeof(MyModel), "ValidateRelatedObject")]
and then simply define the rules to validate the values in the following method:
public static ValidationResult ValidateRelatedObject(object value, ValidationContext context)
{
var context = new ValidationContext(value, validationContext.ServiceContainer, validationContext.Items);
var results = new List<ValidationResult>();
Validator.TryValidateObject(value, context, results);
// TODO: Wrap or parse multiple ValidationResult's into one ValidationResult
return result;
}
For more information, you could visit this link.
You should implement IValidatableObject and take of the Require. Then the validation on the server side will do the job, something like:
public class MyModel : IValidatableObject
{
public int Year { get; set; }
public int Month { get; set; }
public int Day { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (/*Validate properties here*/) yield return new ValidationResult("Invalid Date!", new[] { "valideDate" });
}
}
For client side validation you need to implement your own function, and prompt the error to the user somehow.
EDIT: Given that you still need client side validation, you should do something like this:
$("form").validate({
rules: {
Day: { required: true },
Month : { required: true },
Year : { required: true }
},
groups: {
Date: "Day Month Year"
},
errorPlacement: function(error, element) {
if (element.attr("id") == "Day" || element.attr("id") == "Month" || element.attr("id") == "Year")
error.insertAfter("#Day");
else
error.insertAfter(element);
}
});
Hi I hope this fulfill your requirement
//--------------------------HTML Code-----------------------------
<form id="myform">
<select name="day">
<option value="">select</option>
<option value="1">1</option>
<select>
<select name="mnth">
<option value="">select</option>
<option value="Jan">Jan</option>
<select>
<select name="yr">
<option value="">select</option>
<option value="2015">2015</option>
<select>
<br/>
<input type="submit" />
<br/>
<div id="msg"></div>
</form>
//-------------------------------JS Code to validate-------------
$(document).ready(function() {
$('#myform').validate({
rules: {
day: {
required: true
},
mnth: {
required: true
},
yr: {
required: true
}
},
errorPlacement: function (error, element) {
var name = $(element).attr("name");
error.appendTo($("#msg"));
},
messages: {
day: {
required: "Date of birth is required"
},
mnth: {
required: "Date of birth is required"
},
yr: {
required: "Date of birth is required"
}
},
groups: {
p: "day mnth yr"
},
submitHandler: function(form) { // for demo
alert('valid form');
return false;
}
});
});
Here is Running Example

Breeze# executeQuery returns empty objects

I have Breeze.Sharp application which communicates with legacy WebAPI which doesn't provide metadata.
Query seems to be executed properly - expected number of objects is returned but all of them are empty. Modifying query parameters also works - number of returned objects changes as expected. I was playing around with EntityManager.MetadataStore but nothing helped.
Here is the final code I'm currently using to communicate with WebAPI.
public class DokumentModelBreeze: BaseEntity
{
public string id { get; set; }
public string numer { get; set; }
public decimal suma { get; set; }
}
...
Configuration.Instance.ProbeAssemblies(typeof(DokumentModelBreeze).Assembly);
var manager = new EntityManager("http://localhost:52357/api/");
manager.DataService.HasServerMetadata = false;
var store = manager.MetadataStore;
store.SetResourceName("dokumenty", typeof(DokumentModelBreeze), true);
store.NamingConvention = new CamelCasePropertiesNamingConvention();
var builder = new EntityTypeBuilder<DokumentModelBreeze>(store);
builder.DataProperty(d => d.id).IsPartOfKey();
using(TextWriter writer = File.CreateText("C:/metadata.txt")) {
store.ExportMetadata(writer);
var query = new EntityQuery<DokumentModelBreeze>("dokumenty");
query = query.WithParameter("nrFirmy", 1).Where(p => p.numer=="123");
var results = await manager.ExecuteQuery<DokumentModelBreeze>(query);
List<DokumentModelBreeze> Dokumenty = new List<DokumentModelBreeze>();
foreach(var item in results)
Dokumenty.Add(item);
In the last foreach loop every item is of type DokumentModelBreeze but each member property equals to null or 0 respectively.
I have saved MetadataStore to file, it is included below:
{
"metadataVersion": "1.0.3",
"namingConvention": {
"clientServerNamespaceMap": {},
"name": "camelCaseProperties"
},
"structuralTypes": [
{
"shortName": "BaseEntity",
"namespace": "Breeze.Sharp",
"baseTypeName": "",
"autoGeneratedKeyType": "None"
},
{
"shortName": "DokumentModelBreeze",
"namespace": "BRuNETWPF.ViewModels",
"baseTypeName": "BaseEntity:#Breeze.Sharp",
"autoGeneratedKeyType": "None",
"defaultResourceName": "dokumenty",
"dataProperties": [
{
"name": "id",
"dataType": "String",
"isNullable": false,
"defaultValue": "",
"isPartOfKey": true
},
{
"name": "numer",
"dataType": "String",
"isNullable": false,
"defaultValue": ""
},
{
"name": "suma",
"dataType": "Decimal",
"isNullable": false,
"defaultValue": 0.0
}
]
}
],
"resourceEntityTypeMap": {
"dokumenty": "DokumentModelBreeze:#BRuNETWPF.ViewModels"
}
}
Am I missing something here or perhaps Breeze# doesn't allow to query WebAPI without metadata ?
The same code executed against test WebAPI with exposed metadata works well.
Your GetValue and SetValue properties must be defined like so:
public string id
{
get { return GetValue<string>("id"); }
set { SetValue(value); }
}
It's a pain, I know, but this fixed it for me after one of our awesome tech leads pointed it out :)

Json not parsing correctly from jQuery AJAX call in MVC4

I am passing back Json via a jQuery AJAX call to a MVC function that takes a Folder. MVC correctly parses some of the data but not the list I sent back.
MVC
public class Folder : IValidate
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<SearchCriteria> SearchCriteria { get; set; }
}
public class SearchCriteria
{
public int FolderId { get; set; }
public int SettingsEntryID { get; set; }
public string SearchParameter { get; set; }
}
public ActionResult EditFolder(Folder folder)
{
service.EditFolder(folder);
return this.Json(Json(new { Success = true }));
}
Javascript
var folder = {
Id: $("#groupID").val(),
Name: $("#groupName").val(),
SearchCriteria: []
};
$(".searchCriteria").each(function () {
folder.SearchCriteria.push(
{
FolderId: $("#groupID").val(),
SearchParameter: $(this).val(),
SettingsEntryID: $(this).attr("id").replace("searchCriteria", "")
});
});
$.ajax({
url: "/settings/editfolder/",
type: "POST",
dataType: "json",
data: folder,
traditional: true,
success: function (data) {
alert("wsaved");
}
});
folder, in this function gets set with Id and Name but SearchCriteria is not set properly. It is set to null. If I comment out the traditional: true the list gets created but all the values for each SearchCriteria are 0 or null.
Am I missing something?
You are missing two points
1. contentType: 'application/json; charset=utf-8',
2. data: JSON.stringify(folder)
And one correction.
URL should be like
url : "#Url.Action("ActionName", "ControllerName", new { area = "AreaName" })"
jQuery
$.ajax({
url: "#Url.Action("Action", "Controller", new { area = "Area" })",
type: "POST",
contentType: 'application/json; charset=utf-8',
dataType: "json",
data: JSON.stringify(folder),
traditional: true,
success: function (data) {
alert("wsaved");
}
});

MVC 3 - jquery data key value pair $.ajax with JSON results in 500 internal error - how do I do this?

Edit: I've got the solution and have described it a bit more at the end of the post
Using: MVC 3, C#
Problem: A key/value obj array sent to controller via $.post/$.ajax results in a 500 internal server error at the controller (because the value passed to the method in the C# controller is null)
I have an array that's in the format:
{
"q_1": {
"qid": "1",
"tmr": 0
},
"q_2": {
"qid": "2",
"tmr": 0
}
}
I get this via $("#myid").data() - and this is all fine.
I need to send this to my controller, and tried both post and $.ajax
var d = $("#q_data").data();
$.post("/run/submit", d, function(data) { alert(data);}, "application/json");
and
$.ajax({
url: '/run/submit',
data: d,
contentType: 'application/json',
dataType: 'json',
success: function (data) { alert(data); }
});
The method on the C# side is
public ActionResult Submit(List<PerfObj> dict)
{
int x = dict.Count;
return PartialView("_DummyPartial");
}
Where PerfObj is my model
public class PerfObj
{
public string id { get; set; }
Perfvar perfVar;
}
public class PerfVar
{
public string qid { get; set; }
/* note I've tried both int and string for the tmr param */
public string tmr { get; set; }
}
When I execute this, the call goes to the controller correctly - i.e. it hits the submit method. However, the method parameter dict, in
List<PerfObj> dict
is null.
Why? It seems to be something with my model, can't figure out how else to design it so it extracts the values correctly to the method parameter.
When I print the JSON.Stringify on the console, it shows the key/value pair correctly so I'm thinking it's going correctly to the server but the server/MVC3 doesn't like it for some reason or can't map it to the List of PerfObjs.
EDIT: Solution
Maciej's answer to my post was how I solved it. What I did eventually was to create a arrays of perfObj at the client side
$("#q_data").data(e,{key: e, perfVar: { qid: e, tmr: 0 }})
(ps - ignore redundant usage of 'e', I've got other plans, this is a dummy case)
And then I mapped it to a JSON friendly array
var arr = [];
$.each($('#q_data').data(), function (i, e) {
var p = $(this).data(i);
var obj = { key: i, perfVar: { id: e.perfVar.qid, tmr: e.perfVar.tmr}};
arr.push(obj);
});
Then stringified it
var q = JSON.stringify(arr);
$.ajax'd it as described in Maciej's post.
I redefined my classes properly
public class PerfObj
{
public string key { get; set; }
public PerfVar perfVar { get; set; }
}
public class PerfVar
{
public string id { get; set; }
public int tmr { get; set; }
}
and changed the signature of my controller method
[HttpPost]
public ActionResult Submit(PerfObj[] dict)
{
return PartialView("_DummyPartial");
}
This now works perfectly and I can extend my classes fairly easily to do what I want.
Thank you all!
There are 3 things wrong with your code:
A. The property PerfVar must be made public and there must be a get and set on it:
public class PerfObj
{
public string id { get; set; }
public Perfvar perfVar { get; set; }
}
B. Your JSON representation of the list is incorrect. It should be:
var e = [
{ "id": "foo", "perfVar": { "qid": "a", "tmr": "b"}},
{ "id": "foo", "perfVar": { "qid": "a", "tmr": "b"}}
];
C. You have to stringify the array and specify type: 'POST' to pass it to your MVC controller via ajax:
$.ajax({
url: '/run/submit',
data: JSON.stringify(e),
contentType: 'application/json, charset=utf-8',
dataType: 'json',
type: 'POST',
success: function (data) { alert(data); }
});
You can't directly map a key/value pair to a flat sequence. MVC has no idea how to do that.
You either need a custom model binder, or a better/easier option would be to change how you create the JSON on the client-side, so it actually matches up to your model.
Something like this:
var perfObjs =
{
{ id: 1, perfVar: { qId: 1, tmr: 0 }},
{ id: 2, perfVar: { qId: 2, tmr: 0 }},
}
$.post("/run/submit", perfObjs, function(data) { alert(data);}, "application/json");
Because the controller does not recognize the json value as List.
Why not just pass the raw string to your controller and let your controller convert the json string to object? that will be much more easier.

Resources