I am using MVC 4, ASP.NET. I am having an issue of passing checkbox values to my post method in the controller. It returns as null value.
Here's part of my view (Get works and properly fills data):
#using (Html.BeginForm())
{
<fieldset>
<table>
<tr>
#{
int cnt = 0;
List<LearningEnterprises.ViewModel.AssignedVendor> techs = ViewBag.Tech;
foreach (var tech in techs)
{
if (cnt++ % 3 == 0)
{
#:</tr><tr>
}
#:<td>
<input type="checkbox"
name="selectedTechs"
value="#tech.TechID"
#(Html.Raw(tech.Assigned ? "checked=\"checked\"" : "")) />
#tech.TechID #: #tech.Title
#:</td>
}
#:</tr>
}
</table>
<p>
<input type="submit" value="Save" id="save" />
</p>
</fieldset>
}
here's my post:
[Authorize(Roles = "Admin")]
[HttpPost, ValidateInput(false)]
public ActionResult EditVendor(int? id, string[] selectedTech)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var vendor = db.Vendors
.Include(i => i.VendorType)
.Include(i=>i.Stocks)
.Where(i => i.ID == id)
.Single();
if (TryUpdateModel(vendor, "",
new string[] { "CompanyName", "PhoneNumber", "Address", "ProvinceState", "City", "Country", "PostalCode", "Email", "numberofBooths", "comments", "electric", "internet", "tonicEquipment", "VendorTypeID" }))
{
try
{
UpdateVendorTech(selectedTech, vendor);
db.Entry(vendor).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (Exception /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
PopulateAssignedTechData(vendor);
PopulateDropDownLists(vendor.VendorTypeID);
return View(vendor);
}
string[] selectedTech is displaying a null value in debug. It is supposed to equal the id value of checkboxes selected in the view.
The first thing I can see is that your names mismatch: you have string[] selectedTech in your controller but name = "selectedTechs" in your view, then the ASP.NET binder being unable to find any values, it sets your array to null.
I also note you don't have any post URL in your Html.BeginForm() but if you said you could check the value in debug I suppose you are doing it an other way...
I expect that user3426870 is correct and that the problem is to do with the name on the EditVendor method signature (selectedTech) not matching the name of checkbox on the view (selectedTechs). I suspect that the DefaultModelBinder will not be able to match the parameters to bind the values to.
Additionally, the TryUpdateModel does not seem to include any properties defined in the view that will be passed in your post request so I suspect that this will not work.
May I suggest some other improvements (which you could research) that you could make:
The controller method is quite large. You should look at using
services/repositories to perform any business logic and database
calls to reduce the number of actions the controller is performing.
This will help to make those smaller parts more testable.
If the id on the signature should not be null (as you return a bad request if it is) then you should not make it a nullable int? parameter. You will then no longer need to perform that check.
Also, you may wish to add validation to ensure that the id is greater than 0 or that the list of selectedTech has a count that is greater than 0. Such validation can be performed using data annotations or fluent validation and then a simple check performed in the code such as:
if (ModelState.IsValid) { ... }
to ensure that the parameters passed in pass your defined validation.
You should use a view model for the view which holds exactly the
information that you need. You can then map between this and your
lower layer business classes using mapping classes (or automapper).
Using a view model is the preferred way and avoids use of ViewBag
properties. You can also use some of the HTML helper methods to
write out some of those view model properties.
Hope these ideas help.
Related
I have an ItemsController with an Index action returning and rendering items on the client like
return View(itemViewModels);
Each itemViewModel has some bootstrap tabs. In each tab a partialView is rendered.
When the user edits a partialView and send the data to the Tab1Controller how can I return the View for the whole itemViewModel showing validation errors for the one partial view inside that tab1?
I have made the View work requesting/responsing with the sent itemViewModel but then Only the single item`s html is returned not the full items html.
AND when I return the Index View to the ItemsViewModels then I can NOT return my passed itemViewModels to show the validation errors.
[HttpGet]
public ActionResult Index(ItemViewModel viewModel)
{
return View("ItemsViewModels", viewModel);
}
This code does not work because the View does not match to the viewmodel type.
But I need to show the Html of the ItemsViewModels with the invalid (data-val attributes) html of the single edited itemViewModel.
I can NOT post and return the ItemsViewModels because it has many other properties which would make the modelstate invalid...
Do you have any idea?
UPDATE
I am NOT allowed to use ajax else the problem would be done quickly... this is for a common website and the customer wants a postback no ajax/SPA behavior.
At the moment I get the whole items from the service again and render it but then every ItemViewModel`s html has an invalid e.g. textbox. But I want that only a certain ItemViewModel is invalid.
[HttpPost]
public virtual async Task<ActionResult> SaveLanguage(ItemViewModel itemViewModel)
{
var viewModels = GetItemsViewModelsFromSErvice();
viewModels.Items.ElementAt(0).Settings = itemViewModel;
return View(MVC.Test.Items.Views.Index,viewModels );
}
If you are forced to do a full postback, but each partial view contains just a form with the elements for that 1 item then you'll have to reconstruct the other items in your controller before returning.
Your controller method would be something like
public ActionResult Index()
{
var vm = GetAllItems(); //method to get all your items for first load
return View(vm);
}
[HttpPost]
public ActionResult Index(ItemViewModel viewModel)
{
//get the full list of items, and then replace just the altered one
var vm = GetAllItems(); // assume returns a list
var index = vm.FindIndex(x => x.ID == viewModel.ID);
vm[index] = viewModel;
//might have to rename items in the ModelState.Errors dictionary
//so they are associated with the correct item index.
//first get the list of errors. As viewModel is not a list for this method
//they will have keys like "PropertyName".
//For a listItem need renaming to something like "[#].PropertyName" (# = index)
var errs = from ms in ModelState
where ms.Value.Errors.Any()
let fieldKey = ms.Key
let errors = ms.Value.Errors
from error in errors
select new {fieldKey, error.ErrorMessage};
//clear the ModelState, and then re-add any model-errors with the renamed key
ModelState.Clear();
foreach(var item in errs)
{
ModelState.AddModelError(
String.Format("[{0}].{1}", index, item.fieldKey), item.ErrorMessage);
}
return View("ItemsViewModels", vm);
}
In addition you might need to rename your form elements so that the model binder treats them as list items after postback. I'm not 100% sure this is necessary though.
If you can use ajax this becomes neater...
It looks like your index Model is a List<ItemViewModel>
Then your Main view (Index.cshtml) would be something like..
#model List<ItemViewModel>
...
#for(int i = 0; i < Model.Count; i++)
{
<div id="#String.Format("partialContainer{0}", Model[i].ID)">
#Html.Partial("Partial1", Model[i])
</div>
}
(notice that the ID of the container div is something that we can reference as the ajax update target)
And then have your partial Views use the relevant partial Models
Eg Partial1.cshtml:
#model ItemViewModel
... etc
#using (Ajax.BeginForm("RefreshPartial", "Home", null, new AjaxOptions() {
UpdateTargetId = String.Format("partialContainer{0}", Model.ID), HttpMethod = "Post" }, null))
{
#Html.TextBoxFor(m => m.Property1);
#* form controls... *#
<input type="submit" value="Submit" />
}
I am using MVC3, Razor and C#.
I have implemented a simple and robust inline editing solution for a grid.
Basically I use Razor to build my form which encloses the grid, and then the row that matches the item id gets opened up as the editable row which is coded as a partial View.
The Grid View (part):
#using (Html.BeginForm("Edit", "GridTest"))
{
<table>
<tr>
<th>col1</th>
<th>Col2</th>
</tr>
#{
foreach (var item in Model)
{
<tr>
#{
if ((Model.ItemId == item.Id))
{
Html.RenderPartial("_EditRow", item);
}
else
{
Html.RenderPartial("_DisplayRow", item);
}
}
</tr>
}
</table
}
EditRow.cshtml
#Html.TextBoxFor(p=>p.Name)
Save
Cancel
#Html.HiddenFor(p=>p.Id)
DisplayRow.cshtml
<td>
#Model.Name
</td>
<td>
#Html.ActionLink("Edit", "Edit", "GridTest", new {id = Model.Id}, null)
</td>
GridTest/Edit Action
public ActionResult Edit(int id)
{
var myRecord = db.Orders.First(p => p.Id == id);
return View("Index",myRecord);
}
GridTest/Edit Post Action
[HttpPost]
public ActionResult Edit(Order myRecord, string btn="")
{
if (btn == "Save")
{
if (ModelState.IsValid)
{
Order myCurrentRecord = db.Order.First(p => p.Id == myRecord.Id);
myCurrentRecord.Name = myRecord.Name;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(myRecord);
}
else
{
return RedirectToAction("Index");
}
The above code shows the design. It works greats and is simple. However it causes a postback of the complete page, and I would like to stop this "flashiness". So I suspect I need to somehow tweak the above code such that the "EditRow" posts inline, without refreshing the entire page. I suspect I am looking at using Ajax?
So how can the above code be simply upgraded to prevent complete page refresh, but rather row refresh?
Many thanks in advance.
At its simplest, you can probably just capture the submit event of the form and serialize that form to an AJAX POST. Something like this:
$('form').submit(function () {
$.post('#Url.Action("Edit", "GridTest")', $('form').serialize())
.done(function(data) {
// AJAX call is complete
// do something on the page?
});
return false;
});
This will use the same controller action in the same way that the form does, just via AJAX. The controller action will also respond the same way, which probably isn't what you want with an AJAX request. You might instead want to return some JSON data to indicate success, error conditions, results of server-side processing, etc. Something as simple as this can just indicate success from the controller action:
return Json(true);
You can, of course, return any structured data by passing it to Json(), which will serialize it to JSON data as the data value in the JavaScript done handler.
Edit: If you want to replace a piece of the client-side content wholesale with a partial view, you can still return that partial view in the AJAX request. The controller action can do something like this:
return PartialView("_DisplayRow", myRecord);
(Assuming myRecord is the type that is bound to that partial view.)
Then you'd have something for the done handler in the client-side code. Maybe something like this:
$('tr.someClass').html(data);
The idea here is that the tr element which is the "edit row" should be uniquely identified in some way (I'm using a class in my selector at the moment, but you can use whatever works for you), and its contents should be replaced with the contents of the partial view being returned from the server-side code.
I have a MVC3 application using Entity Framework as the Model layer.
In the EmployeeController, I have:
public ActionResult GetEmployeeEdit(String id)
{
// Get the desired Employee
var model = GetEmployees().FirstOrDefault(o=>o.EFolderid == id);
return View("EmployeeEdit", model);
}
private IQueryable<Employee> GetEmployees()
{
// Returns IQueryable<Employee>
return _employeeService.GetTable();
}
In EmployeeEdit I have:
#model Metastorm.Domain.Models.Employee
#{
ViewBag.Title = "Employee Edit";
}
#using (Html.BeginForm("SaveEmployee", "Employee", FormMethod.Get, Model))
{
<fieldset>
<legend>Edit Employee</legend>
<br />
#Html.Label("firstName", "First Name: ")
#Html.EditorFor(o => #Model.NameFirst)
<br />
#Html.Label("lastName", "Last Name: ")
#Html.EditorFor(o => #Model.NameLast)
</fieldset>
<br />
<input class="button" id="submit" type="submit" value = "Save Employee" />
}
Now back to the EmployeeController:
[HttpGet]
public ActionResult SaveEmployee(Employee employee)
{
if (ModelState.IsValid)
{
// Get the Employee Model again from Entity, Update and save
// Unfortunately, the employee object's FolderId value is null
}
// Just getting a model to satisfy the function
var model = GetEmployees().FirstOrDefault();
return View("EmployeeEdit", model);
}
The problem I'm having is that all properties on the employee object are null, except for Employee.NameFirst and Employee.NameLast, which happen to be the properties that were exposed in the View with Html.EditorFor.
In summary, I get an Employee model object, which is fully hydrated. I pass this model from the Controller to the view. In the view, selected fields are allowed to be updated. The Employee model is then passed back to the Controller where updates are persisted.
My question is how do I keep the Employee model that was originally passed from the Controller to the View intact. In other words, I want to have the model
Try to use this code:
[HttpGet]
public ActionResult SaveEmployee([Bind(Include = "NameFirst,NameLast,EFolderid")] Employee employee)
{
if (ModelState.IsValid)
{
// Do your staff
// The employee object's FolderId value will be not null
}
// Just getting a model to satisfy the function
var model = GetEmployees().FirstOrDefault();
return View("EmployeeEdit", model);
}
This will force the data members "NameFirst, NameLast, EFolderid" of the object employee to be persistent. You need to declare in the include statement all the members that you want to be preserved during the Http request.
You should also add this code in your view, inside the Form, or the EFolderid data will be lost:
#Html.HiddenFor(o=> #Model.EFolderid)
Add an "HiddenFor" declaration foreach member that you want to preserve.
To obtain model properties in a Post action, you need include all the properties inside the form.
It is necessary for a correct model serialization.
I went a different route. I created a view model, then us AutoMapper to map the Employee object's properties to the view model. I pass the view model to the view, make changes, then pass the view model back to the Action. Then I can then user AutoMapper to map the view model back to a new instance of the Employee Object, then persist the changes.
Once again I'm confronted with a "This shouldn't be this ?*!# hard" situation.
Problem: I want to use a form in MVC for creation of an object. One of the elements of the object is a set of limited choices - a perfect candidate for a drop down list.
But if I use a SelectList in my model, and a drop down list in my View, and then try to post the Model back to my Create method, I get the error "Missing Method Exception:No Parameterless constructor for this object". Exploring the MVC source code, it appears that in order to bind to a model, the Binder has to be able to create it first, and it can't create a SelectList because there is no default constructor for it.
Here's the simplified code:
For the model:
public class DemoCreateViewModel
{
public SelectList Choice { get; set; }
}
For the controller:
//
// GET: /Demo/Create
public ActionResult Create()
{
DemoCreateViewModel data = new DemoCreateViewModel();
data.Choice = new SelectList(new string[] { "Choice1", "Choice2", "Choice3" });
ViewData.Model = data;
return View();
}
//
// POST: /Demo/Create
[HttpPost]
public ActionResult Create(DemoCreateViewModel form)
{
try
{
// TODO: Add insert logic here
return RedirectToAction("Index");
}
catch
{
return View();
}
}
And for the View:
<fieldset>
<legend>Fields</legend>
<%= Html.LabelFor(model => model.Choice) %>
<%= Html.DropDownListFor(model => model.Choice, Model.Choice) %>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
Now, I know I can MAKE this work by dropping back 10 yards and punting: bypass model binding and drop back to the FormCollection and validate and bind all the fields myself, but there's got to be a simpler way. I mean, this is about as simple a requirement as it gets. Is there a way to make this work within the MVC ModelBinding architecture? If so, what is it? And if not, how come?
Edit: Well, I have egg on my face, but maybe this will help someone else. I did some more experimenting and found a simple solution that seems to work.
Provide a simple value (string or integer, depending on what your select list value type is), and name that as the model element that you bind to. Then provide a second element as the select list of choices, and name it something else. So my model became:
public class DemoCreateViewModel
{
public string Choice { get; set; }
public SelectList Choices { get; set; }
}
And then the DropDownListFor statement in the View becomes:
<%= Html.DropDownListFor(model => model.Choice, Model.Choices) %>
When I do this, the submit button correctly binds the choice made in the form to the string Choice, and submits the model back to the second Create method.
Here is one approach:
#Html.DropDownListFor(model => model.Choice,
ViewBag.Choices as SelectList,
"-- Select an option--",
new { #class = "editor-textbox" })
Notice that I use ViewBag to contain my SelectList. This way when you post back, the client doesn't send the entire select list up to the server as part of the model.
In your controller code, you just need to set the view bag:
ViewBag.Choices = new SelectList(....
Consider creating a different view model for your post action without the SelectList property:
public class DemoCreateViewModelForUpdate
{
public string Choice { get; set; }
}
Then you can always map from the DemoCreateViewModelPost instance to an DemoCreateViewModel instance if the model state is invalid and you want to re-show the view. I tend to prefer everything needed by the view to be in my display view model class, so using a separate update only view model let's me keep things slim and trim for the trip back to the server.
In your view, you'd do:
#Html.DropDownListFor(m => m.Choice, Model.Choices)
as in the previous answer, so no unnecessary data would round trip.
I have a simple for with at text field where the user enters the address of a RSS feed. I wont act if the field is blank, this is my markup:
<%=Html.ValidationSummary() %>
<table>
<tr>
<td>
Feed Url:
</td>
<td>
<%=Html.TextBox("url", null, new {#style="width:300px"}) %>
</td>
</tr></table>
My controller is also very simple:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult AddFeed(FormCollection collection)
{
string url = collection.Get("url");
string roles = collection.Get("Roles");
if (string.IsNullOrEmpty(url))
{
ModelState.AddModelError("url", "Please provide a propre feed url");
}
if (string.IsNullOrEmpty(roles))
{
ModelState.AddModelError("Roles", "Please select a valid role");
}
if (ModelState.IsValid)
{
Session["url"] = url;
Session["Roles"] = roles;
return RedirectToAction("ValidateFeed");
}
else
{
return View();
}
}
When it fails it reloads the view and makes an exception in the line where it renders my textbox, saying that there has been a null pointer exception. This really botheres me, it should be so simple... but still im struggling
/H4mm3r
Edit Please disregard the Roles element its a drop down i have, but removed it from markup for simplicity
You probably can't use null when using the HTML helper. Try this in your form instead:
<%= Html.TextBox("url", String.Empty, new {#style="width:300px"}) %>
I haven't tested that, but that's the first thing I would try, given that error message.
I think you cant use the ModelSate like that if you didn't do something like:
UpdateModel(collection);
or
TryUpdateModel(collection);
or by Parameter Binding
public ActionResult AddFeed(YourModelType collection)
Because the Modelstate is not associated with any Model.
As the MSDN reference says:
ModelState Class
Encapsulates the state of model
binding to a property of an
action-method argument, or to the
argument itself.