What if a user hits my site with http://www.mysite.com/Quote/Edit rather than http://www.mysite.com/Quote/Edit/1000 In other words, they do not specify a value for {id}. If they do not, I want to display a nice "Not Found" page, since they did not give an ID. I currentl handle this by accepting a nullable int as the parameter in the Controller Action and it works fine. However, I'm curious if there a more standard MVC framework way of handling this, rather than the code I presently use (see below). Is a smoother way to handle this, or is this pretty mush the right way to do it?
[HttpGet]
public ActionResult Edit(int? id)
{
if (id == null)
return View("QuoteNotFound");
int quoteId = (int)id;
var viewModel = new QuoteViewModel(this.UserId);
viewModel.LoadQuote(quoteId);
if (viewModel.QuoteNo > 0)
{
return View("Create", viewModel.Quote.Entity);
}
else
return View("QuoteNotFound");
}
Your other options would be
Having two Edit actions ; One with int id as its parameters and another without any parameters.
Only having Edit(int id) as your action and letting your controller's HandleUnknownAction method to do what it's supposed to do when your entity is not found (this is a bit more complicated).
But I like your approach the best, as it's simple and correctly handles the situation.
BTW, you don't need that local variable, you can just do this for better readability :
//...
if (!id.HasValue)
return View("QuoteNotFound");
var viewModel = new QuoteViewModel(this.UserId);
viewModel.LoadQuote(id.Value);
//...
No real issue with the way you have it but semantically it's not really an invalid quote number, its that they have navigated to an invalid route that they should not have gone to.
In this case, I would tend to redirect to /quote and if you really want to show a message to the user just show an error banner or similar (assuming you have that functionality in your master page etc).
public ActionResult Edit(int? id)
{
if (id == null)
{
// You will need some framework in you master page to check for this message.
TempData["error"] = "Error Message to display";
return RedirectToAction("Index");
}
...
}
Use route constraint
You can always define a route constraint in your routes.MapRoute() call that won't pass through any requests with undefined (or non-numeric) id:
new { id = "\d+" }
This is a regular expression that checks the value of id to be numeric.
You will probably have to create a new route that defines this, because for other controller actions you probably don't want routes to be undefined. In this case, your controller action wouldn't need a nullable parameter, because id will always be defined.
Don't be afraid of using multiple routes. With real life applications this is quite common.
Related
I am trying to implement POST-Redirect-GET in MVC using multiple named parameters.
I can do this with one parameter:
return RedirectToAction("MyGetView", new { bookId = id });
I could hardcode multiple parameters:
return RedirectToAction("MyGetView, new {bookId = id1, bookId=id2});
But how do I get from an IEnumerable< int> of Ids, which is of variable length, to a correct query string without constructing it by hand?
My current code looks like this:
var querystring= string.Join("", BookIds.Select(x => string.Format("bookId={0}&", x)));
querystring= querystring.Trim('&');
return Redirect("MyGetView?" + querystring);
It works fine, but it seems like there should be a better way.
(I want the parameters to be visible in the URL, so that users can bookmark the page. I believe this means I cannot use TempData.)
First, it's not wrong to use query parameters where the ASP.NET MVC Routes fail to work, and they do with arrays (basically). How is your "clean" URL route supposed to look like?
Example: /Books/View/6 (Works nice for one Book, 6 is the int ID)
But what do you want to have for multiple Books? /Books/View/6,5,134 maybe?
In this case you could just use your own convention of formatting the Url as a list of IDs.
Your redirect would look like: return RedirectToAction("View", "Book", new { Id = allIds.Join(",") });
And your View action can support this:
public ActionResult View(string id)
{
if (id == null)
throw new Exception... // Not expected
var ids = id.Split(new char[]{ ',' });
// Further processing...
}
If your're not satisfied with this approach (you might have issues when number of items is getting bigger) you can see what others tried, but it's basically what you already do, or I'm not sure if the other solutions are worth the effort.
I have a site with a lot of routes.
Some routes, e.g. /sector-overview are to a specific page that I want the user to see.
Other routes, e.g. /sectoroverview are to an an action that ultimately renders a partial which is included on the homepage.
the second route is only meant to be internal to the application, but if the user types that into their address bar (it's an easy mistake to make), the system sees that as a valid request and it'll return the HTML partial.
I could rename the second route to something like /internal-sectoroverview, but this isn't really fixing the problem, just hiding it.
Is there any way for me to prevent the request from being processed if the user types this? What's the best way for me to deal with this issue?
You can block the route by using route constraints. However, in your case I would decorate your internal Action with [ChildActionOnly] like this:
[ChildActionOnly]
public ActionResult Overview()
{
return View();
}
By doing this, the action will be only rendered when using #Html.Action or #Html.RenderAction. If you try to access it through a browser, you'll get an error.
UPDATE
To return a 404 instead of an error you can override the OnException method on the controller and handle it there. Something like this:
protected override void OnException(ExceptionContext filterContext)
{
filterContext.ExceptionHandled = true;
//check if filterContext.Exception was thrown by child action only (maybe by text)
filterContext.Result = new HttpStatusCodeResult(404);
}
If I understand right you should resolve the problem of the partial not being called using the attribute ChildActionOnly.just for reference if you don't want that a method in your action can be called at all use the NonActionAttribute
I have a similar problem issue that people finding this might also need - I want to return 404 if a certain criteria is met from a function that returns a PartialViewResult. The solution for me was
public PartialViewResult MyFunction()
{
if( criteria ) {
Response.StatusCode = 404;
return null;
}
}
Let's say I have a Controller that handles a CRUD scenario for a 'Home'. The Get would look something like this:
[HttpGet]
public ActionResult Index(int? homeId)
{
Home home = homeRepo.GetHome(homeId.Value);
return Json(home, JsonRequestBehavior.AllowGet);
}
So far so good. Then I add a post action for adding new ones.
[HttpPost]
public ActionResult Index(Home home)
{
//add the new home to the db
return Json(new { success = true });
}
Awesome. But when I use the same scheme to handle puts (updating an existing home)...
[HttpPut]
public ActionResult Index(Home home)
{
//update existing home in the db
return Json(new { success = true });
}
We run into a problem. The method signatures for Post and Put are identical, which of course C# doesn't like. I could try a few things, like adding bogus parameters to the signature, or changing the method names to directly reflect CRUD. Those are hacky or undesirable, though.
What is the best practice for going about preserving RESTful, CRUD style controllers here?
This is the best solution that I know of:
[HttpPut]
[ActionName("Index")]
public ActionResult IndexPut(Home home)
{
...
}
Basically the ActionNameAttribute was created to deal with these scenarios.
HttpPut and HttpDeletes are restricted by some firewalls so at times simply HttpPost and HttpGet are used. If a record ID is passed in (or some other criteria) you know its an update. Granted - this is for you to determine, httpput may work just fine for you, this is just a warning on it, it usually isn't a big deal.
Either method used - beware of users trying to inject false IDs into the page in order to forcing updates of records they don't have access to. I get around this issue by hashing in this case home.HomeId on the view when we render it
ViewData["IdCheck"] = Encryption.ComputeHash(home.HomeId.ToString());
in your view:
<%: Html.Hidden("IdCheck", ViewData["IdCheck"]) %>
in your HttpPost or HttpPut method (whichever is doing the update)
if (Encryption.ComputeHash(home.HomeId.ToString()) != (string)Request.Form["IdCheck"])
{
throw new Exception("Hashes do not match");
}
Again - this same security issue exists no matter which method you use to do your update if you are trusting form data.
I have the following scenario: my website displays articles (inputted by an admin. like a blog).
So to view an article, the user is referred to Home/Articles/{article ID}.
However, the user selects which article to view from within the Articles.aspx view itself, using a jsTree list.
So I what I need to do is to be able to differentiate between two cases: the user is accessing a specific article, or he is simply trying to access the "main" articles page. I tried setting the "Articles" controller parameter as optional (int? id), but then I am having problems "using" the id value inside the controller.
What is the optimal manner to handle this scenario? Perhaps I simply need a better logic for checking whether or not an id parameter was supplied in the "url"?
I am trying to avoid using two views/controllers, simply out of code-duplication reasons.
Use separate actions, like:
public ActionResult Articles() ...
public ActionResult Article(int id) ...
Alternatively move it to an Articles controller (urls using the default route will be: Articles and Articles/Detail/{id}):
public class ArticlesController : Controller
{
public ActionResult Index() ...
public ActionResult Detail(int id) ...
}
If you still must use it like you posted, try one of these:
public ActionResult Articles(int id = 0)
{
if(id == 0) {
return View(GetArticlesSummaries());
}
return View("Article", GetArticle(id));
}
public ActionResult Articles(int? id)
{
if(id == null) {
return View(GetArticlesSummaries());
}
return View("Article", GetArticle(id.Value));
}
First of all, I agree with #Steve :). But if you really want to use
int? id
you can just check in your controller method if the id is set using a simple
if(id == null)
and if so, load all articles from your DB (or something alike) and pass these to your view (either directly, or by using a view model). If the id is set you just load the article having that id from your DB and send that to the view (possibly in a list as well if you dont use view models)?
Than in your view just load all articles in the list with articles supplied to the view. Which contains either all or just one.
Complete dummy code
public ActionResult showArticles(int? id){
List<Articles> aList;
if(id == null){
aList = repository.GetAllArticles().ToList();
}else{
aList = new List<Articles>();
aList.add(repository.GetArticleByID(id));
}
return View(aList);
}
Your View has something like:
<% Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<List<Articles>>"%>
foreach(Articles a in Model)
//display article
And you call it using either of the next two options:
html.ActionLink("one article","showArticles","Articles",new {id = aID},null}
html.ActionLink("all articles","showArticles","Articles"}
Define a default value for the Id that you know indicated no value was supplied - usually 0.
public ActionResult Articles([DefaultValue(0)]int Id)
{
if (Id == 0)
// show all
else
// show selected
..
The easiest solution is to have two different actions and views but name the actions the same.
public ViewResult Articles()
{
//get main page view model
return View("MainPage", model);
}
public ViewResult Articles(int id)
{
// get article view model
return View(model);
}
this to me sounds like two separate pages and should be treated as such. You have the "Main" view page and the "articles" page.
I would split it into two actions. They should not be much dupliation at all really, both should be doing a call to get the same ModelView and the article will simply get the a extension to that!
I don't know if you tried this, but instead if you are typing the value directly into the URL, then, instead of passing it like this:
controller/action/idValue
try passing it like this:
controller/action?id=value
Make the parameter required then set a default value in the routing that is a value that isn't a valid index and indicates to your action that it should render the main page.
Well, this is the combined solution I am using:
Using same controller, with DefaultValue:
public ActionResult Articles([DefaultValue(0)]int id)
If DefaultValue was used, I refer to "MainArticles" view. If an article ID was provided - I refer to the "Articles" view with the appropriate article passed inside the ViewModel.
In both cases the ViewModel is populated with the data both views need (complete article and category lists).
Thanks everyone for your help!
I have the following case where I want to accept the following routs
'/type/view/23' or '/type/view/hats'
where 23 is the Id for hats.
The controller looks something like this:
public class TypeController
{
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult View(int id)
{
...
}
}
Now if they pass in 23 no problems. If they pass in hats, I have some work to do. Now I was wondering in this case would I translate hats to 23 by using an ActionFilter that looks to see if the value passed in as the id is an int (if so check that it exists in the database) or if it is a string looks up the database for what the id of the string that has been passed in is. In either case if a match is not found I would want redirect the user to a different action.
Firstly is the approach I have named correct, secondly is it posible to do a redirect from within an ActionFilter.
Cheers
Anthony
Change your signature to accept a string. Then check if the value of id is an int. If it is, then lookup by id, if not lookup by name. If you don't find a match, then do your redirect.
public class TypeController
{
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult View(string id)
{
Product product = null;
int productID = -1;
if (int.TryParse( id, out productID))
{
product = db.Products
.Where( p => p.ID == productID )
.SingleOrDefault();
}
else
{
product = db.Products
.Where( p => p.Name == id )
.SingleOrDefault();
}
if (product == null)
{
return RedirectToAction( "Error" );
}
...
}
}
The reason that I would do this is that in order to know what controller/actions to apply, the framework is going to look for one that matches the signature of the route data that's passed in. If you don't have a signature that matches -- in this case one that takes a string -- you'll get an exception before any of your filters are invoked. Unfortunately, I don't think you can have one that takes a string and another that takes an int -- in that case the framework won't be able to tell which one should match if a single parameter is passed, at least if it's a number, that is. By making it a string parameter and handling the translation yourself, you allow the framework to do its work and you get the behavior you want -- no filter needed.
Unsure you can do this. I would think that you'd need to pass in a string and then check to see whether it's a numeric but there may be a better way.
As for redirecting use
return RedirectToAction("MyProfile", "Profile");
You can pass route values as part of the RedirectToAction call so you can pass in id's or names etc if that is what's required.
There are other ones like redirecting to routes which may also be helpful for what you want.