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>
Related
I'm trying to build one view that includes all (Create, Edit, Delete, and Index) in one View which is Index.
The problem is with Editing. Always returns null to the controller as shown in the gif.
I know what is the problem but I can't solve it. The explanation at the end of the question after the hr line
I have Model and ViewModel as follows.
The Model BootstrapCategory
public class BootstrapCategory
{
[Key]
public Guid Id { get; set; }
[MaxLength(20)]
[Required]
public string Category { get; set; }
}
The ViewModel VMBPCategoris
public class VMBPCategoris
{
public List<BootstrapCategory> bootstrapCategories { get; set; }
public Guid Id { get; set; }
public string Category { get; set; }
}
The View
Note: Edit not by the usual button in the table it instead by another
button as shown in the gif
#model VMBPCategoris
#foreach (var item in Model.bootstrapCategories)
{
<tr>
<td>
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="#item.Id" />
<div class="#item.Id d-none">
<div class="input-group">
<input id="btnGroupEdit" type="submit" value="Save" class="input-group-text btn btn-primary" />
<input asp-for="#item.Category" class="form-control" aria-label="Input group example" aria-describedby="btnGroupEdit">
</div>
<span asp-validation-for="#item.Category" class="text-danger"></span>
</div>
</form>
<div class="#item.Id">
#Html.DisplayFor(modelItem => item.Category)
</div>
</td>
<td>
Edit |
<a asp-action="Details" asp-route-id="#item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="#item.Id">Delete</a>
</td>
</tr>
}
The Controller
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit([Bind("Id,Category")] BootstrapCategory bootstrapCategory)
{
_context.Update(bootstrapCategory);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
//return View(vMBPCategoris);
}
In the ViewModel VMBPCategoris
The problem, I get the list of Category values from this list
public List<BootstrapCategory>bootstrapCategories { get; set; }
on edit, I set a new value to one member in the list, not to this
public Guid Id { get; set; }
public string Category { get; set; }
So that It returns Null
However, if I change asp-for="#item.Category" to
asp-for="Category" the input gets empty value, but it returns the edited value not empty
**Here is my question >> can I use asp-for to get value from
public List<BootstrapCategory> bootstrapCategories { get; set; }
and set value to
public Guid Id { get; set; }
public string Category { get; set; }
Because,
using asp-for="#item.Category" is beneficial to get category values but not for setting a value.
And using asp-for="Category" is beneficial to set category value but not for getting a value.
Finally, I found a very simple solution.
to get values I used
value="#item.Category"
and to set a value I used
asp-for="Category"
That's it ...
<input asp-for="Category" value="#item.Category" class="form-control">
In that way, I can get a value from a model and set a value to another model
I have Parent model "Receipt" and it has dictonary of objects as a property called CustomControlDictionary of class CustomControlModel.
In my view i loop throuh each object in CustomControlDictionary property and call EditorTemplate. In the template i simply add label and textbox for my CustomControlModel object.
When code renders html controls have same id and name attributes : id="key_Value" name="key.Value" .
I did try using regualr arrays or lists instead of dictionary ,but with same result. Looking forward for any advise.
Model:
public class ReceiptModel
{
public ReceiptModel(){}
public int ReceiptId { get; set; }
public string ReceiptNumber { get; set; }
public Dictionary<string, CustomControlModel> CustomControlDictionary{get; set; }
// public List<CustomControlModel> CustomControlList { get; set; }
}
public class CustomControlModel
{
public CustomControlModel(){}
public int CustomControlId{ get; set; }
public string CustomControlName{ get; set; }
public string LabelCaption{ get; set; }
public string Value{ get; set; }
}
View:
//View (#ReceiptModel):
#foreach (var key in Model.CustomControlDictionary )
{
#Html.EditorFor(m => key.Value,"CustomControlModel")
}
Editor Template:
#model EWMS_MVC.Classes.CustomControlModel
#Html.HiddenFor(model => model.CustomControlId)
#Html.LabelFor(model => model.CustomControlId, Model.LabelCaption)
#Html.TextBoxFor(model => model.CustomControlId, new { #Value = #Model.Value })
Rendered html has same id and name for all obects in CustomControlDictionary property of ReceiptModel:
<input id="key_Value_CustomControlId" name="key.Value.CustomControlId" type="hidden" value="201922" />//value here should be "id" of the input field
<label for="key_Value_CustomControlId">Receipt number:</label>
<input id="key_Value_CustomControlId" name="key.Value.CustomControlId" type="text" value="201922" />//value here should be "id" of the input field
I think the best approach for this situation would be to avoid the HtmlHelper methods in your template and use html controls instead:
#model EWMS_MVC.Classes.CustomControlModel
<label>#Model.LabelCaption</label>
<input id="#Model.CustomControlId" name="#Model.CustomControlName" type="text" value="#Model.Value" />
I have a view with the name "Create". This view gets the "SchoolViewModel" which contains two classes:
public class SchoolViewModel
{
public List<Teacher> ListTeacher { get; set; }
public List<SchoolClass> ListSchoolClass { get; set; }
public ClassComplete ClassComplete { get; set; }
}
Each list in "SchoolViewModel" provides data from a database.
At the "Create" page you should be able now to select a teacher and class (DropDownList). The "ClassComplete" object contains the two classes (Teacher and SchoolClass) and the roomname
public class ClassComplete
{
public string RoomName { get; set; }
public SchoolClass SchoolClass { get; set; }
public Teacher Teacher { get; set; }
}
I want only to post the "ClassComplete" object.
My ActionResult
[HttpPost]
public ActionResult Create(ClassComplete cp)
{
// Do something
return View();
}
Edit:
Razor View
#using (Html.BeginForm())
{
#Html.EditorFor(m => m.ListTeacher[0].TeacherName)
#Html.EditorFor(m => m.ListSchoolClass[0].ClassName)
#Html.TextBoxFor(m => m.cl.RoomName)<br />
<input type="submit" value="Click" />
}
Is this the right way ?
best regards
If you want to POST only ClassComplete model you will need to indicate the binding prefix:
[HttpPost]
public ActionResult Create([Bind(Prefix="ClassComplete")] ClassComplete cp)
{
// Do something
return View();
}
and in your view:
#using (Html.BeginForm())
{
#Html.TextBoxFor(m => m.ClassComplete.RoomName)
<br />
<input type="submit" value="Click" />
}
The TextBoxFor will generate the following input field in the resulting markup:
<input type="text" name="ClassComplete.RoomName" />
Notice the name of the input field. That's the reason why you need to indicate this prefix in your controller action.
This will also work for the other properties if you want to send them you just need to include the corresponding input fields:
#Html.TextBoxFor(m => m.ClassComplete.SchoolClass.SomeProperty)
#Html.TextBoxFor(m => m.ClassComplete.Teacher.SomeOtherProperty)
...
Im trying to implement a way how to enter recipes correctly, so far I have the following:
Recipe.cs
public class Recipe
{
[Key]
public int RecipeId { get; set; }
[Required]
public string Name { get; set; }
public string Subtitle { get; set; }
public int Serving { get; set; }
public string Instructions { get; set; }
public virtual ICollection<Ingredient> Ingredients
}
Ingredient.cs
public class Ingredient
{
[Key]
public int IngredientId { get; set; }
public string Name { get; set; }
public string Amount { get; set; }
public virtual ICollection<Recipe> Recipes { get; set; }
}
In my Form, I want to Add the Ingredients Inline, with JavaScript Rendering a new pair of fields if needed.
(The ViewModel is nothing else than a class holding an instance of Recipe)
Create.cshtml
#model Vineyard.WebUI.Areas.Admin.Models.RecipeViewModel
<div class="span9">
<h2>Create a new Recipe</h2>
#using (#Html.BeginForm("Create", "Recipes", FormMethod.Post))
{
<fieldset>
#Html.LabelFor(r => r.Recipe.Name)
#Html.TextBoxFor(r => r.Recipe.Name)
</fieldset>
<fieldset>
#Html.LabelFor(r => r.Recipe.Subtitle)
#Html.TextBoxFor(r => r.Recipe.Subtitle)
</fieldset>
<div id="ingredients">
#Html.EditorFor(r => r.Recipe.Ingredients, "Ingredient")
</div>
Add Ingredient
<fieldset>
#Html.LabelFor(r => r.Recipe.Serving)
#Html.TextBoxFor(r => r.Recipe.Serving)
</fieldset>
<fieldset>
#Html.LabelFor(r => r.Recipe.Instructions)
#Html.TextAreaFor(r => r.Recipe.Instructions)
</fieldset>
<input type="submit" value="Save Recipe" />
}
</div>
Shared/EditorTemplates/Ingredient.cshtml
#model Vineyard.Core.Entities.Ingredient
<div class="ingredient form-inline">
#Html.LabelFor(r => r.Amount)
#Html.TextBoxFor(r => r.Amount)
#Html.LabelFor(r => r.Name)
#Html.TextBoxFor(r => r.Name)
</div>
In the rendered HTML tough, I see the following:
<input id="Recipe_Ingredients_Amount" name="Recipe.Ingredients.Amount" type="text" value="">
Which leads my to believe, that it does not Add the Ingredient as a Member of the Collection, but as a full blown Object by itself. Shouldnt it have an index referencing to it? (Based of this http://jarrettmeyer.com/post/2995732471/nested-collection-models-in-asp-net-mvc-3)
When I look at the Model, that is being passed back to the controller, the Ingredients Part is null
So, Im wondering - what am I missing here or doing wrong, that can be changed to actually populate the Collection correctly? From there, I can handle it in the controller to be saved in the right format.
Change
public virtual ICollection<Ingredient> Ingredients
to
public IEnumerable<Ingredient> Ingredients { get; set; }
(lose the virtual and change ICollection to IEnumerable).
Also, in your controller, pre-populate the list of ingredients with a single item (for testing) otherwise nothing will appear in your HTML.
This answer was converted from my comment
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.