Rebuilding an ICollection after MVC edit? - c#

If my model Product has a member ICollection<ProductOption> how do I rebuild my Product with the member collection in my Controller.Edit(...) method on post back from Edit?
(We can assume we never add or remove an option, only ever edit.)
Razor:
#model Models.Products.Product
#{
ViewBag.Title = "Edit";
Layout = "~/Views/Shared/_GlobalLayout.cshtml";
}
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
#Html.HiddenFor(model => model.Id)
<legend>Product</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
#foreach (ProductOption option in Model.Options)
{
<div style="border:1px solid black; margin: 5px; padding:5px 7px;">
#Html.Partial("_ProductOptionInput", option)
</div>
}
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
controller:
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
var p = _repository.ById(id);
UpdateModel<Product>(p);
_repository.SaveChanges();
return RedirectToAction("Index");
}
partial view:
#modelModels.Products.ProductOption
#Html.ValidationSummary(true)
<fieldset>
<legend>ProductOption</legend>
#Html.HiddenFor(model => model.Id)
<div class="editor-label">
#Html.LabelFor(model => model.Term)
</div>
</fieldset>
UPDATE
My FormCollection in ActionResult Edit(int id, FormCollection collection) is essentially a dictionary, so it has the ProductOption values for one of the updated ProductOptions but not the rest of them as the keys (ie the ProductOptions property names) can't be repeated in the dictionary.

You either need to write your own model binder (either for ICollection<ProductOption> or one that pulls your entity out of the database instead of instantiating a new instance), or you can NOT take the model in as a parameter, and instead, pull it out of the database in your action method, then call TryUpdateModel from the controller.
HTH

I added the following in the Razor view and it works like a charm!
#Html.EditorFor(model => model.Options.ToList()[0], templateName: "ProductOptionInput", htmlFieldName: "Options[0]")
#Html.EditorFor(model => model.Options.ToList()[1], templateName: "ProductOptionInput", htmlFieldName: "Options[1]")

Related

Entity Framework Returns No Value With MVC Action

I'm facing a problem with Entity Framework and MVC ActionResult returns Partial View.
I was create very sample code to retrieve data from database to partial view action, and plug data to partial view as normal data model.
But when I use the same code to retrieve data, it's not work and return empty model, and when I use it with the normal ActionResult that's return normal View it's work.
This is the Partial View action:
public ActionResult PaperSpecification()
{
var _sheets = _entity.Sheets.ToList();
return PartialView("_SheetSpecificationPartial", _sheets);
}
_entity variable defined as private in controller class.
The same idea I used to but in Index action
public ActionResult Index()
{
var _sheets = _entity.Sheets.ToList();
return View(_sheets);
}
This is Index view:
#{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#model IEnumerable<PrintManager.Models.Db.Sheet>
<h2>Index</h2>
#{
Html.RenderAction("PaperSpecification", "WorkOrder");
}
<ul>
#foreach (var item in Model)
{
<li>#item.SheetName</li>
}
</ul>
And finally this is partial view:
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Paper Specification</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Paper Specification</label>
<select class="form-control">
#if (ViewBag.sheets_L != null)
{
foreach (var item in Model)
{
<option>#item.SheetName</option>
}
}
</select>
</div>
</div>
<div class="col-md-3"></div>
<div class="col-md-3"></div>
<div class="col-md-3"></div>
</div>
</div>
</div>
I think this is no Entity issue but it's Partial View issue.
As suggested by Pavel in comments replace:
#{
Html.RenderAction("PaperSpecification", "WorkOrder");
}
With
#Html.Partial("_SheetSpecificationPartial")
In this case you don't need to pass explicitly the model to your partial view. Now you can remove PaperSpecification method.

Why is my model not passed as parameter with form post

I'm having trouble understanding why my model is not passed along with its values to my controller when posting a form.
I have a view with a strongly typed model (UnitContract) that is being fetched from a webservice, that holds a set of values. In my action I'm trying to fetch int ID and bool Disabled fields that exists in my model. When debugging, I see that my model being passed from the form doesn't contain any values at all. What am I missing?
My view (UnitContract as strongly typed model):
...
<form class="pull-right" action="~/UnitDetails/EnableDisableUnit" method="POST">
<input type="submit" class="k-button" value="Enable Unit"/>
</form>
My controller action:
[HttpPost]
public ActionResult EnableDisableUnit(UnitContract model)
{
var client = new UnitServiceClient();
if (model.Disabled)
{
client.EnableUnit(model.Id);
}
else
{
client.DisableUnit(model.Id);
}
return RedirectToAction("Index", model.Id);
}
Sounds like you need to add the fields from your model to your form. Assuming your view accepts a UnitContract model, then something like this should work:
<form class="pull-right" action="~/UnitDetails/EnableDisableUnit" method="POST">
#Html.HiddenFor(x => x.Id)
#Html.HiddenFor(x => x.Disabled)
<input type="submit" class="k-button" value="Enable Unit"/>
</form>
Now when you submit the form, it should submit the fields to your model.
The MVC framework will use the data from the form to create the model. As your form is essentially empty, there is no data to create the model from, so you get an object without any data populated.
The only data that is sent from the browser in the request when you post the form, is the data that is inside the form. You have to put the data for the properties in the model as fields in the form, so that there is something to populate the model with.
Look into using #Html.HiddenFor(). Put these in your form, and the data you want to see posted back to your controller should be there. For example, your form would look something like...
<form class="pull-right" action="~/UnitDetails/EnableDisableUnit" method="POST">
#Html.HiddenFor(x => x.Id)
#Html.HiddenFor(x => x.IsDisabled)
<input type="submit" class="k-button" value="Enable Unit"/>
</form>
Let's say you have a model like this:
public class UnitContract
{
public int Id { get; set; }
public DateTime SignedOn { get; set; }
public string UnitName { get; set; }
}
Your view would look something like this:
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<fieldset>
<legend>UnitContract</legend>
#Html.HiddenFor(model => model.Id)
<div class="editor-label">
#Html.LabelFor(model => model.SignedOn)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.SignedOn)
#Html.ValidationMessageFor(model => model.SignedOn)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.UnitName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.UnitName)
#Html.ValidationMessageFor(model => model.UnitName)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
In your controller:
[ValidateAntiForgeryToken]
[HttpPost]
public ActionResult Edit(UnitContract unitContract)
{
// do your business here .... unitContract.Id has a value at this point
return View();
}
Hope this is helpful.

ModelState Errors not showing in view

I have an action that saves a record by calling my BLL entity's Save method. The entity takes care of its own internal validation and if a field is required but fails validation because a user didn't enter a value then the entity throws up an error. I'm catching that error in my action and returning the same view. The problem is the error isn't showing in my ValidationSummary.
Yes I realize I have view model validation by attibute with MVC but this entity is used elsewhere and must have redundant validation if the UI doesn't/can't do it, such as used in a batch service job.
Here is my action:
public ActionResult Edit(EntityModel model) {
if (ModelState.IsValid) {
var entity = new Entity(model.ID, model.Name, model.IsActive);
try {
entity.Save(User.Identity.Name);
return RedirectToAction("List");
}
catch (Exception ex) {
ModelState.AddModelError("", ex.Message);
}
}
return View(model);
}
Here is my View:
#model ELM.Select.Web.Models.EntityModel
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>DefermentTypeViewModel</legend>
#Html.HiddenFor(model => model.ID)
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.IsActive)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.IsActive)
#Html.ValidationMessageFor(model => model.IsActive)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
Why wouldn't the error I add to modelstate be shown in my validationsummary?
Change your View code:
#Html.ValidationSummary(true)
to:
#Html.ValidationSummary(false)
As per the MSDN Reference on ValidationSummary(), here is the method definition:
public static MvcHtmlString ValidationSummary(
this HtmlHelper htmlHelper,
bool excludePropertyErrors
)
Notice that the bool parameter, if you set it to true (like you originally did) you will exclude property errors. Change that to false and that should get you what you want.

Object edited in view delivered by HttpGet Edit() is not received by HttpPost Edit()

I have two edit Action methods, one for HttpGet and one for HttpPost. The HttpGet method takes an id parameter, retrieves the appropriate object, and displays it for editing. The HttpPost method takes a parameter that should be the edited object; however, the ids do not match. Why is that mismatch occurring? I've included the code for my Edit.cshtml view, and for my two Action Methods.
The View:
#model WebApplicationPbiBoard.Models.Sprint
#{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Sprint</legend>
#Html.HiddenFor(model => model.Id)
<div class="editor-label">
#Html.LabelFor(model => model.Start)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Start)
#Html.ValidationMessageFor(model => model.Start)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.End)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.End)
#Html.ValidationMessageFor(model => model.End)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
The Action Methods:
//
// GET: /Sprint/Edit/5
public ActionResult Edit(int id)
{
var sprint = Db.GetById(id);
return View(sprint);
}
//
// POST: /Sprint/Edit/5
[HttpPost]
public ActionResult Edit(Sprint editedSprint)
{
if (ModelState.IsValid)
{
Db.Save(editedSprint);
return RedirectToAction("Index");
}
else
return View(editedSprint);
}
Here's the GetById method. It's pretty much a wrapper around the NHibernate ISession.GetById method.
public T GetById(int id)
{
return Session.Get<T>(id);
}
Here's my solution. Thanks to everyone who helped me with debugging it.
The problem was that, to adhere to NHibernate best practices, the setter for my id property was private. That meant that the controller couldn't set it in the reconstructed model object, so the id was the default value of 0. To solve the problem, I passed the id in as a parameter to the controller and found the right object in my repository, which I then manually updated. Here's the code:
[HttpPost]
public ActionResult Edit(int id, DateTime start, DateTime end)
{
//get the right model to edit
var sprintToEdit = Db.GetById(id);
//copy the changed fields
sprintToEdit.Start = start;
sprintToEdit.End = end;
if (ModelState.IsValid)
{
Db.Save(sprintToEdit);
return RedirectToAction("Index");
}
else
return View(sprintToEdit);
}

AllowHtml attribute not working

I have a model with this property:
[AllowHtml]
[DisplayName("Widget for Table")]
[StringLength(1000, ErrorMessage = "Maximum chars 1000")]
[DataType(DataType.Html)]
public object TableWidget { get; set; }
And here is the create methods in controller:
//
// GET: /Admin/Table/Create
public ActionResult Create(int id)
{
Season season = _seasonRepository.GetSeason(id);
var table = new Table
{
SeasonId = season.SeasonId
};
return View(table);
}
//
// POST: /Admin/Table/Create
[HttpPost]
public ActionResult Create(Table a)
{
if (ModelState.IsValid)
{
_tableRepository.Add(a);
_tableRepository.Save();
return RedirectToAction("Details", "Season", new { id = a.SeasonId });
}
return View();
}
And last here is my view:
#model Stridh.Data.Models.Table
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Fields</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Name) #Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.TableURL)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.TableURL) #Html.ValidationMessageFor(model => model.TableURL)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.SortOrder)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.SortOrder) #Html.ValidationMessageFor(model => model.SortOrder)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.TableWidget)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.TableWidget) #Html.ValidationMessageFor(model => model.TableWidget)
</div>
<div class="editor-label invisible">
#Html.LabelFor(model => model.SeasonId)
</div>
<div class="editor-field invisible">
#Html.EditorFor(model => model.SeasonId)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
When I add a "normal" message without html everything is saved OK, but when saving it says A potentially dangerous Request.Form...
Another strange thing is that I got this [AllowHtml] to work in another model class. I cant find why this is causing me troubble. Need your help. :-)
The way you are using AllowHtml should work. Make sure that you are not accessing the HttpRequest.Form collection anywhere else in your code (controller, filter, etc) as this will trigger ASP.NET Request Validation and the error you are seeing. If you do want access to that variable then you should access it via the following code.
using System.Web.Helpers;
HttpRequestBase request = .. // the request object
request.Unvalidated().Form;
I get the same problem and i solve it with the help of this post.
If you are on .net 4.0 make sure you add this in your web.config
<httpRuntime requestValidationMode="2.0" />
Inside the <system.web> tags
I had the same problem. My model class is named "GeneralContent" and has the property "Content". In my action method i used attribute like this:
public ActionResult Update(GeneralContent content)
when i renamed content argument to cnt, everything works well. I think MVC is confused when some attribude of model class has the same name as the argument in action method.
I also had this issue. I could not get a model property marked with [AllowHtml] to actually allow HTML, and instead encountered the same error you describe. My solution ended up being to mark the Controller action that accepts the posted model with the [ValidateInput(false)] attribute.
The answer that #marcind put me on the right track but my issue was that I was passing the FormCollection into the Controller method, so changing this...
public ActionResult Edit(MyClass myClass, FormCollection collection)
To this...
public ActionResult Edit(MyClass myClass)
Solved the problem.
Subsequently, I was able to access the heck out of the form collection with code like this without issue.
foreach (var key in Request.Form.AllKeys)
{
...
}
So, it was the passing the form collection parameter that caused the problem, not merely accessing the form collection.

Categories

Resources