I have a number of admin pages where the user is presented with a grid of items. The grids has a search panel above them where the user can filter by a number of fields.
When the user clicks search i do a post which then redirects to the Index page. I am sure the code below can be done in a better way (in some scenarios i have up to 20 filter fields).
[HttpPost]
public ActionResult Filter(MySearchModel model)
{
var searchParams = new
{
fielda = model.FieldA,
fieldb = model.FieldB,
fieldc = model.FieldC
};
return RedirectToAction("Index", searchParams);
}
public ActionResult Index(MySearchModel model)
{
//do filtering, return view etc in here
}
Rather than performing a post with a redirect, just change your original form to use a GET method and target the "Index" action directly.
using(Html.BeginForm("Index", null, FormMethod.Get))
{
...
}
That way you can eliminate an HTTP round-trip and get rid of the "Filter" action completely.
Related
I have an MVC5 application which has several controllers, scaffolded with EF6 CRUD actions and associated views. One of these controller/view sets is used for managing a table of patient identifiers, and on completion of an edit or delete, the controller returns an action link to the identifiers index view, as expected.
However, the patient identifiers are also displayed on the various views of the patients controller, and from the Patient.Edit view I have Html.ActionLink calls to the identifier controller's edit or delete actions. When the latter are called from the Patient.Edit view, I would like them to return to that on completion.
Is there any way I can accomplish this?
Yes, but this is always a manual process. There's nothing built into MVC specifically for return URLs.
Essentially, your links to edit/delete will need to include a GET param, usually called returnUrl, though the name doesn't matter, which will be set to the current page URL. For example:
#Html.ActionLink("Edit", new { id = patient.Id, returnUrl = Request.RawUrl })
Then, your edit/delete GET action should accept this parameter, and set a ViewBag member:
public ActionResult Edit(int id, string returnUrl = null)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
In your edit form, add a hidden field:
#Html.Hidden("returnUrl", ViewBag.ReturnUrl)
In your POST edit action, again, accept the param:
[HttpPost]
public ActionResult Edit(int id, Patient model, string returnUrl = null)
But inside this action is where you'll do something different now. Typically, when you've got a successful post and have saved the object or whatever, you then do something like:
return RedirectToAction("Index");
However, instead, you should now check to see if returnUrl has a value, and if it does, redirect to that instead:
if (!string.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index");
The MVC5 with Identity sample project has a nice helper method that it uses:
private ActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
That would just go into your controller and basically does the same as I've already described with two notable differences:
It uses Url.IsLocalUrl to check that the return url is actually a URL on this site. That's a smart check, as since this is initially passed in the query string of the URL, it's open to be manipulated by a user.
It encapsulates the logic, so you don't have to remember how to this should be handled. When you have a successful POST, you simply return RedirectToLocal(returnUrl), and if there's a return URL set, it will be used. Otherwise, the fallback redirect will used.
This is how I did it in one of my projects:
public ActionResult Edit(int id, string returnUrl)
{
// find the model (code not shown)
return View(model);
}
In the Edit view you don't need to do anything special, in the Post Action you have
[HttpPost]
public ActionResult Edit(Model model)
{
if (ModelState.IsValid)
{
// save Model...
return Redirect(Request.Params["returnUrl"]);
// Request.Query will work as well since it is in the querystring
// of course you should check and validate it as well...
}
// else return the View as usual, not shown
}
To use it, when creating the "Edit" link from your pages you simply need to specify the extra returnUrl parameter:
#Html.ActionLink("Edit", "Edit",
new { controller = "YourController",
returnUrl = Url.Action("Index", "ThisController",)
})
Hope it helps.
I'm attempting a multiple page form where I use a single controller action and returning a view depending on a value on my model.
My model has a property that I put in an input field on my views, using Html.HiddenFor().
Here's my simplified model:
public class MyModel
{
public virtual int Step { get; set; }
}
And in my views, I have:
#model MyModel
...
#Html.HiddenFor(model => model.Step)
Then in my controller I have:
public ActionResult Create()
{
...
myModel.Step = 1;
return View("View1", myModel);
}
[HttpPost]
public ActionResult Create(MyModel myModel)
{
...
if (myModel.Step == 1)
{
myModel.Step = 2;
return View("View2", myModel);
}
else if (myModel.Step == 2)
{
...
}
...
}
My problem is, my controller always sees mymodel.Step as having the value of 1. Why is that?
What's weird is that I tried to display it on the form with these:
#Html.DisplayFor(model => model.Step)
#Html.EditorFor(model => model.Step)
The second time the page was displayed, the first line showed the text "2". The second showed an input field with "1". I'm confused.
ADDITIONAL INFO:
My model also has a Guid property which is passed onto the View in a hidden field. I tried to change it also on postback, and check its value the second time around. The new value did not register. The model returned the original value before the first post. So it is consistent with the other field.
I may have to use different controller actions if I couldn't find why it is behaving the way it does at the moment.
SOLUTION:
As Reda suggested below, I fixed it by doing this in my post action method:
Before displaying "View2" and to effect changes my controller makes to a value in my model, I run ModelState.Clear()
Here is a blog post which confirms the need to clear ModelState for this scenario.
Usually, when you return to view from your post action, it means that something failed during validation process and the form should be displayed again with the submitted values. That's why the ModelState remembers your inputs when you return to View, and your inputs will be filled from the ModelState, not your view model.
In my opinion you have two solutions :
ModelState.Clear, which will erase your old value, before setting new ones
redirecting to a GET action, with new values
Second solution is a better one, because you're not displaying the old form with validation errors, you're just showing a new view with different values.
Here's an example (of course you adapt it to your needs) :
public ActionResult Create(int? step)
{
...
myModel.Step = step.HasValue ? step.Value : 1; // or whatever logic you need to apply
return View("View1", myModel);
}
[HttpPost]
public ActionResult Create(MyModel myModel)
{
...
if (myModel.Step == 1)
{
return RedirectToAction("Create", new { step = 2 });
}
else if (myModel.Step == 2)
{
...
}
...
}
I have an Edit page and once the form is submitted I'm refreshing the page instead of redirecting the user to the Index page. To do so I'm saving the ID of the item in a temp variable and then use it to redirect the user to the edit page using the temp variable ID. Something like this:
[HttpGet]
public ActionResult Edit(Guid id)
{
TempData["CategoryID"] = id;
Category c = new CategoriesBL().GetCategory(id);
return View(c);
}
[HttpPost]
public ActionResult Edit(Category c)
{
new CategoriesBL().UpdateCategory(c);
return RedirectToAction("Edit", (Guid)TempData["CategoryID"]);
}
That's working fine. However I have two methods in a different form on the same page and whenever I submit either of these two methods the redirection is not working and I'm getting an exception.
One of the methods that's not working:
[HttpPost]
public ActionResult AddNewThumbnail()
{
List<byte[]> thumbs = new List<byte[]>();
for (int i = 0; i < Request.Files.Count; i++)
{
thumbs.Add(ConvertToByteArray(Request.Files[i].InputStream));
}
new CategoriesBL().AddCategoryThumbnail(thumbs, (Guid)TempData["CategoryID"]);
return RedirectToAction("Edit", (Guid)TempData["CategoryID"]);
}
Exception:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Guid'....
I think it's an issue with routing but the fact is that the same implementation is used and it's working on one form and not the other. I'm not sure whether I'm doing something wrong or if there's any better way to do this.
Note: I have debugged the code several times and the ID I'm passing to the method does have a value in it. However when the page reloads the URL has no ID present.
Debugging
The problem seems to be due to the different forms I'm using. The first form I'm just editing text and it is like so:
#using (Html.BeginForm()) {
// ....
}
In the second form I'm saving and uploading images so the form has to be different
#using (Html.BeginForm("AddNewThumbnail", "Category", FormMethod.Post, new { enctype = "multipart/form-data" })) {
// ....
}
Somehow when I changed the form to the 'normal' one everything worked. But of course I can't use it as I want to save images from this form.
pass the value from your view. Something like this
[HttpPost]
public ActionResult Edit(Category c, FormCollection f)
{
Guid categoryID = (Guid)f["catergoryID"];
new CategoriesBL().UpdateCategory(c);
return RedirectToAction("Edit", catergoryID);
}
In your first example, you have initialisation:
TempData["CategoryID"] = id;
in GET method. So, you have to init your (Guid)TempData["CategoryID"] before you try to access it here:
return RedirectToAction("Edit", (Guid)TempData["CategoryID"]);
I have a weird need in an ASP.NET MVC 3 application which blocks my current progress. Here is the case:
I have a little search engine for the products and I render this search engine on multiple pages. This SE makes a HTTP POST request to product controller's search action. It fine till here.
Let's assume that I am on home controller's index action (/home/index). I make a search and check if ModelState.IsValid. As a result, it is not valid. So, I should return this back with the entered model (so that user won't lose the values) and model state errors. But when I do that I ended up with different URL (/product/search) as expected.
If I do a redirect, I lose the ModelState and cannot display error messages.
I have different solutions so far and they all look dirty. Any idea?
Edit
Here is a little project which demonstrates this:
This is the ProductController:
public class ProductController : Controller {
[HttpPost]
public ActionResult Search(SearchModel searchModel) {
if (ModelState.IsValid) {
//Do some stuff...
return RedirectToAction("Index", "SearchResult");
}
return View(searchModel);
}
}
This is the SearchModel:
public class SearchModel {
[Required]
public string ProductCategory { get; set; }
[Required]
public string ProductName { get; set; }
}
This is the *_SearchPartial*:
#model MvcApplication20.SearchModel
#using (Html.BeginForm("search", "product"))
{
#Html.EditorForModel()
<input type="submit" value="Search" />
}
And finally this is the Home controller Index action view which renders the *_SearchPartial*:
#{
ViewBag.Title = "Home Page";
}
<h2>#ViewBag.Message</h2>
#Html.Partial("_SearchPartialView")
Here, when I submit the form and if the model state fails, how should I proceed at the Product controller Search action?
Here, when I submit the form and if the model state fails, how should
I proceed at the Product controller Search action?
Normally in this case you should render the _SearchPartialView but not as a partial but as a full view with layout so that the user can fix his errors. No need to stay at Home/Index in this case:
[HttpPost]
public ActionResult Search(SearchModel searchModel) {
if (ModelState.IsValid) {
//Do some stuff...
return RedirectToAction("Index", "SearchResult");
}
// since we are returning a view instead of a partial view,
// the _SearchPartialView template should be displayed with the layout
return View("_SearchPartialView", searchModel);
}
And if you wanted to stay on the same page upon error you could use an AJAX call to perform the search. So you would AJAXify this search form and then in the success callback test the result of the Search action and based on it decide whether to refresh the partial in order to show the error or redirect to the results action using window.location.href:
something along the lines of:
$(document).on('submit', '#searchForm', function() {
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function(result) {
if (result.redirectTo) {
// no validation errors we can redirect now:
window.location.href = result.redirectTo;
} else {
// there were validation errors, refresh the partial to show them
$('#searchContainer').html(result);
// if you want to enable client side validation
// with jquery unobtrusive validate for this search form
// don't forget to call the .parse method here
// since we are updating the DOM dynamically and we
// need to reattach client side validators to the new elements:
// $.validator.unobtrusive.parse(result);
}
}
});
return false;
});
This obviously assumes that you have now wrapped the partial call in a div with id="searchContainer" and that you provided an id="searchForm" when generating the search form:
<div id="searchContainer">
#Html.Partial("_SearchPartialView")
</div>
and now the search action:
[HttpPost]
public ActionResult Search(SearchModel searchModel) {
if (ModelState.IsValid) {
//Do some stuff...
return Json(new { redirectTo = Url.Action("Index", "SearchResult") });
}
return PartialView("_SearchPartialView", searchModel);
}
As far as I know the ModelState is lost when doing a RedirectToAction, the solution would be to save the modelstate in the TempData one example of this, that I'm using is this:
http://weblogs.asp.net/rashid/archive/2009/04/01/asp-net-mvc-best-practices-part-1.aspx#prg
This is also discussed in various posts for instance MVC Transfer Data Between Views
I have the following simple (for the purpose of the question) controller/action:
public ActionResult Edit(int id)
{
User user = repository.GetUser(id);
return View(user);
}
[HttpPost]
public ActionResult Edit(User user)
{
user.Name = user.Name.ToUpper();
return View(user);
}
and the view was generated as Edit for User using regular steps in VS
The problem I'm facing is that whatever has been entered in the Name text-box will be showed back to the user but without uppercase. I have checked and the Edit action for POST is executed and user.Name value become UPPER CASE but .TextBoxFor is still using the value entered by the user which is in lower case.
I have checked also that if I print (without the use of any Html Helper) the value of Model.Name after the POST, it will be in UPPER CASE as I would like the Html.TextBoxFor to behave.
Is that a bug of MVC?
Is there a way of making this work in the way I want, meaning that the action/controller is the piece in the puzzle to decide data values for the View and not the view using the ones in the Request object and ignoring the changes that the controller might have done?
Thanks in advance
Use RedirectToAction and pass the Id for the user, it's not a bug, is the POST Behaviour
Update: You need to persist the information before RedirectToAction as Dismissile said, or use TempData if you do not want to persist.
[HttpPost]
public ActionResult Edit(User user)
{
user.Name = user.Name.ToUpper();
return RedirectToAction("Edit", new { id = user.id });
}
You can use ModelState.Remove(nameOfProperty) like:
[HttpPost]
public ActionResult Edit(User user)
{
ModelState.Remove("Name");
user.Name = user.Name.ToUpper();
return View(user);
}
It's will work.