Asp.net MVC2 ModelBindingContext.ModelName empty - asp.net-mvc

I'm not even quite sure where to start explaining this problem. I've been working on this for about the past 10 hours without a clue as to what the root cause is. If any additional details are needed, I'd be happy to provide. I'm just guessing at what is relevant at this point.
I have an MVC2 site with routes set up by by Steve Hodgkiss' wonderful RestfulRouting package, using the default route setup with nested controllers (e.g. /customer/{custid}/location/{locid} and such).
In this, I have one particular model that is giving me issues. For some reason, when the create page post's the data back to my server, the ModelName property in the ModelBindingContext object passed to the DefaultModelBinder (well, my custom class inherited from DefaultModelBinder, to handle grabbing objects from a repository). This happens only for this one model. And I can't spot the differences at all.
The broken model
public class RemedialItem : Entity
{
public virtual int Id { get; set; }
....
A working model:
public class Customer : Entity
{
public virtual int Id { get; set; }
....
Entity is just an empty class used as a marker for Reflection use.
The broken controller method in RemedialItemController.cs
[HttpGet]
public ActionResult New(int? locationId, int? applianceId)
{
var model = ViewModelFactory.Create<CreateRemedialItemViewModel>();
model.Categories = (from c in repository.Query<RemedialItemCategory>()
orderby c.Name
select c).ToList();
model.RemedialItem = new RemedialItem();
return View(model);
}
A working controller method in CustomerController.cs
[HttpGet]
public ActionResult New()
{
var viewModel = ViewModelFactory.Create<SingleCustomerViewModel>();
viewModel.Customer = new Customer();
return View(viewModel);
}
ViewModelFactory is an injected class that handles setting up some basic properties common to all view models (mainly is the user logged in and user details right now)
A broken viewmodel:
public class CreateRemedialItemViewModel : ViewModelBase
{
public RemedialItem RemedialItem { get; set; }
public IList<Location> Locations { get; set; }
public IList<Appliance> Appliances { get; set; }
public IList<RemedialItemCategory> Categories { get; set; }
}
A working ViewModel:
public class SingleCustomerViewModel : ViewModelBase
{
public Customer Customer { get; set; }
}
ViewModelBase contains a handful of properties populated by the ViewModelFactory.
The broken form in thew New view for RemedialItem:
<% using(Html.BeginForm("Create","RemedialItem",FormMethod.Post))
{%>
<%: Html.AntiForgeryToken() %>
<fieldset>
<legend>General</legend>
<div>
<%: Html.LabelFor(m=>m.RemedialItem.Category) %>
<%:Html.DropDownListFor(m=>m.RemedialItem.Category.Id, new SelectList(Model.Categories,"Id","Name")) %>
</div>
<div>
<%: Html.LabelFor(m=>m.RemedialItem.Item) %>
<%: Html.TextAreaFor(m=>m.RemedialItem.Item) %>
</div>
<div>
<%: Html.LabelFor(m=>m.RemedialItem.Note) %>
<%: Html.TextAreaFor(m=>m.RemedialItem.Note) %>
</div>
<input type="submit" value="Create Item" />
</fieldset>
<%}%>
A working New view:
<% using (Html.BeginForm("Create","Customer",FormMethod.Post)) {%>
<%: Html.ValidationSummary(true) %>
<%:Html.AntiForgeryToken() %>
<fieldset>
<legend>Fields</legend>
<p>
<%: Html.LabelFor(m=>m.Customer.Name) %>
<%: Html.TextBoxFor(m=>m.Customer.Name) %>
</p>
<p>
<%: Html.LabelFor(m=>m.Customer.Street) %>
<%: Html.TextBoxFor(m=>m.Customer.Street) %>
</p>
[...tl;dr...]
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
<% } %>
Both produce similar field names:
Broken:
<label for="RemedialItem_Item">Item</label>
<textarea cols="20" id="RemedialItem_Item" name="RemedialItem.Item" rows="2">
</textarea>
Working:
<label for="Customer_Name">Name</label>
<input id="Customer_Name" name="Customer.Name" type="text" value="" />
I apologize for the overly long code dump, in short:
The working set of stuff, when posted back on the create form, has the ModelName set to Customer. The broken stuff is an empty string
Is there something I'm missing? Has anyone encountered something like this before?

I found the issue. In the ViewModel the property that held the instance of RemedialItem to display was called RemedialItem. In the action it posted to, the parameter that took the RemedialItem instance was called item, and that broke everything.
In short, when using ViewModels, make sure the parameter name that takes an object from the ViewModel is the same as the property name in the viewmodel.
There went my day.

Related

MVC Binding to checkbox

I have found so many questions about this, but none of them go over or seem to go over my scenario. I have a model:
public class CheckBoxModel
{
public int Id{ get; set; }
public bool IsSelected { get; set; }
}
In then try and bind my IsSelected bool to a checkbox like this:
<%= Html.CheckBox("IsSelectedCheck",Model.IsSelected)%>
I have lots of items on a page, they all have a checkbox next to them, all i am trying to do is pass back to the controller all the id's of the items and which ones have been selected.
At the moment, the value of IsSelected is always false. Should Html.CheckBox set the value of Model.IsSelected each time the user toggles the checkbox.
Thanks
Try like this:
<%= Html.CheckBoxFor(x => x.IsSelected) %>
Also if you want to pass along the id don't forget to do so:
<%= Html.HiddenFor(x => x.Id) %>
And if you had a collection of those:
public class MyViewModel
{
public CheckBoxModel[] CheckBoxes { get; set; }
}
you could:
<% for (var i = 0; i < Model.CheckBoxes.Length; i++) { %>
<div>
<%= Html.HiddenFor(x => x.CheckBoxes[i].Id) %>
<%= Html.CheckBoxFor(x => x.CheckBoxes[i].IsSelected) %>
</div>
<% } %>
which will successfully bind to:
[HttpPost]
public ActionResult MyAction(MyViewModel model)
{
// model.CheckBoxes will contain everything you need here
...
}
An alternative to Darin's fantastic answer
I definitely recommend following Darin's approach for returning classes which will be most of the time. This alternative is a 'quick' and dirty hack if all you need is the checked Ids:
<% foreach (var cb in Model.CheckBoxes) { %>
<div>
<input type="checkbox"
value="<%= cb.Id %>"
<%= cb.IsSelected ? "checked=\"checked\"" : "" %>
name="ids" />
</div>
<% } %>
Will bind to the int[] ids parameter in the following action:
[HttpPost]
public ActionResult MyAction(int[] ids)
{
// ids contains only those ids that were selected
...
}
The benefit is cleaner html as there is no hidden input.
The cost is writing more code in the view.
In MVC 4.0 (Razor 2.0) you can use the following syntax in your view:
<input type="checkbox" value="#cb.Id" checked="#cb.IsSelected" name="ids" />

Problem returning ViewModel from edit

I trying to return the same model back to the view that edited the model. Making it sort of like Word or something with ctrl+s functionality for saving the mode. This works fine though the model that is returned to the view contains a bunch of nulls for some stupid reason. Is it because things were not serialized properly when the controller got the view model back or am I handling MVC the wrong way?
This is the Model
public class EditInvoiceModel
{
private readonly IEnumerable<Customer> _customers;
public EditInvoiceModel()
{
CreateProduct = new Product { Invoice = Invoice };
CreateWorkday = new Workday { Invoice = Invoice };
}
public EditInvoiceModel(Invoice invoice, IEnumerable<Customer> customers)
{
Invoice = invoice;
_customers = customers;
Customers = _customers.Select(x =>
new SelectListItem
{
Selected = x.Id == Invoice.CustomerID,
Text = x.Name,
Value = x.Id.ToString()
});
Products = Invoice.Products;
Workdays = Invoice.Workdays;
CreateProduct = new Product {Invoice = Invoice};
CreateWorkday = new Workday { Invoice = Invoice };
}
public IEnumerable<SelectListItem> Customers { get; set; }
public IEnumerable<Product> Products { get; set; }
public IEnumerable<Workday> Workdays { get; set; }
public Product CreateProduct { get; set; }
public Workday CreateWorkday { get; set; }
public Invoice Invoice { get; set; }
}
And this is the controller action that returns the model back to the same view.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(EditInvoiceModel invoiceModel)
{
try
{
_repository.UpdateInvoice(invoiceModel.Invoice);
}
catch (Exception ex)
{
Log.Error(ex);
}
return View(invoiceModel);
}
All properties except the Invoice is null when this is returned to the view. I have no idea why this happens. Hope someone can help.
The problem that occurs (in the view) is the following: This is not because of a typo since it is working fine the first time the view is run. This must be to a problem with the modelbinder or my usage of the model binder.
The ViewData item that has the key 'Invoice.CustomerID' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.InvalidOperationException: The ViewData item that has the key 'Invoice.CustomerID' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.
Source Error:
Line 28: <div class="editor-field">
Line 29: <%: Html.DropDownListFor(x => x.Invoice.CustomerID, Model.Customers)%>
Line 30: <%: Html.ValidationMessageFor(x => x.Invoice.CustomerID)%>
Line 31: </div>
Lastly part of the view that displays the view model.
<%# page language="C#" masterpagefile="~/Views/Shared/Site.Master"
inherits="System.Web.Mvc.ViewPage<FakturaLight.WebClient.Models.EditInvoiceModel>" %>
<asp:content id="Content2" contentplaceholderid="MainContent" runat="server">
<%= Html.ValidationSummary() %>
<% using (Html.BeginForm())
{ %>
<%= Html.AntiForgeryToken() %>
<div class="content-left">
<%: Html.EditorFor(x => x.Invoice) %>
<div class="editor-label">
<%: Html.LabelFor(x => x.Invoice.CustomerID)%>
</div>
<div class="editor-field">
<%: Html.DropDownListFor(x => x.Invoice.CustomerID, Model.Customers)%>
<%: Html.ValidationMessageFor(x => x.Invoice.CustomerID)%>
</div>
<% } %>
</div>
<div class="content-right" id="details" style=" clear:both;">
<div id="workdays">
<%: Html.DisplayFor(x => x.Workdays) %>
</div>
<div id="products">
<%: Html.DisplayFor(x => x.Products) %>
</div>
</div>
</asp:content>
Darin is correct. Remember that you are still working with a disconnected client. This is just HTML and HTTP under the covers. The model binder is only able to bind values that are pushed to the server in the HTTP POST. All other properties for the class will receive no assignment, so if you want a more complete model pushed back to the browser in response in the
return View(invoiceModel);
you will need to complete those property assignments on the server side within your controller or with your repository's update method perhaps.
The reason why only the Invoice property is populated is because in your form you are only having input fields and dropdown lists for it. The model binder populates properties from what's sent in the request. You are posting a form which contains values only for the Invoice. As far as the Workdays and Products properties are concerned you are only displaying them (Html.DisplayFor) and they are never sent to the server. Also the model binder invokes the default constructor of your model which doesn't initialize those properties neither, so they are null at postback.

ASP.NET MVC 2 client-side validation rules not being created

MVC isn't generating the client-side validation rules for my viewmodel. The HTML just contains this:
<script type="text/javascript">
//<![CDATA[
if (!window.mvcClientValidationMetadata) { window.mvcClientValidationMetadata = []; }
window.mvcClientValidationMetadata.push({"Fields":[],"FormId":"form0","ReplaceValidationSummary":false});
//]]>
</script>
Note that Fields[] is empty!
My view is strongly-typed and uses the new strongly-typed HTML helpers (TextBoxFor(), etc).
View Model / Domain Model
public class ItemFormViewModel
{
public Item Item { get; set; }
[Required] [StringLength(100)] public string Whatever { get; set; } // for demo
}
[MetadataType(typeof(ItemMetadata))]
public class Item
{
public string Name { get; set; }
public string SKU { get; set; }
public int QuantityRequired { get; set; }
// etc.
}
public class ItemMetadata
{
[Required] [StringLength(100)] public string Name { get; set; }
[Required] [StringLength(50)] public string SKU { get; set; }
[Range(0, Int32.MaxValue)] public int QuantityRequired { get; set; }
// etc.
}
(I know I'm using a domain model as my / as part of my view model, which isn't a good practice, but disregard that for now.)
View
<%# Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<ItemFormViewModel>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Editing item: <%= Html.Encode(Model.Item.Name) %></h2>
<% Html.EnableClientValidation(); %>
<%= Html.ValidationSummary("Could not save the item.") %>
<% using (Html.BeginForm()) { %>
<%= Html.TextBoxFor(model => model.Item.Name) %>
<%= Html.TextBoxFor(model => model.Item.SKU) %>
<%= Html.TextBoxFor(model => model.Item.QuantityRequired) %>
<%= Html.HiddenFor(model => model.Item.ItemID) %>
<%= Html.TextBox("Whatever", Model.Whatever) %>
<input type="submit" value="Save" />
<% } %>
</asp:Content>
I included the Whatever property on the view model because I suspected that MVC wasn't recursively inspecting the sub-properties of ItemFormViewModel.Item, but even that isn't being validated? I've even tried delving into the MVC framework source code but have come up empty. What could be going on?
About five seconds after I posted the question, I realized something: My view didn't have ValidationMessage placeholders anywhere. I added <%= Html.ValidationMessageFor(model => model.Item.Name) %> and lo and behold, MVC added validation rules for Item.Name to the JS block at the bottom of the page.
It turns out that MVC does not emit client-side validation rules for a field unless you actually do one of the following:
Call Html.ValidationMessage() for the property.
Call Html.Validate() for the property. (This one won't output error messages)
Render the controls using Html.EditorForModel(). (source)
Doing any of these tells MVC, "This property of my viewmodel is editable by the user, so you should be validating it." Just using the HTML helper to stick a textbox on the page -- even if you're using the new strongly-typed helpers -- isn't enough.
I have had no luck getting this to work in MVC 2 RC. According to other questions here on SO, you have to get the MicrosoftMvcJQueryValidation.js file from the MVC Futures release, hold your left foot behind your head, and whistle Dixie for half an hour. I did this and more and have not been able to make it work.
Hopefully it will be fixed in RTM.

ASP.NET MVC 2 UI Templates displaying data without markup. How to fix?

Using EditorFor( model lambda, "viewTemplateName"), my output is completely not as expected. This doesn't produce any errors, but it is rendering output without markup. What am I doing wrong?
The Output:
HarryTomRichard
The Expected Output (I need to figure out how to render the List [] indexes on id too but not to that problem yet):
<table>
<tr><td><span><input type="Text" id="Name[0]" value="Harry" /></span></td></tr>
<tr><td><span><input type="Text" id="Name[1]" value="Tom" /></span></td></tr>
<tr><td><span><input type="Text" id="Name[2]" value="Richard" /></span></td></tr>
</table>
My Classes:
namespace Marcs.Models {
public class Student { public string Name { get; set; } }
public class Classroom { public List<Student> Students { get; set; }
}
My Controller:
public ActionResult Index() {
var myStudents = new List<Student>();
myStudents.Add(new Student { Name = "Harry" });
myStudents.Add(new Student { Name = "Tom" });
myStudents.Add(new Student { Name = "Richard" });
var myClass = new Classroom {Students = myStudents};
return View(myClass);
}
My Index View:
Inherits="System.Web.Mvc.ViewPage<Marcs.Models.Classroom>" %>
<% using (Html.BeginForm()) { %>
<%= Html.EditorFor(m => m.Students, "Classroom") %>
<input type="submit" value="Save" />
<% } %>
My Classroom Template (notice the m => item so I can use the item, not the model):
Inherits="System.Web.Mvc.ViewUserControl<List<Marcs.Models.Student>>" %>
<table>
<% foreach (Marcs.Models.Student item in Model)
{ %><tr><td><%= Html.EditorFor(m => item, "Student")%></td></tr><%
} %>
</table>
My Student Template:
Inherits="System.Web.Mvc.ViewUserControl<Marcs.Models.Student>"
%><span><%= Html.Encode( Html.EditorFor( m => m.Name)) %></span>
jfar has the answer, and I will mark it appropriately when added. The solution was simply to ensure the files were located in Views->ControllerName->EditorTemplates and Views->ControllerName->DisplayTemplates. These can also be located in the Shared folder too.
I like this post. Now I need to learn how to use the MVC 2 template Html helpers that reference collections. It's in MVC 2 RC.

Pass SelectList "SelectedValue" to Controller Action Method

I have a registration form which displays a users Name (textbox), Email (textbox) and Division (SelectList). The Name and Email are pre-populated (I'm using Windows Authentication, Intranet app), and I want to send the SelectedValue from the DropDown to my controller as an Int32, I don't want to send the entire SelectList back. This list is small now, but will grow to considerable size.
I a class called RegistrationViewModel, it contains public properties for these fields. However, when I use SelectList for the DivisionList, I receive this error: No parameterless constructor defined for this object..
If i change the Type, it works no problem, but Division is null or 0. Is there a way to pass the SelectedValue from a DropDown to a Controller Action method as a Int32?
Edit 1:
I'm not really sure what I'm doing, I've been using MVC for about 48 hours, watched the PDF, TechEd, and TechDays videos.
My apologies, here is my controller code:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Register(RegistrationViewModel rvm)
{
IApplicationContext context = ContextRegistry.GetContext();
IValidationErrors errors = new ValidationErrors();
IValidator validator = (IValidator)context.GetObject("RegistrationValidator");
bool valid = validator.Validate(rvm, errors);
if (valid)
repo.SaveRegistration();
else
ViewData["DivisionList"] = repo.GetDivisions();
return View(rvm);
}
RegistrationViewModel Class
public class RegistrationViewModel
{
public string Name { get; set; }
public string Email { get; set; }
//public SelectList DivisionList { get; private set; }
public int Division { get; set; }
}
Here's the view
<%# Page Language="C#"
MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<RegistrationViewModel>" %>
<%# Import Namespace="Project1.Entities"%>
<%# Import Namespace="Project1.Models"%>
<asp:Content ID="registerTitle" ContentPlaceHolderID="TitleContent" runat="server">
Register
</asp:Content>
<asp:Content ID="registerContent" ContentPlaceHolderID="MainContent" runat="server">
...
<% using (Html.BeginForm())
{ %>
<div>
<fieldset>
<legend>Account Information</legend>
<p>
<label for="name">Name:</label>
<%= Html.TextBox("Name", User.Identity.Name.GetDisplayName()) %>
<%= Html.ValidationMessage("username") %>
</p>
<p>
<label for="email">Email:</label>
<%= Html.TextBox("email", User.Identity.Name.GetEmailFromLogin()) %>
<%= Html.ValidationMessage("email") %>
</p>
<p>
<label for="division">Division:</label>
<%= Html.DropDownList("DivisionList", ViewData["DivisionList"] as SelectList)%>
<%= Html.ValidationMessage("confirmPassword") %>
</p>
<p>
<input type="submit" value="Register" />
</p>
</fieldset>
</div>
<% } %>
</asp:Content>
Edit 2:
Eilon: Here is what I changed it too:
Controller:
public ActionResult Register()
{
ViewData["DivisionList"] = repo.GetDivisions();
return View();
}
View:
<%= Html.DropDownList("DivisionValue", ViewData["DivisionList"] as SelectList)%>
I recieve this exception:
There is no ViewData item with the key 'DivisionValue' of type 'IEnumerable'.
When I updated the View to this:
<%= Html.DropDownList("DivisionList", ViewData["DivisionList"] as SelectList)%>
It works just great! It only seems to work if all the "Division" items named identically. If I change the name the View crashes or the ViewModel "Division" property is sent as 0.
Why is that?
The RegistrationViewModel type should contain a simple-typed property such as:
public string DivisionValue { get; set; }
Or change the type to int, DateTime, or whatever the appropriate type is.
In HTML and HTTP the only thing that gets posted back for a drop down list is the name of the field and the selected value.
To get everything to match up you also need to change the view to render a different input name for the drop down list:
<%= Html.DropDownList("DivisionValue", ViewData["DivisionList"] as SelectList)%>
Notice that I'm using "DivisionValue" is the value of the list, and DivisionList as the list of all available items.
I'd just be more explicit with the SelectList type. I'd suggest creating the SelectList in the controller action and forget about casting it in the view. My code works like this (CRUD Edit page):
..in the Action:
ViewData["WorkType.ID"] = new SelectList(this._vacancySvc.GetVacancyWorkTypes(),
"ID", "Name", ViewData["WorkType.ID"] ?? vacancy.WorkType.ID);
..and in the view:
<p><% =Html.Encode("Work Type:") %><br />
<% =Html.DropDownList("Worktype.ID")%><span class="smallgrey">(required)</span><br />
.. you can see that either the initial selection (from DB) is persisted or the ViewData from post backs (like if the form fails validation) thru the use of the [null coalescing operator][1] (??).
Moreover, if i refactored this code, i'd prob like to use a ViewModel object like you are.
The only thing is: (1) you'd never need to reference the ViewModel SelectList property in the view coz MVC auto binds this for us by the Html.DropDownList() overload.. and (2) i'd still need to ref the ViewData in the action anyway to get the selected value from a failed validation post back so what's the point really??

Resources