I'm trying to get model bind a sub class in a form.
I have a Page class with a related PageContent class.
There are many PageContent objects in a Page object.
ie Page.PageContents
I return Page as the Model and I can see all the PageContent items.
However I'm a bit blurry on how to assign the PageContent items to the form so it'll keep it's model binding.
When I Post the Page model back into the controller, the PageContent list is empty.
Here is a snippet of the form:
<% using (Html.BeginForm("SavePage", "Admin")) {%>
<fieldset>
<legend>Fields</legend>
<%=Html.Hidden("PageId", Model.PageId) %>
<p>
<label for="Title">Title:</label>
<%= Html.TextBox("Title", Model.Title) %>
<%= Html.ValidationMessage("Title", "*") %>
</p>
<p>
<label for="SysName">SysName:</label>
<%= Html.TextBox("SysName", Model.SysName) %>
<%= Html.ValidationMessage("SysName", "*") %>
</p>
<%
int i = 0;
foreach (var pageContent in Model.PageContents)
{ %>
<div>
<%=Html.Hidden("PageContents[" + i + "].PageContentId", pageContent.PageContentId) %>
<%=Html.TextArea("PageContents[" + i + "].Content", pageContent.Content)%>
</div>
<%
i++;
} %>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
I'm thinking I haven't made the PageContents aspect of the form correctly.
EDIT:
Here is the basic controller method I POST to:
public ActionResult SavePage(Page page)
{
// do stuff with page.PageContents
return View("PageEdit", page);
}
I'm not using the Request.Form style
Please help :D
You said Page.PageContents is an EntitySet<PageContent> and that you are using auto-generated classes. So, I guess, you are using LINQ to SQL? If so, then you can use partial classes in order to add properties to the generated class
I remember that I had a similiar problem, I wasnt able to bind to EntitySet<T> properties. I think I fixed it by using a proxy / placeholder property which was an IList<T> what effectively set the actual property. So, in your case, that would look something like this:
// Your partial class, adding a property to the generated class
public partial class Page
{
// Bind to this property
public IList<PageContent> MyPageContents
{
set
{
var set = new EntitySet<PageContent>();
set.AddRange(value);
PageContents = set;
}
get { return PageContents; }
}
}
// This and probably a lot more is created by LINQ to SQL
public partial class Page {
// ...
public EntitySet<PageContent> PageContents
{
get;
set;
}
// ...
}
I think partial classes have to be in the same namespace as the auto generated class. More info here: Adding new methods to LINQ to SQL generated classes
And then, in your form, you bind to MyPageContents (you can use a better name of course) instead of PageContents.
Hopefully it works for you, it is quite some time ago since I last used LINQ to SQL.
Related
I've been learning MVC 3 rapidly over the last couple weeks but something's come up that I just haven't been able to solve searching for hours. I'm developing a simple shopping cart and I'm trying to pass data in a linear path through the checkout process. I've been unable to get a model to POST to the next view no matter what I try.
To start with, the 'Cart' entity is being pulled from Session using an implementation of IModelBinder. It's essentially available for any method. It's been working great for a while. My issue is trying to pass the same model between /cart/confirm and /cart/checkout.
Can someone help figure out why the model is always empty in the controller for /cart/checkout?
public class CartController : Controller
{
public ActionResult Index (Cart cart)
{
//Works fine, anonymous access to the cart
return View(cart);
}
[Authorize]
public ActionResult Confirm (Cart cart)
{
//Turn 'Cart' from session (IModelBinder) into a 'Entities.OrderDetail'
OrderDetail orderDetail = new OrderDetail();
orderDetail.SubTotal = cart.ComputeTotalValue();
...
...
return View(orderDetail);
}
[Authorize]
public ActionResult Checkout(OrderDetail model)
{
//PROBLEM: model is always null here.
}
}
/Views/Cart/Index.aspx looks like this (sorry, no Razor):
<%# Page Language="C#" MasterPageFile="~/Views/Shared/Site-1-Panel.Master" Inherits="System.Web.Mvc.ViewPage<My.Namespace.Entities.Cart>" %>
...
...
<% using(Html.BeginForm("confirm", "cart")) { %>
Not much to see here, just a table with the cart line items
<input type="submit" value="Check Out" />
<% } %>
I suspect the problem is here, but I've tried every variation of Html.BeginForm() I can try and can't get the model to pass to /cart/checkout. Anyway, /Views/Cart/Confirm.aspx looks like this:
<%# Page Language="C#" MasterPageFile="~/Views/Shared/Site-1-Panel.Master" Inherits="System.Web.Mvc.ViewPage<My.Namespace.Entities.OrderDetail>" %>
...
...
<% using (Html.BeginForm("checkout", "cart", Model)) { %>
<%: Model.DBUserDetail.FName %>
<%: Model.DBUserDetail.LName %>
<%: Html.HiddenFor(m => m.DBOrder.ShippingMethod, new { #value = "UPS Ground" })%>
<%: Html.HiddenFor(m => m.DBOrder.ShippingAmount, new { #value = "29.60" })%>
...
...
<input type="submit" value="Confirm & Pay" />
<% } %>
And finally /Views/Cart/Checkout.aspx looks like this:
<%# Page Language="C#" MasterPageFile="~/Views/Shared/Site-1-Panel.Master" Inherits="System.Web.Mvc.ViewPage<My.Namespace.Entities.OrderDetail>" %>
...
...
<%: Html.Hidden("x_first_name", Model.DBUserDetail.FName) %>
<%: Html.Hidden("x_last_name", Model.DBUserDetail.LName) %>
...
It doesn't really matter what's here, an exception gets throw in the controller because the model is always null
Most likely your model state is invalid. Add this extension method and call it on the first line of the action like:
ModelState.DumpErrors();
Put a breakpoint one line after it and examine the Output window for more information about what is wrong with the binding.
Edit - The full extension method:
public static class ModelExtensions
{
public static void DumpErrors(this System.Web.Mvc.ModelStateDictionary ModelState)
{
var errors = from key in ModelState
let errorList = ModelState[key.Key].Errors
where errorList.Any()
select new
{
Item = key.Key,
Value = key.Value,
errorList
};
foreach (var errorList in errors)
{
System.Diagnostics.Debug.WriteLine("MODEL ERROR:");
System.Diagnostics.Debug.WriteLine(errorList.Item);
System.Diagnostics.Debug.WriteLine(errorList.Value);
foreach (var error in errorList.errorList)
{
System.Diagnostics.Debug.WriteLine(error.ErrorMessage);
System.Diagnostics.Debug.WriteLine(error.Exception);
}
System.Diagnostics.Debug.WriteLine("-----");
}
}
}
I don't know asp specifically, but it seems like you have access to cart in index and confirm b/c you are passing it explicitly. Since you are not passing it to checkout, you would not be able to access it. I might be totally off
Let's say I've the bellow model
public class UserInformation
{
public List<UserInRole> RolesForUser { get; set; }
//Other properties omitted...
}
public class UserInRole
{
public string RoleName { get; set; }
public bool InRole { get; set; }
}
On my page I have something like
<%using(Html.BeginForm()){%>
.../...
<%for(int i =0; i<Model.InRoles.Cout; i++%>
<p><%: Html.CheckBox(Model.Roles[i].RoleName, Model.Roles[i].InRole)%></p>
<%}%>
The idea is to be able to check/uncheck the checkbox so that when the form is posted to the action, the action acts appropriately by adding/removing the user from each role.
The problem is when form is posted to the action method, the Roles property (which is a list UserInRole object) doesn't reflect the change made by the user. ModelBinder works properly on the all other properties but 'Roles property'
I wonder how I can do that. I suspect that the name/id given to the checkbox is not appropriate. But, I'm just stack. Maybe I should do it differently.
Thanks for helping
You should see Phil Haack's post on model binding to a list.
Essentially what you need to is simply submit a bunch of form fields each having the same name.
<%# Page Inherits="ViewPage<UserInformation>" %>
<% for (int i = 0; i < 3; i++) { %>
<%: Html.EditorFor(m => m.RolesForUser[i].RoleName) %>
<%: Html.EditorFor(m => m.RolesForUser[i].InRole) %>
<% } %>
I think the problem is how your submitting your form data. For model binding to work it needs the key name with its associated value. The below code based on your code should bind correctly:
<%using(Html.BeginForm()){%>
.../...
<%for(int i =0; i<Model.RolesForUser.Count; i++%>
<p>
<%: Html.Hidden("UserInformation.RolesForUser[" + i + "].RoleName", Model.RolesForUser[i].RoleName) %>
<%: Html.CheckBox("UserInformation.RolesForUser[" + i + "].InRole", Model.RolesForUser[i].InRole) %>
<%: Model.RolesForUser[i].RoleName %>
</p>
<%}%>
I have the following View Data:
public class ShoppingCartViewData
{
public IList<IShoppingCartItem> Cart
{
get;
set;
}
}
I populate the viewdata in my controller:
viewData.Cart = CurrentSession.CartItems;
return View(viewData);
And send the data to the view and display it using:
<% for (int i = 0; i < Model.Cart.Count; i++ ) { %>
<%= Html.TextBoxFor(m => m.Cart[i].Quantity)%>
<%= Html.HiddenFor(m => m.Cart[i].Id) %>
<% } %>
I want to be able to catch the viewdata on the post. When I try:
[HttpPost]
public ActionResult UpdateCart(ShoppingCartViewData viewData)
{
...
}
When I run this I get a: System.MissingMethodException: Cannot create an instance of an interface.
Can anyone shed some light on this. What would I have to do to get this to work?
Many Thanks
You could try adding the formcollection as a parameter. And shouldn't viewdata be the viewmodel you're using?
[HttpPost]
public ActionResult UpdateCart(ShoppingCartViewModel viewModel, FormCollection collection)
{
...
}
Not sure if this is the exact solution, i'm also busy learning MVC2.0 and .NET4 ;-)
I'd create a model binder for your ViewModel, and then you can instantiate a concrete type that implements the appropriate interface when it binds to the method parameters.
You can insert logic into your model binder to read the form fields as appropriate and then instantiate the right IList or IShoppingCartItem data, so no need to worry about being pinned to a single implementation of the interface either.
Given my two comments this is how I would do it:
// you don't need this
// viewData.Cart = CurrentSession.CartItems;
// return View(viewData);
// do it like this
return View(CurrentSession.CartItems);
Then have a strongly typed View either this:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Administration.Master" Inherits="System.Web.Mvc.ViewPage<ShoppingCartViewData>" %>
or this:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Administration.Master" Inherits="System.Web.Mvc.ViewPage<List<IShoppingCartItem>>" %>
Also this code won't work. This will generate you a bunch of textboxes with the same name and id. You need to generate textboxes with a count and for that you won't be able to use
Html.TextBoxFor(). You will have to revert to Html.TextBox() or create a new extension TextBoxFor() method which would also accept a number (for you count).
<% for (int i = 0; i < Model.Cart.Count; i++ ) { %>
<%= Html.TextBoxFor(m => m.Cart[i].Quantity)%> // this won't work, if you want to post back all textboxes after they are edited
<%= Html.HiddenFor(m => m.Cart[i].Id) %>
<% } %>
HTH
I got the same exception after adding an 'int' parameter to an action method.
I discovered by putting a break point in the controllers constructor that one of the other action methods (not the one specified in the forms post arguments) was being called instead.
Using the Authors/Books catalog example, let's say I want to edit the info for the books of a specific author.
When someone navigates to domain.com/Books/Edit/2, I want to display an edit view for all the books where Author_ID = 2. Among the various book info is the book category (fiction, non-fiction, textbook, whatever) These categories are in their own table and are referenced by a Category_ID.
What's the best way to set up the edit form?
Currently in my controller I have something like this:
public ActionResult Edit(int id)
{
IQueryable<Book> books = bookRepository.FindBooksForAuthor(id);
return View(books);
}
And in my partial view:
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IQueryable<Authors.Models.Book>>" %>
<%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>
<% using (Html.BeginForm()) {%>
<fieldset>
<legend>Fields</legend>
<%var i = 0;
foreach (var book in Model)
{%>
<p>
<label for="Category_ID">Category_ID:</label>
<%= Html.TextBox("Category_ID", book.Category_ID)%>
<%= Html.ValidationMessage("Category_ID", "*")%>
</p>
<p>
<label for="Description">Description:</label>
<%= Html.TextBox("Description", book.Description)%>
<%= Html.ValidationMessage("Description", "*")%>
</p>
<%i++;
} %>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
Is my Inherits set properly at the top of the view since I'm passing an IQueryable object?
More importantly, how do I get the Category_ID field to be a DropDown with the correct category selected?
Can I just send the data for the dropdown to the view and figure out the selected item at the view level?
ViewData["categories"] = new SelectList(_db.BookCategories.ToList().OrderBy(b => b.Category_Title), "Category_ID", "Category_Title");
You could create view model class containing list of books and select list of categories:
public class BooksEditViewModel
{
public IQueryable<Authors.Models.Book> Books { get; set; }
public IQueryable<BookCategory> BookCategories { get; set; }
}
Then use BooksEditViewModel as view model
System.Web.Mvc.ViewUserControl<BooksEditViewModel>
and code dropdown with
Html.DropDownList("Category_ID", new SelectList(Model.BookCategories,"Category_ID", "Category_Title",book.Category_ID);
You should also read about list binding:
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
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??