The concept of routes is nothing new, and it works great for the concept of {area}/{controller}/{action}/{parameter}, but few sites are standalone UI interaction.
Websites often need parts of themselves that aren't really dedicated to taking data, but presenting it. For instance one of the sites I am working on has a large part of itself dedicated to user interaction (which the MVC system solves expertly. A Membership area, a place to manage information, a way to purchase items, etc.) - but it also needs a part that functions more like an old-fashioned website, where you're simply looking at pages like a folder structure.
one solution I have found is to try a custom view engine. This worked, but I quick found myself lost in a convoluted routing scheme. Another I guess I could go with is to just have an IgnoreRoute and put files in the ignored folder like normal html/aspx, but I'd really rather have the option of using Controllers so that there is a chance I can have data returned from a database, etc in the future.
So let me show you my current scenario...
Areas
Membership
Rules
Controllers
HomeController
FileView(string folder, string file)
Views
Home
General
Customize
Content
yyy.cshtml
xxx.cshtml
#Html.Partial("Content/yyy.cshtml")
xxx.cshtml
xxx.cshtml
etc. The Rules area is basically setup to function like a normal /folder/file/ structure. So here is my Controller for it..
public class HomeController : Controller
{
//
// GET: /Information/Home/
public ActionResult Index()
{
return View();
}
// **************************************
// URL: /Rules/{controller}/{folder}/{file}
// **************************************
public ViewResult FileView(string folder, string filename)
{
return View(String.Format("{0}/{1}", folder, filename));
}
}
Now, if I have a category, I simply have a lightweight controller that inherits from that Area's HomeController, like this...
public class GeneralController : Rules.Controllers.HomeController
{
// **************************************
// URL: /Rules/General/Customize/{id}
// **************************************
public ViewResult Customize(string id)
{
return FileView("Customize", id);
}
}
So then, for each folder in the 'sub' controller, I have a single Route that takes in the name of the file.
This works, but I feel it's excessively clunky. Can anyone suggest a better alternative? There are just too many pages, and too much nesting, to have a full ActionResult for each one. I also want to maintain clean urls.
Perhaps you can use a catch-all route for the Membership area, route it to a controller (MembershipController?) and have that controller just render the view that is catched by the route, like this:
public class MembershipController : Controller
{
public ActionResult Index(string pageTitle)
{
return View(pageTitle);
}
}
And the route:
routes.MapRoute(
"Membership",
"Membership/{*pageTitle}",
new {controller = "Membership", action = "Index", pageTitle = "NotFound"});
Of course, in the controller you should check whether the view exists or not, but this one should get you moving. Although I don't see why you want to have MVC in front of this when you just want to display (static?) content.
Related
I love the areas feature in ASP .NET Core.
For example, the admin section resides inside ~/Areas/Admin folder which has it's own Views, Controllers and Models inside it. An example route for the admin area is /domain.com/admin/users/manage
Say I have another area called Customers which can contain all customer related stuff. The customer area will have function related to CRUD operations only. So I need the route for customer area be like domain.com/customer/create. But as far as I know the only way to achieve this is through placing the views inside the common Views folder.
Is there any way to organize the customer related stuff in the Areas folder and access it through the url I mentioned above?
I find the easiest way is using attribute route, you have to enable it first inside RouteConfig.cs for ASP.NET MVC. In ASP.NET core, i am not sure you have to enable it or not, i belive it is inside Startup.cs?
public static void RegisterRoutes(RouteCollection routes)
{
//routes.AppendTrailingSlash = false;
//routes.LowercaseUrls = true;
routes.MapMvcAttributeRoutes();
....
}
Now in your customer area controller
[RouteArea("Customer", AreaPrefix = "")]
public class UsersController : Controller
{
//Customer/Create
[Route("Customer/Create")]
public ActionResult Create()
{
}
}
RouteArea is the Area name, setting the prefix to null so it won't show up in the url as Customer/Customer/Create, Route is the Controller Action Name, in this case the result will be Customer/Create
I have a Document type, Template, and a page in the CMS content tree which uses these for a Contact page. The document type has no CMS data properties, because it doesn't need any. I use Models Builder for other pages with no issue, but for this page I've created my own custom model within my MVC project.
I've read every tutorial I can find, and looked at every forum post and issue on the Umbraco forums and Stackoverflow, and for the life of me I can't figure out what I'm doing wrong. The model name and namespace do not conflict with the autogenerated Models builder one.
My understanding is for posting forms a SurfaceController is the way to go - a RenderController is intended more for presenting stuff. So my controller extends SurfaceController. uses Umbraco.BeginUmbracoForm(etc)
I've tried every combination of SurfaceController and RenderController with UmbracoTemplatePage, UmbracoViewPage and every way of changing my model to extend both RenderModel and IPublishedContent to test each. When trying RenderController I've overridden default Index method with RenderModel parameter to create an instance of my model with the renderModel parameter.
Usually the error I get is "Cannot bind source type Umbraco.Web.Models.RenderModel to model type xxx". Sometimes combinations I've attempted allow the Get to succeed, then give this error on Post.
I've even tried to remove the page from the CMS and use a standard MVC controller and route - this allows me to display the page, and even using a standard Html.BeginForm on my view, I get an error when trying to post the form (despite a breakpoint in the code in controller being hit) which also states it "Cannot bind source type Umbraco.Web.Models.RenderModel to model type xxx"
This CANNOT be this difficult. I'm ready to throw laptop out window at this stage.
What am I doing wrong???? Or without seeing my code at least can anyone tell me how this is supposed to be done? How do you post a custom model form to an Umbraco 7.5 controller, with no CMS published content properties required?
As it stands, my View looks like this:
#inherits UmbracoViewPage<Models.Contact>
...
using (Html.BeginUmbracoForm<ContactController>("Contact", FormMethod.Post
My Controller looks like this:
public class ContactController : SurfaceController
{
public ActionResult Contact()
{
return View("~/Views/Contact.cshtml");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Contact(Contact contactForm)
{
...
}
And my model looks like this:
public class Contact : RenderModel
{
public Contact() : base(UmbracoContext.Current.PublishedContentRequest.PublishedContent, UmbracoContext.Current.PublishedContentRequest.Culture)
{
}
public Contact(IPublishedContent content) : base(content, CultureInfo.CurrentUICulture)
{
}
[Display(Name = "First Name", Prompt = "First Name")]
public string FirstName { get; set; }
...
Update: If I use the model for my CMS page created automatically by models builder, the Get and Post work ok. However when I customise the model (i.e. I put a partial class of the same name in ~/App_Data/Models and regenerate models on Developer tab), the custom properties in my posted model are always null.
I can populate these manually from the request form variables, however this seems wrong and messy. What's going on here?
public class ContactPageController : SurfaceController
{
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Contact(ContactPage contactForm)
{
try
{
contactForm.FirstName = Request.Form["FirstName"];
contactForm.LastName = Request.Form["LastName"];
contactForm.EmailAddress = Request.Form["EmailAddress"];
contactForm.Telephone = Request.Form["Telephone"];
contactForm.Message = Request.Form["Message"];
var captchaIsValid = ReCaptcha.Validate(ConfigurationManager.AppSettings["ReCaptcha:SecretKey"]);
if (ModelState.IsValid && captchaIsValid)
{
// Do what you need
TempData["EmailSent"] = true;
return RedirectToCurrentUmbracoPage();
}
if (!captchaIsValid)
{
ModelState.AddModelError("ReCaptchaError", "Captcha validation failed - Please try again.");
}
return RedirectToCurrentUmbracoPage();
}
catch (Exception ex)
{
LogHelper.Error(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType, null, ex);
return new HttpStatusCodeResult(HttpStatusCode.InternalServerError);
}
}
Further Info: Robert's approach first time, thanks for that. I had tried one approach using PartialViews and ChildActions but clearly I didn't do it correctly. Would I be right in saying the reason this approach is required at all (i.e. why you can't add the custom properties to the model the main view is bound to) is because I am using Models Builder?
So among the strange errors I received was one about 2 classes both wanting to represent the contact page, and another about it expecting one type of class in a dictionary (ContactPage) but receiving another (Contact) - even though I had made no reference to ContactPage in either view or controller. This suggests to me ModelsBuilder adds a mapping for document types to models on app startup behind the scenes? Which is maybe why you're better to take this approach of letting ModelsBuilder do its thing, and build your own model on top of that with a partial view in this way?
I've found the quality of documentation on this topic very poor indeed. Not sure if maybe the good stuff is behind closed doors, i.e. requires a paid Umbraco membership? For a supposedly open source system, that feels kinda shady to me.
Easy when you know how!!
For your situation, SurfaceController is the most likely candidate as you've surmised, however think of the actions in that Controller as applying to partial views, not the full view used by the page.
Don't try to use the ModelsBuilder ContactPage model, but rather create your own as you were originally (the original Contact model) - think of it as a Data Transfer Object perhaps if you do need to apply any of the properties back to the ContactPage model for any reason.
I've found I've had the greatest success with SurfaceController with the following conditions:
The Page Template does not inherit from the model intended for the Form; it inherits directly from the standard Umbraco PublishedContentModel or a ModelsBuilder generated model.
Implement a Partial View for the action defined in your SurfaceController - this view inherits from Umbraco.Web.Mvc.UmbracoViewPage<Contact> in your example.
The Partial View utilises BeginUmbracoForm<ContactController>, specifying POST action as one of the parameters (depending on which signature you're using)
You shouldn't need to populate any model properties using Request.Form like this.
For example, the code in one of my projects looks something like this:
SurfaceController
public class FormsController : SurfaceController
{
[ChildActionOnly]
public ActionResult ContactUs()
{
return PartialView(new ContactForm ());
}
[HttpPost]
public async Task<ActionResult> HandleContactUs(ContactForm model)
{
if (ModelState.IsValid)
{
if (!await model.SendMail(Umbraco)) // Do something with the model.
{
}
}
return RedirectToCurrentUmbracoPage(); // Send us back to the page our partial view is on
}
}
Partial View:
#inherits Umbraco.Web.Mvc.UmbracoViewPage<ContactForm>
#using Digitalsmith.ReCaptcha
#using (Html.BeginUmbracoForm<FormsController>("HandleContactUs"))
{
...
}
Contact Page Template:
#inherits Umbraco.Web.Mvc.UmbracoTemplatePage<ContactPage>
#{
Layout = "_Layout.cshtml";
}
#Html.Action("ContactUs", "Forms")
I have been running an Umbraco v7 site for about 3 years. Traditionally I have not rendered any pages using controllers, I have however set up some controllers that I POST forms to but that is about it. My existing View pages have logic baked into them and use external helper methods instead of controllers.
I am about to develop a new page and thought this would be my time to test a better design pattern. This page will render data from a controller as well as required form submission via a controller. What is the best route to proceed that will also go smoothly if I decide to do a refactor on my existing View pages?
I am more specifically looking for an answer around Render vs Surface controllers and which one would be better.It is my understanding that my routing would be unchanged if I went with a Render controller, but if I went with surface, I would have to have special routing?
But if I used a render controller, this does not support form submission?
Not sure what else I am missing?
Thanks Again,
Devin
You don't have to configure any special routing - everything is baked right in to Umbraco.
As a rule of thumb Surface Controllers are best used for reusable actions, custom controllers (Route Hijacking) are better for adding custom logic to whole pages (Document Types/Templates) in Umbraco.
Both approaches will enable you to accomplish exactly the same results - the only difference between them is abstraction.
Surface controllers are MVC Child Actions that inherit from Umbraco.Web.Mvc.SurfaceController - this adds helpful Umbraco specific properties and methods.
Surface Controllers are good for creating reusable things like forms or anywhere you need a partial to do anything complicated (i.e. backed by a controller). Have a look at the documentation here.
When you use a custom controller to change how pages are rendered that's called Route Hijacking
To do this you create your own controller than inherits from Umbraco.Web.Mvc.RenderMvcController like this:
public class HomeController : Umbraco.Web.Mvc.RenderMvcController
{
public ActionResult MobileHomePage(RenderModel model)
{
//Do some stuff here, the return the base Index method
return base.Index(model);
}
}
This is a custom controller for the "Home" Document Type. You can of course return a custom model that inherits from RenderModel with your own properties and methods.
Fuller examples and documentation can be found here.
Post Requests
Both options allow you to handle POST requests by adding the [httppost] attribute like so:
Surface Controller:
public class YourSurfaceController: SurfaceController
{
public ActionResult YourAction()
{
// Do stuff
}
[HttpPost]
public ActionResult YourAction()
{
// Do stuff on POST
}
}
Controller for route Hijacking:
public class HomeController : Umbraco.Web.Mvc.RenderMvcController
{
public ActionResult MobileHomePage(RenderModel model)
{
//Do some stuff here, the return the base Index method
return base.Index(model);
}
[HttpPost]
public ActionResult MobileHomePage(RenderModel model)
{
//Do some stuff on POST, the return the base Index method
return base.Index(model);
}
}
I’m working out the concepts for a new project where I need to support for multilingual URL’s. Ideally all URL’s need to be in the native language of the user. So we don’t want to use domain.com/en/contact and domain.com/es/contact but we like domain.com/contact and domain.com/contactar (contactar is Spanish for contact). Internally both should be routed to the same ContactController class.
This could be handled by adding multiple static routes to Global.asax.cs for each language but we’d like to make this very dynamic and would like the user of the system to be able to change the translation of the URL’s through the content management system. So we need some kind of dynamic mapping from URL’s to controllers and actions.
By looking at the source code of MVC3 I figured out that the ProcessRequestInit method of MvcHandler is responsible for determining which controller to create. It simply looks in the RouteData to get the name of the controller. One way to override the default MVC routing would be to create a simple default route that uses a custom RouteHandler. This RouteHandler forces MVC to use my own custom subclassed version of MvcHandler that overrides the ProcessRequestInit method. This overridden method insert my own dynamically found controller and action into the RouteData before calling back to the original ProcessRequestInit.
I’ve tried this:
Global.asax.cs
routes.Add(
new Route("{*url}", new MultilingualRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Default", action = "Default" })
}
);
MultilingualRouteHandler.cs
public class MultilingualRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MultilingualMVCHandler(requestContext);
}
}
MultilingualMvcHandler.cs
public class MultilingualMVCHandler : MvcHandler
{
public MultilingualMVCHandler(RequestContext context) : base(context)
{
}
protected override void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
{
if (RequestContext.RouteData.Values.ContainsKey("controller"))
{
RequestContext.RouteData.Values.Remove("controller");
}
if (RequestContext.RouteData.Values.ContainsKey("action"))
{
RequestContext.RouteData.Values.Remove("action");
}
RequestContext.RouteData.Values.Add("controller", "Product");
RequestContext.RouteData.Values.Add("action", "Index");
base.ProcessRequestInit(httpContext, out controller, out factory);
}
}
In this handler I hardcoded the controller and action for testing purposes to some fixed values but it’s not difficult to make this dynamic. It works but the only problem is that I had to modify the source code of ASP.NET MVC3 to get it working. The problem is that the ProcessRequestInit method of MvcHandler is private and thus cannot be overridden. I’ve modified the source code and changed it to protected virtual which allows me to override it.
This is all great but possibly not the best solution. It’s cumbersome that I would always need to distribute my own version of System.Web.Mvc.dll. It would be much better that it would work with the RTM version.
Am I missing any other possibilities of hooking into ASP.NET MVC that would allow me to dynamically determine the controller and action to launch, depending on the URL? One other way I thought of is to build the RouteCollection dynamically on *Application_Start* but I think that will make it more difficult to change it on the fly.
I would appreciate any tips of hooks that I’ve not yet found.
This is fairly old now, nut just in case anyone else is looking for something similar...
Unless I'm completely misunderstanding what you want to do, it's pretty simple really.
Step 1: Add a new route to global.ascx.cs containing a reference to your personal routing engine
routes.Add(new MyProject.Routing.ContentRoutingEngine());
Make sure that it is in the right place in the list of routes so that other routing engines can catch stuff before it if required, or continue the route search if your engine doesn't handle a particular route. I put it after the ignores, but before the MVC default routes.
Step 2: Create the Content Routing Engine, making sure that it inherites from System.Web.Routing.RouteBase abstract class, and overrides the GetRouteData and GetVirtualPath methods as required e.g.
public class ContentRoutingEngine : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeHandler = new MvcRouteHandler();
var currentRoute = new Route("{controller}/{action}", routeHandler);
var routeData = new RouteData(currentRoute, routeHandler);
// set your values dynamically here
routeData.Values["controller"] = "Home" ;
// or
routeData.Values.Add("action", "Index");
// return the route, or null to have it passed to the next routing engine in the list
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//implement this to return url's for routes, or null to just pass it on
return null;
}
}
and that should do it. You can change routes as dynamically as you wish within your engine, and no changes to MVC source required. Let the standard MVC RouteHandler actually invoke the controller.
Postscript: Obviously the code above is not production standard - it's written to make it as obvious as possible what's going on.
If you are allowing modification of urls through your CMS, then you will have to keep all old versions of the urls so that you can 301 redirect to the new ones.
The best bet for this will be to put the url tokens eg "contactar" in the db along with its corresponding controller.
query that, and create your routes out of that.
create a route that will handle the 301s
I think that most elegant solution would be using some action filter combined with custom ActionInvoker. That way, you could invoke an action that has specific filters applied. Something like ActionName attribute, only capable to accept multiple values (names).
Edit: Take a look at ActionMethodSelectorAttribute, meybe you don't need a custom ActionInvoker after all.
If I want the default url of my web app to display completely different UIs depending on the user, what is the best way to accomplish this? I don't really want to use the same controller for every type of user. To put it another way, if a user is logged in and goes to http://mysweetapp.com and is an admin user, they should get what they would see the same thing as if they had gone to http://mysweetapp.com/admin. If the user is logged in as a normal user, they should see the same thing as if they had gone to http://mysweetapp.com/normaluser
Should I just make a "redirect" controller as my default and have it send the client to the appropriate controller?
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Redirect", action = "Index", id = "0" });
I've also tried creating my own ControllerFactory, but I don't think I was clear on the concept and couldn't get it to work.
Thanks
The cleanest way in my opinion would be to create a custom route handler to be used by your default route. Then you can separate out which controller to be used if the controller name is your default controller name, in the example below, it is: Home. Then check if the user is an administrator or not and process the request with the controller you would like to use.
Here is the code:
public class CustomHttpHandler : IHttpHandler
{
public RequestContext RequestContext { get; private set; }
public CustomHttpHandler(RequestContext requestContext)
{
try
{
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
if (controllerName.Equals("home", StringComparison.CurrentCultureIgnoreCase))
{
bool isAdmin = RequestContext.HttpContext.User.IsInRole("Admin");
controllerName = isAdmin ? "admin" : "normaluser";
}
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(RequestContext, controllerName);
if (controller != null)
{
controller.Execute(RequestContext);
}
}
finally
{
factory.ReleaseController(controller);
}
}
}
public class CustomRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new CustomHttpHandler(requestContext);
}
}
// Now use the CustomRouteHandler when you map your default route.
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
).RouteHandler = new CustomRouteHandler();
Hope this helps.
For simplicity, in your HomeController Index method (or whatever default controller you are using) you could put some code like this and then the links from the AdminIndex view or the Index view can send the users to appropriate areas when they start navigating round your site - that way you have one shared controller and the other controllers can be specific to the user type.
return user.IsAdministrator ? View("AdminIndex") : View("Index");
the user.IsAdministrator call is pseudocode of course - replace this with whatever method you are using to work out if the user is an admin user
If you don't want to use the same controller set up individual controllers and views for each item first - mysweetapp.com/admin and mysweetapp.com/normaluser.
You can then redirect specific users to this page through a default controller based on their logged in role.
if (User.IsInRole("Admin")
{
return RedirectToAction("Index", "admin");
}
else if (User.IsInRole("Standard")
{
return RedirectToAction("Index", "normaluser");
}
What you might want to consider is areas. This would allow you to have separate controllers for each area. Then permit access to those areas based on roles or whatever you wish.
This will give you routes like '/admin/controller/action', '/users/controller/action', etc. The 'pattern' separates all your controllers by namespace, and handles the routing quite well. Separate master pages easily, etc.
It won't give you the (potentially confusing, IMO) '/' and '/admin/' looking the same to an admin user, but it will let you separate the content and controllers.
What you are describing would lead to potentially tons of methods for each controller, something that is generally frowned upon by the MVC/REST crowd. It's not horrible, but its not considered best practice either.
You can read about areas at this blog here. Google 'asp.net mvc areas' for more.
--------edit-----------
To expand a bit:
Without custom routes or some other shenanigans, actions are mapped to controllers by the url. So if you want to keep all admin actions and views different, but on the root url, along with normal user actions, this would lead to one big controller that has to handle all these actions, or some strange "if this role, this view; if that role, that view" sort of nonsense that would have to happen in each action. Kind of a mess to debug potentially.
Similarly, the default view engine finds the views based on the url as well.
This would mean that all of your views are going to sit in one big ugly directory full of all sorts of weird similarly named but differently behaving views.
In short, this would become a potentially horrific maintenance nightmare, depending on complexity of the application.
Could you create a class that extends from DefaultControllerFactory and overrides CreateController?
public class RedirectControllerFactory : DefaultControllerFactory
{
public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
if (controllerName.Equals("Redirect"))
{
controllerName = requestContext.HttpContext.User.IsInRole("Admin") ? "Admin" : "NormalUser";
}
return base.CreateController(requestContext, controllerName);
}
}
Then in your Application_Start():
protected void Application_Start()
{
// ...
ControllerBuilder.Current.SetControllerFactory(new RedirectControllerFactory());
}