Using Editor Templates to Display Multiple Forms - c#

My question is very similar to this one. The application I'm developing is written in MVC 3 and Razor. It lets its users select items from a store and send each one to a different address.
Here are my ViewModels:
public class DeliveryDetailsViewModel
{
public FromDetailsViewModel From { get; set; }
public IList<ToDetailsViewModel> To { get; set; }
}
public class DetailsViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public class FromDetailsViewModel : DetailsViewModel
{
public string StreetAddress { get; set; }
public string Suburb { get; set; }
public string Postcode { get; set; }
}
public class ToDetailsViewModel : DetailsViewModel
{
public string Message { get; set; }
}
My View is similar to below.
#model Store.ViewModels.DeliveryDetailsViewModel
#Html.EditorFor(m => m.From)
#Html.EditorFor(m => m.To)
My intention is that a collection of forms (one per item in their shopping cart) will be displayed to enable the user to type different delivery details in. Each form has its own submit button.
The editor template which renders the "To" form is as follows:
#model Store.ViewModels.ToDetailsViewModel
#using (Html.BeginForm("ConfirmTo", "Delivery"))
{
#Html.TextBoxFor(m => m.FirstName)
#Html.TextBoxFor(m => m.LastName)
#Html.TextBoxFor(m => m.Email)
#Html.TextBoxFor(m => m.Message)
<input type="submit" value="Confirm" />
}
My controller:
public class DeliveryController : Controller
{
public ActionResult Index()
{
var model = new DeliveryDetailsViewModel();
model.From = new FromDetailsViewModel();
model.To = new List<ToDetailsViewModel>();
return View(model);
}
public ActionResult ConfirmTo(ToDetailsViewModel toDetails)
{
// Save to database.
}
}
I have a couple of problems:
The "to" editor template isn't rendering anything (though it used to). It cites that the model types don't match (i.e., ToDetailsViewModel isn't the same as List<ToDetailsViewModel>), even though I thought editor templates were supposed to append indices to input field names to enable proper binding.
When clicking Confirm and submitting the first form in the To list the controller receives the view model with the correct bindings. Submitting any of the following forms (with index 1 or greater) calls the ConfirmTo action and passes a ToDetailsViewModel which is null.
Any help would be appreciated, and if you'd like more information about the problem I'm having or the code I'm using, please don't hesitate to ask.

1) The "to" editor template isn't rendering anything
In your controller action you didn't put anything in the list. You just instantiated it. So put some elements:
model.To = new List<ToDetailsViewModel>();
model.To.Add(new ToDetailsViewModel());
model.To.Add(new ToDetailsViewModel());
...
2) When clicking Confirm and submitting the first form in the To list the controller receives the view model with the correct bindings. Submitting any of the following forms (with index 1 or greater) calls the ConfirmTo action and passes a ToDetailsViewModel which is null.
I would be surprised if this works even for the first element because the input fields currently do not have correct names. They are all prefixed with To[someIndex] whereas your ConfirmTo expects a flat model, not a collection.
So You could set the prefix to an empty string so that correct input elements are generated in your ~/Views/Shared/EditorTemplates/ToDetailsViewModel.cshtml editor template:
#model ToDetailsViewModel
#{
ViewData.TemplateInfo.HtmlFieldPrefix = "";
}
#using (Html.BeginForm("ConfirmTo", "Home"))
{
#Html.TextBoxFor(m => m.FirstName)
#Html.TextBoxFor(m => m.LastName)
#Html.TextBoxFor(m => m.Email)
#Html.TextBoxFor(m => m.Message)
<input type="submit" value="Confirm" />
}

1) have you try this, because your view model has
public IList<ToDetailsViewModel> To { get; set; }
To is a list therefore your editor template should have
#model IEnumerable<Store.ViewModels.ToDetailsViewModel>
and the template should use a foreach
#foreach(model in Model){}

Related

Submitting nested class within a model to a controller

I am trying to submit a form with a model that has a nested class within it. However when I get the data to the controller, the nested class has null fields.
Model:
public class Person
{
public string Name { get; set; }
public ExtraStuff Stuff { get; set; } = new ExtraStuff();
}
public class ExtraStuff
{
public string Field1 { get; set; }
public string Field2 { get; set; }
}
View:
#model ProjectName.Models.Person
#Html.TextBoxFor(model => model.Name)
#Html.TextBoxFor(model => model.Stuff.Field1)
#Html.TextBoxFor(model => model.Stuff.Field2)
Controller:
public ActionResult ActionName (Person data)
{
data.Name //this is fine
data.Stuff.Field1 //comes in empty
data.Stuff.Field2 //comes in empty
}
I'm wondering if you're doing some custom form submission, because there doesn't seem to be anything wrong with the code provided. If you make a form like the following it should work:
#using (Html.BeginForm("ActionName", "MyController"))
{
#Html.TextBoxFor(model => model.TopValue)
#Html.TextBoxFor(model => model.Nested.SomeValue)
<input type="submit" value="Submit"/>
}
Take a look at this fiddle for example.

Form that containing Multi Select Dropdown not submitiing form data to the Server?

I have to simple form that contains.
First Field: Dropdown list with single selection mode.
Second Field: Dropdown list with multi Selection mode.
I have Created a viewModel for that when we submit the form to the server, then that viewMode will receive the data using MVC model binding but unfortunately it won't worked ?
Form Code:
<h2><strong>New Customer Details Record</strong></h2>
<form action="~/CustomerCategoryRecorder/Create" method="post">
<div class="form-group">
<label>Customer</label>
#Html.DropDownListFor(m => m.Customers, new SelectList(Model.Customers, "id", "name"), "Select Customer", new { #class = "form-control" })
</div>
<div class="form-group">
<label>Category</label>
#Html.DropDownListFor(m => m.Category, new MultiSelectList(Model.Categories, "id", "name"), "Select Customers Categories", new { multiple = "true", #class = "form-control"})
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
ViewModel:
public class CustomerIdAndCategoriesIdsViewModel
{
public int CustomerId { get; set; }
public int[] CategoriesIds { get; set; }
}
Action Method.
// Using ViewModel.
[AllowAnonymous]
public ActionResult Create(CustomerIdAndMoviesIdsViewModel ids)
{
return View();
}
// without ViewModel.
[AllowAnonymous]
public ActionResult Create(int CustomerId, int[] categoryIds)
{
return View();
}
In Both Cases of the action method the data of a method parameter is null.
How to solve it ? I shall be very thanksful.
Your form element names are Customers and Category. But your model names are different:
public class CustomerIdAndCategoriesIdsViewModel
{
public int CustomerId { get; set; }
public int[] CategoriesIds { get; set; }
}
Which means you're using a different model to render the page than you're using to receive the resulting form post. While this isn't inherently invalid, the names do need to match. When the model binder receives properties called Customers and Category, it has no way of knowing how to map them to your other model.
Update your model property names:
public class CustomerIdAndCategoriesIdsViewModel
{
public int Customers { get; set; }
public int[] Category { get; set; }
}
You may be reluctant to do this, because now the pluralization of the properties is incorrect. Which means your naming is misleading somewhere. So correct that naming in both models.
Basically, whatever your form element names are, so much your model property names be. That's how the model binder maps posted values to model properties.
Update your ViewModel as below.
public class CustomerIdAndCategoriesIdsViewModel
{
public int Customers { get; set; }
public int[] Category { get; set; }
}
Add action Create and add [HttpPost] annotation.
[AllowAnonymous]
[HttpPost]
public ActionResult Create(CustomerIdAndMoviesIdsViewModel ids)
{
return View();
}

Form POST gives empty model fields when using EditorFor

Currently I have a form which I am wanting to place in multiple views. Currently when I submit this form, the model return has null for all it's values and i am unsure why.
When I debug through it is reaching the POST event but the model values are null. (not the model itself).
Contact Model
public class ContactModel : BasePageModel
{
public string Introduction { get; set; }
public ContactForm Form { get; set; }
public ContactModel(TreeNode node, ContactForm form = null) : base(node)
{
Introduction = node.GetEditableTextStringValue("Introduction", String.Empty);
Form = form;
}
}
View Model for the form
public class ContactForm
{
public string Title { get; set; }
public string FirstName { get; set; }
....
}
Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SendContact(ContactForm model)
{
// Do stuff
}
This is the view of the page I want to "embed" if you will the contact form I want to submit.
#model MVCApp.Models.PageModels.ContactModel
....
#using (Html.BeginForm("SendContact", "Contact", FormMethod.Post))
{
#Html.AntiForgeryToken()
#Html.EditorFor(model => model.Form)
}
And the editor template of the Contact Form itself
#model MVCApp.Models.FormModels.ContactForm
....
#Html.LabelFor(model => model.Title)
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title, "")
#Html.LabelFor(model => model.FirstName)
#Html.EditorFor(model => model.FirstName)
#Html.ValidationMessageFor(model => model.FirstName, "")
....
<input type="submit" value="Submit" />
The form renders both parts (The surrounding text and fields) as well as the contact form correctly. But as I said above when I click on submit the model that gets passed back in the POST to the controller (which it does hit through debugging) has all it's fields as null.
Any pointers in where I may have gone wrong would be great.
Marking this as the answer since Stephens comment can't be marked as such.
Using Stephens suggestion, I changed the ActionResult to use
public ActionResult SendContact([Bind(Prefix = "Form")]ContactForm model)
And it worked perfectly. The fields and the model are populated correctly.

How can I correctly use ViewModel to build a two way binding with the view when capturing input records?

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
}
}

Posting a ViewModel containing a list of objects with Ajax.BeginForm (MVC 4)

The view successfully passes the controller a SidebarViewModel object, but the Filters data member is null. So the binding is failing somewhere.
After some reading, I feel like the list of FilterViewModels isn't binding because the binder can't deduce that Filters[0].Value is a FilterViewModel, because none of the other data members for that view model are present. But adding the remaining data members as hidden fields didn't fix the issue.
Any idea why the list of FilterViewModels isn't binding?
ViewModels
public class SidebarViewModel
{
public List<FilterViewModel> Filters;
}
public class FilterViewModel
{
public string DisplayName { get; internal set; }
public string EditorTemplate { get; internal set; }
public string Value { get; set; }
public bool Visible { get; set; }
public IEnumerable<SelectListItem> Items { get; set; }
}
View
#model SidebarViewModel
#using (Ajax.BeginForm("Filter", "Controller", Model, new AjaxOptions
{
UpdateTargetId = "tool-wrapper",
LoadingElementId = "loading-image",
HttpMethod = "POST"
}))
{
<fieldset>
#foreach (var filter in Model.Filters.Where(x => x.Visible).Select((value, i) => new {i, value}))
{
<div class="control-group">
#Html.DisplayFor(m => m.Filters[filter.i].DisplayName)
#Html.EditorFor(m => m.Filters[filter.i], "Template")
</div>
}
<button type="submit">Filter</button>
</fieldset>
}
Editor Template
#model FilterViewModel
#Html.DropDownListFor(m => m.Value, Model.Items, "(Select)")
Generated HTML Form
<select id="Filters_0__Value" name="Filters[0].Value">
<option value="">(Select)</option>
// More options
</select>
<select id="Filters_1__Value" name="Filters[1].Value">
<option value="">(Select)</option>
// More options
</select>
Controller
[HttpPost]
public ActionResult Filter(SidebarViewModel sidebar)
{
// Stuff.
}
Filters is a field, not a property, so you need to add getter and setter so the DefaultModelBinder can set the value.
public class SidebarViewModel
{
public List<FilterViewModel> Filters { get; set; }
}
However you also have some other problems since your code will not give you correct model binding. You need to rename you EditorTemplate to /Views/Shared/EditorTemplates/FilterViewModel.cshtml and then in the main view use (no foreach loop)
#Html.EditorFor(m => m)
The EditorFor() method will correctly generate the controls for each item with the correct name attributes (including the indexer). Note also that you should filter the collection in the controller before you pass it to the view.

Categories

Resources