I am creating configurable forms in MVC which will contain dynamic controls base off of this SO post. The controls are all built from my base ControlViewModel which just contains properties for all of the controls
public abstract class ControlViewModel
{
public abstract string Type { get; }
public bool Visible { get; set; }
public string Label { get; set; }
public string Name { get; set; }
}
Each control type is defined seperately by inheriting ControlViewModel
public class TextBoxViewModel : ControlViewModel
{
public override string Type
{
get { return "textbox"; }
}
public string Value { get; set; }
}
I have text boxes, check boxes, and drop downs all defined in a similar manner. The issue I am having is when the controls are displayed on the page, their name and id attributes are not rendering as expected. In my controller I have
public ActionResult Index()
{
var model = new MyViewModel
{
Controls = new ControlViewModel[]
{
new TextBoxViewModel
{
Visible = true,
Label = "label 1",
Name = "TextBox1",
Value = "value of textbox"
}
}
}
return View(model)
}
In my Index.cshtml I render each control like so:
#model DynamicForms.Models.MyViewModel
#using (Html.BeginForm())
{
for (int i = 0; i < Model.Controls.Length; i++)
{
if (Model.Controls[i].Visible)
{
<div>
#Html.HiddenFor(x => x.Controls[i].Type)
#Html.HiddenFor(x => x.Controls[i].Name)
#Html.EditorFor(x => x.Controls[i])
</div>
}
}
<input type="submit" value="OK" />
}
The editor just renders the control and the label
#model DynamicForms.Models.TextBoxViewModel
#Html.LabelFor(x => x.Value, Model.Label)
#Html.TextBoxFor(x => x.Value)
The issue is that when the page renders, the name and id attributes don't render as the actual string values, but instead as the type
<div>
<input id="Controls_0__Type" name="Controls[0].Type" type="hidden" value="textbox">
<input id="Controls_0__Name" name="Controls[0].Name" type="hidden" value="TextBox1">
<label for="Controls_0__Value">label 1</label>
Does anyone know how I can populate the name and id attributes correctly here?
Related
I have a form that needs to capture values from checkboxes in a form. Each checkbox should have an integer value and when the form is submitted, the view model should validate the values and at least one should be selected.
I need to also build a two way binding so that the framework will auto select the options that are selected when the page is loaded.
Here is what my model looks like
public class SomeViewModel
{
[Required(ErrorMessage = "You must select at least one site.")]
[Display(Name = "All site to have access too")]
public int[] Sites { get; set; }
}
The I encapsulate my ViewModel in a Presentation class called Presenter like so
public class Presenter
{
public SomeViewModel Access { get; set; }
public IEnumerable<Site> AvailableSites { get; set; }
}
Now, I pass Presenter to my view and want to render the
And here is how my view looks like
<div class="form-group">
#Html.LabelFor(m => m.Access.Sites, new { #class = "col-sm-2 control-label" })
<div class="col-sm-10">
#for (int i = 0; i < Model.AvailableSites.Count(); i++ )
{
<label class="radio-inline">
#Html.CheckBoxFor(m => m.Access.Sites[i])
</label>
}
</div>
#Html.ValidationMessageFor(m => m.Access.Sites)
</div>
since #Html.CheckBoxFor accepts bool value, and I am passing an integer I am getting an error on the #Html.CheckBoxFor(m => m.Access.Sites[i]) line inside the view.
How can I correct this issue? and how can I correctly render checkboxes in this view?
As you have discovered, you can only use CheckBoxFor() to bind to a bool property. Its a bit unclear why you have 2 view models for this when you could simplify it by just using
public class Presenter
{
[Required(ErrorMessage = "You must select at least one site.")]
[Display(Name = "All site to have access too")]
public int[] Sites { get; set;
public IEnumerable<Site> AvailableSites { get; set; }
}
One option you can consider (based on the above model and assuming type of Site contains properties int ID and string Name)
Change the view to to manually generate <input type="checkbox" /> elements
#foreach(var site in Model.AvailableSites)
{
// assumes your using Razor v2 or higher
bool isSelected = Model.Sites.Contains(s.ID);
<label>
<input type="checkbox" name="Sites" value="#site.ID" checked = #isSelected />
<span>#s.Name</span>
</label>
}
#Html.ValidationMessageFor(m => m.Sites)
Note the #Html.LabelFor(m => m.Sites) is not appropriate. A <label> is an accessibility element for setting focus to its associated form control and you do not have a form control for Sites. You can use <div>#Html.DisplayNameFor(m => m.Sites)</div>
A better option (and the MVC way) is to create a view model representing what you want in the view
public class SiteVM
{
public int ID { get; set; }
public string Name { get; set; }
public bool IsSelected { get; set; }
}
and in your GET method, return a List<SiteVM> based on all available site (for example
List<SiteVM> model = db.Sites.Select(x => new SiteVM() { ID = x.ID, Name = x.Name };
return View(model);
and in the view use a for loop of EditorTemplate (refer this answer for more detail) to generate the view
#model List<SiteVM>
....
#using (Html.BeginForm())
{
for(int i = 0; i < Model.Count; i++)
{
#Html.HiddenFor(m => m[i].ID)
#Html.HiddenFor(m => m[i].Name)
<label>
#Html.CheckBoxFor(m => m[i].IsSelected)
<span>#Model[i].Name</span>
</label>
#Html.ValidationMessage("Sites")
}
....
}
and then in the POST method
[HttpPost]
public ActionResult Edit(List<SiteVM> model)
{
if (!model.Any(x => x.IsSelected))
{
ModelState.AddModelError("Sites", "Please select at least one site");
}
if (!ModelState.IsValid)
{
return View(model);
}
...
}
In either case you cannot get client side validation using jquery.validate.unobtrusive because you do not (and cannot) create a form control for a property which is a collection. You can however write your own script to display the validation message and cancel the default submit, for example (assumes you use #Html.ValidationMessageFor(m => m.Sites, "", new { id = "sitevalidation" }))
var sitevalidation = $('#sitevalidation');
$('form').submit(function() {
var isValid = $('input[type="checkbox"]:checked').length > 0;
if (!isValid) {
sitevalidation.text('Please select at least one site');
return false; // cancel the submit
}
}
$('input[type="checkbox"]').click(function() {
var isValid = $('input[type="checkbox"]:checked').length > 0;
if (isValid) {
sitevalidation.text(''); // remove validation message
}
}
After checking boxes and submitting the form, the ShouldSend field is the default value (false) in the Controller post method, even if I checked the appropriate box to make it true. I've looked up similar issues, and the two most common recommendations are to check that:
the complex objects are indexed so the model binder can put them back together in the list
I use CheckBoxFor in order to bind the result of the checkbox to my model
I'm currently doing both of those things, but it's still not binding. Everything else is binding just fine (the list of MessageTypeViewModels in SidebarViewModel, for example).
Any idea why my checkboxes aren't binding?
ViewModels
public class WrapperViewModel
{
public WrapperViewModel()
{
Sidebar = new SidebarViewModel();
Content = new ContentViewModel();
}
public SidebarViewModel Sidebar { get; set; }
public ContentViewModel Content { get; set; }
}
public class SidebarViewModel
{
public SidebarViewModel()
{
MessageTypeViewModels = new List<MessageTypeViewModel>
{
new TypeViewModel {Type = "Type 1", Label = "Label 1"},
new TypeViewModel {Type = "Type 2", Label = "Label 2"},
// etc.
};
}
public IEnumerable<MessageTypeViewModel> MessageTypeViewModels { get; set; }
public int Field2 { get; set; }
public int? Field3 { get; set; }
}
public class MessageTypeViewModel
{
public string Type { get; set; }
public string Label { get; set; }
public bool ShouldSend { get; set; }
}
Views
// Index.cshtml
#model MessageGeneratorViewModel
#using (Ajax.BeginForm("SendMessages", new AjaxOptions
{
HttpMethod = "POST",
UpdateTargetId = "content",
}))
{
<div id="sidebar">
<h3>Message Types</h3>
<ul>
#Html.EditorFor(m => m.MessageTypeViewModels, "MessageTypeEditorTemplate")
</ul>
<h3>Target</h3>
<ul>
<li>
#Html.LabelFor(m => m.Field2)
#Html.TextBoxFor(m => m.Field2)
</li>
<li>
#Html.LabelFor(m => m.Field3)
#Html.TextBoxFor(m => m.Field3)
</li>
</ul>
<button type="submit">Send Message</button>
</div>
}
// MessageTypeEditorTemplate.cshtml
#model List<MessageTypeViewModel>
#for (int i = 0; i < Model.Count; i++)
{
<li>
<label>
#Html.CheckBoxFor(m => m[i].ShouldSend)
#Model[i].Label
</label>
</li>
}
Controller
public ActionResult SendMessages(WrapperViewModel model)
{
// model.Sidebar.MessageTypeViewModels[<any-index>].ShouldSend is false, even if I checked the appropriate box.
}
Ah, I should have been posting the SidebarViewModel, not the WrapperViewModel. Now everything binds correctly.
public ActionResult SendMessages(SidebarViewModel sidebar)
{
// do stuff
}
I have the following cshtml form
#using (Html.BeginForm(Html.BeginForm("Create", "UserRole", Model, FormMethod.Post)))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>Role</legend>
<div class="editor-label">
#Html.Label(Model.User.UserName)
</div>
<div class="editor-field">
#Html.CheckBoxList(Model.CheckboxList)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
And I wish to get the Model.CheckboxList selected Items in my action.
I have the following Create Action in my Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(UserRoleViewModel userRoleViewModel)
{
if (ModelState.IsValid)
{
//_context.Role.Add(role);
//_context.SaveChanges();
//return RedirectToAction("Index");
}
return View(viewModel);
}
However the viewModel.CheckboxList is 0.
How can I pass the selected values of the checkboxlist, and also the Model.User to the Controller Action?
My ViewModel looks like this :-
public User User { get; set; }
public IEnumerable<Role> RoleList { get; set; }
public List<UserRoleViewModel> UserList { get; set; }
public IEnumerable<SelectListItem> CheckboxList { get; set; }
public UserRoleViewModel()
{
}
public UserRoleViewModel(User user, IEnumerable<Role> roleList )
{
User = user;
RoleList = roleList;
}
Thanks for your help and time!
UPDATE ----------- After reading this post enter link description here, I tried to adapt my code to follow the example, but I am still finding problems with this updated code.
Now I have the following :-
cshtml :-
#model IEnumerable<MvcMembership.ViewModels.RoleCheckboxListViewModel>
#using (Html.BeginForm())
{
#Html.EditorForModel()
<input type="submit" value="OK" />
}
Views/Role/EditorTemplates/RoleCheckboxListViewModel.cshtml
#model MvcMembership.ViewModels.RoleCheckboxListViewModel
#Html.HiddenFor(x => x.RoleId)
#Html.HiddenFor(x => x.RoleName)
<div>
#Html.CheckBoxFor(x => x.Checked)
#Html.LabelFor(x => x.Checked, Model.RoleName)
</div>
ViewModels :-
public class RoleCheckboxListViewModel
{
public string RoleId { get; set; }
public string RoleName { get; set; }
public bool Checked { get; set; }
}
and the controller action is as follows :-
public ActionResult Create(int? uid)
{
var checkBoxList = new[]
{
new RoleCheckboxListViewModel() {
RoleId = "1", Checked = true, RoleName = "item 1" },
new RoleCheckboxListViewModel() {
RoleId = "2", Checked = true, RoleName = "item 2" },
new RoleCheckboxListViewModel() {
RoleId = "3", Checked = true, RoleName = "item 3" },
};
return View(checkBoxList);
}
The problem I have now is that on the Create.cshtml. I cannot see the checkboxlist, but only 123 displayed as well as the OK button.
Any help would be very much appreciated cause I am at a dead end at the moment.
I've accomplished this with the following parts:
1) A view model for the child element that adds the bool property that will represent whether or not the checkbox is checked in the View later... ie:
public class CategoryViewModel
{
public int ID { get; set; }
public string Name { get; set; }
public bool Assigned { get; set; }
}
2) A view model for the parent element that adds a collection property for this new child element view model, ie:
public class ManufacturerViewModel
{
public Manufacturer Manufacturer { get; set; }
public IList<CategoryViewModel> Categories { get; set; }
public ManufacturerViewModel()
{
Categories = new List<CategoryViewModel>();
}
}
3) A service layer method for getting a list of all child elements, while also setting the bool property for each ("Assigned" in my example). To be used by your controller.
public IList<CategoryViewModel> GetCategoryAssignments(Manufacturer mfr)
{
var categories = new List<CategoryViewModel>();
foreach (var category in GetCategories())
{
categories.Add(new CategoryViewModel
{
ID = category.ID,
Name = category.Name,
Assigned = mfr.Categories.Select(c => c.ID).Contains(category.ID)
});
}
return categories;
}
4) A method for updating the parent item's collection based on your checkboxlist selections. To be used by your controller.
public void UpdateCategories(string[] selectedCategories, ManufacturerViewModel form)
{
if (selectedCategories == null)
selectedCategories = new string[] { };
var selectedIds = selectedCategories.Select(c => int.Parse(c)).ToList();
var assignedIds = form.Manufacturer.Categories.Select(c => c.ID).ToList();
foreach (var category in GetCategories())
{
if (selectedIds.Contains(category.ID))
{
if (!assignedIds.Contains(category.ID))
form.Manufacturer.Categories.Add(category);
}
else
{
if (assignedIds.Contains(category.ID))
form.Manufacturer.Categories.Remove(category);
}
}
}
5) Modifications to your Create/Edit view. ie:
#Html.EditorFor(model => model.Categories)
You must also add this so that the original assigned values are included in post data. You'll have to add a HiddenFor for each property that you have set as Required through validation.
for (int i = 0; i < Model.Manufacturer.Categories.Count; i++)
{
#Html.HiddenFor(model => model.Manufacturer.Categories[i].ID);
#Html.HiddenFor(model => model.Manufacturer.Categories[i].Name);
}
6) And finally, a new EditorTemplate for your child view model element. ie:
#model YourProject.ViewModels.CategoryViewModel
<li>
<input type="checkbox"
id="#string.Format("cb{0}{1}", #Model.Name, #Model.ID)"
name="selectedCategories" //Notice this name corresponds to string[] selectedCategories so that it can be extracted from the post data
value="#Model.ID"
#(Html.Raw(Model.Assigned ? "checked=\"checked\"" : "")) />
<label for="#string.Format("cb{0}{1}", #Model.Name, #Model.ID)">#Model.Name</label>
#Html.HiddenFor(model => model.ID)
</li>
Hopefully my own application gives you a better idea of how to solve this issue.
Store your selected value into the variable as follows, and pass it to an hidden field, then you can access it easily
var modelSelected = document.getElementById("modelName");
document.getElementById('selectedModel').value =
modelSelected.options[modelSelected.selectedIndex].text;
<input id="selectedModel" name="selectedModel" type="hidden" runat="server" />
I generate a number of textbox based on the number of element in a list.
The model :
public class MyModel
{
public List<Language> Languages { get; set; }
public string Code { get; set; }
}
public class Language
{
public int Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public bool IsDefault { get; set; }
}
The view :
#model MyModel
<form id="formDetail">
#Html.TextBoxFor(m => m.Code)
#foreach (var item in Model.Language)
{
<input type="text" id="#item.Code"/> //the code are : FR, EN, GE, ...
}
</form>
I post the form (POST) with Ajax.
Controller :
[HttpPost]
public ActionResult Save(MyModel myModel)
{
..
}
The number of textbox can be different depending of the number of language in the Language list. Could you tell me how get the value of these textboxes in the controller
Thanks,
Replace:
#foreach (var item in Model.Language)
{
<input type="text" id="#item.Code"/> //the code are : FR, EN, GE, ...
}
with:
#for (var i = 0; i < Model.Language.Count; i++)
{
#Html.HiddenFor(x => x.Language[i].Id)
#Html.HiddenFor(x => x.Language[i].Name)
#Html.HiddenFor(x => x.Language[i].IsDefault)
#Html.EditorFor(x => x.Language[i].Code)
}
and the model binder's gonna take care for the rest.
You have to use an indexed for loop to render the corresponding values for the nameattribute for the inputs, the TextboxFor helper and a hidden field, that sends the value for the Code property:
#for (int i=0; i<Model.Language.Count; i++)
{
#Html.TextboxFor(m => Model.Language[i].Name)
#Html.HiddenFor(m => Model.Language[i].Code)
}
I currently have an object Tag defined as follows:
public class Tag
{
public string Name { get; set; }
}
Now, this is a collection property of a Model which I'm defining as:
public class MyModel
{
public string Name { get; set; }
public IList<Tag> Tags { get; set; }
}
In my view I have the following code:
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(m => m.Name)
#Html.TextBoxFor(m => m.Name)
</div>
<div>
<!--
Here I'd like a collection of checkbox inputs, where the selected names
get passed back to my controller via the IList<Tag> collection
-->
</div>
<input type="submit" value="Submit" />
}
How do I return the selected items on my checkbox group (specified in comments) via the IList collection of my model?
Use Editor Templates
For having the Checkbox, Add another Proeprty to your Tag classs to specify whether it is selected or not.
public class Tag
{
public string Name { get; set; }
public bool IsSelected { set; get; }
}
Now from your GET Action, you can set a List of Tags in your Model's Tags Property and sent it to the View.
public ActionResult AddTag()
{
var vm = new MyModel();
//The below code is hardcoded for demo. you mat replace with DB data.
vm.Tags.Add(new Tag { Name = "Test1" });
vm.Tags.Add(new Tag { Name = "Test2" });
return View(vm);
}
Now Let's create an Editor Template, Go to The View/YourControllerName and Create a Folder called EditorTemaplates and Create a new View there with the same name as of the Property type ( Tag.cshtml).
Add this content to the new editor template now.
#model Tag
<p>
<b>#Model.Name</b> :
#Html.CheckBoxFor(x => x.IsSelected) <br />
#Html.HiddenFor(x=>x.Name)
</p>
Now in your Main View, Call your Editor template using the EditorFor Html Helper method.
#model MyModel
<h2>AddTag</h2>
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(m => m.Name)
#Html.TextBoxFor(m => m.Name)
</div>
<div>
#Html.EditorFor(m=>m.Tags)
</div>
<input type="submit" value="Submit" />
}
Now when You Post the Form, Your Model will have the Tags Collection where the Selected Checkboxes will be having a True value for the IsSelected Property.
[HttpPost]
public ActionResult AddTag(MyModel model)
{
if(ModelState.IsValid)
{
//Check for model.Tags collection and Each items IsSelected property value.
//Save and Redirect(PRG pattern)
}
return View(model);
}
Like this
This is similar to something i have done in a site im working on.
I used this extension #Html.CheckBoxListFor()
Hope this helps.
If you can add a bool IsChecked property to your Tag model then you can just use EditorFor (or CheckBoxFor) in a loop. The trick is to use a for loop with indexer (not foreach) such that you access the property via the views main model. Then the modelbinder will do the rest for you so your POST action will receive MyModel with its Tags IsChecked properties set to the correct states.
Models:
public class Tag
{
public string Name { get; set; }
public bool IsChecked { get; set; }
}
public class MyModel
{
public string Name { get; set; }
public List<Tag> Tags { get; set; }
}
The View:
#model MyMvcApplication.Models.MyModel
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(m => m.Name)
#Html.TextBoxFor(m => m.Name)
</div>
<div>
#for (int i = 0; i < Model.Tags.Count; i++)
{
#Html.DisplayFor(x => Model.Tags[i].Name)
#Html.EditorFor(x => Model.Tags[i].IsChecked)
}
</div>
<input type="submit" value="Submit" />
}