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
Related
I have a problem with what should be a very simple ASP.NET MVC Create method.
After I populate data and submit the form in the Create view (and all the validation rules are passed), the instance of the model passed to the POST method will contain either null or 0 values for every field and ModelState.IsValid will return false.
I use Firebug to check the POST form parameters, and the submit action obviously has posted all the parameters from the fields. However, these parameters are not bound to the model passed to the POST method in the Controller, hence, the model (namely material in the code) contains null values in all field (as checked using Watch and breakpoints placed at the beginning of the POST method).
POST parameters from Firebug:
Content-Type: application/x-www-form-urlencoded Content-Length: 228 MaterialNoMass.MaterialNoMassID=9mmPlasterboard&MaterialNoMass.RoughnessTypeID=3&MaterialNoMass.Resistance=0.0560&MaterialNoMass.ThermalAbsorptance=0.09&MaterialNoMass.SolarAbsorptance=0.07&MaterialNoMass.VisibleAbsorptance=0.07
Create View:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Database.Master"
Inherits="System.Web.Mvc.ViewPage<MvcDeepaWebApp.ViewModels.DatabaseSimpleMaterialViewModel>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<% Html.EnableClientValidation(); %>
<% using (Html.BeginForm())
{%>
<%: Html.ValidationSummary(true) %>
<fieldset>
<legend>Create MaterialNoMass</legend>
<%: Html.EditorFor(model => model.MaterialNoMass, new { RoughnessTypes = Model.RoughnessTypes })%>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
<div>
<%: Html.ActionLink("Back to List", "Index") %>
</div>
</asp:Content>
GET and POST methods of Create in Controller:
//
// GET: /DatabaseSimpleMaterial/Create
public ActionResult Create()
{
var viewModel = new DatabaseSimpleMaterialViewModel
{
MaterialNoMass = new MaterialNoMass(),
RoughnessTypes = deepaDB.RoughnessTypes.ToList()
};
return View(viewModel);
}
//
// POST: /DatabaseSimpleMaterial/Create
[HttpPost]
public ActionResult Create(MaterialNoMass material)
{
if (ModelState.IsValid)
{
MaterialAllType mt = new MaterialAllType();
mt.MatID = material.MaterialNoMassID;
mt.MatTypeID = deepaDB.MaterialTypes.Single(type => type.MatType == "SimpleMaterial").MatTypeID;
material.MatTypeID = mt.MatTypeID;
deepaDB.AddToMaterialAllTypes(mt);
deepaDB.AddToMaterialNoMasses(material);
deepaDB.SaveChanges();
return RedirectToAction("Index");
}
//Error occurred
var viewModel = new DatabaseSimpleMaterialViewModel
{
MaterialNoMass = material,
RoughnessTypes = deepaDB.RoughnessTypes.ToList()
};
return View(viewModel);
}
FYI, whenever a new MaterialNoMass is created, a new instance of MaterialAllType should also be created using the same ID of MaterialNoMass. I haven't even reached the stage of modifying the database yet, since the model binding seems to be not working.
I've used similar Create approach in other Controllers in the application and they all work fine except in this Controller. I've been having a hard time in debugging this for the past two days. Please help!
The problem comes from the fact that the model you are passing to the view is DatabaseSimpleMaterialViewModel and inside the POST action you expect MaterialNoMass. So make sure you use the correct prefix:
[HttpPost]
public ActionResult Create([Bind(Prefix="MaterialNoMass")]MaterialNoMass material)
I suppose that you editor template generates fields like this:
<input type="text" name="MaterialNoMass.MaterialNoMassID" value="" />
<input type="text" name="MaterialNoMass.RoughnessTypeID" value="" />
...
So you need to indicate the model binder that there's such prefix.
Okay, I'm not trying to do anything super complex, I have searched, and searched, and roamed every blog and forum I can find. I am about to tear my hair out because no one just outright shows it working.
I want to use an IDictionary in my Model. Nevermind the reason why, that's irrelevant. I just want to do something very simple, and understand what is going on.
Can someone please help me figure out the HTML I should be putting in the View to make this work right? Please? I have tried Html.TextBoxFor and it comes back null on the postback. I have tried manually using the index of the dictionary item, and it comes back null. I am really getting frustrated, and all I keep seeing is redirection to blogs that don't answer the question.
Controller
public ActionResult DictionaryView()
{
var model = new Dictionary<string, int>
{
{ "First", 0 },
{ "Second", 0 },
{ "Third", 0 },
{ "Fourth", 0 }
};
return View(model);
}
[HttpPost]
public ActionResult DictionaryView(Dictionary<string, int> dictionary)
{
return null;
}
HTML
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<System.Collections.Generic.IDictionary<string,int>>" %>
<asp:Content ContentPlaceHolderID="MainContent" runat="server">
<h2>Dictionary</h2>
<% using (Html.BeginForm()) {%>
<%: Html.ValidationSummary(true) %>
<% for (int i = 0; i < Model.Count; i++) { %>
**// This is there I am stuck!**
<% } %>
<p>
<input type="submit" value="Submit" />
</p>
<% } %>
</asp:Content>
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Example
<%
int i = 0;
foreach (KeyValuePair<string, int> pair in Model)
{ %>
<p>Key: <%= Html.TextBox(String.Format("[{0}].key", i), pair.Key)%></p>
<p>Value: <%= Html.TextBox(String.Format("[{0}].value", i), pair.Value)%></p>
<%
i++;
}
%>
Edit: This should work
Can you use a ViewModel (POCO) instead of a Dictionary? That way your model will have something you can select.
EDIT
ok. So my chops aren't perfect without intellisense... sorry. But this is my general idea
Public Function DictionaryView() As ActionResult
Dim model = New System.Collections.Generic.List(Of MyDictionary)
model.Add(new MyDictionary("First", 0))
model.Add(new MyDictionary("Second", 0))
model.Add(new MyDictionary("Third", 0))
model.Add(new MyDictionary("Forth", 0))
Return View(model)
End Function
''# This would be your view model.
Public Class MyDictionary
Public Property TheString
Public Property TheInt
End Class
Then in your view you can use
model.TheString
model.TheInt
Stacey,
I don't believe the ModelBinder (the default one anyway), will be able to figure out what you're trying to do, hence the IDictionary param being null.
Your items will be there in the Request.Params collection. For example:
Request.Params["First"]
will return whatever you entered in the first text box. From there, you can pluck them out by hand.
If you want to make this work without using a ViewModel as #rockinthesixstring suggested, you are most likely going to have to write a custom ModelBinder that would do what you want.
If you need an example of this, let me know and I'll dig up the custom model binder stuff.
BTW, a working view can look like this:
foreach (var pair in Model)
{
Response.Write(Html.TextBox(pair.Key, pair.Value));
}
You are missing the concept of how to have a separate counter so you have an index and also iterate through the dictionary with a foreach loop:
<% using (Html.BeginForm()) {%>
<%: Html.ValidationSummary(true) %>
<% int counter = 0;
foreach (var kv in Model) { %>
<input type="text" name="dictionary[<%= counter %>].Key" value="<%= kv.Key %>" />
<input type="text" name="dictionary[<%= counter %>].Value" value="<%= kv.Value %>" />
<%
counter++;
} %>
<p>
<input type="submit" value="Submit" />
</p>
<% } %>
Just tested this now, my Controller is a CCP of yours.
And FYI I simply read that blog post to get here. You just had to match your html elements to exactly what Scott posted.
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.
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.
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.