ASP .NET MVC VirtualPathProvider - asp.net

I am writing a VirtualPathProvider to dynamically load my MVC views, which are located in a different directory. I successfully intercept the call before MVC (in FileExists), but in my VirtualPathProvider, I get the raw, pre-routed url like:
~/Apps/Administration/Account/LogOn
Personally, I know that MVC will look for
~/Apps/Administration/Views/Account/LogOn.aspx
and that I should be reading the file contents from
D:\SomeOtherNonWebRootDirectory\Apps\Administration\Views\Account\LogOn.aspx
but I'd rather not hard code the logic to "add the directory named Views and add aspx to the end".
Where is this logic stored and how can I get it into my virtual path provider?
Thanks. Sorry if I'm not being clear.

Edited
You need to make a class that inherits WebFormViewEngine and sets the ViewLocationFormats property (inherited from VirtualPathProviderViewEngine).
The default values can be found in the MVC source code:
public WebFormViewEngine() {
MasterLocationFormats = new[] {
"~/Views/{1}/{0}.master",
"~/Views/Shared/{0}.master"
};
AreaMasterLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.master",
"~/Areas/{2}/Views/Shared/{0}.master",
};
ViewLocationFormats = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
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",
};
PartialViewLocationFormats = ViewLocationFormats;
AreaPartialViewLocationFormats = AreaViewLocationFormats;
}
You should then clear the ViewEngines.Engines collection and add your ViewEngine instance to it.

As SLaks mentioned above, you need to create a Custom View Engine and add your view-finding logic in the FindView method.
public class CustomViewEngine : VirtualPathProviderViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
//Set view path
string viewPath = GetCurrentViewPath();
//Set master path (if need be)
string masterPath = GetCurrentMasterPath();
return base.FindView(controllerContext, viewPath, masterPath, useCache);
}
}
In the Application_Start, you can register your View Engine like this:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());

The answer was that MVC was not finding my controller properly. If MVC does in fact find your controller properly, there should be two requests processed by the VirtualPathProvider:
An initial request with the acutal url requested (ie. http://.../Account/LogOn).
A subsequent FileExists check for http://.../Views/Account/LogOn.aspx, after the request in 1. returns false calling FileExists. This actually retuns the aspx content.

Related

ASP.NET VirtualPathProvider .Execute()': no suitable method found to override

I am currently trying to implement Razor Web Pages in older WebForms project, and also, make it possible to render partial views from string (taken from database elsewhere). I've implemented custom VirtualPathProvider with all overrides specified here: ASP.NET MVC and Virtual views and also overwritten these methods:
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
if (virtualPath.Contains("RazorMigration.cshtml") && HttpContext.Current.Items.Contains("RazorTestingPage"))
{
return null;
}
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override String GetFileHash(String virtualPath, IEnumerable virtualPathDependencies)
{
if (virtualPath.Contains("RazorMigration.cshtml") && HttpContext.Current.Items.Contains("RazorTestingPage"))
{
return Guid.NewGuid().ToString();
}
return Previous.GetFileHash(virtualPath, virtualPathDependencies);
}
Then when I am trying to actually render page (completeTemplate already contains pure HTML with razor already parsed) like this:
var rt = new RouteData();
rt.Values.Add("controller", "WebFormShimController");
var httpCtx = new HttpContextWrapper(System.Web.HttpContext.Current);
var ctx = new ControllerContext(new RequestContext(httpCtx, rt), new WebFormShimController());
try
{
HttpContext.Current.Items.Add("RazorTestingPage", completeTemplate);
IView view = ViewEngines.Engines.FindPartialView(ctx, System.IO.Path.GetFileName("RazorMigration")).View;
ViewContext vctx = new ViewContext(ctx, view,
new ViewDataDictionary { Model = model },
new TempDataDictionary(), httpCtx.Response.Output);
view.Render(vctx, System.Web.HttpContext.Current.Response.Output);
}
I always catch Exception On view.Render line saying this:
c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\root\0e7dfb6a\c63cc9d1\App_Web_razormigration.cshtml.afd51fd3.jujzzimy.0.cs(41): error CS1009: Unrecognized escape sequence
I am not really sure what is the issue here, or where and how this path is constructed. If someone could point me in the right direction I would be very happy as I am trying to get it working for almost a week, but still no success.
EDIT: I found the bug - It was in my encoding when writing to file (so when file was written \NUL characters were added, which confused IIS)
Now I am getting this error:
CS0115: c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\root\0e7dfb6a\c63cc9d1\App_Web_4b7b71f4-87c2-4364-be0e-19ec2a81ceccR.0.cs.Execute()': no suitable method found to override
I am sure I don't have error in my view as it only contains this:
<h1>Hello</h1>

Infinite URL Parameters for ASP.NET MVC Route

I need an implementation where I can get infinite parameters on my ASP.NET Controller. It will be better if I give you an example :
Let's assume that I will have following urls :
example.com/tag/poo/bar/poobar
example.com/tag/poo/bar/poobar/poo2/poo4
example.com/tag/poo/bar/poobar/poo89
As you can see, it will get infinite number of tags after example.com/tag/ and slash will be a delimiter here.
On the controller I would like to do this :
foreach(string item in paramaters) {
//this is one of the url paramaters
string poo = item;
}
Is there any known way to achieve this? How can I get reach the values from controller? With Dictionary<string, string> or List<string>?
NOTE :
The question is not well explained IMO but I tried my best to fit it.
in. Feel free to tweak it
Like this:
routes.MapRoute("Name", "tag/{*tags}", new { controller = ..., action = ... });
ActionResult MyAction(string tags) {
foreach(string tag in tags.Split("/")) {
...
}
}
The catch all will give you the raw string. If you want a more elegant way to handle the data, you could always use a custom route handler.
public class AllPathRouteHandler : MvcRouteHandler
{
private readonly string key;
public AllPathRouteHandler(string key)
{
this.key = key;
}
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var allPaths = requestContext.RouteData.Values[key] as string;
if (!string.IsNullOrEmpty(allPaths))
{
requestContext.RouteData.Values[key] = allPaths.Split('/');
}
return base.GetHttpHandler(requestContext);
}
}
Register the route handler.
routes.Add(new Route("tag/{*tags}",
new RouteValueDictionary(
new
{
controller = "Tag",
action = "Index",
}),
new AllPathRouteHandler("tags")));
Get the tags as a array in the controller.
public ActionResult Index(string[] tags)
{
// do something with tags
return View();
}
That's called catch-all:
tag/{*tags}
Just in case anyone is coming to this with MVC in .NET 4.0, you need to be careful where you define your routes. I was happily going to global.asax and adding routes as suggested in these answers (and in other tutorials) and getting nowhere. My routes all just defaulted to {controller}/{action}/{id}. Adding further segments to the URL gave me a 404 error. Then I discovered the RouteConfig.cs file in the App_Start folder. It turns out this file is called by global.asax in the Application_Start() method. So, in .NET 4.0, make sure you add your custom routes there. This article covers it beautifully.
in asp .net core you can use * in routing
for example
[HTTPGet({*id})]
this code can multi parameter or when using send string with slash use them to get all parameters

Adding sub-directory to "View/Shared" folder in ASP.Net MVC and calling the view

I'm currently developing a site using ASP.Net MVC3 with Razor. Inside the "View/Shared" folder, I want to add a subfolder called "Partials" where I can place all of my partial views (for the sake of organizing the site better.
I can do this without a problem as long as I always reference the "Partials" folder when calling the views (using Razor):
#Html.Partial("Partials/{ViewName}")
My question is if there is a way to add the "Partials" folder to the list that .Net goes through when searching for a view, this way I can call my view without having to reference the "Partials" folder, like so:
#Html.Partial("{ViewName}")
Thanks for the help!
Solved this. To add the "Shared/Partials" sub directory I created to the list of locations searched when trying to locate a Partial View in Razor using:
#Html.Partial("{NameOfView}")
First create a view engine with RazorViewEngine as its base class and add your view locations as follows. Again, I wanted to store all of my partial views in a "Partials" subdirectory that I created within the default "Views/Shared" directory created by MVC.
public class RDDBViewEngine : RazorViewEngine
{
private static readonly string[] NewPartialViewFormats =
{
"~/Views/{1}/Partials/{0}.cshtml",
"~/Views/Shared/Partials/{0}.cshtml"
};
public RDDBViewEngine()
{
base.PartialViewLocationFormats = base.PartialViewLocationFormats.Union(NewPartialViewFormats).ToArray();
}
}
Note that {1} in the location format is the Controller name and {0} is the name of the view.
Then add that view engine to the MVC ViewEngines.Engines Collection in the Application_Start() method in your global.asax:
ViewEngines.Engines.Add(new RDDBViewEngine());
Thank you for your answers. This has organized my Shared folder, but why do you create a new type of view engine? I just made a new RazorViewEngine, set it's PartialViewLocationFormats and added it to the list of ViewEngines.
ViewEngines.Engines.Add(new RazorViewEngine
{
PartialViewLocationFormats = new string[]
{
"~/Views/{1}/Partials/{0}.cshtml",
"~/Views/Shared/Partials/{0}.cshtml"
}
});
It´s nice to custom the view engine, but if you just want to have a subfolder por partials you don´t need that much...
Just use the full path to the partial view, as done for the Layout View:
#Html.Partial("/Views/Shared/Partial/myPartial.cshtml")
Hope it helps someone...
If you are doing this in ASP.NET Core, simple go to the Startup class, under ConfigureServices method, and put
services.AddMvc()
.AddRazorOptions(opt => {
opt.ViewLocationFormats.Add("/Views/{1}/Partials/{0}.cshtml");
opt.ViewLocationFormats.Add("/Views/Shared/Partials/{0}.cshtml");
});
I've updated lamarant's excellent answer to include Areas:
public class RDDBViewEngine : RazorViewEngine
{
private static readonly string[] NewPartialViewFormats =
{
"~/Views/{1}/Partials/{0}.cshtml",
"~/Views/Shared/Partials/{0}.cshtml"
};
private static List<string> AreaRegistrations;
public RDDBViewEngine()
{
AreaRegistrations = new List<string>();
BuildAreaRegistrations();
base.PartialViewLocationFormats = base.PartialViewLocationFormats.Union(NewPartialViewFormats).ToArray();
base.PartialViewLocationFormats = base.PartialViewLocationFormats.Union(areaRegistrations).ToArray();
}
private static void BuildAreaRegistrations()
{
string[] areaNames = RouteTable.Routes.OfType<Route>()
.Where(d => d.DataTokens != null && d.DataTokens.ContainsKey("area"))
.Select(r => r.DataTokens["area"].ToString()).ToArray();
foreach (string areaName in areaNames)
{
AreaRegistrations.Add("~/Areas/" + areaName + "/Views/Shared/Partials/{0}.cshtml");
AreaRegistrations.Add("~/Areas/" + areaName + "/Views/{1}/Partials/{0}.cshtml");
}
}
}
Then don't forget to include in your application start:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
...
ViewEngines.Engines.Add(new RDDBViewEngine());
}
}
You can also update the partialview-location-formats of the registered RazorViewEngine.
Place the below code in Application_Start in Global.asax.
RazorViewEngine razorEngine = ViewEngines.Engines.OfType<RazorViewEngine>().FirstOrDefault();
if (razorEngine != null)
{
string[] newPartialViewFormats = new[] {
"~/Views/{1}/Partials/{0}.cshtml",
"~/Views/Shared/Partials/{0}.cshtml"
};
razorEngine.PartialViewLocationFormats =
razorEngine.PartialViewLocationFormats.Union(newPartialViewFormats).ToArray();
}
You can create register your own view engine that derives from whatever view engine your are using (Webforms/Razor) and specify whatever locations you want in the constructor or just add them to the list of already existing locations:
this.PartialViewLocationFormats = viewLocations;
Then in application start you would add your view engine like so:
ViewEngines.Engines.Add(new MyViewEngineWithPartialPath());

Can ASP.NET Routing be used to create "clean" URLs for .ashx (IHttpHander) handlers?

I have some REST services using plain old IHttpHandlers. I'd like to generate cleaner URLs, so that I don't have the .ashx in the path. Is there a way to use ASP.NET routing to create routes that map to ashx handlers? I've seen these types of routes previously:
// Route to an aspx page
RouteTable.Routes.MapPageRoute("route-name",
"some/path/{arg}",
"~/Pages/SomePage.aspx");
// Route for a WCF service
RouteTable.Routes.Add(new ServiceRoute("Services/SomeService",
new WebServiceHostFactory(),
typeof(SomeService)));
Trying to use RouteTable.Routes.MapPageRoute() generates an error (that the handler does not derive from Page). System.Web.Routing.RouteBase only seems to have 2 derived classes: ServiceRoute for services, and DynamicDataRoute for MVC. I'm not sure what MapPageRoute() does (Reflector doesn't show the method body, it just shows "Performance critical to inline this type of method across NGen image boundaries").
I see that RouteBase is not sealed, and has a relatively simple interface:
public abstract RouteData GetRouteData(HttpContextBase httpContext);
public abstract VirtualPathData GetVirtualPath(RequestContext requestContext,
RouteValueDictionary values);
So perhaps I can make my own HttpHandlerRoute. I'll give that a shot, but if anyone knows of an existing or built-in way of mapping routes to IHttpHandlers, that would be great.
Ok, I've been figuring this out since I originally asked the question, and I finally have a solution that does just what I want. A bit of up front explanation is due, however. IHttpHandler is a very basic interface:
bool IsReusable { get; }
void ProcessRequest(HttpContext context)
There is no built in property for accessing the route data, and the route data also can't be found in the context or the request. A System.Web.UI.Page object has a RouteData property , ServiceRoutes do all the work of interpreting your UriTemplates and passing the values to the correct method internally, and ASP.NET MVC provides its own way of accessing the route data. Even if you had a RouteBase that (a) determined if the incoming url was a match for your route and (b) parsed the url to extract all of the individual values to be used from within your IHttpHandler, there's no easy way to pass that route data to your IHttpHandler. If you want to keep your IHttpHandler "pure", so to speak, it takes responsibility for dealing with the url, and how to extract any values from it. The RouteBase implementation in this case is only used to determine if your IHttpHandler should be used at all.
One problem remains, however. Once the RouteBase determines that the incoming url is a match for your route, it passes off to an IRouteHandler, which creates the instances of the IHttpHandler you want to handle your request. But, once you're in your IHttpHandler, the value of context.Request.CurrentExecutionFilePath is misleading. It's the url that came from the client, minus the query string. So it's not the path to your .ashx file. And, any parts of your route that are constant (such as the name of the method) will be part of that execution file path value. This can be a problem if you use UriTemplates within your IHttpHandler to determine which specific method within your IHttpHandler should handing the request.
Example: If you had a .ashx handler at /myApp/services/myHelloWorldHandler.ashx
And you had this route that mapped to the handler: "services/hello/{name}"
And you navigated to this url, trying to call the SayHello(string name) method of your handler:
http://localhost/myApp/services/hello/SayHello/Sam
Then your CurrentExecutionFilePath would be: /myApp/services/hello/Sam. It includes parts of the route url, which is a problem. You want the execution file path to match your route url. The below implementations of RouteBase and IRouteHandler deal with this problem.
Before I paste the 2 classes, here's a very simple usage example. Note that these implementations of RouteBase and IRouteHandler will actually work for IHttpHandlers that don't even have a .ashx file, which is pretty convenient.
// A "headless" IHttpHandler route (no .ashx file required)
RouteTable.Routes.Add(new GenericHandlerRoute<HeadlessService>("services/headless"));
That will cause all incoming urls that match the "services/headless" route to be handed off to a new instance of the HeadlessService IHttpHandler (HeadlessService is just an example in this case. It would be whatever IHttpHandler implementation you wanted to pass off to).
Ok, so here are the routing class implementations, comments and all:
/// <summary>
/// For info on subclassing RouteBase, check Pro Asp.NET MVC Framework, page 252.
/// Google books link: http://books.google.com/books?id=tD3FfFcnJxYC&pg=PA251&lpg=PA251&dq=.net+RouteBase&source=bl&ots=IQhFwmGOVw&sig=0TgcFFgWyFRVpXgfGY1dIUc0VX4&hl=en&ei=z61UTMKwF4aWsgPHs7XbAg&sa=X&oi=book_result&ct=result&resnum=6&ved=0CC4Q6AEwBQ#v=onepage&q=.net%20RouteBase&f=false
///
/// It explains how the asp.net runtime will call GetRouteData() for every route in the route table.
/// GetRouteData() is used for inbound url matching, and should return null for a negative match (the current requests url doesn't match the route).
/// If it does match, it returns a RouteData object describing the handler that should be used for that request, along with any data values (stored in RouteData.Values) that
/// that handler might be interested in.
///
/// The book also explains that GetVirtualPath() (used for outbound url generation) is called for each route in the route table, but that is not my experience,
/// as mine used to simply throw a NotImplementedException, and that never caused a problem for me. In my case, I don't need to do outbound url generation,
/// so I don't have to worry about it in any case.
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericHandlerRoute<T> : RouteBase where T : IHttpHandler, new()
{
public string RouteUrl { get; set; }
public GenericHandlerRoute(string routeUrl)
{
RouteUrl = routeUrl;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// See if the current request matches this route's url
string baseUrl = httpContext.Request.CurrentExecutionFilePath;
int ix = baseUrl.IndexOf(RouteUrl);
if (ix == -1)
// Doesn't match this route. Returning null indicates to the asp.net runtime that this route doesn't apply for the current request.
return null;
baseUrl = baseUrl.Substring(0, ix + RouteUrl.Length);
// This is kind of a hack. There's no way to access the route data (or even the route url) from an IHttpHandler (which has a very basic interface).
// We need to store the "base" url somewhere, including parts of the route url that are constant, like maybe the name of a method, etc.
// For instance, if the route url "myService/myMethod/{myArg}", and the request url were "http://localhost/myApp/myService/myMethod/argValue",
// the "current execution path" would include the "myServer/myMethod" as part of the url, which is incorrect (and it will prevent your UriTemplates from matching).
// Since at this point in the exectuion, we know the route url, we can calculate the true base url (excluding all parts of the route url).
// This means that any IHttpHandlers that use this routing mechanism will have to look for the "__baseUrl" item in the HttpContext.Current.Items bag.
// TODO: Another way to solve this would be to create a subclass of IHttpHandler that has a BaseUrl property that can be set, and only let this route handler
// work with instances of the subclass. Perhaps I can just have RestHttpHandler have that property. My reticence is that it would be nice to have a generic
// route handler that works for any "plain ol" IHttpHandler (even though in this case, you have to use the "global" base url that's stored in HttpContext.Current.Items...)
// Oh well. At least this works for now.
httpContext.Items["__baseUrl"] = baseUrl;
GenericHandlerRouteHandler<T> routeHandler = new GenericHandlerRouteHandler<T>();
RouteData rdata = new RouteData(this, routeHandler);
return rdata;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
// This route entry doesn't generate outbound Urls.
return null;
}
}
public class GenericHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new()
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new T();
}
}
I know this answer has been quite long winded, but it was not an easy problem to solve. The core logic was easy enough, the trick was to somehow make your IHttpHandler aware of the "base url", so that it could properly determine what parts of the url belong to the route, and what parts are actual arguments for the service call.
These classes will be used in my upcoming C# REST library, RestCake. I hope that my path down the routing rabbit hole will help anyone else who decides to RouteBase, and do cool stuff with IHttpHandlers.
I actually like Joel's solution better, as it doesn't require you to know the type of handler while you're trying to setup your routes. I'd upvote it, but alas, I haven't the reputation required.
I actually found a solution which I feel is better than both mentioned. The original source code I derived my example from can be found linked here http://weblogs.asp.net/leftslipper/archive/2009/10/07/introducing-smartyroute-a-smarty-ier-way-to-do-routing-in-asp-net-applications.aspx.
This is less code, type agnostic, and fast.
public class HttpHandlerRoute : IRouteHandler {
private String _VirtualPath = null;
public HttpHandlerRoute(String virtualPath) {
_VirtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
IHttpHandler httpHandler = (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(_VirtualPath, typeof(IHttpHandler));
return httpHandler;
}
}
And a rough example of use
String handlerPath = "~/UploadHandler.ashx";
RouteTable.Routes.Add(new Route("files/upload", new HttpHandlerRoute(handlerPath)));
EDIT: I just edited this code because I had some issues with the old one. If you're using the old version please update.
This thread is a bit old but I just re-wrote some of the code here to do the same thing but on a more elegant way, using an extension method.
I'm using this on ASP.net Webforms, and I like to have the ashx files on a folder and being able to call them either using routing or a normal request.
So I pretty much grabbed shellscape's code and made an extension method that does the trick. At the end I felt that I should also support passing the IHttpHandler object instead of its Url, so I wrote and overload of the MapHttpHandlerRoute method for that.
namespace System.Web.Routing
{
public class HttpHandlerRoute<T> : IRouteHandler where T: IHttpHandler
{
private String _virtualPath = null;
public HttpHandlerRoute(String virtualPath)
{
_virtualPath = virtualPath;
}
public HttpHandlerRoute() { }
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return Activator.CreateInstance<T>();
}
}
public class HttpHandlerRoute : IRouteHandler
{
private String _virtualPath = null;
public HttpHandlerRoute(String virtualPath)
{
_virtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
if (!string.IsNullOrEmpty(_virtualPath))
{
return (IHttpHandler)System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(_virtualPath, typeof(IHttpHandler));
}
else
{
throw new InvalidOperationException("HttpHandlerRoute threw an error because the virtual path to the HttpHandler is null or empty.");
}
}
}
public static class RoutingExtension
{
public static void MapHttpHandlerRoute(this RouteCollection routes, string routeName, string routeUrl, string physicalFile, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null)
{
var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute(physicalFile));
routes.Add(routeName, route);
}
public static void MapHttpHandlerRoute<T>(this RouteCollection routes, string routeName, string routeUrl, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null) where T : IHttpHandler
{
var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute<T>());
routes.Add(routeName, route);
}
}
}
I'm putting it inside the same namespace of all the native routing objects so it will be automatically available.
So to use this you just have to call:
// using the handler url
routes.MapHttpHandlerRoute("DoSomething", "Handlers/DoSomething", "~/DoSomething.ashx");
Or
// using the type of the handler
routes.MapHttpHandlerRoute<MyHttpHanler>("DoSomething", "Handlers/DoSomething");
Enjoy,
Alex
Yeah, I noticed that, too. Perhaps there is a built-in ASP.NET way to do this, but the trick for me was to create a new class derived from IRouteHandler:
using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;
namespace MyNamespace
{
class GenericHandlerRouteHandler : IRouteHandler
{
private string _virtualPath;
private Type _handlerType;
private static object s_lock = new object();
public GenericHandlerRouteHandler(string virtualPath)
{
_virtualPath = virtualPath;
}
#region IRouteHandler Members
public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
{
ResolveHandler();
IHttpHandler handler = (IHttpHandler)Activator.CreateInstance(_handlerType);
return handler;
}
#endregion
private void ResolveHandler()
{
if (_handlerType != null)
return;
lock (s_lock)
{
// determine physical path of ashx
string path = _virtualPath.Replace("~/", HttpRuntime.AppDomainAppPath);
if (!File.Exists(path))
throw new FileNotFoundException("Generic handler " + _virtualPath + " could not be found.");
// parse the class name out of the .ashx file
// unescaped reg-ex: (?<=Class=")[a-zA-Z\.]*
string className;
Regex regex = new Regex("(?<=Class=\")[a-zA-Z\\.]*");
using (var sr = new StreamReader(path))
{
string str = sr.ReadToEnd();
Match match = regex.Match(str);
if (match == null)
throw new InvalidDataException("Could not determine class name for generic handler " + _virtualPath);
className = match.Value;
}
// get the class type from the name
Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly asm in asms)
{
_handlerType = asm.GetType(className);
if (_handlerType != null)
break;
}
if (_handlerType == null)
throw new InvalidDataException("Could not find type " + className + " in any loaded assemblies.");
}
}
}
}
To create a route for an .ashx:
IRouteHandler routeHandler = new GenericHandlerRouteHandler("~/somehandler.ashx");
Route route = new Route("myroute", null, null, null, routeHandler);
RouteTable.Routes.Add(route);
The code above may need to be enhanced to work with your route arguments, but it's starting point. Comments welcome.
All of these answers are very good. I love the simplicity of Mr. Meacham's GenericHandlerRouteHandler<T> class. It is a great idea to eliminate an unnecessary reference to a virtual path if you know the specific HttpHandler class. The GenericHandlerRoute<T> class is not needed, however. The existing Route class which derives from RouteBase already handles all of the complexity of route matching, parameters, etc., so we can just use it along with GenericHandlerRouteHandler<T>.
Below is a combined version with a real-life usage example that includes route parameters.
First are the route handlers. There are two included, here -- both with the same class name, but one that is generic and uses type information to create an instance of the specific HttpHandler as in Mr. Meacham's usage, and one that uses a virtual path and BuildManager to create an instance of the appropriate HttpHandler as in shellscape's usage. The good news is that .NET allows both to live side by side just fine, so we can just use whichever we want and can switch between them as we wish.
using System.Web;
using System.Web.Compilation;
using System.Web.Routing;
public class HttpHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new() {
public HttpHandlerRouteHandler() { }
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
return new T();
}
}
public class HttpHandlerRouteHandler : IRouteHandler {
private string _VirtualPath;
public HttpHandlerRouteHandler(string virtualPath) {
this._VirtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
return (IHttpHandler) BuildManager.CreateInstanceFromVirtualPath(this._VirtualPath, typeof(IHttpHandler));
}
}
Let's assume that we created an HttpHandler that streams documents to users from a resource outside our virtual folder, maybe even from a database, and that we want to fool the user's browser into believing that we are directly serving a specific file rather than simply providing a download (i.e., allow the browser's plug-ins to handle the file rather than forcing the user to save the file). The HttpHandler may expect a document id with which to locate the document to provide, and may expect a file name to provide to the browser -- one that may differ from the file name used on the server.
The following shows the registration of the route used to accomplish this with a DocumentHandler HttpHandler:
routes.Add("Document", new Route("document/{documentId}/{*fileName}", new HttpHandlerRouteHandler<DocumentHandler>()));
I used {*fileName} rather than just {fileName} to allow the fileName parameter to act as an optional catch-all parameter.
To create a URL for a file served by this HttpHandler, we can add the following static method to a class where such a method would be appropriate, such as in the HttpHandler class, itself:
public static string GetFileUrl(int documentId, string fileName) {
string mimeType = null;
try { mimeType = MimeMap.GetMimeType(Path.GetExtension(fileName)); }
catch { }
RouteValueDictionary documentRouteParameters = new RouteValueDictionary { { "documentId", documentId.ToString(CultureInfo.InvariantCulture) }
, { "fileName", DocumentHandler.IsPassThruMimeType(mimeType) ? fileName : string.Empty } };
return RouteTable.Routes.GetVirtualPath(null, "Document", documentRouteParameters).VirtualPath;
}
I omitted the definitions of MimeMap and and IsPassThruMimeType to keep this example simple. But these are intended to determine whether or not specific file types should provide their file names directly in the URL, or rather in a Content-Disposition HTTP header. Some file extensions could be blocked by IIS or URL Scan, or could cause code to execute that might cause problems for users -- especially if the source of the file is another user who is malicious. You could replace this logic with some other filtering logic, or omit such logic entirely if you are not exposed to this type of risk.
Since in this particular example the file name may be omitted from the URL, then, obviously, we must retrieve the file name from somewhere. In this particular example, the file name can be retrieved by performing a look-up using document id, and including a file name in the URL is intended solely to improve the user's experience. So, the DocumentHandler HttpHandler can determine if a file name was provided in the URL, and if it was not, then it can simply add a Content-Disposition HTTP header to the response.
Staying on topic, the important part of the above code block is the usage of RouteTable.Routes.GetVirtualPath() and the routing parameters to generate a URL from the Route object that we created during the route registration process.
Here's a watered-down version of the DocumentHandler HttpHandler class (much omitted for the sake of clarity). You can see that this class uses route parameters to retrieve the document id and the file name when it can; otherwise, it will attempt to retrieve the document id from a query string parameter (i.e., assuming that routing was not used).
public void ProcessRequest(HttpContext context) {
try {
context.Response.Clear();
// Get the requested document ID from routing data, if routed. Otherwise, use the query string.
bool isRouted = false;
int? documentId = null;
string fileName = null;
RequestContext requestContext = context.Request.RequestContext;
if (requestContext != null && requestContext.RouteData != null) {
documentId = Utility.ParseInt32(requestContext.RouteData.Values["documentId"] as string);
fileName = Utility.Trim(requestContext.RouteData.Values["fileName"] as string);
isRouted = documentId.HasValue;
}
// Try the query string if no documentId obtained from route parameters.
if (!isRouted) {
documentId = Utility.ParseInt32(context.Request.QueryString["id"]);
fileName = null;
}
if (!documentId.HasValue) { // Bad request
// Response logic for bad request omitted for sake of simplicity
return;
}
DocumentDetails documentInfo = ... // Details of loading this information omitted
if (context.Response.IsClientConnected) {
string fileExtension = string.Empty;
try { fileExtension = Path.GetExtension(fileName ?? documentInfo.FileName); } // Use file name provided in URL, if provided, to get the extension.
catch { }
// Transmit the file to the client.
FileInfo file = new FileInfo(documentInfo.StoragePath);
using (FileStream fileStream = file.OpenRead()) {
// If the file size exceeds the threshold specified in the system settings, then we will send the file to the client in chunks.
bool mustChunk = fileStream.Length > Math.Max(SystemSettings.Default.MaxBufferedDownloadSize * 1024, DocumentHandler.SecondaryBufferSize);
// WARNING! Do not ever set the following property to false!
// Doing so causes each chunk sent by IIS to be of the same size,
// even if a chunk you are writing, such as the final chunk, may
// be shorter than the rest, causing extra bytes to be written to
// the stream.
context.Response.BufferOutput = true;
context.Response.ContentType = MimeMap.GetMimeType(fileExtension);
context.Response.AddHeader("Content-Length", fileStream.Length.ToString(CultureInfo.InvariantCulture));
if ( !isRouted
|| string.IsNullOrWhiteSpace(fileName)
|| string.IsNullOrWhiteSpace(fileExtension)) { // If routed and a file name was provided in the route, then the URL will appear to point directly to a file, and no file name header is needed; otherwise, add the header.
context.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", HttpUtility.UrlEncode(documentInfo.FileName)));
}
int bufferSize = DocumentHandler.SecondaryBufferSize;
byte[] buffer = new byte[bufferSize];
int bytesRead = 0;
while ((bytesRead = fileStream.Read(buffer, 0, bufferSize)) > 0 && context.Response.IsClientConnected) {
context.Response.OutputStream.Write(buffer, 0, bytesRead);
if (mustChunk) {
context.Response.Flush();
}
}
}
}
}
catch (Exception e) {
// Error handling omitted from this example.
}
}
This example uses some additional custom classes, such as a Utility class to simplify some trivial tasks. But hopefully you can weed through that. The only really important part in this class with regard to the current topic, of course, is the retrieval of the route parameters from context.Request.RequestContext.RouteData. But I've seen several posts elsewhere asking how to stream large files using an HttpHandler without chewing up server memory, so it seemed like a good idea to combine examples.

Change the Views location

I am developing a website in MVC 2.0. I want to change the View folder location in my website. I wanted to keep the views folder inside other folders, When I try to do so i am getting following errors
The view 'Index' or its master was not found. The following locations were searched:
~/Views/Search/Index.aspx
~/Views/Search/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
My Views folder will be in ~/XYZ/ABC/Views instead of ~/Views. Please solve my problem. Will I get any problems If I change the default Views folder location. Do I need to change anything in HTML Helper classes because I don't know anything in MVC as this is my starting project i dont want to risk..Please help me out...
You'll need to create a custom view engine and use that instead. Fortunately you can just inherit from the default one and change the locations on the constructor. Here's a guide to creating your own view engine: http://www.singingeels.com/Articles/Creating_a_Custom_View_Engine_in_ASPNET_MVC.aspx
From the article:
protected void Application_Start()
{
//... other things up here.
// I want to REMOVE the ASP.NET ViewEngine...
ViewEngines.Engines.Clear();
// and then add my own :)
ViewEngines.Engines.Add(new HoTMeaTViewEngine());
}
public class HoTMeaTViewEngine : VirtualPathProviderViewEngine
{
public HoTMeaTViewEngine()
{
// This is where we tell MVC where to look for our files. This says
// to look for a file at "Views/Controller/Action.html"
base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.html" };
base.PartialViewLocationFormats = base.ViewLocationFormats;
}
}
Check this place out. How to change default view location scheme in ASP.NET MVC?
base.ViewLocationFormats = new string[] {
"~/Views/{1}/{2}/{0}.aspx",
"~/Views/{1}/{2}/{0}.ascx",
"~/Views/Shared/{2}/{0}.aspx",
"~/Views/Shared/{2}/{0}.ascx" ,
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
Even easier is this one Can I specify a custom location to “search for views” in ASP.NET MVC?
As an alternative, you can override the view engine locations for a specific controller without affecting the view engines for the other controllers.
These are some snippets from a product I am developing, but it shows the constructor for one of my controllers, and a view engine I made specificially for controllers that inherit from KBRenderMvcController.
So any controller based off KBRenderMvcController will also have my view engine.
However at no point did I clear the view engine collection, which is relevant. Because I wanted the views my product is using to fall back to default locations.
In short, if you delete \App_plugins\Product\Views\MyView And instead create a \Views\MyView it will still render from \Views\MyView instead.
Also in the ViewEngine I demonstrate code that determines the type of controller being used and if it's not a target controller I return empty view locations so they don't get used for other controllers.
#region Constructor
public KBRenderMvcController()
: base()
{
viewEngine = new KBFrontEndViewEngine();
if (!this.ViewEngineCollection.Contains(viewEngine))
this.ViewEngineCollection.Insert(0, viewEngine);
}
#endregion
public class KBFrontEndViewEngine : RazorViewEngine
{
#region Fields
private static bool _Initialized = false;
private static string[] viewLocationFormats = null;
private static string[] partialViewLocationFormats = null;
private static string[] viewEngineFileExtensions = new string[] { "cshtml" };
#endregion
#region Constructor
public KBFrontEndViewEngine()
{
if (!_Initialized)
{
viewLocationFormats = new string[]
{
string.Concat(KBApplicationCore.PluginRelUrl, "/Views/{1}/{0}.cshtml"),
string.Concat(KBApplicationCore.PluginRelUrl, "/Views/Partials/{0}.cshtml")
};
partialViewLocationFormats = new string[]
{
string.Concat(KBApplicationCore.PluginRelUrl, "/Views/{1}/Partials/_partial{0}.cshtml"),
string.Concat(KBApplicationCore.PluginRelUrl, "/Views/Partials/_partial{0}.cshtml"),
string.Concat(KBApplicationCore.PluginRelUrl, "/Views/{1}/Dialogs/_dialog{1}.cshtml"),
string.Concat(KBApplicationCore.PluginRelUrl, "/Views/Dialogs/_dialog{1}.cshtml"),
};
_Initialized = true;
}
base.ViewLocationFormats = viewLocationFormats;
base.PartialViewLocationFormats = partialViewLocationFormats;
base.MasterLocationFormats = viewLocationFormats;
base.FileExtensions = viewEngineFileExtensions;
}
#endregion
#region Methods
//Don't run on requests that are not for our hijacked controllers
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
Type controllerType = controllerContext.Controller.GetType();
Type baseType = controllerType.BaseType;
if ((baseType != null) && (baseType.Name == "KBRenderMvcController`1") || (baseType.Name == "KBFrontEndBaseSurfaceController"))
return base.FindPartialView(controllerContext, partialViewName, useCache);
else
return new ViewEngineResult(new List<string>());
}
#endregion
}

Resources