Umbraco MVC-like routing - asp.net-mvc

I currently have a controller that needs to process parameters passed to umbraco's actions. What I did was adding a controller for my Document Type:
public class MyDocumentTypeController : RenderMvcController
{
public ActionResult SignUp(RenderModel model, [Bind(Prefix="rc")] string myArgument = null)
{
// some logic
return View(model);
}
}
Now I'm able to pass arguments to that action. Assuming my content of MyDocumentType is available under address http://mypage/mydocumenttype, so I can pass arguments to my action like this: http://mypage/mydocumenttype/signup?rc=1234ef. What I dislike is:
I cannot put constraints on the argument
I can't make use of MVC-like paths, like: http://mypage/mydocumenttype/signup/1234ef
To achieve it I added routing:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapUmbracoRoute(
"MyDocumentType",
"MyDocumentType/{action}/{myArgument}",
new { controller = "MyDocumentType", action = "Index", myArgument = UrlParameter.Optional },
/* WHAT GOES HERE? */,
new { myArgument = #"^[a-zA-Z0-9]{5}$" });
}
However, if I create plain-old-vanila MVC routing I loose Umbraco's context which I need. If I use MapUmbracoRoute I don't know what IRouteHandler should be passed. Any ideas?

Related

Make ASP.net MVC 5 Routing Ignore Async Suffix on Action methods

WebAPI 2 intelligently handles the Async suffix on action methods. For example, if I create a default WebAPI project it will route to the correct action regardless of the suffix. I.e.:
http://host/api/controller/action - SUCCEEDS
http://host/api/controller/actionAsync - SUCCEEDS
However, if I create an equivalent controller using MVC 5 the behavior is different:
http://host/controller/actionAsync - SUCCEEDS
http://host/controller/action - FAILS - 404
The fact that it fails with a 404 when the Async suffix isn't present is surprising. Nevertheless, I tried to add a route to handle it, but it still fails:
routes.MapRoute(
name: "DefaultAsync",
url: "{controller}/{action}Async/{id}",
defaults: new {controller = "Home", action = "Index", id = UrlParameter.Optional}
);
Here's the MVC and WebAPI controllers that I used to test (based on a new MVC/WebAPI project with default routes):
public class SampleDto { public string Name; public int Age; }
public class SampleMvcController : Controller
{
public Task<JsonResult> GetJsonAsync()
{
// Illustration Only. This is not a good usage of an Async method,
// in reality I'm calling out to an Async service.
return Task.FromResult(
Json(new SampleDto { Name="Foo", Age = 42}, JsonRequestBehavior.AllowGet));
}
}
public class SampleWebApiController : ApiController
{
public Task<SampleDto> GetJsonAsync()
{
return Task.FromResult(new SampleDto {Name="Bar", Age=24});
}
}
As I'm in the middle of making a bunch of methods async, I'd prefer not to specify an action name. The routing documentation suggests that it can pick up literals which can separate segments, but I haven't had any luck yet.
UPDATE:
The problem is that the Action as retrieved by MVC contains the Async suffix, but no corresponding action (or action name) exists on the controller. The piece that matches the action, MyAction, doesn't identify MyActionAsync as a match.
In retrospect, that's why the route doesn't work. It attempts to identify the action as ending with Async but leave off the Async suffix from the action used in matching, which is not what I wanted to do. It would be useful in the event that I wanted to create only a MyAction method (that was async but didn't follow the Async naming convention) and have it map to the corresponding MyAction method.
Initially I was sorely disappointed that the AsyncController type accomplishes exactly what we're looking for out-of-the-box. But apparently it's old-hat and is only still around for "backwards compatibility with MVC3."
So what I ended up doing was making a custom AsyncControllerActionInvoker and assigning it to a custom controller's ActionInvoker. The CustomAsyncControllerActionInvoker overrides the BeginInvokeAction method to see if an action method ending in "Async" for the appropriate action exists (ex. you pass in "Index" it looks for "IndexAsync"). If it does, invoke that one instead, otherwise, continue on as you were.
public class HomeController : CustomController
{
public async Task<ActionResult> IndexAsync()
{
ViewBag.Header = "I am a page header."
var model = new List<int> {1, 2, 3, 4, 5};
await Task.Run(() => "I'm a task");
return View(model);
}
public ActionResult About()
{
return View();
}
}
public class CustomController : Controller
{
public CustomController()
{
ActionInvoker = new CustomAsyncControllerActionInvoker();
}
}
public class CustomAsyncControllerActionInvoker : AsyncControllerActionInvoker
{
public override IAsyncResult BeginInvokeAction(ControllerContext controllerContext, string actionName, AsyncCallback callback, object state)
{
var asyncAction = FindAction(controllerContext, GetControllerDescriptor(controllerContext), $"{actionName}Async");
return asyncAction != null
? base.BeginInvokeAction(controllerContext, $"{actionName}Async", callback, state)
: base.BeginInvokeAction(controllerContext, actionName, callback, state);
}
}
Alas, navigating to /Home/Index properly calls the IndexAsync() method and serves up the Index.cshtml (not IndexAsync.cshtml) view appropriately. Routes to synchronous actions (ex. "About") are handled as normal.
Instead of trying to put a literal together with a placeholder, you should make a constraint to ensure your action name ends with Async.
routes.MapRoute(
name: "DefaultAsync",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "IndexAsync", id = UrlParameter.Optional },
constraints: new { action = #".*?Async" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

MVC route constraint custom attribute

I'd like to setup a custom route constraint that would allow me to decorate a controller with an attribute so that I don't have to pass in string to the route and remember to update it with new controller names.
I thought I could setup use the IRouteConstraint but I can't seem to get the attribute. Perhaps I'm just missing something obvious here.
routes.MapRoute("test",
"foo/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = new TestConstraint()}
);
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
public class TestConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
return false;
}
}
[AttributeUsage(AttributeTargets.Class)]
public class CustomConstraintControllerAttribute : Attribute
{
}
[CustomConstraintController]
public class TestController : Controller
{
public ActionResult Index()
{
return View();
}
}
Edit:
Current Method:
routes.MapSubdomainRoute("Store",
"{store}/{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = UrlParameter.Optional},
new {controller = "StoreHome|Contacts|..." }
);
This route configuration ensures that a url must match subdomain.test.com/GiggleInc/Contacts or subdomain.test.com/GiggleInc/StoreHome.
subdomain.test.com/GiggleInc/StoreHome
MapSubdomainRoute /{store} /{controller}/{action}/{id}
This method requires that each controller that should be used this way must be added to the controller constraint.
As I mentioned in the comments, I want to replace the hard coded strings for an attribute or something similar.
First, thank you for this sweet line of code I didn't know was possible:
constraints: new { controller = "StoreHome|Contacts" }
I didn't know I could filter my MapRoute to a list of Controllers so easily.
Second, you don't need to implement a custom IRouteConstraint for this.
MVC offers the Attribute-Routing you are looking for.
You may even include Default/Optional Values, just like in your MapRoute.
Decorate your Controller Class like so:
[RoutePrefix("{store}/Test")]
[Route("{action=Index}/{id?}")]//Without this, you need to define "[Route]" above every Action-Method.
public class TestController : Controller
{
public ActionResult Index(string store)//Adding "string store" is optional.
{
return View();
}
}
That's it.
Remember to add the "store" Parameter in all your Actions under each Controller (but it is not Required).
Note:
If you use Attributes instead of MapRoute, then you will not be able to hit the Controller without the "store" Prefix.
With the Custom and Default MapRoutes, you could have accessed your controller either way.
By decorating your Controller with these Attributes, you now force it to only use this exact path.
This may be what you want, but if you start IIS Express from Visual Studio on one of your Views, it will not find it, because Visual Studio doesn't know to add the RoutePrefix for you.
See this link for more information about Attribute-Routing:
https://blogs.msdn.microsoft.com/webdev/2013/10/17/attribute-routing-in-asp-net-mvc-5/
Try this...
public class TestRouteAttribute : RouteFactoryAttribute
{
public TestRouteAttribute(string template) : base(template) { }
public override RouteValueDictionary Constraints
{
get
{
var constraints = new RouteValueDictionary();
constraints.Add("TestConstraint", new TestConstraint());
return constraints;
}
}
}
Then you should be able to decorate your action methods using [TestRoute]
By the way, would love to know how to accomplish this in asp.net core if anyone knows.

How can I convert MVC Route to parameter of type Boolean?

Change route segment into boolean
I have a simple action method in my controller and would like to pass a boolean value to my action when the string "Company" is passed in the url.
public ActionResult DoSomething(Boolean ? isCompany) { }
I would like to call it like this.
http://mysite/Feature/Configure/Company
So basically, when "Company" is in the url then pass isCompany as true boolean.
Is this possible?
According to the answer below I think I should then use the following mapping.
routes.MapRoute(
name: "FeatureConfigure",
url: "Feature/Configure/{company}",
defaults: new { controller = "Feature", action="Configure",
company = UrlParameter.Optional }
);
public ActionResult Configure(String company) {
var isCompany = company == "Company";
// maybe use a switch if there are multiple values company can be
// and probably rename the variable to something more generic.
}
Not directly, you'd have to handle it like:
public ActionResult DoSometing(string foo)
{
var isCompany = foo == "Company";
}
Really comes back to what you are trying to achieve.
Having a check on an input like this, suggests that you will be planning to have a fork in your logic and possibly display different content/views.
Simpler option would be to let MVC handle it and keep your code cleaner.
Option 1:
Changing the {company} portion of your route to {action} and fork your logic at the action level.
routes.MapRoute(
name: "FeatureConfigure",
url: "Feature/Configure/{action}",
defaults: new { controller = "Feature", action="Index" }
);
public ActionResult Company() {
// Do company stuff
}
public ActionResult Branch() {
// Do branch stuff
}
Option 2:
Same as above but using route attributes (assuming you're using MVC5).
To do that remove the FeatureConfigure route from the config.
Then include the following on the controller and actions:
[RoutePrefix("features/configure")]
public class FeatureController : Controller
{
....
[Route("{type:regex(company)}")]
public ActionResult Company()
{
// do company stuff
}
[Route("{type:regex(branch)}")]
public ActionResult Branch()
{
// do branch stuff
}
...
}
Note: in either case it would probably make more sense to rename FeatureController to ConfigureContoller but that's up to you

MVC 2 AreaRegistration Routes Order

I noticed that in MVC 2 Preview 2, AreaRegistration is loading the routes for each area in an arbitrary order. Is there a good way to get one before the other?
For example, I have two areas - "Site" and "Admin". Both have a "Blog" controller.
I would like the following:
/admin/ --> go to Admin's Blog controller
/ --> go to Site's Blog controller.
The problem is that it is loading the site's route first, so it is matching {controller}/{action}/{id} instead of admin/{controller}/{action}/{id} when I go to the url "/admin/". I then get a 404, because there is no Admin controller in the "Site" area.
Both areas default to the "Blog" controller. I realize I could simply put site/{controller}/... as the url, but I would rather have it at the root if possible. I also tried keeping the default route in the global RegisterRoutes function, however, it is then not sent to the "Sites" area.
Thanks in advance!
Aside from what Haacked said, it is very much possible to order area registrations (and thus their routes). All you have to do is register each area manually, in whatever order you want. It's not as sleek as calling RegisterAllAreas() but it's definitely doable.
protected void Application_Start() {
var area1reg = new Area1AreaRegistration();
var area1context = new AreaRegistrationContext(area1reg.AreaName, RouteTable.Routes);
area1reg.RegisterArea(area1context);
var area2reg = new Area2AreaRegistration();
var area2context = new AreaRegistrationContext(area2reg.AreaName, RouteTable.Routes);
area2reg.RegisterArea(area2context);
var area3reg = new Area3AreaRegistration();
var area3context = new AreaRegistrationContext(area3reg.AreaName, RouteTable.Routes);
area3reg.RegisterArea(area3context);
}
Another option is to take the code for RegisterAllAreas(), copy it into your own app, and build your own mechanism for determining the order. It is quite a bit of code to copy if you want all the fancy caching logic that the built-in method does, but your app might not even need that.
Currently it's not possible to order areas. However, I think it makes sense to try and make each area as independent from other areas as possible so the order doesn't matter.
For example, instead of having the default {controller}/{action}/{id} route, maybe replace that with specific routes for each controller. Or add a constraint to that default route.
We are mulling over options to allow ordering, but we don't want to overcomplicate the feature.
I make this solution:
AreaUtils.cs
using System;
using System.Web.Mvc;
using System.Web.Routing;
namespace SledgeHammer.Mvc.Site
{
public static class Utils
{
public static void RegisterArea<T>(RouteCollection routes,
object state) where T : AreaRegistration
{
AreaRegistration registration =
(AreaRegistration)Activator.CreateInstance(typeof(T));
AreaRegistrationContext context =
new AreaRegistrationContext(registration.AreaName, routes, state);
string tNamespace = registration.GetType().Namespace;
if (tNamespace != null)
{
context.Namespaces.Add(tNamespace + ".*");
}
registration.RegisterArea(context);
}
}
}
In global.asax:
Utils.RegisterArea<SystemAreaRegistration>(RouteTable.Routes, null);
Utils.RegisterArea<ClientSitesAreaRegistration>(RouteTable.Routes, null);
//AreaRegistration.RegisterAllAreas(); do not dublicate register areas
No requred changes to generated area registration code.
I also use custom constrant in routes to filter routes by type of domain in request (system domain or user site).
This is my area registrations as example:
namespace SledgeHammer.MVC.Site.Areas.System
{
public class SystemAreaRegistration : AreaRegistration
{
public override string AreaName
{
get { return "System"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"System_Feedback",
"Feedback",
new { controller = "Feedback", action = "Index" }
);
context.MapRoute(
"System_Information",
"Information/{action}/{id}",
new { controller = "Information", action = "Index", id = UrlParameter.Optional }
);
}
}
}
namespace SledgeHammer.MVC.Site.Areas.ClientSites
{
public class ClientSitesAreaRegistration : AreaRegistration
{
public override string AreaName
{
get { return "ClientSites"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"ClientSites_default",
"{controller}/{action}/{id}",
new { controller = "Site", action = "Index", id = UrlParameter.Optional },
new { Host = new SiteInGroups("clients") }
);
}
}
}
For reference,
In MVC3 (don't know about MVC2) when you just want to map root to a specific area/controller you could simply use a global route.
Just remember to specify the namespace/area.
routes.MapRoute(
"CatchRoot", "",
new { controller = "SITEBLOG-CONTROLLER-NAME", action = "Index"}
).DataTokens.Add("area", "SITE-AREA-NAME");

ASP.NET MVC Routing Question

I must be dense. After asking several questions on StackOverflow, I am still at a loss when it comes to grasping the new routing engine provided with ASP.NET MVC. I think I've narrowed down the problem to a very simple one, which, if solved, would probably allow me to solve the rest of my routing issues. So here it is:
How would you register a route to support a Twitter-like URL for user profiles?
www.twitter.com/username
Assume the need to also support:
the default {controller}/{action}/{id} route.
URLs like:
www.twitter.com/login
www.twitter.com/register
Is this possible?
What about
routes.MapRoute(
"Profiles",
"{userName}",
new { controller = "Profiles", action = "ShowUser" }
);
and then, in ProfilesController, there would be a function
public ActionResult ShowUser(string userName)
{
...
In the function, if no user with the specified userName is found, you should redirect to the default {controller}/{action}/{id} (here, it would be just {controller}) route.
Urls like www.twitter.com/login should be registered before that one.
routes.MapRoute(
"Login",
"Login",
new { controller = "Security", action = "Login" }
);
The important thing to understand is that the routes are matched in the order they are registered. So you would need to register the most specific route first, and the most general last, or all requests matching the general route would never reach the more specific route.
For your problem i would register routing rules for each of the special pages, like "register" and "login" before the username rule.
You could handle that in the home controller, but the controller method would not be very elegant. I'm guessing something like this might work (not tested):
routes.MapRoute(
"Root",
"{controller}/{view}",
new { controller = "Home", action = "Index", view = "" }
);
Then in your HomeController:
public ActionResult Index(string view) {
switch (view) {
case "":
return View();
case "register":
return View("Register");
default:
// load user profile view
}
}
OK I haven't ever properly tried this, but have you tried to extend the RouteBase class for dealing with users. The docs for RouteBase suggest that the method GetRouteData should return null if it doesn't match the current request. You could use this to check that the request matches one of the usernames you have.
You can add a RouteBase subclass using:
routes.Add(new UserRouteBase());
When you register the routes.
Might be worth investigating.
i think your question is similar to mine. ASP.NET MVC Routing
this is what robert harvey answered.
routes.MapRoute( _
"SearchRoute", _
"{id}", _
New With {.controller = "User", .action = "Profile", .id = ""} _
)
Here is an alternative way to standar route registration:
1. Download RiaLibrary.Web.dll and reference it in your ASP.NET MVC website project
2. Decoreate controller methods with the [Url] Attributes:
public SiteController : Controller
{
[Url("")]
public ActionResult Home()
{
return View();
}
[Url("about")]
public ActionResult AboutUs()
{
return View();
}
[Url("store/{?category}")]
public ActionResult Products(string category = null)
{
return View();
}
}
BTW, '?' sign in '{?category}' parameter means that it's optional. You won't need to specify this explicitly in route defaults, which is equals to this:
routes.MapRoute("Store", "store/{category}",
new { controller = "Store", action = "Home", category = UrlParameter.Optional });
3. Update Global.asax.cs file
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoutes(); // This do the trick
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
How to set defaults and constraints? Example:
public SiteController : Controller
{
[Url("admin/articles/edit/{id}", Constraints = #"id=\d+")]
public ActionResult ArticlesEdit(int id)
{
return View();
}
[Url("articles/{category}/{date}_{title}", Constraints =
"date=(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])")]
public ActionResult Article(string category, DateTime date, string title)
{
return View();
}
}
How to set ordering? Example:
[Url("forums/{?category}", Order = 2)]
public ActionResult Threads(string category)
{
return View();
}
[Url("forums/new", Order = 1)]
public ActionResult NewThread()
{
return View();
}

Resources