Knockout.js bind array of objects to repeated <select>'s - asp.net-mvc

I've got this Knockout.js view model:
{
"LanguageFromTos":
[{
"LanguageFromToId":0,
"LanguageFromId":2,
"LanguageFrom":null,
"AllLanguagesFrom":[
{"Selected":false,"Text":"English","Value":"1"},
{"Selected":false,"Text":"French","Value":"2"},
{"Selected":false,"Text":"Spanish","Value":"3"}
],
"LanguageToId":1,
"LanguageTo":null,
"AllLanguagesTo":[
{"Selected":false,"Text":"English","Value":"1"},
{"Selected":false,"Text":"French","Value":"2"},
{"Selected":false,"Text":"Spanish","Value":"3"}
],
"Users":null
}]
}
And these html <select>s:
<div class="LanguageFromToRow">
<input type="hidden" name="languageFromTos.index" autocomplete="off" value="c50532b0-65d2-4a81-baeb-59b768fd120f" />
<label for="languageFromTos_c50532b0-65d2-4a81-baeb-59b768fd120f__LanguageFromId">From</label>:
<select data-bind="???" data-val="true" data-val-number="The field From must be a number." data-val-required="The From field is required." id="languageFromTos_c50532b0-65d2-4a81-baeb-59b768fd120f__LanguageFromId" name="languageFromTos[c50532b0-65d2-4a81-baeb-59b768fd120f].LanguageFromId">
<option value="1">English</option>
<option selected="selected" value="2">French</option>
<option value="3">Spanish</option>
</select>
<span class="field-validation-valid" data-valmsg-for="languageFromTos[c50532b0-65d2-4a81-baeb-59b768fd120f].LanguageFromId" data-valmsg-replace="true"></span>
<label for="languageFromTos_c50532b0-65d2-4a81-baeb-59b768fd120f__LanguageToId">To</label>:
<select data-bind="???" data-val="true" data-val-number="The field To must be a number." data-val-required="The To field is required." id="languageFromTos_c50532b0-65d2-4a81-baeb-59b768fd120f__LanguageToId" name="languageFromTos[c50532b0-65d2-4a81-baeb-59b768fd120f].LanguageToId">
<option selected="selected" value="1">English</option>
<option value="2">French</option>
<option value="3">Spanish</option>
</select>
<span class="field-validation-valid" data-valmsg-for="languageFromTos[c50532b0-65d2-4a81-baeb-59b768fd120f].LanguageToId" data-valmsg-replace="true"></span>
delete
</div>
Can anyone tell me what I need to add to the data-bind attributes of each select to tie them up to the Knockout.js view model? The value of the first select should be bound to LanguageFromId in the view model and the value of the second select should be bound to LanguageToId in the view model.
LanguageFromTos in the view model is an array, so all of what you see inside may be repeated (2 LanguageFromTos would result in the LanguageFromToRow div being repeated twice, for example). The number of repeats is set both server-side (data posted by the controller may have multiple LanguageFromTos) and client-side (an 'add' button that allows the user to add another div with its contained selects) in different cases, so I presume Knockout's templating is a no-go as MVC needs to loop through and render each row so that it can catch them all on a 'normal' post back.
Any help much appreciated!
Edit
Here's the MVC model for the page:
public class DirectorySearchModel
{
[Display(Name = "User name contains")]
public string UserName { get; set; }
[Display(Name = "First name contains")]
public string FirstName { get; set; }
[Display(Name = "Last name contains")]
public string LastName { get; set; }
[Display(Name = "Languages translated")]
public IEnumerable<LanguageFromTo> LanguageFromTos { get; set; }
}
Here's the LanguageFromTo object:
public class LanguageFromTo
{
[Key]
public virtual int LanguageFromToId { get; set; }
[Display(Name = "From")]
public virtual int LanguageFromId { get; set; }
[ForeignKey("LanguageFromId")]
public virtual Language LanguageFrom { get; set; }
public virtual IEnumerable<SelectListItem> AllLanguagesFrom { get; set; }
[Display(Name = "To")]
public virtual int LanguageToId { get; set; }
[ForeignKey("LanguageToId")]
public virtual Language LanguageTo { get; set; }
public virtual IEnumerable<SelectListItem> AllLanguagesTo { get; set; }
public virtual ICollection<User> Users { get; set; }
}
And here's the cshtml code for the MVC view. This code sits in a partial that's repeated depending on how many LanguageFromTos there are in DirectorySearchModel that's passed to the MVC view:
<div class="LanguageFromToRow">
#using(Html.BeginCollectionItem("languageFromTos")) {
#: #Html.LabelFor(m => m.LanguageFromId): #Html.DropDownListFor(m => m.LanguageFromId, Model.AllLanguagesFrom, new { data_bind = "value: getLanguageFromToById(0).LanguageFromId" }) #Html.ValidationMessageFor(m => m.LanguageFromId)
#: #Html.LabelFor(m => m.LanguageToId): #Html.DropDownListFor(m => m.LanguageToId, Model.AllLanguagesTo, new { data_bind = "value: getLanguageFromToById(0).LanguageToId" }) #Html.ValidationMessageFor(m => m.LanguageToId)
delete
}
</div>

Your design here has some poor choices. Knockout's Templating is the right choice for this, but it is going to require you to start thinking in a different sort of pattern. You do not need to use partials to accomplish the repetition you are after.
Here is a fiddle demonstrating a templating solution. http://jsfiddle.net/tyrsius/XgwLD/3/
Some notes: to populate incomingData, an easy method will be to use #Html.Raw(Json.Encode(Model.DirectorySearchModel ));. This will turn your model into a JSON object, which the viewModel's constructor can easily use.
Now, I didn't use any MVC code in the fiddle because I can't, but you've obviously already found how you can put data-bind in the MVC helpers. This isn't always a bad idea, but for things like selects and DIVs that you want to use as templates, it will probably just makes things harder to read.

Related

razor MVC Only the first element has been added to the controller?

I have the following code, When I select multiple departments, it's showing correctly in the front end,
but when it's passed to the back end only the first one has been selected, any idea?
<div class="form-control-container">
<select class="form-control " asp-for="Actioned_DepartmentName" asp-items="Model.Departments" multiple>
<option value="">Please select department(s)...</option>
</select>
</div>
public class ActionedReportRequestDto
{
public DateTime From { get; set; }
public DateTime To { get; set; }
public List<DepartmentType?> DepartmentName { get; set; }
}
Thanks just find out cannot pass a list of enum type, it will always select the first one, change it to list of string and split it works fine!

ASP.net MVC Html.RadioButtonFor Grouping within Html.DisplayFor

I'm having an issue getting a dynamic set of radio buttons (driven by a CMS) working with Model binding.
So far I've gotten the radios to render, but they behave more like checkboxes, which I believe is down to the name atribute on the input being different. I had thought I'd resolved this by using a parent id to group them, but that doesn't work. On save I want to bind the state (checked / not checked) to the IsSelected property on my Option model. If I pass in a name attribute as part of the RadioButtonFor I can get the radio to work as a group but lose the model binding.
I'm using the same setup with a different Display template for checkboxes which works fine, not sure where I'm going wrong here. Anyone get any suggestions?
Models
public class Question
{
public int QuestionId { get; set; } // 1044
public string QuestionText { get; set; } // Whats your favourite colour
public HelpText HelpText { get; set; }
public IEnumerable<Option> Options { get; set; } // colours
}
public class Option
{
public int OptionId { get; set; } //
public int ParentId { get; set; } // Set as QuestionId for radio button grouping :: 1044
public string Label { get; set; }
public string Value { get; set; }
public bool IsSelected { get; set; }
}
public class RadioOption : Option {} // derived classes to drive DisplayFor template matching
public class CheckboxOption : Option {}
Views
Simplified partial view, which is passed the Question model and is looping through all the options and rendering a display template
<fieldset>
#Html.DisplayFor(x => x.Options)
</fieldset>
Radio button Display Template
#model ViewModels.RadioOption
#Html.RadioButtonFor(m => m.ParentId, Model.Label, Model.OptionId)
#Html.LabelFor(m => m.ParentId, Model.Label)
#Html.HiddenFor(m => m.Value)
#Html.HiddenFor(m => m.OptionId)
#Html.HiddenFor(m => m.Label)
#Html.HiddenFor(m => m.ParentId)
Rendered HTML
<fieldset>
<input id="Questions_5__Options_0__ParentId" name="Questions[5].Options[0].ParentId" type="radio" value="Red">
<label for="Questions_5__Options_0__ParentId">Red</label>
<input id="Questions_5__Options_1__ParentId" name="Questions[5].Options[1].ParentId" type="radio" value="Green">
<label for="Questions_5__Options_1__ParentId">Green</label>
<input id="Questions_5__Options_2__ParentId" name="Questions[5].Options[2].ParentId" type="radio" value="Blue">
<label for="Questions_5__Options_2__ParentId">Blue</label>
<input id="Questions_5__Options_3__ParentId" name="Questions[5].Options[3].ParentId" type="radio" value="Yellow">
<label for="Questions_5__Options_3__ParentId">Yellow</label>
</fieldset>

How manage a View (Validation and Binding etc) for Different models in a view in MVC

Consider a user can create a Sale advertising (Post model). but every advertising have different properties depend on its Group. Properties are not certain and can be added by admin with different constraints(Required. MinLength etc.)
I define a class like this:
public class Property
{
public int Id { get; set; }
public int Priority { get; set; }
[Required()]
public InputType Type { get; set; }
[Required()]
[MaxLength(150)]
public string Title { get; set; }
[Index(IsUnique=true)]
[Required()]
[MaxLength(100)]
public string Values { get; set; }
[MaxLength(100)]
public string Description { get; set; }
public ICollection<GroupProperty> GroupProperties { get; set; }
public ICollection<PostProperty> PostProperties { get; set; }
}
For example admin can add a model's car property to cars group. after that users must fill a model car field for advertisings in car group.
Create view for advertising is like this:
#model IEnumerable<Property>
<section>
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
<h1>New Advertising</h1>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
foreach (var item in Model)
{
#Html.EditorFor(m => item)
}
<button type="submit">hvah</button>
}
</div>
<div class="col-md-6">
</div>
</div>
</div>
</section>
Ah everything goes harder! I have a Editor template for Property class like this:
#model Property
#helper Helper(Property model)
{
switch (model.Type)
{
case WebSite.Models.DomainModels.InputType.NonNegative:
{
<div class="form-group">
<label for="#(model.Name)">#(model.Title)</label>
<span class="field-validation-valid text-danger" data-valmsg-for="#(model.Name)" data-valmsg-replace="true"></span>
<input class="form-control text-box single-line valid" data-val="true"
name="#(model.Name)" type="number" value="0"/>
</div>
return;
}
case WebSite.Models.DomainModels.InputType.RequiredShortString:
{
<div class="form-group">
<label for="#(model.Name)">#(model.Title)</label>
<span class="field-validation-valid text-danger" data-valmsg-for="#(model.Name)" data-valmsg-replace="true"></span>
<input class="form-control text-box single-line" data-val="true"
id="#(model.Name)" name="#(model.Name)" type="text" value="BB"/>
</div>
return;
}
}
}
#Helper(Model)
After all i have Client validation for properties. with hard code i can validate them in server side too. but new problem is Binding! if server side validation goes wrong i need to pass a model to view again. so i am think im doing this with a wrong way. can some one help me? maybe about how solve my problem or a better way to implement this? a simple way to use MVC validation On a complex model like this?
I think you want to create a class and validate ModelState. you can do it like-
Example:
You can pass your model state around like this:
public class MyClass{
public static void errorMessage(ModelStateDictionary ModelState) {
if (something) ModelState.AddModelError("", "Error Message");
}
}
Use in controller:
MyClass.errorMessage(ModelState);
If you need more information about modaestate validation outside then you can fiend more help from this link.

Getting different validation message "must be a number" for dropdownListFor

I have a DropDownListFor for which i have validation message as "field required"
#Html.DropDownListFor(m => m.CategoryId, new SelectList(Model.Categories, "CategoryId", "CategoryName"), "-- Select Category--", new { id = "idCategory", style = "float:left;" })
#Html.ValidationMessageFor(model => model.CategoryId)
but i'm always getting errormessage as "The field CategoryId must be a number"
my model
[Required(ErrorMessage = "This field is required")]
public long CategoryId { get; set; }
Make sure that the CategoryId property on your view model is a nullable integer:
[Required(ErrorMessage = "field required")]
public int? CategoryId { get; set; }
Also you seem to be binding your DropDownList values to a Categories property on your view model. Ensure that this property is an IEnumerable<T> where T is a type containing CategoryId and CategoryName properties. For example:
public class CategoryViewModel
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
}
and now your view model could look like this:
public class MyViewModel
{
[Required(ErrorMessage = "field required")]
public int? CategoryId { get; set; }
public IList<CategoryViewModel> Categories { get; set; }
}
And most importantly inspect the generated HTML and ensure that the values of all the <option> field of this dropdown are indeed integers:
<select class="input-validation-error" data-val="true" data-val-number="The field CategoryId must be a number." data-val-required="The CategoryId field is required." id="idCategory" name="CategoryId" style="float:left;">
<option value="">-- Select Category--</option>
<option value="1">category 1</option>
<option value="2">category 2</option>
<option value="3">category 3</option>
<option value="4">category 4</option>
<option value="5">category 5</option>
</select>
Notice the data-val-number="The field CategoryId must be a number." attribute that gets added to the <select> element. If the options values are not integers you will get this error.

Advance Binding in MVC

I have the following classes
public class Lookup
{
public int Id { get; set; }
public string Description { get; set; }
...
}
public class AccountTypeLookUp: Lookup
{
...
}
[Serializable]
public class Account
{
[HiddenInput]
public int AccountId { get; set; }
[Required, Display(Name="Account Name", Order=1)]
public string AccountName { get; set; }
[Display(Name="Account Description", Order=2)]
public string AccountDescription { get; set; }
[Required, Display(Name="Institution Name")]
public string InstitutionName { get; set; }
[Required, Display(Name="Account Number"), RegularExpression(#"[0-9]+",ErrorMessage="{0} can only be digits")]
public string AccountNumber { get; set; }
[Display(Name = "Routing Number"), RegularExpression(#"[0-9]+", ErrorMessage = "{0} can only be digits")]
public string RoutingNumber { get; set; }
[Required, Display(Name = "Account Type")]
public AccountTypeLookup AccountType { get; set; }
[ScaffoldColumn(false)]
public List<Transaction> TransactionCollection { get; set;}
public Account()
{
AccountId = default(int);
TransactionCollection = new List<Transaction>();
}
Now in my view I have a combobox which displays the description of Account Type with the values equal to the Id
<select class="input-validation-error" data-val="true" data-val-required="The Account Type field is required." id="AccountType" name="AccountType">
<option value="1">Savings</option>
<option selected="selected" value="2">Checking</option>
<option value="3">CreditCard</option>
</select>
Now how can I bind that comboBox to my local variable
account.AccountType.Id =?
My method signature is
[HttpPost]
public ActionResult Create(Account account)
Currently I get the error "The value '2' is invalid." which makes sense since it is looking for a complex type.
You have two options. The first option is to do it the way you're doing it now and that is NOT using an html helper:
<select class="input-validation-error" data-val="true" data-val-required="The Account Type field is required." id="AccountType" name="AccountType">
<option value="1">Savings</option>
<option selected="selected" value="2">Checking</option>
<option value="3">CreditCard</option>
</select>
But you have to change the id and the name to the following. But take note that the AccountType will not have the description with it. As you can only bind to a single property from the comboxbox and that would be to bind to the Id property.
id="AccountType_Id" name="AccountType.Id"
Your second option is to use DropDownListFor so you are guaranteed that binding works well. You have to build the selectlistitem though, either in the controller or in the view, best practice dictates that you build it in the controller and pass it to your view. So you can have something like:
#Html.DropDownListFor(m => m.AccountType.Id, Model.AccountTypes)
Model though is a viewmodel that can be mapped back to your Account class. If you do not want to use a viewmodel and use Account directly on your view then you need to create the list (Model.AccountTypes) in a ViewBag. Then you need to modify the DropDownListFor:
#Html.DropDownListFor(m => m.AccountType.Id,
(IEnumerable<SelectListItem>)ViewBag.AccountTypes)
// you might be pulling these values from a database,
// this is just an example of how you will build the list in the controller,
// you might build this in a for-loop, foreach or linq
ViewBag.AccountTypes = new List<SelectListItem> {
new SelectListItem { Value = "1", Text="Savings"},
new SelectListItem { Value = "2", Text="Checking"},
new SelectListItem { Value = "3", Text="CreditCard"},
};
I think MVC model binding knows to bind the query string Controller/Create?accountType.id=3 to account.AccountType.Id = 3
So try changing
<select class="input-validation-error" data-val="true" data-val-required="The Account Type field is required." id="AccountType" name="AccountType">
to
<select class="input-validation-error" data-val="true" data-val-required="The Account Type field is required." id="AccountType_Id" name="AccountType.Id">
See the recursive model binding section here: http://msdn.microsoft.com/en-us/magazine/hh781022.aspx

Resources