routing and URL structure - asp.net-mvc

Is there anyway that I can exclude an ID field from the generated URL, but still be able to use the ID value? If you take a StackOverflow URL:
http://stackoverflow.com/questions/3409196/asp-net-mvc-routing-question
Can that URL be rendered without the Question ID?

You can display the question without an ID but question title has to be unique for every question.
Also you can still use the IDs for finding questions then redirect to another URL that only displays the question title. If that's how you wanna do it I can post an example.
Here's an example:
// this method finds a file from database using the id
//and passes the object with TempData
public ActionResult InitialDetail(int id)
{
var question = questionRepository.GetFile(id);
if (question==null)
return View("NotFound");
else
{
TempData["question"] = question;
return Redirect("/questions/" + question.Name);
}
}
//this method uses model passed from other method and displays it
public ActionResult Details(string questionName)
{
if (TempData["question"] == null)
{
return View("NotFound");
}
else
return View("Details", TempData["question"]);
}
You also have to define a route for this to work
routes.MapRoute("QuestionPage", //Files/id/fileName
"questions/{questionName}",
new { controller = "Questions", action = "Details" } );
Add this route just before the default route. It may mess things if you have routes for URL starting with http://domain.com/questions.
Note: This may not be the best solution. If your question titles are not unique, you can't put links with this structure in your page. First it has to look for question using ID.

Related

Multiple views modifying one object MVC

I am building a service which requires a somewhat lengthy setup process. I have it broken into 4 models and 4 corresponding views. They are Setup, Setup2, Setup3, and Setup4. Each of these views gathers information from the user which is stored in a User object. I have been passing the user along like this:
[HttpPost]
public ActionResult Setup(FormCollection values)
{
User registeringUser = new User();
registeringUser.email = User.Identity.Name;
registeringUser.fName = values["fName"];
registeringUser.lName = values["lName"];
registeringUser.phone = values["phone"];
return RedirectToAction("/Setup2", registeringUser);
}
For some reason, this seems to work just fine for the first jump (from Setup to Setup2) but after that I'm getting weird behavior, such as User. getting set to null when the User is passed to another View.
In a related, but slightly different issue, I need the last screen (Setup4) to be recursive. This screen adds a course in which the user is enrolled, and if they don't check the "This was my last class" button, it needs to basically clear the form so they can enter another course.
The entire Controller looks like this:
[HttpPost]
public ActionResult Setup4(FormCollection values, User registeringUser)
{
// values["allClassesAdded"] returns "false" as a string if box is unchecked, returns "true,false" if checked.
// Solution: parse string for "true"
if (utils.parseForTrue(values["allClassesAdded"]))
{
// TODO Redirect to "congratulations you're done" page.
database.CreateUserInDB(registeringUser);
return Redirect("/Home");
}
else
{
// Build course and add it to the list in the User
course c = new course(values);
if (Request.IsAuthenticated)
{
//registeringUser.currentCourses.Add(c);
registeringUser.AddCourse(c);
return RedirectToAction("/Setup4", registeringUser); // <---- This doesn't really work right
//return View();
}
else
{
return Redirect("/Account/Login");
}
}
}
This is my first project with MVC, so if you find that I'm doing the entire thing completely incorrectly, feel free to not answer the question I asked and offer the proper solution to this need. I'm moving an existing (pure) C# project to MVC and I'm mainly just stuck on how to work within MVC's interesting structure. I'm very grateful for any help you can give!
Thanks!
You can store user related data in session without passing it between requests
Smth like this
[HttpPost]
public ActionResult Step1(Step1Model model)
{
Session["UserRegistration"] = new UserRegistration
{
FirstName = model.fName,
....
}
....
}
[HttpPost]
public ActionResult Step2(Step2Model model)
{
var userRegistration = Session["UserRegistration"] as UserRegistration;
if (userRegistration == null) { return Redirrect("Step1"); }
userRegistration.SomeField = model.someField;
...
Session["UserRegistration"] = userRegistration;
....
}

Manipulate the url using routing

In my website I have the following route defined:
routes.MapRoute(
name: "Specific Product",
url: "product/{id}",
defaults: new { controller = "", action = "Index", id = UrlParameter.Optional }
);
In that way I want customers to be able to add the ID of the product and go to the product page.
SEO advisors have said that it would be better if we could add a description of the product on the URL, like product-name or something. So the URL should look something like:
/product/my-cool-product-name/123
or
/product/my-cool-product-name-123
Of course the description is stored in the db and I cannot do that with a url rewrite (or can I?)
Should I add a redirection on my controller (this would seem to do the job, but it just doesn't feel right)
On a few sites I checked they do respond with a 301 Moved Permanently. Is that really the best approach?
UPDATE
As per Stephen Muecke's comment I checked on what is happening on SO.
The suggested url was my own Manipulate the url using routing and i opened the console to see any redirections. Here is a screenshot:
So, first of all very special thanks to #StephenMuecke for giving the hint for slugs and also the url he suggested.
I would like to post my approach which is a mix of that url and several other articles.
My goal was to be able to have the user enter a url like:
/product/123
and when the page loads to show in the address bar something like:
/product/my-awsome-product-name-123
I checked several web sites that have this behaviour and it seems that a 301 Moved Permanently response is used in all i checked. Even SO as shown in my question uses 301 to add the title of the question. I thought that there would be a different approach that would not need the second round trip....
So the total solution i used in this case was:
I created a SlugRouteHandler class which looks like:
public class SlugRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var url = requestContext.HttpContext.Request.Path.TrimStart('/');
if (!string.IsNullOrEmpty(url))
{
var slug = (string)requestContext.RouteData.Values["slug"];
int id;
//i care to transform only the urls that have a plain product id. If anything else is in the url i do not mind, it looks ok....
if (Int32.TryParse(slug, out id))
{
//get the product from the db to get the description
var product = dc.Products.Where(x => x.ID == id).FirstOrDefault();
//if the product exists then proceed with the transformation.
//if it does not exist then we could addd proper handling for 404 response here.
if (product != null)
{
//get the description of the product
//SEOFriendly is an extension i have to remove special characters, replace spaces with dashes, turn capital case to lower and a whole bunch of transformations the SEO audit has requested
var description = String.Concat(product.name, "-", id).SEOFriendly();
//transform the url
var newUrl = String.Concat("/product/",description);
return new RedirectHandler(newUrl);
}
}
}
return base.GetHttpHandler(requestContext);
}
}
From the above i need to also create a RedirectHandler class to handle the redirections. This is actually a direct copy from here
public class RedirectHandler : IHttpHandler
{
private string newUrl;
public RedirectHandler(string newUrl)
{
this.newUrl = newUrl;
}
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext httpContext)
{
httpContext.Response.Status = "301 Moved Permanently";
httpContext.Response.StatusCode = 301;
httpContext.Response.AppendHeader("Location", newUrl);
return;
}
}
With this 2 classes i can transform product ids to SEO friendly urls.
In order to use these i need to modify my route to use the SlugRouteHandler class, which leads to :
Call SlugRouteHandler class from the route
routes.MapRoute(
name: "Specific Product",
url: "product/{slug}",
defaults: new { controller = "Product", action = "Index" }
).RouteHandler = new SlugRouteHandler();
Here comes the use of the link #StephenMuecke mentioned in his comment.
We need to find a way to map the new SEO friendly url to our actual controller. My controller accepts an integer id but the url will provide a string.
We need to create an Action filter to handle the new param passed before calling the controller
public class SlugToIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var slug = filterContext.RouteData.Values["slug"] as string;
if (slug != null)
{
//my transformed url will always end in '-1234' so i split the param on '-' and get the last portion of it. That is my id.
//if an id is not supplied, meaning the param is not ending in a number i will just continue and let something else handle the error
int id;
Int32.TryParse(slug.Split('-').Last(), out id);
if (id != 0)
{
//the controller expects an id and here we will provide it
filterContext.ActionParameters["id"] = id;
}
}
base.OnActionExecuting(filterContext);
}
}
Now what happens is that the controller will be able to accept a non numeric id which ends in a number and provide its view without modifying the content of the controller. We will only need to add the filter attribute on the controller as shown in the next step.
I really do not care if the product name is actually the product name. You could try fetching the following urls:
\product\123
\product\product-name-123
\product\another-product-123
\product\john-doe-123
and you would still get the product with id 123, though the urls are different.
Next step is to let the controller know that it has to use a special filer
[SlugToId]
public ActionResult Index(int id)
{
}

MVC3 using routes or using controller logic?

I'm relatively new with MVC3, but I'm using it, C# and EF4 to create an application website. The routing that I'm using is the same as in the default Microsoft project created when I selected MVC3 pattern, nothing special:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new[] { "MySite.Controllers" }
);
}
And everything is working fine there. We're using the default Membership Provider, and users also get an INT value that identifies their account. This lets them see their profile pretty easily with a simple routing like:
www.mysite.com/profile/4
...for example. However, the client has asked that a lot of accounts be pre-generated and distributed to selected users. I've worked up a way to run that through SQL Server and it works fine, all the accounts got created (about a thousand). Additionally, I've add a bit field ('Claimed') that can help identify whether one of these pre-generated accounts has been 'activated' by these users.
My question is, when a user is given a link to come visit their (un-activated) account, should I use a test when doing the initial routing on that page to identify their account as un-claimed and send them somewhere else to finish entering details into their account? Or should I let them go to the same page as everyone else, and have something in the controller logic that identifies this record as un-claimed, and then send them to another page to finish entering details etc.? Is there a good reason for doing one over the other?
And what about people who make up (or have a typographical error) in their Id value, like:
www.mysite.com/profile/40000000000
(and the site only has a thousand users so far), should that be handled similarly, or through different means entirely? (I.e., in one scenario we're identifying an existing account that is not yet claimed, and in another scenario we're having to figure out that the account doesn't even exist.)
Any help would be greatly appreciated.
EDIT:
I'm trying to implement Soliah's suggested solution, and got stuck a bit on the fact that the if (id != 0) didn't like that the id might not be in an INT. I'm past that now, and attempting to figure out a way to do the check if valid portion, but possibly I have not solved the problem with the id not being treated as an INT? Something is definitely not right, even though I'm trying to convert it again during my database test for validity. Any ideas on why I'm getting the error below? What am I missing?
public class ValidProfileIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var id = (Convert.ToInt32(filterContext.ActionParameters["Id"]));
if (id != 0)
{
// Check if valid and behave accordingly here.
Profile profile = db.Profiles.Where(q => q.ProfileId == (Convert.ToInt32(id))).FirstOrDefault();
}
base.OnActionExecuting(filterContext);
}
}
Cannot implicitly convert type 'System.Linq.IQueryable<Mysite.Models.Profile>' to 'Mysite.Models.Profile'. An explicit conversion exists (are you missing a cast?)
EDIT #2:
I'm working on Robert's suggestion, and have made partial progress. My code currently looks like this:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
bool isActivated = // some code to get this state
return isActivated;
}
}
which I got to after reading the blog entry, and (believe it or not) this posting: http://pastebin.com/Ea09Gf4B
I needed to change ActionSelectorAttribute to ActionMethodSelectorAttribute in order to get things moving again.
However, what I don't see how to do is to get the Id value into the bool isActivated test. My database has a view ('Claimed') which can give back a true/false value, depending on the user's profile Id that it is handed, but I don't see where to add the Id. Would something like what Soliah edited work?
if (int.TryParse(filterContext.ActionParameters["Id"], id) && id != 0) {
bool isActivated = db.Claimed.Where(c => c.ProfileId == id).FirstOrDefault();
EDIT #3:
Here is my current state of the code:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
// get profile id first
int id = int.Parse((string)controllerContext.RouteData.Values["id"]);
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
bool isActivated = profile;// some code to get this state
return isActivated;
}
}
For me, I had to change things to int.Parse((string)controllerContext.RouteData.Values to get them to work, which they seem to do (to that point.) I discovered that formatting here: Bind a routevalue to a property of an object that is part of viewmodel
The line
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
errors on the db. section, with error message as follows:
Cannot access a non-static member of outer type 'MySite.Controllers.HomeController' via nested type 'MySite.Controllers.HomeController.UserAccountActivatedAttribute'
...which is something that I have diligently tried to figure out with MSDN and Stack, only to come up empty. Does this ring any bells?
Others have suggested many things already, but let me bring something else to the table here.
Action Method Selector
In order to keep your controller actions clean, you can write an action method selector attribute to create two simple actions:
[ActionName("Index")]
public ActionResult IndexNonActivated(int id)
{
...
}
[ActionName("Index")]
[UserAccountActivated]
public ActionResult IndexActivated(int id)
{
...
}
This way you don't deal with checking code in your actions keeping them really thin. Selector filter will make sure that correct action will get executed related to user account activation state.
You can read more about action selector attributes in my blog post but basically you'd have to write something similar to this:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
// get profile id first
int id = int.Parse(controllerContext.RouteData.Values["id"] ?? -1);
bool isActivated = // some code to get this state
return isActivated;
}
}
And that's basically it.
This will make it possible for users to access their profile regardless whether their account has been activated or not. Or maybe even deactivated at some later time... And it will work seamlessly in the background.
One important advantage
If you'd have two actions with different names (as Juraj suggests), one for active profiles and other for activation, you'd have to do the checking in both, because even active users would be able to access activation action:
profile/4 > for active profiles
profile/activate/4 > for inactive profiles
Both actions should be checking state and redirect to each other in case that state doesn't "fit". This also means that each time a redirection would occur, profile will get checked twice. In each action.
Action method selector will check profiles only once. No matter what state user profile is in.
I'd prefer to keep my controller thin and place this in an action filter that you can annotate on the Index action of the Profile controller.
public class ValidProfileIdAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActinExecutingContext filterContext) {
int id;
if (int.TryParse(filterContext.ActionParameters["Id"], id) && id != 0) {
// Check if valid and behave accordingly here.
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
}
base.OnActionExecuting(filterContext);
}
}
The OnActionExecuting method will be called before your controller's action.
In your controller:
[ValidProfileId]
public ActionResult Index(int id) {
...
}
I would suggest to have that logic in the controller, as once he/she is activated, they may be able to use the same link to access their profile.
Checking whether an account is activated or not is a part of application logic and should be implemented inside the controller (or deeper). Within the controller, you can redirect un-activated users to any other controller/action to finish the activation. URL routing mechanism should route simply according to a shape of an incoming URL and shouldn't contact the database.

Handle action with invalid Id parameter

I am new to ASP.NET MVC and I wonder if the way I handled these cases is the most appropriate.
I have an "ArticleController", which has an action called "Details" (Used the auto-generate edit template).
By default, there is an optional id at the routing table,
and I want to know how to handle the cases when I don't receive any Id or when I receive a wrong id parameter.
In order to fix it I've wrote this (Note the DefaultValue attribute):
public ViewResult Details([DefaultValue(0)]int id)
{
Article article = db.Articles.Find(id);
if (article == null)
{
return View();
}
return View(article);
}
And at the view I've wrote this:
#if (Model == null)
{
<div>Wrong article id was given.</div>
}
else
{
// Handle as a normal case
}
You would have handled these cases differently? If yes, how?
I think the cleanest approach is to set up your routes so that when no ID is present, a user is routed to a different action. That's what the default route does. For example: /Articles/ will invoke ArticleController::Index(), and /Articles/4 will invoke ArticleController::Details(4).
As far as the case goes where an ID is not found, personally, I prefer to return a 404 error:
return new HttpNotFoundResult("This doesn't exist");
You can make your Id nullable like this:
public ViewResult Details(int? id)
If the user provides no id or an incorrect one, the id won't have a value which you can check with id.HasValue. If the id has a value, you can obtain it with id.Value.

ASP.NET MVC: action methods with one param not named ID and non-integer

Consider an ASP.NET MVC 1.0 project using the Areas convention as described on this Nov. 2008 Phil Haack blog post. This solution works great once it's set up!
My trouble is starting thanks to my limited knowledge of ASP.NET MVC's routing rules.
My intention is to create an action method and URL structure like this:
http://mysite/Animals/Dogs/ViewDog/Buster
DogsController.ViewDog() looks like this:
public ActionResult ViewDog(string dogName)
{
if (dogName!= null)
{
var someDog = new DogFormViewModel(dogName); //snip a bunch more
return View(someDog);
}
else { return View("DogNotFound"); }
}
The task at hand is ensuring that the RegisterRoutes() has the correct entries.
UPDATE
Here's the new route being mapped:
routes.MapRoute("ViewDog", "Animals/{controller}/{action}/{dogName}",
new { controller = "Dogs",
action = "ViewDog", dogName = "" });
The link to the URL is created:
<%= Html.RouteLink("Brown Buster", "ViewDog", new RouteValueDictionary(new { controller="Dogs", action="ViewDog", dogName="Buster" }))%>
The URL is created as expected. Thanks to Craig Stuntz and his blog post on Html.RouteLink.
http://mySite/Animals/Dogs/ViewDog/Buster
New Problem: The param dogName doesn't pickup the string value "Buster" from the URL. The call to the method succeeds, but the argument evaluates to null.
Questions
How can you:
make this route work with a string, and remove the default convention int id in the route? I'd like to change the name of the parameter away from int.
Are you sure that ActionLink is actually matching the route you show them the question? When you have more than one route, I strongly recommend using RouteLink instead of ActionLink, as I explain in great detail in this post. When you use RouteLink, there is no possibility that you will match the wrong route, at least in URL generation.
The default parameter "id" doesn't have to be an int. It'll match whatever type you declare in your action method. Why not just do the following?
public ActionResult ViewDog(string id)
{
if (id!= null)
{
var someDog = new DogFormViewModel(id); //snip a bunch more
return View(someDog);
}
else { return View("DogNotFound"); }
}

Resources