How to Implement a Custom Razorviewengine to Find Views in Non-Standard Locations

How do I implement a custom RazorViewEngine to find views in non-standard locations?

Ok in the end I opted for an approach detailed here: http://weblogs.asp.net/imranbaloch/archive/2011/06/27/view-engine-with-dynamic-view-location.aspx

thanks to @Adriano for the answers and pointers but in the end I think this approach fits my needs better. The approach below allows me to keep the standard functionality but to create a new higher priority view location to be searched.

public class Travel2ViewEngine : RazorViewEngine
{
protected BrandNameEnum BrandName;
private string[] _newAreaViewLocations = new string[] {
"~/Areas/{2}/%1Views/{1}/{0}.cshtml",
"~/Areas/{2}/%1Views/{1}/{0}.vbhtml",
"~/Areas/{2}/%1Views//Shared/{0}.cshtml",
"~/Areas/{2}/%1Views//Shared/{0}.vbhtml"
};

private string[] _newAreaMasterLocations = new string[] {
"~/Areas/{2}/%1Views/{1}/{0}.cshtml",
"~/Areas/{2}/%1Views/{1}/{0}.vbhtml",
"~/Areas/{2}/%1Views/Shared/{0}.cshtml",
"~/Areas/{2}/%1Views/Shared/{0}.vbhtml"
};

private string[] _newAreaPartialViewLocations = new string[] {
"~/Areas/{2}/%1Views/{1}/{0}.cshtml",
"~/Areas/{2}/%1Views/{1}/{0}.vbhtml",
"~/Areas/{2}/%1Views/Shared/{0}.cshtml",
"~/Areas/{2}/%1Views/Shared/{0}.vbhtml"
};

private string[] _newViewLocations = new string[] {
"~/%1Views/{1}/{0}.cshtml",
"~/%1Views/{1}/{0}.vbhtml",
"~/%1Views/Shared/{0}.cshtml",
"~/%1Views/Shared/{0}.vbhtml"
};

private string[] _newMasterLocations = new string[] {
"~/%1Views/{1}/{0}.cshtml",
"~/%1Views/{1}/{0}.vbhtml",
"~/%1Views/Shared/{0}.cshtml",
"~/%1Views/Shared/{0}.vbhtml"
};

private string[] _newPartialViewLocations = new string[] {
"~/%1Views/{1}/{0}.cshtml",
"~/%1Views/{1}/{0}.vbhtml",
"~/%1Views/Shared/{0}.cshtml",
"~/%1Views/Shared/{0}.vbhtml"
};

public Travel2ViewEngine()
: base()
{
Enum.TryParse<BrandNameEnum>(Travel2.WebUI.Properties.Settings.Default.BrandName, out BrandName);

AreaViewLocationFormats = AppendLocationFormats(_newAreaViewLocations, AreaViewLocationFormats);

AreaMasterLocationFormats = AppendLocationFormats(_newAreaMasterLocations, AreaMasterLocationFormats);

AreaPartialViewLocationFormats = AppendLocationFormats(_newAreaPartialViewLocations, AreaPartialViewLocationFormats);

ViewLocationFormats = AppendLocationFormats(_newViewLocations, ViewLocationFormats);

MasterLocationFormats = AppendLocationFormats(_newMasterLocations, MasterLocationFormats);

PartialViewLocationFormats = AppendLocationFormats(_newPartialViewLocations, PartialViewLocationFormats);
}

private string[] AppendLocationFormats(string[] newLocations, string[] defaultLocations)
{
List<string> viewLocations = new List<string>();
viewLocations.AddRange(newLocations);
viewLocations.AddRange(defaultLocations);
return viewLocations.ToArray();
}

protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
return base.CreateView(controllerContext, viewPath.Replace("%1", BrandName.ToString()), masterPath);
}

protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
return base.CreatePartialView(controllerContext, partialPath.Replace("%1", BrandName.ToString()));
}

protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
return base.FileExists(controllerContext, virtualPath.Replace("%1", BrandName.ToString()));
}
}

then register in Gloabal.asax

protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);

//Register our customer view engine to control T2 and TBag views and over ridding
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new Travel2ViewEngine());
}

Search for view files in a custom location only for Specified Area in MVC 5

Try this: in RouteConfig.cs where you are setting up the route which links to a controller which you want to find the custom view location, add this:

var route = routes.MapRoute(<route params here>);
route.DataTokens["area"] = "AreaName";

This will tell MVC that when you follow this route, you are going into area 'AreaName'. Then it will subsequently look for views in that area. Your custom ViewEngine will only affect view locations when MVC is looking for them in some area. Otherwise it won't have any effect, as the location format lists for areas are the only ones it overrides.

Asp.net custom razor view engine for access views out of the box

Is there any solution for avoid compile the views into the main common web project?

Yes, you could embed them as resources into some plugin assembly and reuse them in multiple projects. For this you will have to write a custom VirtualPathProvider that is able to load views from non-standard locations outside of your current application. You could also take a look at the RazorGenerator project. Here's an accompanying blog post explaining some of the required steps. Basically you need to install the Razor Generator Extension which will precompile your Razor views as part of a class library that you could reference in your MVC application.

ASP.NET MVC 5 custom RazorViewEngine for multiple portal structure

These are placeholders in the string that can be used to put the area name, controller name or action name into the string by the controller. {2} is area, {1} is controller,{0} is the action.

You may also be interested to know that when using Asp.Net Core it's easy to get the standard Razor View Engine to locate views and such in custom locations via a ViewLocationExpander rather than needing to create a new view engine that inherits from the Razor View Engine. I only mention this because you added the asp.net-core-mvc tag on your question.

Here is a stack overflow answer that shows how:
How to specify the view location in asp.net core mvc when using custom locations?

Using a custom RazorViewEngine AND RazorGenerator precompiled views

For anyone else wanting to try this approach I'll post the answer. Basically you need to implement a custom view engine that derives from the PrecompiledMvcEngine found in the RazorGenerator assembly.

public class PosPrecompileEngine : PrecompiledMvcEngine
{
private IUserProfileService _profileService;

public PosPrecompileEngine(Assembly assembly) : base(assembly)
{
LocatorConfig();
}

public PosPrecompileEngine(Assembly assembly, string baseVirtualPath) : base(assembly, baseVirtualPath)
{
LocatorConfig();
}

public PosPrecompileEngine(Assembly assembly, string baseVirtualPath, IViewPageActivator viewPageActivator) : base(assembly, baseVirtualPath, viewPageActivator)
{
LocatorConfig();
}

protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
return base.CreatePartialView(controllerContext, partialPath.ReplaceOrderType(CurrentOrderingMode()));
}

protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
OrderType orderType = CurrentOrderingMode();
return base.CreateView(controllerContext, viewPath.ReplaceOrderType(orderType), masterPath.ReplaceOrderType(orderType));
}

protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
return base.FileExists(controllerContext, virtualPath.ReplaceOrderType(CurrentOrderingMode()));
}
}

In this class - I override the Locator Paths. Because I have the "base" compiled views in another assembly from the web application - we implemented a convention where the view engine will first look in a PosViews/{ordering mode}/{controller}/{view} path in the web application. If a view is not located -then it will look in the traditional /Views/controller/view. The trick here is the later is a virtual path located in another class library.

This allowed us to "override" an existing view for the application.

    private void LocatorConfig()
{
//{0} = View Name
//{1} = ControllerName
//{2} = Area Name
AreaViewLocationFormats = new[]
{
//First look in the hosting application area folder / Views / ordering type
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"PosAreas/{2}/Views/%1/{1}/{0}.cshtml",

//Next look in the hosting application area folder / Views / ordering type / Shared
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"PosAreas/{2}/Views/%1/Shared/(0}.cshtml",

//Next look in the POS Areas Shared
"PosAreas/{2}/Views/Shared/(0}.cshtml",

//Finally look in the IMS.POS.Web.Views.Core assembly
"Areas/{2}/Views/{1}/{0}.cshtml"
};

//Same format logic
AreaMasterLocationFormats = AreaViewLocationFormats;

AreaPartialViewLocationFormats = new[]
{
//First look in the hosting application area folder / Views / ordering type
//Areas/{AreaName}/{OrderType}/{ControllerName}/Partials/{PartialViewName}.cshtml
"PosAreas/{2}/Views/%1/{1}/Partials/{0}.cshtml",

//Next look in the hosting application area folder / Views / ordering type / Shared
//Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
"PosAreas/{2}/Views/%1/Shared/(0}.cshtml",

//Next look in the hosting application shared folder
"PosAreas/{2}/Views/Shared/(0}.cshtml",

//Finally look in the IMS.POS.Web.Views.Core
"Areas/{2}/Views/{1}/{0}.cshtml"
};

ViewLocationFormats = new[]
{
"~/PosViews/%1/{1}/{0}.cshtml",
"~/PosViews/%1/Shared/{0}.cshtml",
"~/PosViews/Shared/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};

MasterLocationFormats = ViewLocationFormats;

PartialViewLocationFormats = new[]
{
"~/PosViews/%1/{1}/{0}.cshtml",
"~/PosViews/%1/Shared/{0}.cshtml",
"~/PosViews/Shared/{0}.cshtml",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
}

Register this engine in your application start up events.

   public static void Configure()
{
var engine = new PosPrecompileEngine(typeof(ViewEngineConfig).Assembly)
{
UsePhysicalViewsIfNewer = true,
PreemptPhysicalFiles = true
};
ViewEngines.Engines.Add(engine);

// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}

Here is the final key. When RazorGenerator gets installed view NuGet - you end up with this start-up class that will run on startup

[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(Views.Core.RazorGeneratorMvcStart), "Start")]

public static class RazorGeneratorMvcStart
{
public static void Start()
{
var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly)
{
UsePhysicalViewsIfNewer = true,
PreemptPhysicalFiles = true
};
ViewEngines.Engines.Add(engine);

// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
}

By default - RazorGenerator adds ViewEngine to the first in the collection

ViewEngines.Engines.Insert(0,engine);

You need to change that to an add

ViewEngines.Engines.Add(engine); 

So it is added to engines last - this way your custom ViewEngine is used FIRST in locating views.

This approach allows you to reuse views in multiple applications while allowing a means to override that view.

This may be overkill for most applications - bust as I mentioned in the question - this is base product that we use to develop multiple client applications. Trying achieve reuse while maintaining a level of flexibility on a per client basis is something we were trying to achieve.

Finding my View in asp.net mvc

You could fix that by implementing a custom RazorViewEngine, where you can specify the search path for the views per request, per controller and so on.

ASP.NET MVC Load Views / Partial Views Dynamically from Different Folder

You'll need to create a custom view engine, most likely inheriting from the Razor view engine, and override a method that determines where to look for views.

One blog on the subject:

http://theshravan.net/blog/configure-the-views-search-locations-in-asp-net-mvc/

How to specify the view location in asp.net core mvc when using custom locations?

You can expand the locations where the view engine looks for views by implementing a view location expander. Here is some sample code to demonstrate the approach:

public class ViewLocationExpander: IViewLocationExpander {

/// <summary>
/// Used to specify the locations that the view engine should search to
/// locate views.
/// </summary>
/// <param name="context"></param>
/// <param name="viewLocations"></param>
/// <returns></returns>
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations) {
//{2} is area, {1} is controller,{0} is the action
string[] locations = new string[] { "/Views/{2}/{1}/{0}.cshtml"};
return locations.Union(viewLocations); //Add mvc default locations after ours
}

public void PopulateValues(ViewLocationExpanderContext context) {
context.Values["customviewlocation"] = nameof(ViewLocationExpander);
}
}

Then in the ConfigureServices(IServiceCollection services) method in the startup.cs file add the following code to register it with the IoC container. Do this right after services.AddMvc();

services.Configure<RazorViewEngineOptions>(options => {
options.ViewLocationExpanders.Add(new ViewLocationExpander());
});

Now you have a way to add any custom directory structure you want to the list of places the view engine looks for views, and partial views. Just add it to the locations string[]. Also, you can place a _ViewImports.cshtml file in the same directory or any parent directory and it will be found and merged with your views located in this new directory structure.

Update:

One nice thing about this approach is that it provides more flexibility then the approach later introduced in ASP.NET Core 2 (Thanks @BrianMacKay for documenting the new approach). So for example this ViewLocationExpander approach allows for not only specifying a hierarchy of paths to search for views and areas but also for layouts and view components. Also you have access to the full ActionContext to determine what an appropriate route might be. This provides alot of flexibility and power. So for example if you wanted to determine the appropriate view location by evaluating the path of the current request, you can get access to the path of the current request via context.ActionContext.HttpContext.Request.Path.

Can I specify a custom location to search for views in ASP.NET MVC?

You can easily extend the WebFormViewEngine to specify all the locations you want to look in:

public class CustomViewEngine : WebFormViewEngine
{
public CustomViewEngine()
{
var viewLocations = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx",
"~/AnotherPath/Views/{0}.ascx"
// etc
};

this.PartialViewLocationFormats = viewLocations;
this.ViewLocationFormats = viewLocations;
}
}

Make sure you remember to register the view engine by modifying the Application_Start method in your Global.asax.cs

protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());
}


Related Topics



Leave a reply



Submit