Single Controller with Multiple Views - asp.net-mvc

I am trying to create an MVC application with multiple view, but using a single controller. I started by creating a second route with another property that I could use to redirect to a second folder.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"xml", // Route name
"xml/{controller}/{action}/{id}", // URL with parameters
new { mode = "xml", controller = "Home", action = "Index", id = "" } // Parameter defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
SessionManager.Instance.InitSessionFactory("acstech.helpWanted");
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ModeViewEngine());
}
I followed that by descending from WebFormViewEngine and changed the path from ~/View to ~/{mode}View. This worked and ran rendered the pages properly. The problem that I ran into is that the Html.ActionLink always uses the mode version no matter what the view rendered. Is this the proper direction for accomplishing my goal, if so what am I missing to get the Action Link to work properly. Below is the ViewEngine. This is a lab test, so some liberties have been taken.
public class ModeViewEngine : WebFormViewEngine
{
public ModeViewEngine()
{
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
string mode = String.Empty;
if (controllerContext.RouteData.Values["mode"] != null)
mode = controllerContext.RouteData.Values["mode"] as string;
return new WebFormView(partialPath.Replace("~/Views", "~/" + mode + "Views"), null);
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
string mode = String.Empty;
if (controllerContext.RouteData.Values["mode"] != null)
mode = controllerContext.RouteData.Values["mode"] as string;
return new WebFormView(viewPath.Replace("~/Views", "~/" + mode + "Views"), masterPath);
}
}

Have you tried adding mode="" in the defaults array in the default route? That is, as I understand it, how the "Index" action gets omitted from URLs, so that should in theory make it match your default route I believe.

Why not have your controller just pick a different view based on the mode? Then you can return your Home view or Home_Xml.
I guess your way moves this decision out of the controller and centralizes the logic, but it also requires you to create a matching view in each Mode you have.

Well the great thing about the Asp.Net framework is that is that is is very extensible. I think you should check out this link. It will have exactly what you are looking for. My opion, along with the authors is to create a ActionFilter and decorate your views in the controller needing XML or even JSON views. I have even seen cases where all the serialization to XML happens in the Filter and that is returned, thus not needing a ViewEngine.
http://jesschadwick.blogspot.com/2008/06/aspnet-mvc-using-custom-view-engines.html

Related

Is it possible to have an anonymous controller in asp.net MVC 5?

We have built an MVC app that publishes a complete website with hierarchal Folders, SubFolders and Pages. The resulting pages, are strictly HTML and are not published in our MVC app. Our customers are able to name their Folders and Pages with any compliant string they choose. So conceivably, once the site is hosted, they could end up with a URL such as:
someDomain.com/folder/subfolder1/subfolder2/page-slug. There is no limit to the number of nested subfolders.
We would like to replicate their sites in our MVC app, so that they are able to test them before they publish and perhaps so we can provide hosting ourselves if required.
The obvious problem, is how can we handle,
ourMVCApp.com/folder/subfolder1/subfolder2/page-slug in an MVC app?
If there was a way that we could set routing to handle such a thing, then we could easily get the content required for the request by splitting the url into an array by "/".
The last segment would be a page contained in the previous segment's folder. We could then search our DB using these strings to get the required content.
Your help is greatly appreciated.
FURTHER QUESTION:
In response to the answer provided by Tomi.
I added the code to my controller's class but I am receiving the following warning:
I am not sure what I am missing? Did I put the code in the place? Thanks again.
UPDATE 2. I realized I had not actually created the controller factory, so I followed a partial example I found here: http://develoq.net/2010/custom-controller-factory-in-asp-net-mvc/. And since implementing it, I no longer receive any build-errors, but when I run the the debug, it crashes the built-in IISEXPRESS without any error message.
Here is my controller factory code:
public class FolderControllerFactory : IControllerFactory
{
public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
try
{
// Get the path
string path = requestContext.RouteData.Values["pathInfo"].ToString();
IController controller = new FolderController(path);
return controller;
}
catch
{
// Log routing error here and move on
return CreateController(requestContext, controllerName);
}
}
public void ReleaseController(IController controller)
{
var disposable = controller as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{
return SessionStateBehavior.Default;
}
}
Here is my global:
ControllerBuilder.Current.SetControllerFactory(typeof(ProofPixApp.Controllers.FolderControllerFactory));
And finally my controller:
public class FolderController : Controller
{
private string _path;
public FolderController(string path)
{
_path = path;
}
public ActionResult Index(string name)
{
ViewBag.Message = "Hello " + name;
return View("/Views/" + _path);
}
}
A couple of notes:
1. I removed the 'override' from public IController CreateController
because I kept receiving the initial error I posted.
2. I added public void ReleaseController and the public
SessionStateBehavior GetControllerSessionBehavior methods to the
CreateController class to avoid other build errors.
3. I removed 'base.' from the catch clause because it too was causing a
build error.
SOLUTION:
I was able to avoid the error by checking to see pathValue was not null in the createController method, like so:
public IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
// Get the path
string path = "";
if (requestContext.RouteData.Values["pathInfo"] != null)
{
path = requestContext.RouteData.Values["pathInfo"].ToString();
}
IController controller = new FolderController(path);
return controller;
}
I have no idea what page slug is but here's my solution on how to achieve the routing you requested.
I made a custom ControllerFactory which handles the url and passes it to controller. This ControllerFactory constructs the controller we use to handle folder-route requests. We get the path from routevalues and then pass it to the FolderController.
public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
try
{
// Get the path
string path = requestContext.RouteData.Values["pathInfo"].ToString();
IController controller = new FolderController(path);
return controller;
}
catch
{
// Log routing error here and move on
return base.CreateController(requestContext, controllerName);
}
}
Here's the controller. The actionmethod, which redirects to given path is called Index for now. The actionmethod returns view it finds from the url.
public class FolderController : Controller
{
private string _path;
public FolderController(string path)
{
_path = path;
}
public FolderController()
{
}
public ActionResult Index(string name)
{
ViewBag.Message = "Hello " + name;
return View("/Views/"+_path);
}
}
Last step is to write our own route and register the factory. Open up RouteConfig.cs. My new RegisterRoutes method looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Dynamic",
url: "{*pathInfo}",
defaults: new { controller = "Folder", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
In global.asax we need to register our FolderControllerFactory by adding this line into Application_Start method
ControllerBuilder.Current.SetControllerFactory(typeof(FolderControllerFactory));
And that's it! There's still much to be done, like handling improper urls and such. Also I don't think this supports plain html files, the files must be in .cshtml or asp format.
Here's the test:
My folder structure:
Url I request:
localhost:port/Mainfolder/Subfolder/Subfolder2/view.cshtml?name=Tomi
The result with Route Debugger plugin:

ASP.net MVC support for URL's with hyphens

Is there an easy way to get the MvcRouteHandler to convert all hyphens in the action and controller sections of an incoming URL to underscores as hyphens are not supported in method or class names.
This would be so that I could support such structures as sample.com/test-page/edit-details mapping to Action edit_details and Controller test_pagecontroller while continuing to use MapRoute method.
I understand I can specify an action name attribute and support hyphens in controller names which out manually adding routes to achieve this however I am looking for an automated way so save errors when adding new controllers and actions.
C# version of John's Post for anyone who would prefer it: C# and VB version on my blog
public class HyphenatedRouteHandler : MvcRouteHandler{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
...and the new route:
routes.Add(
new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Default", action = "Index", id = "" }),
new HyphenatedRouteHandler())
);
You can use the following method too but bear in mind you would need to name the view My-Action which can be annoying if you like letting visual studio auto generate your view files.
[ActionName("My-Action")]
public ActionResult MyAction() {
return View();
}
I have worked out a solution. The requestContext inside the MvcRouteHandler contains the values for the controller and action on which you can do a simple replace on.
Public Class HyphenatedRouteHandler
Inherits MvcRouteHandler
Protected Overrides Function GetHttpHandler(ByVal requestContext As System.Web.Routing.RequestContext) As System.Web.IHttpHandler
requestContext.RouteData.Values("controller") = requestContext.RouteData.Values("controller").ToString.Replace("-", "_")
requestContext.RouteData.Values("action") = requestContext.RouteData.Values("action").ToString.Replace("-", "_")
Return MyBase.GetHttpHandler(requestContext)
End Function
End Class
Then all you need to replace the routes.MapRoute with an equivalent routes.Add specifying the the new route handler. This is required as the MapRoute does not allow you to specify a custom route handler.
routes.Add(New Route("{controller}/{action}/{id}", New RouteValueDictionary(New With {.controller = "Home", .action = "Index", .id = ""}), New HyphenatedRouteHandler()))
All you really need to do in this case is name your views with the hyphens as you want it to appear in the URL, remove the hyphens in your controller and then add an ActionName attribute that has the hyphens back in it. There's no need to have underscores at all.
Have a view called edit-details.aspx
And have a controller like this:
[ActionName("edit-details")]
public ActionResult EditDetails(int id)
{
// your code
}
I realize this is quite an old question, but to me this is only half the story of accepting url's with hyphens in them, the other half is generating these urls while still being able to use Html.ActionLink and other helpers in the MVC framework, I solved this by creating a custom route class similar, here is the code in case it helps anyone coming here from a google search. It also includes the lower casing of the url too.
public class SeoFriendlyRoute : Route
{
// constructor overrides from Route go here, there is 4 of them
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var path = base.GetVirtualPath(requestContext, values);
if (path != null)
{
var indexes = new List<int>();
var charArray = path.VirtualPath.Split('?')[0].ToCharArray();
for (int index = 0; index < charArray.Length; index++)
{
var c = charArray[index];
if (index > 0 && char.IsUpper(c) && charArray[index - 1] != '/')
indexes.Add(index);
}
indexes.Reverse();
indexes.Remove(0);
foreach (var index in indexes)
path.VirtualPath = path.VirtualPath.Insert(index, "-");
path.VirtualPath = path.VirtualPath.ToLowerInvariant();
}
return path;
}
}
then when adding routes, you can either create a RouteCollection extensions or just use the following in your global routing declarations
routes.Add(
new SeoFriendlyRoute("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Default", action = "Index", id = "" }),
new HyphenatedRouteHandler())
);
Thanks dsteuernol for this answer - exactly what I was looking for. However I found that I needed to enhance the HyphenatedRouteHandler to cover the scenario where the Controller or Area was implied from the current page. For example using #Html.ActionLink("My Link", "Index")
I changed the GetHttpHandler method to the following:
public class HyphenatedRouteHandler : MvcRouteHandler
{
/// <summary>
/// Returns the HTTP handler by using the specified HTTP context.
/// </summary>
/// <param name="requestContext">The request context.</param>
/// <returns>
/// The HTTP handler.
/// </returns>
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = ReFormatString(requestContext.RouteData.Values["controller"].ToString());
requestContext.RouteData.Values["action"] = ReFormatString(requestContext.RouteData.Values["action"].ToString());
// is there an area
if (requestContext.RouteData.DataTokens.ContainsKey("area"))
{
requestContext.RouteData.DataTokens["area"] = ReFormatString(requestContext.RouteData.DataTokens["area"].ToString());
}
return base.GetHttpHandler(requestContext);
}
private string ReFormatString(string hyphenedString)
{
// lets put capitals back in
// change dashes to spaces
hyphenedString = hyphenedString.Replace("-", " ");
// change to title case
hyphenedString = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(hyphenedString);
// remove spaces
hyphenedString = hyphenedString.Replace(" ", "");
return hyphenedString;
}
}
Putting the capitals back in meant that the implied controller or area was then hyphenated correctly.
I've developed an open source NuGet library for this problem which implicitly converts EveryMvc/Url to every-mvc/url.
Dashed urls are much more SEO friendly and easier to read. (More on my blog post)
NuGet Package: https://www.nuget.org/packages/LowercaseDashedRoute/
To install it, simply open the NuGet window in the Visual Studio by right clicking the Project and selecting NuGet Package Manager, and on the "Online" tab type "Lowercase Dashed Route", and it should pop up.
Alternatively, you can run this code in the Package Manager Console:
Install-Package LowercaseDashedRoute
After that you should open App_Start/RouteConfig.cs and comment out existing route.MapRoute(...) call and add this instead:
routes.Add(new LowercaseDashedRoute("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
new DashedRouteHandler()
)
);
That's it. All the urls are lowercase, dashed, and converted implicitly without you doing anything more.
Open Source Project Url: https://github.com/AtaS/lowercase-dashed-route
Don't know of a way without writing a map for each url:
routes.MapRoute("EditDetails", "test-page/edit-details/{id}", new { controller = "test_page", action = "edit_details" });
If you upgrade your project to MVC5, you can make use of attribute routing.
[Route("controller/my-action")]
public ActionResult MyAction() {
return View();
}
I much prefer this approach to the accepted solution, which leaves you with underscores in your controller action names and view filenames, and hyphens in your view's Url.Action helpers. I prefer consistency, and not having to remember how the names are converted.
In MVC 5.2.7 you can simply specifiy using the attribute
ActionName
[ActionName("Import-Export")]
public ActionResult ImportExport()
{
return View();
}
Then name the view
Import-Export.cshtml
The link would then be:
#Html.ActionLink("Import and Export", "Import-Export", "Services")
Which is of the form:
#Html.ActionLink("LinkName", "ActionName", "ControllerName")

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");

How to set a Default Route (To an Area) in MVC

Ok this has been asked before but there is no solid solution out there. So for purpose of myself and others who may find this useful.
In MVC2 (ASP.NET) I want it so when someone navigates to the website, there is a default area specified. So navigating to my site should send you to ControllerX ActionY in AreaZ.
Using the following route in the Global.asax
routes.MapRoute(
"Area",
"",
new { area = "AreaZ", controller = "ControllerX ", action = "ActionY " }
);
Now this works as in it does try to serve the correct page. However MVC proceeds to look for the View in the root of the site and not in the Area folder.
Is there a way to resolve this?
EDIT
There is a 'Solution' and that is in ControllerX, ActionY return the full path of the view. Bit of a hack but it does work. However I'm hoping there is a better solution.
public ActionResult ActionY()
{
return View("~/Areas/AreaZ/views/ActionY.aspx");
}
Edit:
This also becomes an issue when having a HTML ActionLink of the page. If the area is not set the Action Link is output blank.
Is all of this by design or a flaw?
This one interested me, and I finally had a chance to look into it. Other folks apparently haven't understood that this is an issue with finding the view, not an issue with the routing itself - and that's probably because your question title indicates that it's about routing.
In any case, because this is a View-related issue, the only way to get what you want is to override the default view engine. Normally, when you do this, it's for the simple purpose of switching your view engine (i.e. to Spark, NHaml, etc.). In this case, it's not the View-creation logic we need to override, but the FindPartialView and FindView methods in the VirtualPathProviderViewEngine class.
You can thank your lucky stars that these methods are in fact virtual, because everything else in the VirtualPathProviderViewEngine is not even accessible - it's private, and that makes it very annoying to override the find logic because you have to basically rewrite half of the code that's already been written if you want it to play nice with the location cache and the location formats. After some digging in Reflector I finally managed to come up with a working solution.
What I've done here is to first create an abstract AreaAwareViewEngine that derives directly from VirtualPathProviderViewEngine instead of WebFormViewEngine. I did this so that if you want to create Spark views instead (or whatever), you can still use this class as the base type.
The code below is pretty long-winded, so to give you a quick summary of what it actually does: It lets you put a {2} into the location format, which corresponds to the area name, the same way {1} corresponds to the controller name. That's it! That's what we had to write all this code for:
BaseAreaAwareViewEngine.cs
public abstract class BaseAreaAwareViewEngine : VirtualPathProviderViewEngine
{
private static readonly string[] EmptyLocations = { };
public override ViewEngineResult FindView(
ControllerContext controllerContext, string viewName,
string masterName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentNullException(viewName,
"Value cannot be null or empty.");
}
string area = getArea(controllerContext);
return FindAreaView(controllerContext, area, viewName,
masterName, useCache);
}
public override ViewEngineResult FindPartialView(
ControllerContext controllerContext, string partialViewName,
bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(partialViewName))
{
throw new ArgumentNullException(partialViewName,
"Value cannot be null or empty.");
}
string area = getArea(controllerContext);
return FindAreaPartialView(controllerContext, area,
partialViewName, useCache);
}
protected virtual ViewEngineResult FindAreaView(
ControllerContext controllerContext, string areaName, string viewName,
string masterName, bool useCache)
{
string controllerName =
controllerContext.RouteData.GetRequiredString("controller");
string[] searchedViewPaths;
string viewPath = GetPath(controllerContext, ViewLocationFormats,
"ViewLocationFormats", viewName, controllerName, areaName, "View",
useCache, out searchedViewPaths);
string[] searchedMasterPaths;
string masterPath = GetPath(controllerContext, MasterLocationFormats,
"MasterLocationFormats", masterName, controllerName, areaName,
"Master", useCache, out searchedMasterPaths);
if (!string.IsNullOrEmpty(viewPath) &&
(!string.IsNullOrEmpty(masterPath) ||
string.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(CreateView(controllerContext, viewPath,
masterPath), this);
}
return new ViewEngineResult(
searchedViewPaths.Union<string>(searchedMasterPaths));
}
protected virtual ViewEngineResult FindAreaPartialView(
ControllerContext controllerContext, string areaName,
string viewName, bool useCache)
{
string controllerName =
controllerContext.RouteData.GetRequiredString("controller");
string[] searchedViewPaths;
string partialViewPath = GetPath(controllerContext,
ViewLocationFormats, "PartialViewLocationFormats", viewName,
controllerName, areaName, "Partial", useCache,
out searchedViewPaths);
if (!string.IsNullOrEmpty(partialViewPath))
{
return new ViewEngineResult(CreatePartialView(controllerContext,
partialViewPath), this);
}
return new ViewEngineResult(searchedViewPaths);
}
protected string CreateCacheKey(string prefix, string name,
string controller, string area)
{
return string.Format(CultureInfo.InvariantCulture,
":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:",
base.GetType().AssemblyQualifiedName,
prefix, name, controller, area);
}
protected string GetPath(ControllerContext controllerContext,
string[] locations, string locationsPropertyName, string name,
string controllerName, string areaName, string cacheKeyPrefix,
bool useCache, out string[] searchedLocations)
{
searchedLocations = EmptyLocations;
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
if ((locations == null) || (locations.Length == 0))
{
throw new InvalidOperationException(string.Format("The property " +
"'{0}' cannot be null or empty.", locationsPropertyName));
}
bool isSpecificPath = IsSpecificPath(name);
string key = CreateCacheKey(cacheKeyPrefix, name,
isSpecificPath ? string.Empty : controllerName,
isSpecificPath ? string.Empty : areaName);
if (useCache)
{
string viewLocation = ViewLocationCache.GetViewLocation(
controllerContext.HttpContext, key);
if (viewLocation != null)
{
return viewLocation;
}
}
if (!isSpecificPath)
{
return GetPathFromGeneralName(controllerContext, locations, name,
controllerName, areaName, key, ref searchedLocations);
}
return GetPathFromSpecificName(controllerContext, name, key,
ref searchedLocations);
}
protected string GetPathFromGeneralName(ControllerContext controllerContext,
string[] locations, string name, string controllerName,
string areaName, string cacheKey, ref string[] searchedLocations)
{
string virtualPath = string.Empty;
searchedLocations = new string[locations.Length];
for (int i = 0; i < locations.Length; i++)
{
if (string.IsNullOrEmpty(areaName) && locations[i].Contains("{2}"))
{
continue;
}
string testPath = string.Format(CultureInfo.InvariantCulture,
locations[i], name, controllerName, areaName);
if (FileExists(controllerContext, testPath))
{
searchedLocations = EmptyLocations;
virtualPath = testPath;
ViewLocationCache.InsertViewLocation(
controllerContext.HttpContext, cacheKey, virtualPath);
return virtualPath;
}
searchedLocations[i] = testPath;
}
return virtualPath;
}
protected string GetPathFromSpecificName(
ControllerContext controllerContext, string name, string cacheKey,
ref string[] searchedLocations)
{
string virtualPath = name;
if (!FileExists(controllerContext, name))
{
virtualPath = string.Empty;
searchedLocations = new string[] { name };
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext,
cacheKey, virtualPath);
return virtualPath;
}
protected string getArea(ControllerContext controllerContext)
{
// First try to get area from a RouteValue override, like one specified in the Defaults arg to a Route.
object areaO;
controllerContext.RouteData.Values.TryGetValue("area", out areaO);
// If not specified, try to get it from the Controller's namespace
if (areaO != null)
return (string)areaO;
string namespa = controllerContext.Controller.GetType().Namespace;
int areaStart = namespa.IndexOf("Areas.");
if (areaStart == -1)
return null;
areaStart += 6;
int areaEnd = namespa.IndexOf('.', areaStart + 1);
string area = namespa.Substring(areaStart, areaEnd - areaStart);
return area;
}
protected static bool IsSpecificPath(string name)
{
char ch = name[0];
if (ch != '~')
{
return (ch == '/');
}
return true;
}
}
Now as stated, this isn't a concrete engine, so you have to create that as well. This part, fortunately, is much easier, all we need to do is set the default formats and actually create the views:
AreaAwareViewEngine.cs
public class AreaAwareViewEngine : BaseAreaAwareViewEngine
{
public AreaAwareViewEngine()
{
MasterLocationFormats = new string[]
{
"~/Areas/{2}/Views/{1}/{0}.master",
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.master",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Views/{1}/{0}.master",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.master"
"~/Views/Shared/{0}.cshtml"
};
ViewLocationFormats = new string[]
{
"~/Areas/{2}/Views/{1}/{0}.aspx",
"~/Areas/{2}/Views/{1}/{0}.ascx",
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.aspx",
"~/Areas/{2}/Views/Shared/{0}.ascx",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.aspx"
"~/Views/Shared/{0}.ascx"
"~/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = ViewLocationFormats;
}
protected override IView CreatePartialView(
ControllerContext controllerContext, string partialPath)
{
if (partialPath.EndsWith(".cshtml"))
return new System.Web.Mvc.RazorView(controllerContext, partialPath, null, false, null);
else
return new WebFormView(controllerContext, partialPath);
}
protected override IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)
{
if (viewPath.EndsWith(".cshtml"))
return new RazorView(controllerContext, viewPath, masterPath, false, null);
else
return new WebFormView(controllerContext, viewPath, masterPath);
}
}
Note that we've added few entries to the standard ViewLocationFormats. These are the new {2} entries, where the {2} will be mapped to the area we put in the RouteData. I've left the MasterLocationFormats alone, but obviously you can change that if you want.
Now modify your global.asax to register this view engine:
Global.asax.cs
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new AreaAwareViewEngine());
}
...and register the default route:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Area",
"",
new { area = "AreaZ", controller = "Default", action = "ActionY" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
Now Create the AreaController we just referenced:
DefaultController.cs (in ~/Controllers/)
public class DefaultController : Controller
{
public ActionResult ActionY()
{
return View("TestView");
}
}
Obviously we need the directory structure and view to go with it - we'll keep this super simple:
TestView.aspx (in ~/Areas/AreaZ/Views/Default/ or ~/Areas/AreaZ/Views/Shared/)
<%# Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<h2>TestView</h2>
This is a test view in AreaZ.
And that's it. Finally, we're done.
For the most part, you should be able to just take the BaseAreaAwareViewEngine and AreaAwareViewEngine and drop it into any MVC project, so even though it took a lot of code to get this done, you only have to write it once. After that, it's just a matter of editing a few lines in global.asax.cs and creating your site structure.
This is how I did it. I don't know why MapRoute() doesn't allow you to set the area, but it does return the route object so you can continue to make any additional changes you would like. I use this because I have a modular MVC site that is sold to enterprise customers and they need to be able to drop dlls into the bin folder to add new modules. I allow them to change the "HomeArea" in the AppSettings config.
var route = routes.MapRoute(
"Home_Default",
"",
new {controller = "Home", action = "index" },
new[] { "IPC.Web.Core.Controllers" }
);
route.DataTokens["area"] = area;
Edit: You can try this as well in your AreaRegistration.RegisterArea for the area you want the user going to by default. I haven't tested it but AreaRegistrationContext.MapRoute does sets route.DataTokens["area"] = this.AreaName; for you.
context.MapRoute(
"Home_Default",
"",
new {controller = "Home", action = "index" },
new[] { "IPC.Web.Core.Controllers" }
);
even it was answered already - this is the short syntax (ASP.net 3, 4, 5):
routes.MapRoute("redirect all other requests", "{*url}",
new {
controller = "UnderConstruction",
action = "Index"
}).DataTokens = new RouteValueDictionary(new { area = "Shop" });
Thanks to Aaron for pointing out that it's about locating the views, I misunderstood that.
[UPDATE] I just created a project that sends the user to an Area per default without messing with any of the code or lookup paths:
In global.asax, register as usual:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = ""} // Parameter defaults,
);
}
in Application_Start(), make sure to use the following order;
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
in you area registration, use
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"ShopArea_default",
"{controller}/{action}/{id}",
new { action = "Index", id = "", controller = "MyRoute" },
new { controller = "MyRoute" }
);
}
An example can be found at
http://www.emphess.net/2010/01/31/areas-routes-and-defaults-in-mvc-2-rc/
I really hope that this is what you were asking for...
////
I don't think that writing a pseudo ViewEngine is the best solution in this case. (Lacking reputation, I can't comment). The WebFormsViewEngine is Area aware and contains AreaViewLocationFormats which is defined per default as
AreaViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.aspx",
"~/Areas/{2}/Views/{1}/{0}.ascx",
"~/Areas/{2}/Views/Shared/{0}.aspx",
"~/Areas/{2}/Views/Shared/{0}.ascx",
};
I believe you don't adhere to this convention. You posted
public ActionResult ActionY()
{
return View("~/Areas/AreaZ/views/ActionY.aspx");
}
as a working hack, but that should be
return View("~/Areas/AreaZ/views/ControllerX/ActionY.aspx");
IF you don't want to follow the convention, however, you might want to take a short path by either deriving from the WebFormViewEngine (that is done in MvcContrib, for example) where you can set the lookup paths in the constructor, or -a little hacky- by specifying your convention like this on Application_Start:
((VirtualPathProviderViewEngine)ViewEngines.Engines[0]).AreaViewLocationFormats = ...;
This should be performed with a little more care, of course, but I think it shows the idea. These fields are public in VirtualPathProviderViewEngine in MVC 2 RC.
I guess you want user to be redirected to ~/AreaZ URL once (s)he has visited ~/ URL.
I'd achieve by means of the following code within your root HomeController.
public class HomeController
{
public ActionResult Index()
{
return RedirectToAction("ActionY", "ControllerX", new { Area = "AreaZ" });
}
}
And the following route in Global.asax.
routes.MapRoute(
"Redirection to AreaZ",
String.Empty,
new { controller = "Home ", action = "Index" }
);
First, what version of MVC2 are you using? There have been significant changes from preview2 to RC.
Assuming you use the RC, I think you route-mapping should look differently. In the AreaRegistration.cs in your area, you can register some kind of default route, e.g.
context.MapRoute(
"ShopArea_default",
"{controller}/{action}/{id}",
new { action = "Index", id = "", controller="MyRoute" }
);
The code above will send the user to the MyRouteController in our ShopArea per default.
Using an empty string as a second parameter should throw an exception, because a controller must be specified.
Of course you will have to change the default route in Global.asax so it doesn't interfere with this default route, e.g. by using a prefix for the main site.
Also see this thread and Haack's answer: MVC 2 AreaRegistration Routes Order
Hope this helps.
Adding the following to my Application_Start works for me, although I'm not sure if you have this setting in RC:
var engine = (WebFormViewEngine)ViewEngines.Engines.First();
// These additions allow me to route default requests for "/" to the home area
engine.ViewLocationFormats = new string[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Areas/{1}/Views/{1}/{0}.aspx", // new
"~/Areas/{1}/Views/{1}/{0}.ascx", // new
"~/Areas/{1}/Views/{0}.aspx", // new
"~/Areas/{1}/Views/{0}.ascx", // new
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
What I did to get this to work is the following:
I created a default controller in the root/Controllers folder. I named my controller DefaultController.
In the controller I added the following code:
namespace MyNameSpace.Controllers {
public class DefaultController : Controller {
// GET: Default
public ActionResult Index() {
return RedirectToAction("Index", "ControllerName", new {area = "FolderName"});
}
} }
In my RouterConfig.cs I added the following:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new {controller = "Default", action = "Index", id = UrlParameter.Optional});
The trick behind all this is that I made a default constructor which will always be the startup controller every time my app starts. When it hits that default controller it will redirect to any controller I specify in the default Index Action. Which in my case is
www.myurl.com/FolderName/ControllerName
.
routes.MapRoute(
"Area",
"{area}/",
new { area = "AreaZ", controller = "ControlerX ", action = "ActionY " }
);
Have you tried that ?
Locating the different building blocks is done in the request life cycle. One of the first steps in the ASP.NET MVC request life cycle is mapping the requested URL to the correct controller action method. This process is referred to as routing. A default route is initialized in the Global.asax file and describes to the ASP.NET MVC framework how to handle a request. Double-clicking on the Global.asax file in the MvcApplication1 project will display the following code:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing;
namespace MvcApplication1 {
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index",
id = "" } // Parameter defaults
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}
In the Application_Start() event handler, which is fired whenever the application is compiled or the web server is restarted, a route table is registered. The default route is named Default, and responds to a URL in the form of http://www.example.com/{controller}/{action}/{id}. The variables between { and } are populated with actual values from the request URL or with the default values if no override is present in the URL. This default route will map to the Home controller and to the Index action method, according to the default routing parameters. We won't have any other action with this routing map.
By default, all the possible URLs can be mapped through this default route. It is also possible to create our own routes. For example, let's map the URL http://www.example.com/Employee/Maarten to the Employee controller, the Show action, and the firstname parameter. The following code snippet can be inserted in the Global.asax file we've just opened. Because the ASP.NET MVC framework uses the first matching route, this code snippet should be inserted above the default route; otherwise the route will never be used.
routes.MapRoute(
"EmployeeShow", // Route name
"Employee/{firstname}", // URL with parameters
new { // Parameter defaults
controller = "Employee",
action = "Show",
firstname = ""
}
);
Now, let's add the necessary components for this route. First of all, create a class named EmployeeController in the Controllers folder. You can do this by adding a new item to the project and selecting the MVC Controller Class template located under the Web | MVC category. Remove the Index action method, and replace it with a method or action named Show. This method accepts a firstname parameter and passes the data into the ViewData dictionary. This dictionary will be used by the view to display data.
The EmployeeController class will pass an Employee object to the view. This Employee class should be added in the Models folder (right-click on this folder and then select Add | Class from the context menu). Here's the code for the Employee class:
namespace MvcApplication1.Models {
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}
Well, while creating a custom view engine can work for this, still you can have an alternative:
Decide what you need to show by default.
That something has controller and action (and Area), right?
Open that Area registration and add something like this:
public override void RegisterArea(AreaRegistrationContext context)
{
//this makes it work for the empty url (just domain) to act as current Area.
context.MapRoute(
"Area_empty",
"",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "Area controller namespace" }
);
//other routes of the area
}
Cheers!
Accepted solution to this question is, while correct in summing up how to create a custom view engine, does not answer the question correctly. Issue here is that Pino is incorrectly specifying his default route. Particularly his "area" definition is incorrect. "Area" is checked via DataTokens collection and should be added as such:
var defaultRoute = new Route("",new RouteValueDictionary(){{"controller","Default"},{"action","Index"}},null/*constraints*/,new RouteValueDictionary(){{"area","Admin"}},new MvcRouteHandler());
defaultRoute.DataTokens.Add("Namespaces","MyProject.Web.Admin.Controller");
routes.Add(defaultRoute);
Specified "area" in defaults object will be ignored. Code above creates a default route, which catches on requests to your site's root and then calls Default controller, Index action in Admin area. Please also note "Namespaces" key being added to DataTokens, this is only required if you have multiple controllers with same name. This solution is verified with Mvc2 and Mvc3 .NET 3.5/4.0
ummm, I don't know why all this programming, I think the original problem is solved easily by specifying this default route ...
routes.MapRoute("Default", "{*id}",
new { controller = "Home"
, action = "Index"
, id = UrlParameter.Optional
}
);

How to simulate Server.Transfer in ASP.NET MVC?

In ASP.NET MVC you can return a redirect ActionResult quite easily:
return RedirectToAction("Index");
or
return RedirectToRoute(new { controller = "home", version = Math.Random() * 10 });
This will actually give an HTTP redirect, which is normally fine. However, when using Google Analytics this causes big issues because the original referrer is lost, so Google doesn't know where you came from. This loses useful information such as any search engine terms.
As a side note, this method has the advantage of removing any parameters that may have come from campaigns but still allows me to capture them server side. Leaving them in the query string leads to people bookmarking or twitter or blog a link that they shouldn't. I've seen this several times where people have twittered links to our site containing campaign IDs.
Anyway, I am writing a 'gateway' controller for all incoming visits to the site which I may redirect to different places or alternative versions.
For now I care more about Google for now (than accidental bookmarking), and I want to be able to send someone who visits / to the page that they would get if they went to /home/7, which is version 7 of a homepage.
Like I said before if I do this I lose the ability for google to analyse the referrer:
return RedirectToAction(new { controller = "home", version = 7 });
What I really want is a
return ServerTransferAction(new { controller = "home", version = 7 });
which will get me that view without a client side redirect.
I don't think such a thing exists, though.
Currently the best thing I can come up with is to duplicate the whole controller logic for HomeController.Index(..) in my GatewayController.Index Action. This means I had to move 'Views/Home' into 'Shared' so it was accessible. There must be a better way.
How about a TransferResult class? (based on Stans answer)
/// <summary>
/// Transfers execution to the supplied url.
/// </summary>
public class TransferResult : ActionResult
{
public string Url { get; private set; }
public TransferResult(string url)
{
this.Url = url;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var httpContext = HttpContext.Current;
// MVC 3 running on IIS 7+
if (HttpRuntime.UsingIntegratedPipeline)
{
httpContext.Server.TransferRequest(this.Url, true);
}
else
{
// Pre MVC 3
httpContext.RewritePath(this.Url, false);
IHttpHandler httpHandler = new MvcHttpHandler();
httpHandler.ProcessRequest(httpContext);
}
}
}
Updated: Now works with MVC3 (using code from Simon's post). It should (haven't been able to test it) also work in MVC2 by looking at whether or not it's running within the integrated pipeline of IIS7+.
For full transparency; In our production environment we've never use the TransferResult directly. We use a TransferToRouteResult which in turn calls executes the TransferResult. Here's what's actually running on my production servers.
public class TransferToRouteResult : ActionResult
{
public string RouteName { get;set; }
public RouteValueDictionary RouteValues { get; set; }
public TransferToRouteResult(RouteValueDictionary routeValues)
: this(null, routeValues)
{
}
public TransferToRouteResult(string routeName, RouteValueDictionary routeValues)
{
this.RouteName = routeName ?? string.Empty;
this.RouteValues = routeValues ?? new RouteValueDictionary();
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var urlHelper = new UrlHelper(context.RequestContext);
var url = urlHelper.RouteUrl(this.RouteName, this.RouteValues);
var actualResult = new TransferResult(url);
actualResult.ExecuteResult(context);
}
}
And if you're using T4MVC (if not... do!) this extension might come in handy.
public static class ControllerExtensions
{
public static TransferToRouteResult TransferToAction(this Controller controller, ActionResult result)
{
return new TransferToRouteResult(result.GetRouteValueDictionary());
}
}
Using this little gem you can do
// in an action method
TransferToAction(MVC.Error.Index());
Edit: Updated to be compatible with ASP.NET MVC 3
Provided you are using IIS7 the following modification seems to work for ASP.NET MVC 3.
Thanks to #nitin and #andy for pointing out the original code didn't work.
Edit 4/11/2011: TempData breaks with Server.TransferRequest as of MVC 3 RTM
Modified the code below to throw an exception - but no other solution at this time.
Here's my modification based upon Markus's modifed version of Stan's original post. I added an additional constructor to take a Route Value dictionary - and renamed it MVCTransferResult to avoid confusion that it might just be a redirect.
I can now do the following for a redirect:
return new MVCTransferResult(new {controller = "home", action = "something" });
My modified class :
public class MVCTransferResult : RedirectResult
{
public MVCTransferResult(string url)
: base(url)
{
}
public MVCTransferResult(object routeValues):base(GetRouteURL(routeValues))
{
}
private static string GetRouteURL(object routeValues)
{
UrlHelper url = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData()), RouteTable.Routes);
return url.RouteUrl(routeValues);
}
public override void ExecuteResult(ControllerContext context)
{
var httpContext = HttpContext.Current;
// ASP.NET MVC 3.0
if (context.Controller.TempData != null &&
context.Controller.TempData.Count() > 0)
{
throw new ApplicationException("TempData won't work with Server.TransferRequest!");
}
httpContext.Server.TransferRequest(Url, true); // change to false to pass query string parameters if you have already processed them
// ASP.NET MVC 2.0
//httpContext.RewritePath(Url, false);
//IHttpHandler httpHandler = new MvcHttpHandler();
//httpHandler.ProcessRequest(HttpContext.Current);
}
}
You can use Server.TransferRequest on IIS7+ instead.
I found out recently that ASP.NET MVC doesn't support Server.Transfer() so I've created a stub method (inspired by Default.aspx.cs).
private void Transfer(string url)
{
// Create URI builder
var uriBuilder = new UriBuilder(Request.Url.Scheme, Request.Url.Host, Request.Url.Port, Request.ApplicationPath);
// Add destination URI
uriBuilder.Path += url;
// Because UriBuilder escapes URI decode before passing as an argument
string path = Server.UrlDecode(uriBuilder.Uri.PathAndQuery);
// Rewrite path
HttpContext.Current.RewritePath(path, false);
IHttpHandler httpHandler = new MvcHttpHandler();
// Process request
httpHandler.ProcessRequest(HttpContext.Current);
}
Couldn't you just create an instance of the controller you would like to redirect to, invoke the action method you want, then return the result of that? Something like:
HomeController controller = new HomeController();
return controller.Index();
Rather than simulate a server transfer, MVC is still capable of actually doing a Server.TransferRequest:
public ActionResult Whatever()
{
string url = //...
Request.RequestContext.HttpContext.Server.TransferRequest(url);
return Content("success");//Doesn't actually get returned
}
I wanted to re-route the current request to another controller/action, while keeping the execution path exactly the same as if that second controller/action was requested. In my case, Server.Request wouldn't work because I wanted to add more data. This is actually equivalent the current handler executing another HTTP GET/POST, then streaming the results to the client. I'm sure there will be better ways to achieve this, but here's what works for me:
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Public");
routeData.Values.Add("action", "ErrorInternal");
routeData.Values.Add("Exception", filterContext.Exception);
var context = new HttpContextWrapper(System.Web.HttpContext.Current);
var request = new RequestContext(context, routeData);
IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(filterContext.RequestContext, "Public");
controller.Execute(request);
Your guess is right: I put this code in
public class RedirectOnErrorAttribute : ActionFilterAttribute, IExceptionFilter
and I'm using it to display errors to developers, while it'll be using a regular redirect in production. Note that I didn't want to use ASP.NET session, database, or some other ways to pass exception data between requests.
Just instance the other controller and execute it's action method.
You could new up the other controller and invoke the action method returning the result. This will require you to place your view into the shared folder however.
I'm not sure if this is what you meant by duplicate but:
return new HomeController().Index();
Edit
Another option might be to create your own ControllerFactory, this way you can determine which controller to create.
Server.TransferRequest is completely unnecessary in MVC. This is an antiquated feature that was only necessary in ASP.NET because the request came directly to a page and there needed to be a way to transfer a request to another page. Modern versions of ASP.NET (including MVC) have a routing infrastructure that can be customized to route directly to the resource that is desired. There is no point of letting the request reach a controller only to transfer it to another controller when you can simply make the request go directly to the controller and action you want.
What's more is that since you are responding to the original request, there is no need to tuck anything into TempData or other storage just for the sake of routing the request to the right place. Instead, you arrive at the controller action with the original request intact. You also can be rest assured that Google will approve of this approach as it happens entirely on the server side.
While you can do quite a bit from both IRouteConstraint and IRouteHandler, the most powerful extension point for routing is the RouteBase subclass. This class can be extended to provide both incoming routes and outgoing URL generation, which makes it a one stop shop for everything having to do with the URL and the action that URL executes.
So, to follow your second example, to get from / to /home/7, you simply need a route that adds the appropriate route values.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Routes directy to `/home/7`
routes.MapRoute(
name: "Home7",
url: "",
defaults: new { controller = "Home", action = "Index", version = 7 }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
But going back to your original example where you have a random page, it is more complex because the route parameters cannot change at runtime. So, it could be done with a RouteBase subclass as follows.
public class RandomHomePageRoute : RouteBase
{
private Random random = new Random();
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Only handle the home page route
if (httpContext.Request.Path == "/")
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = "Home";
result.Values["action"] = "Index";
result.Values["version"] = random.Next(10) + 1; // Picks a random number from 1 to 10
}
// If this isn't the home page route, this should return null
// which instructs routing to try the next route in the route table.
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
if (controller.Equals("Home", StringComparison.OrdinalIgnoreCase) &&
action.Equals("Index", StringComparison.OrdinalIgnoreCase))
{
// Route to the Home page URL
return new VirtualPathData(this, "");
}
return null;
}
}
Which can be registered in routing like:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Routes to /home/{version} where version is randomly from 1-10
routes.Add(new RandomHomePageRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Note in the above example, it might make sense to also store a cookie recording the home page version the user came in on so when they return they receive the same home page version.
Note also that using this approach you can customize routing to take query string parameters into consideration (it completely ignores them by default) and route to an appropriate controller action accordingly.
Additional Examples
https://stackoverflow.com/a/31958586
https://stackoverflow.com/a/36774498
https://stackoverflow.com/a/36168395
Doesn't routing just take care of this scenario for you? i.e. for the scenario described above, you could just create a route handler that implemented this logic.
For anyone using expression-based routing, using only the TransferResult class above, here's a controller extension method that does the trick and preserves TempData. No need for TransferToRouteResult.
public static ActionResult TransferRequest<T>(this Controller controller, Expression<Action<T>> action)
where T : Controller
{
controller.TempData.Keep();
controller.TempData.Save(controller.ControllerContext, controller.TempDataProvider);
var url = LinkBuilder.BuildUrlFromExpression(controller.Request.RequestContext, RouteTable.Routes, action);
return new TransferResult(url);
}
Not an answer per se, but clearly the requirement would be not only for the actual navigation to "do" the equivalent functionality of Webforms Server.Transfer(), but also for all of this to be fully supported within unit testing.
Therefore the ServerTransferResult should "look" like a RedirectToRouteResult, and be as similar as possible in terms of the class hierarchy.
I'm thinking of doing this by looking at Reflector, and doing whatever RedirectToRouteResult class and also the various Controller base class methods do, and then "adding" the latter to the Controller via extension methods. Maybe these could be static methods within the same class, for ease/laziness of downloading?
If I get round to doing this I'll post it up, otherwise maybe somebody else might beat me to it!
I achieved this by harnessing the Html.RenderAction helper in a View:
#{
string action = ViewBag.ActionName;
string controller = ViewBag.ControllerName;
object routeValues = ViewBag.RouteValues;
Html.RenderAction(action, controller, routeValues);
}
And in my controller:
public ActionResult MyAction(....)
{
var routeValues = HttpContext.Request.RequestContext.RouteData.Values;
ViewBag.ActionName = "myaction";
ViewBag.ControllerName = "mycontroller";
ViewBag.RouteValues = routeValues;
return PartialView("_AjaxRedirect");
}

Resources