MVC ActionLink triggers a RouteConstraint? - c#

I have this simple User Area in my MVC 4 project.
public class UserAreaRegistration : AreaRegistration
{
public override string AreaName { get { return "User"; } }
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute("User_Constraint",
"{userName}/{controller}/{action}/{id}",
new { userName = string.Empty, controller = "Products", action = "Index", id = UrlParameter.Optional },
new { userName = new UserNameRouteConstraint() },
new[] { "T2b.Web.Areas.User.Controllers" }
);
}
}
To make sure the User Name exists I have a RouteConstraint called UserNameRouteConstraint()
All this does is a simple lookup in my users table and return true if the user has been found.
So far so good, this construction works fine!
Now; My view in the User Area has the following line of code
#Html.ActionLink("More information", "details", new {id = product.Guid})
This single line causes the UserNameRouteConstraint() to be called....
How and why!? If I write the link in plain HTML (see example below) it works well, but I want to keep to the MVC Principles as close as possible.
More information
Is there any way to prevent the RouteConstraint call?

Whenever routes are generated the constraints are processed.
You can add this check to stop the constraint depending on whether the constraint is handling an incoming request or generating a URL from a function like ActionLink:
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if(routeDirection == RouteDirection.UrlGeneration)
return false;
...
}

When you call ActionLink behind the scenes it creates a RouteValueDictionary and runs RouteCollection.GetVirtualPath(). This part is not open source but my best guess as to how it works is that it checks the parameters of the generated route value dictionary against the defaults and constraints of each route until it finds one that matches. Because of this it runs your constraints, and you should want it to run your constraints so that it doesn't end up matching to the wrong route.

Related

How to setup multilingual and localized Views in ASP.NET MVC?

i already have one MVC website in english language. Now I need to add arabic language. To do that i want to use structure below to store my localized .cshtml files
English Views
~/Views/index.cshtml
~/Views/about.cshtml
Arabic Views
~/ar/Views/index.cshtml
~/ar/Views/about.cshtml
English URL (It's working)
www.samplesite.com/home/index
www.samplesite.com/home/index
Arabic URL (It's not working)
www.samplesite.com/ar/home/index
www.samplesite.com/ar/home/index
I need to return english view if url with /en or no language parameter and if URL starts with /ar i need to return view in ar/Views folder. What should i change in route config and actions to get the correct view based on URL ?
In order to localize the ASP.NET MVC app with different views for different languages and cultures, you need to at least do:
Save language code from URL to route data collection, if exists
Set current culture based on the language code, if exists
Route the request to localized views, if language code appears
1. Save language code from URL to route data collection, if exists
You need to define the route mapping so that whenever there is a language code on the URL, it would be put to the route data collection.
You can define the extra route mapping in the RouteConfig:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.LowercaseUrls = true;
routes.MapRoute(
name: "DefaultWithLang",
url: "{lang}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { lang = new LanguageRouteConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
}
We will come back to the route constraint, but the idea here is to run this "DefaultWithLang" mapping first, and see if the incoming request has the language code attached on it or not. If yes, save it to the route collection with the key lang. Otherwise, just process it as normal.
Route constraints
Imagine we have those 2 mappings defined, and the incoming request looks like /home/index. Without that route constraint, this would be mapped to the "DefaultWithLang" with lang being "home", which is incorrect.
We will need a way to test whether the language code is a valid 2-letter code. To do that, you can create a route constraint:
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;
namespace DL.SO.Globalization.DotNet.Web.UI.RouteConstraints
{
public class LanguageRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName,
RouteValueDictionary values, RouteDirection routeDirection)
{
return Regex.IsMatch((string)values[parameterName], #"^[a-z]{2}$");
}
}
}
With this in placed, the request "/home/index" will first come in with "home" being the language code, but it will fail this route constraint because it has more than 2 letters. This would result no matching for "DefaultWithLang" map. Then the MVC will continue to use the 2nd mapping and see if the request is a valid request, which it is!
2. Set current culture based on the language code, if exists
Now we know the language code will be put into the route data collection. We can set the current culture and the current UI culture of the current thread based on that.
To do so, we can create an ActionFilter:
namespace DL.SO.Globalization.DotNet.Web.UI.Filters
{
public class LanguageFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var values = filterContext.RouteData.Values;
string languageCode = (string)values["lang"] ?? "en";
var cultureInfo = new CultureInfo(languageCode);
Thread.CurrentThread.CurrentCulture = cultureInfo;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(cultureInfo.Name);
}
}
}
Here I am reading the language code value from the route data collection, or it defaults to "en" if it doesn't exist.
To enable it for the whole application, you can add it to FilterConfig.cs:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new LanguageFilter());
filters.Add(new HandleErrorAttribute());
}
}
}
3. Route the request to localized views, if language code appears
Similar to Step #2, now we have the language code, we need to create a custom view engine to route the request to different views:
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class GlobalizationRazorViewEngine : RazorViewEngine
{
protected override IView CreatePartialView(ControllerContext controllerContext,
string partialPath)
{
partialPath = GetGlobalizeViewPath(controllerContext, partialPath);
return base.CreatePartialView(controllerContext, partialPath);
}
protected override IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)
{
viewPath = GetGlobalizeViewPath(controllerContext, viewPath);
return base.CreateView(controllerContext, viewPath, masterPath);
}
private string GetGlobalizeViewPath(ControllerContext controllerContext,
string viewPath)
{
var request = controllerContext.HttpContext.Request;
var values = controllerContext.RouteData.Values;
string languageCode = (string)values["lang"];
if (!String.IsNullOrWhiteSpace(languageCode))
{
string localizedViewPath = Regex.Replace(viewPath,
"^~/Views/",
String.Format("~/{0}/Views/", languageCode)
);
if (File.Exists(request.MapPath(localizedViewPath)))
{
viewPath = localizedViewPath;
}
}
return viewPath;
}
}
}
The logic here should be straight forward: if the language code exists, try to append it to the original view path the MVC app was going to use to find the corresponding view.
Note: you would need to copy _viewStart.cshtml as well as web.config from the original Views folder to the new localized Views folder. You might consider using different folder structure to contain all localized views. One possible way is to put them all inside the ~/Views folder.
To use this custom view engine, you would need to add it to the view engine collection at Application_Start():
namespace DL.SO.Globalization.DotNet.Web.UI
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new GlobalizationRazorViewEngine());
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
}
Screenshots
With zh/ on the URL:
The folder structure:
Github source code: https://github.com/davidliang2008/DL.SO.Globalization.DotNet/tree/master/DL.SO.Globalization.DotNet.Web.UI

Add parameter to all Incoming/Outgoing URL's

I'm having trouble adding a URL parameter to every URL generated, or redirected to in an ASP MVC 4 application.
I want to generate an ID, and use this ID at any point throughout the application. Storing the id in session is not an option as a single session may have multiple browser windows/tabs open concurrently (each with a different id)
RouteConfig
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{customId}",
defaults: new { controller = "Home", action = "Index", customid = UrlParameter.Optional }
);
HomeController.cs
public class HomeController : Controller
{
public ActionResult Index()
{
var customId = Guid.NewGuid();
ControllerContext.RequestContext.RouteData.Values.Add("customId", customId);
//How do I get this redirect to add customid to the url?
//E.g. /Home/Start/{customId}
return RedirectToAction("Start");
//I could do this: But I want it this to happen for every URL,
//and I don't want to replicate this code everywhere
//return RedirectToAction("Start", new { customId = customId });
}
public ActionResult Start()
{
object customId;
//Redirect Loop
if (!Request.RequestContext.RouteData.Values.TryGetValue("customId", out customId))
{
//To generate the ID
return RedirectToAction("Index");
}
ViewData["customId"] = Guid.Parse(customId.ToString());
return View();
}
public ActionResult Next()
{
object customId;
//Redirect Loop
if (!Request.RequestContext.RouteData.Values.TryGetValue("customId", out customId))
{
//To generate the ID
return RedirectToAction("Index");
}
ViewData["customId"] = Guid.Parse(customId.ToString());
return View();
}
}
Not only do I want the ID to be automatically inserted into any Redirect results, but when a View is rendered #Url.Action() and #Html.ActionLink() should also add the ID to the generated URL's.
Start.cshtml
#*Both of these should generate an href="~/Home/Next/{customId}"*#
#Html.ActionLink("Go to Next", "Next", "Home")
Go to Next
How do I automatically add an ID to ALL outgoing routes in ASP MVC?
Create an action filter that will add the ID to the route data in the OnActionExecuting method? You can access the controller through the filter context (and the viewbag). As long as your viewbag contains the customId, you should be able to add it to the route data. At least this way you only need to remember to add the attribute on the controller.
OR
Create a base class that inherits from System.Web.Mvc.Controller and implement your own RedirectToAction. Then have all your controllers inherit form MyControllerBase. Something like this.
public class MyControllerBase : Controller
{
public RedirectToRouteResult RedirectToAction<TController>(Expression<Func<TController, object>> actionExpression)
{
var custId = ControllerContext.Controller.ViewBag["customId"];
string controllerName = typeof(TController).GetControllerName();
string actionName = actionExpression.GetActionName();
return RedirectToAction(actionName, controllerName, new {cId = custId});
}
}
PART 2:
Another way I've modified a URL (I knew I had the code somewhere!) on every view, I needed the URL to link from a mobile site to a full browser site and read the mappings from the database. So in my footer, I have the following:
<a id="fullSiteLink" href="<%= ViewData[AppConstants.MainSiteUrl] %>">Visit our Full Browser site</a><br />
I then added a filter to the base controller class and onactionexecuting (before the action),
public void OnActionExecuting(ActionExecutingContext filterContext)
{
var mainSiteUrl = _mobileToMainRedirect.GetMainSiteUrl(filterContext.HttpContext.Request.Url);
filterContext.Controller.ViewData.Add(AppConstants.MainSiteUrl, string.IsNullOrEmpty(mainSiteUrl) ? UrlHelperExtensions.FullBrowserSite(filterContext.HttpContext.Request.Url) : mainSiteUrl);
}
Complete shot in the dark....
You can set up the route so that if a value is not provided, you create the Id. This way, if the value is there, it will use the provided one. Otherwise, it will create one.
Since this is leveraging the routes, you will be able to generate the Id even when using:
#Html.ActionLink("Go to Next", "Next", "Home")
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{customid}",
defaults: new { controller = "Home", action = "Index", customid = Guid.NewGuid() }
);
NOTE: You would replace Guid.NewGuid() with your own Id generator.

Is it possible for two areas to share the same route and still both be reachable?

I have two areas that register routes as shown below:
"Website" area:
context.MapRoute(
"Landing Controllers",
"{controller}/{action}",
new { controller = "Home", action = "Index" }
);
"Mobile" area:
context.MapRoute(
"Mobile Defaults",
"{controller}/{action}",
new { controller = "MobileHome", action = "Index" },
new { controller = "MobileHome", action = "Index" }
);
By default, one or the other of these routes would be consistently taken when trying to go to the root URL /. But suppose we decorated our controller actions with a custom AuthorizeAttribute, where the OnAuthorization method is overridden to redirect the user to the correct controller when appropriate, as below. (Idea taken from a great blog post.)
public class MobileRedirectAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var result = // Logic to generate the ActionResult that conditionally
// takes us to the other route goes here.
filterContext.Result = result;
}
}
I've tried using a new RedirectResult and RedirectToRouteResult, neither of which work as I'd like because of the routing conflict. Is there a way to set AuthorizationContext.Result to a value that would take us to the action that we're not currently executing? (As a last resort, I can just prefix the mobile route with some sort of namespacing variable, but I'd like to avoid going down that road just yet.)
My question can probably also be summarized by having a look at Wikipedia's desktop/mobile routing. Their two sites, http://en.m.wikipedia.org/wiki/Main_Page and http://en.wikipedia.org/wiki/Main_Page also share identical routes, but, depending on which mode you're in, return very different results.
Would it be possible to set up Wikipedia's routing in an MVC project where each environment (mobile/desktop) is registered in its own area?
A colleague led me to a promising solution using a custom IRouteConstraint.
public class HelloWorldConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values,
RouteDirection routeDirection)
{
// Determine whether to accept the route for this request.
var browser = BrowserDetector.Parse(httpContext.Request.UserAgent);
if (browser == BrowserPlatform.Mobile)
{
return true;
}
return false;
}
}
And my route declaration now looks like the below, where the route constraint is attached to a route parameter chosen at random.
context.MapRouteLowercase(
"Mobile Defaults",
"{controller}/{action}",
new { controller = "MobileHome", action = "Index" },
// In this case, it's not so much necessary to attach the constraint to
// a particular route parameter as it is important to be able to inspect
// the HttpContextBase provided by the IRouteConstraint.
new {
controller = new HelloWorldConstraint()
}
);
Not with standard MVC Routing. You can probably do with attribute routing, available in either MVC 5 or via the nuget package, AttributeRouting.

Can I stop RedirectToAction from adding route values to the querystring?

I am trying to see if I can change Mvc routing to use a a ticket based system where the route is an guid string to an external dictionary of values. So I setup a new route like so:
routes.Add(new Route("{ticketid}", new Shared.TicketRouteHandler()));
And created a new route handler:
public class TicketRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new TicketHttpHandler(requestContext);
}
}
And a new HttpHandler class. In the ProcessRequest method, I read the values for the controller and action out of the ticket, and place them into the route data collection:
string ticketidString = this.Request.RouteData.GetRequiredString("ticketid");
var ticket = Shared.Ticket.GetTicket(ticketId);
foreach (var item in ticket)
{
Request.RouteData.Values[item.Key] = item.Value;
}
The controller is executed like normal from there.
In the controller I have it looking for a ticketid parameter and if not found it creates one, and does a RedirecToAction, populating the new Ticket with the controller / action values:
public ActionResult Index(Guid? ticketid)
{
var ticket = Shared.Ticket.GetOrCreateTicket(ticketid);
if (ticketid == null)
{
//push the new values into the ticket dictionary instead of the route
string controller = "Home";
string action = "Index";
ticket.SetRoute(controller, action);
return RedirectToAction(action, controller, new { ticketid = ticket.Id });
}
return View();
}
This is all working fine, the problem is the URL that is created from the redirect action looks like this:
http://localhost:49952/df3b9f26-6c1c-42eb-8d0d-178e03b7e6f6?action=Index&controller=Home
Why is ?action=Index&controller=Home getting attached to the url?
If I remove the extra pieces and refresh the page, everything loads perfectly from the ticket collection.
I imagine that it's happening after the HttpHandler code is finished.
What can I do to stop this behavior?
The RedirectToAction method makes a new request and changes the URL in the browser:
This part is changing the URL:
return RedirectToAction(action, controller, new { ticketid = ticket.Id });
1) Return View doesn't make a new requests, it just renders the view without changing URLs in the browser's address bar.
2) Return RedirectToAction makes a new requests and URL in the browser's address bar is updated with the generated URL by MVC.
3) Return Redirect also makes a new requests and URL in the browser's address bar is updated, but you have to specify the full URL to redirect
4) Between RedirectToAction and Redirect, best practice is to use RedirectToAction for anything dealing with your application actions/controllers. If you use Redirect and provide the URL, you'll need to modify those URLs manually when you change the route table.
5) RedirectToRoute redirects to the specifies route defined in the the Route table.
But, View doesn't, try to adapt your code to return a View instead! Or, in the worst case, make something to handle the URL from the Response!
After digging around the mvc4 source, I think I found a solution:
Derive a new type from RedirectToRouteResult, override ExecuteResult and modify the url generation to only return the ticket string:
public class TicketRedirectResult : RedirectToRouteResult
{
public override void ExecuteResult(ControllerContext context)
{
string destinationUrl = UrlHelper.GenerateUrl(
RouteName,
null /* actionName */,
null /* controllerName */,
//Only return the ticket id, not the entire dictionary
new RouteValueDictionary(new { ticketid = RouteValues["ticketid"] }),
Routes,
context.RequestContext, false /* includeImplicitMvcValues */);
// snip other code
}
}
Then in your controller override RedirectToAction to return an instance of the new derived type:
protected override RedirectToRouteResult RedirectToAction(string actionName, string controllerName, RouteValueDictionary routeValues)
{
RouteValueDictionary mergedRouteValues;
if (RouteData == null)
{
mergedRouteValues = MergeRouteValues(actionName, controllerName, null, routeValues, includeImplicitMvcValues: true);
}
else
{
mergedRouteValues = MergeRouteValues(actionName, controllerName, RouteData.Values, routeValues, includeImplicitMvcValues: true);
}
//Only change here
return new TicketRedirectResult(mergedRouteValues);
}
Now the redirects only populates the ticket portion in the url.

how to do a url rewrite in asp.net with just a username

I have an asp.net web application and right now users can get there profiles by putting int www.webdomain.com/page.aspx?usename=myusername. I would like to change it to www.webdomain.com/username. Thanks for any help.
Sample IRouteConstraint:
public class IsUserActionConstraint : IRouteConstraint
{
//This is a static variable that handles the list of users
private static List<string> _users;
//This constructor loads the list of users on the first call
public IsUserActionConstraint()
{
_users= (from u in Models.Users.Get() select u.Username.ToLower()).ToList();
}
//Code for checking to see if the route is a username
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return _users.Contains((values["username"] as string).ToLower());
}
}
And, to register the route in the Global.asax:
routes.MapRoute(
"User Profile", // Route name
"{username}",
new { controller = "User", action = "Index", id = UrlParameter.Optional },// This stuff is here for ASP.NET MVC
new { IsUserAction = new IsUserActionConstraint() } //Your IRouteconstraint
);
In my case, my user list never changes during the application life cycle so I can cache it by using a static list. I would suggest that you modify the code so that you're doing what ever check to make sure the value entered is a username inside of the Match.
Use the MVC routing. This is a good article on how to use the mvc routing with webforms:
http://msdn.microsoft.com/en-us/magazine/dd347546.aspx
and
http://msdn.microsoft.com/en-us/library/cc668177.aspx
rewriterule www.webdomain.com/.+? www.webdomain.com/page.aspx?usename=$1
There are a few ways to do it. You could use ASP.NET MVC and create a route using a IRouteConstraint to make sure that the username exists.
You could also create an IHttpModule that captures the Application_BeginRequest and handel request for www.webdomain.com/username and ReWriting or TransferRequest them to www.webdomain.com/page.aspx?usename=myusername.
You could also do the code directly in the Global.asax the same way you would the IHttpModule.

Categories

Resources