In the conventional website a url displayed as:
http://www.mySite.com/Topics
would typically mean a page sits in a subfolder below root named 'Topics' and have a page named default.htm (or similar).
I'm trying to get my head in gear with the MVC way of doing things and understand just enough of routing to know i should be thinking of URLs differently.
So if i have a db-driven page that i'd typically script in a physical page located at /Topics/index.aspx - how does this look in an MVC app?
mny thx
--steve...
It sounds like you are used to breaking down your website in terms of resources(topics, users etc) to structure your site. This is good, because now you can more or less think in terms of controllers rather than folders.
Let's say you have a structure like this in WebForms ASP.NET.
-Topics
-index.aspx
-newtopic.aspx
-topicdetails.aspx
-Users
-index.aspx
-newuser.aspx
-userdetails.aspx
The structure in an MVC app will be pretty much the same from a users point of view, but instead of mapping a url to a folder, you map a url to a controller. Instead of the folder(resource) having files inside it, it has actions.
-TopicController
-index
-new
-details
-UserController
-index
-new
-details
Each one of these Actions will then decide what view (be this html, or json/xml) needs to be returned to the browser.
Actions can act differently depending on what HTTP verb they're repsonding to. For example;
public class UserController : Controller
{
public ActionResult Create()
{
return View(new User());
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(User user)
{
// code to validate /save user
if (notValid)
return new View(user);
else
return new View("UserCreatedConfirmation");
}
}
This is sort of a boiled down version of RESTful URLs, which I recommend you take a look at. They can help simplify the design of your application.
It looks just like you want it to be.
Routing enables URL to be quite virtual. In asp.net mvc it will end at specified controller action method which will decide what to do further (i.e. - it can return specified view wherever it's physical location is, it can return plain text, it can return something serialized in JSON/XML).
Here are some external links:
URL routing introduction by ScottGu
ASP.NET MVC tutorials by Stephan Walther
You would have an default view that is associated with an action on the Topics controller.
For example, a list page (list.aspx) with the other views that is tied to the list action of the Topics controller.
That is assuming the default routing engine rules, which you can change.
Read more here:
http://weblogs.asp.net/scottgu/archive/2007/12/03/asp-net-mvc-framework-part-2-url-routing.aspx
IMHO this is what you need for your routes.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Topics", action = "Index", id = "" } // Parameter defaults
);
You would need a TopicsController that you build the View (topics) on.
Related
I've built a number of ASP.Net MVC sites and in each of them there are a number of pages suited to MVC's Controllers and Actions, and a number of pages that are really just brochure pages - /why, /why/ouradvantage, /about, etc - pages that have no real functionality, just a View, maybe a Layout, and that's it.
For these brochure-style pages I'd really prefer to have just the View and a good Route to find it, so I could put the /why at /Brochure/Why.cshtml or /Brochure/Why/Index.cshtml and either way it will be picked up just fine. I'd like to avoid making silly Controllers and Actions (as I've done in the past) to handle this set of URLs and pages.
How can I go about this in an ASP.Net MVC project? This must be a common need.
EDIT: An example of how I can do this the verbose way:
I could use the standard MVC route ({controller}/{action}/{id}) and spam a bunch of useless Controllers to get the set of URLs and pages I want. Every time I want to add a brochure-style (no functionality, just a View) page I'd add a Controller or Action like this:
Why Controller:
public class WhyController : Controller
{
public ViewResult Index()
{
return View();
}
public ViewResult OurAdvantage()
{
return View();
}
}
This puts a View at /why and /why/ouradvantage - clean URLs. If I wanted an /about page, I could add another Controller that does nothing but return a View named AboutController. If it had 5 subpages I could add 5 Actions to that Controller, all of those Actions doing nothing but return a View.
If these brochure-style pages in the MVC site amounted to say, 100 pages, I'd have quite a few needless Controllers and Actions all doing nothing really. Not very DRY. I'm interested in a way to just put Views in a folder in my Project and have them accessible just for being there (Configuration through Convention), at clean URLs like /why and /why/ouradvantage.
There are a few ways that get me close:
I could put a bunch of .cshtml pages in and visit them directly - but then I have to have the file extension in the URL and the View files themselves have to sit in the root.
I could use ASP.Net Areas to define an Area for these, but then all brochure-style pages have to sit at least one URL segment deep and I still have the above problem of file extensions in URLs.
There are crazy Routes I can define.
I suspect this comes up often in MVC projects that have a small or large number of these Brochure-style pages - it seems like there should be a clean way to do this.
EDIT: A crappy solution that spams the routing engine.
Create a class that maps routes like:
public static void MapRoutes(RouteCollection routes, string appRoot, string path)
{
if (!path.Contains("~/"))
throw new NotSupportedException("Pages path must be virtual (use ~/ syntax).");
var physicalPath = appRoot + path.Substring(2).Replace("/", "\\");
var dir = new DirectoryInfo(physicalPath);
var pages = dir.GetFiles("*.cshtml", SearchOption.AllDirectories);
int rootLength = appRoot.Length;
var rootParsed = pages.Select(p => "~/" + p.FullName.Substring(rootLength).Replace("\\", "/"));
int folderPathLength = path.Length + 1;
var mapped = rootParsed.Select(p => new {
Url = p.Substring(folderPathLength, p.Length - 7 - folderPathLength),
File = p
});
var routedPages = mapped.Select(m => routes.MapRoute(
name: m.Url,
url: "{*url}",
defaults: new { path = m.File, Controller = "Brochure", Action = "Catchall" },
constraints: new { url = m.Url }
)).ToArray();
}
You can call this in RouteConfig like:
BrochureRoute.MapRoutes(routes, Server.MapPath("~/"), "~/Brochure");
That obviously maps all these pages to a BrochureController, which you'll need as well:
public class BrochureController : Controller
{
public ViewResult Catchall(string path)
{
return View(path);
}
}
2 problems:
It spams the routing engine as I mentioned - if you have 100 pages you have 100 routes.
Passing the path as above seems to upset the normal Razor pipeline - visiting a page in this manner gets you an error like:
The view at '~/Brochure/About.cshtml' must derive from WebViewPage
To map individual pages RouteCollection.MapPageRoute method can be used.
routes.MapPageRoute("", "why", "~/Brochure/Why.aspx");
Sure, you could use Razor view engine (.cshtml) instead of WebForms.
Check MSDN documentation for some more details.
Update 2
You are right you won't be able to use this for for .cshtml pages. However, you don't need to use routing to access Web Pages (.cshtml files). It is enough to create the files, and omit the extension in URLs. To achieve your desired structure you could do this:
Your web project must allow Web Pages rendering. To enable this go to web.config and set webpages.Enabled to true. (<add key="webpages:Enabled" value="true" />)
Create folder why in your web application root
Add MVC ViewPage named Index.cshtml. This will be accessible from http://yoursite.com/why
Add MVC ViewPage named ouradvantages.cshtml. This will be accessible from http://yoursite.com/why/ouradvantages
You will also be able to access url data using #UrlData collection
Check more about this here. More about Web Pages in general is also available on asp.net website.
If for some reason you really need to use routing, you'll need custom RouteHandler. You can find one implemented on Nuget. Usage examples of this package are here.
Update
If you'd like to avoid manually adding each route you have few choices.
1) Create a convention to identify brochure pages
You could do this by expecting URLs to be brochure pages by default, and isolating "non-brochure" pages to specific sections:
routes.MapPageRoute("Default", "{brochurepage}", "~/Brochure/{brochurepage}.aspx");
// isolate non-brochure pages to "site" section
routes.MapRoute(
"",
"site/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
2) Hold brochure page names in a collection
List<string> brochurePages = new List<string>()
{ "about", "why", "contact" }; // add more pages here
....
foreach (var brochurePage in brochurePages)
routes.MapPageRoute("", brochurePage, "~/Brochure/" + brochurePage + ".aspx");
I have to be very specific about two URLs you mentioned in your question. To implement routing for "/why" and "/why/ouradvantage" you will NOT be able to write elegant code. This will take special handling.
If your issue is that you don't want to have separate controllers for all these "silly" pages, may I suggest that you create a logical way to route them to a singular controller and then render the correct data/ view based on the route?
For instance, your URLs could read "/Pages/Why/About" and you could route that to your pages controller and it would receive the "Why" and "About" as parameters and you could render your content accordingly. Does that make sense?
There were questions about multilingual apps in MVC here on SO but they were mostly answered by giving details about implementing Resource files and then referencing those Resource strings in Views or Controller. This works fine for me in conjunction with the detection of user's locale.
What I want to do now is support localized routes. For instance, we have some core pages for each website like the Contact Us page.
What I want to achieve is this:
1) routes like this
/en/Contact-us (English route)
/sl/Kontakt (Slovenian route)
2) these two routes both have to go to the same controller and action and these will not be localized (they will be in English because they are hidden away from the user since they are part of the site's core implementation):
My thought is to make the Controller "Feedback" and Action "FeedbackForm"
3) FeedbackForm would be a View or View User control (and it would use references to strings in RESX files, as I said before, I already have set this up and it works)
4) I have a SetCulture attribute attached to BaseController which is the parent of all of my controllers and this attribute actually inherits FilterAttribute and implements IActionFilter - but what does it do? Well, it detects browser culture and sets that culture in a Session and in a cookie (forever) - this functionality is working fine too. It already effects the Views and View User Controls but at this time it does not effect routes.
5) at the top of the page I will give the user a chance to choose his language (sl|en). This decision must override 4). If a user arrives at our website and is detected as Slovenian and they decide to switch to English, this decision must become permanent. From this time on SetCulture attribute must somehow loose its effect.
6) After the switch, routes should immediately update - if the user was located at /sl/Kontakt
he should immediately be redirected to /en/Contact-us.
These are the constraints of the design I would like. Simply put, I do not want English routes while displaying localized content or vice-versa.
Suggestions are welcome.
EDIT:
There's some information and guidance here - Multi-lingual websites with ASP.NET MVC, but I would still like to hear more thoughts or practices on this problem.
Translating routes (ASP.NET MVC and Webforms)
How about this?
Create custom translate route class.
Localization with ASP.NET MVC using Routing
Preview:
For my site the URL schema should look
like this in general:
/{culture}/{site}
Imagine there is a page called FAQ,
which is available in different
languages. Here are some sample URLs
for these pages:
/en-US/FAQ /de-DE/FAQ /de-CH/FAQ
Why not create the action names desired and simply RedirectToAction for the single, real implementation?
public ActionResult Kontakt() {
return RedirectToAction("Contact");
}
public ActionResult Contact() {
return View();
}
I just used a simple solution with "Globalization Resources", like this:
routes.MapRoute(
"nameroute", // Route name
App_GlobalResources.Geral.Route_nameroute+"/{Obj}", // URL with parameters
new { controller = "Index", action = "Details", Obj = UrlParameter.Optional } // Parameter defaults
);
But, you could customize as needed.
I am still learning ASP.NET MVC. With webforms, I would create a new folder let's call it admin. In there I might have many pages for create_product, edit_product, etc. So the URL might look like http://somesite.com/admin/create_product.aspx.
But with MVC it is a little different. I am trying to see what would be the best way to do this.
Would doing http://somesite.com/admin/product/create be right? Or should it just be http://somesite.com/product/create? If I do it as the first way, do I put everything in the "admin" controller or should it be separated into a "product" controller?
I know this is probably subjective or personal choice, but I would like to get some advise.
Thanks.
Part of the benefit of ASP.NET MVC (and more generally, the URL Routing Engine common to all of ASP.NET in .NET 3.5 SP1) is that the URLs can be flexibly configured to map to any folder / file structure you prefer. That means it's much easier than it was in the days of WebForms to modify your URLs after you've started building your project.
To your specific questions:
One Admin Controller vs. Product Controller - In general, the guidance is to keep controllers focused so that they are easier to test and maintain. For that reason, I would suggest using a single controller per object type (like Product) with your CRUD actions. Examples in your case:
/admin/product/create
/admin/product/edit/34 or /admin/product/edit/red-shoes (if name is unique)
In either case, the Create, Edit, Deatils actions will all be in the ProductController. You may just have custom routes for the "admin actions" (like Create and Edit) that limit their usage (and add the "admin" text to the URL), and then the Details action would be usable by all visitors to your site.
Securing Admin Views - One important fact to remember with MVC: all requests go directly to controllers, not views. That means the old "secure a directory with web.config" does not apply (usually) to MVC for securing your Admin. Instead, you should now apply security directly to the controllers. This can easily be achieved by using attributes to Controller classes like:
[Authorize] - Just checks that the user is logged-in
[Authorize(Roles = "Admin")] - Limit to specific user roles
[Authorize(Users = "Joe")] - Limit to specific users
You can even create a custom route for "Admin" views in your site and limit access to those views by enforcing your authorization check in the URL routing, like this:
routes.MapRoute(
"Admin",
"Admin/{controller}/{action}",
new { controller = "Product", action = "Index" },
new { authenticated= new AuthenticatedConstraint()}
);
Where AuthenticatedConstraint looks something like:
using System.Web;
using System.Web.Routing;
public class AuthenticatedConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return httpContext.Request.IsAuthenticated;
}
}
Good details on Stephen Walther's blog:
ASP.NET MVC Tip #30 – Create Custom Route Constraints
For admin stuff, just mark with [Authorize] attribute. To ensure only admins can use it, do something like [Authorize(Roles = "Admin")]. Check out this question
Also, /product/create is most common, I think :)
I3Dx definitely has the right guidance for the Authorize attribute, this is essential for keeping controller secure, you can apply to a controller or individual actions.
As far as the URL depth, I would not worry about the depth, I would be more concerned that the route made logical sense for example:
domain.com/admin/products/edit/1
domain.com/admin/groups/edit/1
domain.com/products/view/1
domain.com/groups/view/1
This way you know what is happening with each route. it is obvious that one is an admin and one is an end user.
The easyest way to check is to get someone to read your URL and ask them what they would expect to see.
Hope this helps.
OH and one last thing, for client side routes we often use "slugs" rather than ids so that it is more readable. So when someone creates a product we slugify the name so it can be used in the route such as:
domain.com/products/view/big-red-bucket
rather than
domain.com/products/view/1
I want to implemented URL like :
www.domain.com/Business/Manufacturing/Category/Product1
This will give the details of specific product(such as specifications).
Since the list of Categories & Products in each Category are limited, I thought it is not worth to use database for products.
So I want to implement it using the HTML or ASPX files. Also I want the URL to be without the files extention(i.e. URL's should not have .html or .aspx extension)
How can I implement it with ASP.NET MVC? Should I use nested folder structure with HTML/ASPX files in it, so that it corresponds to URL? Then how to avoid extensions in URL?
I am confused with these thoughts
Asp.net Mvc uses the routing library from Microsoft. So it is very easy to get this kind of structure without thinking about the folder structure or the file extensions. With asp.new mvc you do not point a request at a specific file. Instead you point at a action that handles the request and use the parameters to determine what to render and send to the client. To implement your example you can do something like this:
_routes.MapRoute(
"Product",
"Business/Manufacturing/Category/Product{id}",
new {controller = "Product", action = "Details", id = ""}
);
This route will match the url you described and execute the action named "Details" on a controller named "ProductController" (if you are using the default settings). That action can look something like this:
public ActionResult Details(int id) {
return View(string.Format("Product{0}", id);
}
This action will then render views depending on what id the product have (the number after "Product" in the end of your example url). This view should be located in the Views/Product folder if you use the default settings. Then if you add a view named "Product1.aspx" in that folder, that is the view that will be rendered when you visit the url in your example.
All tough it is very possible to do it that way I would strongly recommend against it. You will have to do a lot of duplicated work even if you only have a few products and use partial views in a good way to minimize the ui duplications. I would recommend you use a database or some other kind of storage for you products and use a single view as template to render the product. You can use the same route for that. You just edit your action a little bit. It can look something like this:
public ActionResult Details(int id) {
var product = //get product by id from database or something else
return View(product);
}
This way you can strongly type your view to a product object and you will not have that much duplication.
The routing engine is very flexible, and when you have played around with it and learned how it works you will be able to change your url in just about any way you want without changing any other code or moving any files.
If you're not ready to dive into ASP.Net MVC, you can still get the nice URLs by using URL Rewriting for ASP.Net. That'd be simpler if you're already familiar with ASP.Net WebForms. The MSDN article on URL Rewriting should be a good start:
http://msdn.microsoft.com/en-us/library/ms972974.aspx
I'd be really sure you won't eventually have more products before deciding not to use a database. Both MVC and WebForms would allow you to make one page that dyamically shows a product and still have a nice URL- plus you might save yourself development time down the road. Something to think about.
I want to build a ASP.NET MVC site so that the controller for a specific url is stored in the database instead of the URL.
The reason for that is that i'm building a CMS system and the users should be able to change the template (controller) without changing the URL. I also think that the name of the controller is not relevant for the end users and i want clean URL:s.
I realise that i could just add all routes at application start, but for a system with like 100 000 pages it feels like a bad idea.
Is it possible to store the url:s in the database and make a lookup for each request and then map that request to a specific controller?
Basically you'll have to implement your own IRouteHandler.
Part of the answer and some example code is in Option 3 of this question's answer:
ASP.NET MVC custom routing for search
More information:
http://weblogs.asp.net/fredriknormen/archive/2007/11/18/asp-net-mvc-framework-create-your-own-iroutehandler.aspx
Why couldn't you just do something like this:
-- Global.asax.cs --
routes.MapRoute(null, // Route name
"content/{id}", // URL with parameters
new { Controller = "Content", Action = "Show", Id = (string) null }); // Parameter defaults
-- /Controllers/ContentController.cs --
public class ContentController : Controller
{
public ActionResult Show(string id)
{
// Lookup the 'content' (article, page, blog post, etc) in the repository (database, xml file, etc)
ContentRepository repository = new ContentRepository();
Content content = repository.FindContent(id);
return View(content);
}
}
Such that a request to your site www.yoursite.com/content/welcome-to-my-first-blog-post would call ContentController.Show("welcome-to-my-first-blog-post").
I suppose ASP.NET can do many of the same things as PHP. If so there is a simple approach.
With rewrite rules you can easily send any traffic to any URL of the 100K to the same place. On that destination you could simply use the server variables containing the URL requested by the client and extract the location. Look it up in the DB and send the corresponding data for that URL back to the client on-the-fly.
"for a system with like 100,000 pages it feels like a bad idea."
It is a bad idea if you are creating a routing system that cannot be reused. The basic {controller}/{action}/{id} schema points you in the direction of reuse. This schema can be extended/revamped/recreated according to your needs.
Instead of thinking about how many pages you have think about how your resources can be grouped.
Instead of creating a heavy routing system why not create an anchor link control (ascx) which allows user to only add valid internal links. Keep a table in the db of your templates and their controllers to populate the control with it.