Reputation: 57832
I have the following layout for my mvc project:
However, when I have this for DemoArea1Controller
:
public class DemoArea1Controller : Controller
{
public ActionResult Index()
{
return View();
}
}
I get the "The view 'index' or its master could not be found" error, with the usual search locations.
How can I specify that controllers in the "Demo" namespace search in the "Demo" view subfolder?
Upvotes: 109
Views: 79243
Reputation: 412
I did it this way in MVC 5. I didn't want to clear the default locations.
Helper Class:
namespace ConKit.Helpers
{
public static class AppStartHelper
{
public static void AddConKitViewLocations()
{
// get engine
RazorViewEngine engine = ViewEngines.Engines.OfType<RazorViewEngine>().FirstOrDefault();
if (engine == null)
{
return;
}
// extend view locations
engine.ViewLocationFormats =
engine.ViewLocationFormats.Concat(new string[] {
"~/Views/ConKit/{1}/{0}.cshtml",
"~/Views/ConKit/{0}.cshtml"
}).ToArray();
// extend partial view locations
engine.PartialViewLocationFormats =
engine.PartialViewLocationFormats.Concat(new string[] {
"~/Views/ConKit/{0}.cshtml"
}).ToArray();
}
}
}
And then in Application_Start:
// Add ConKit View locations
ConKit.Helpers.AppStartHelper.AddConKitViewLocations();
Upvotes: 1
Reputation: 15559
Most of the answers here, clear the existing locations by calling ViewEngines.Engines.Clear()
and then add them back in again... there is no need to do this.
We can simply add the new locations to the existing ones, as shown below:
// note that the base class is RazorViewEngine, NOT WebFormViewEngine
public class ExpandedViewEngine : RazorViewEngine
{
public ExpandedViewEngine()
{
var customViewSubfolders = new[]
{
// {1} is conroller name, {0} is action name
"~/Areas/AreaName/Views/Subfolder1/{1}/{0}.cshtml",
"~/Areas/AreaName/Views/Subfolder1/Shared/{0}.cshtml"
};
var customPartialViewSubfolders = new[]
{
"~/Areas/MyAreaName/Views/Subfolder1/{1}/Partials/{0}.cshtml",
"~/Areas/MyAreaName/Views/Subfolder1/Shared/Partials/{0}.cshtml"
};
ViewLocationFormats = ViewLocationFormats.Union(customViewSubfolders).ToArray();
PartialViewLocationFormats = PartialViewLocationFormats.Union(customPartialViewSubfolders).ToArray();
// use the following if you want to extend the master locations
// MasterLocationFormats = MasterLocationFormats.Union(new[] { "new master location" }).ToArray();
}
}
Now you can configure your project to use the above RazorViewEngine
in Global.asax:
protected void Application_Start()
{
ViewEngines.Engines.Add(new ExpandedViewEngine());
// more configurations
}
See this tutoral for more info.
Upvotes: 3
Reputation: 4049
Now in MVC 6 you can implement IViewLocationExpander
interface without messing around with view engines:
public class MyViewLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context) {}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
return new[]
{
"/AnotherPath/Views/{1}/{0}.cshtml",
"/AnotherPath/Views/Shared/{0}.cshtml"
}; // add `.Union(viewLocations)` to add default locations
}
}
where {0}
is target view name, {1}
- controller name and {2}
- area name.
You can return your own list of locations, merge it with default viewLocations
(.Union(viewLocations)
) or just change them (viewLocations.Select(path => "/AnotherPath" + path)
).
To register your custom view location expander in MVC, add next lines to ConfigureServices
method in Startup.cs
file:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new MyViewLocationExpander());
});
}
Upvotes: 52
Reputation: 9497
If you want just add new paths, you can add to the default view engines and spare some lines of code:
ViewEngines.Engines.Clear();
var razorEngine = new RazorViewEngine();
razorEngine.MasterLocationFormats = razorEngine.MasterLocationFormats
.Concat(new[] {
"~/custom/path/{0}.cshtml"
}).ToArray();
razorEngine.PartialViewLocationFormats = razorEngine.PartialViewLocationFormats
.Concat(new[] {
"~/custom/path/{1}/{0}.cshtml", // {1} = controller name
"~/custom/path/Shared/{0}.cshtml"
}).ToArray();
ViewEngines.Engines.Add(razorEngine);
The same applies to WebFormEngine
Upvotes: 23
Reputation: 796
Instead of subclassing the RazorViewEngine, or replacing it outright, you can just alter existing RazorViewEngine's PartialViewLocationFormats property. This code goes in Application_Start:
System.Web.Mvc.RazorViewEngine rve = (RazorViewEngine)ViewEngines.Engines
.Where(e=>e.GetType()==typeof(RazorViewEngine))
.FirstOrDefault();
string[] additionalPartialViewLocations = new[] {
"~/Views/[YourCustomPathHere]"
};
if(rve!=null)
{
rve.PartialViewLocationFormats = rve.PartialViewLocationFormats
.Union( additionalPartialViewLocations )
.ToArray();
}
Upvotes: 13
Reputation: 8858
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());
}
Upvotes: 123
Reputation: 65436
There's actually a lot easier method than hardcoding the paths into your constructor. Below is an example of extending the Razor engine to add new paths. One thing I'm not entirely sure about is whether the paths you add here will be cached:
public class ExtendedRazorViewEngine : RazorViewEngine
{
public void AddViewLocationFormat(string paths)
{
List<string> existingPaths = new List<string>(ViewLocationFormats);
existingPaths.Add(paths);
ViewLocationFormats = existingPaths.ToArray();
}
public void AddPartialViewLocationFormat(string paths)
{
List<string> existingPaths = new List<string>(PartialViewLocationFormats);
existingPaths.Add(paths);
PartialViewLocationFormats = existingPaths.ToArray();
}
}
And your Global.asax.cs
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ExtendedRazorViewEngine engine = new ExtendedRazorViewEngine();
engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.cshtml");
engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.vbhtml");
// Add a shared location too, as the lines above are controller specific
engine.AddPartialViewLocationFormat("~/MyThemes/{0}.cshtml");
engine.AddPartialViewLocationFormat("~/MyThemes/{0}.vbhtml");
ViewEngines.Engines.Add(engine);
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
One thing to note: your custom location will need the ViewStart.cshtml file in its root.
Upvotes: 42
Reputation: 145950
Note: for ASP.NET MVC 2 they have additional location paths you will need to set for views in 'Areas'.
AreaViewLocationFormats
AreaPartialViewLocationFormats
AreaMasterLocationFormats
Creating a view engine for an Area is described on Phil's blog.
Note: This is for preview release 1 so is subject to change.
Upvotes: 3
Reputation: 10514
Try something like this:
private static void RegisterViewEngines(ICollection<IViewEngine> engines)
{
engines.Add(new WebFormViewEngine
{
MasterLocationFormats = new[] {"~/App/Views/Admin/{0}.master"},
PartialViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.ascx"},
ViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.aspx"}
});
}
protected void Application_Start()
{
RegisterViewEngines(ViewEngines.Engines);
}
Upvotes: 3
Reputation: 19358
Last I checked, this requires you to build your own ViewEngine. I don't know if they made it easier in RC1 though.
The basic approach I used before the first RC was, in my own ViewEngine, to split the namespace of the controller and look for folders which matched the parts.
EDIT:
Went back and found the code. Here's the general idea.
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
string ns = controllerContext.Controller.GetType().Namespace;
string controller = controllerContext.Controller.GetType().Name.Replace("Controller", "");
//try to find the view
string rel = "~/Views/" +
(
ns == baseControllerNamespace ? "" :
ns.Substring(baseControllerNamespace.Length + 1).Replace(".", "/") + "/"
)
+ controller;
string[] pathsToSearch = new string[]{
rel+"/"+viewName+".aspx",
rel+"/"+viewName+".ascx"
};
string viewPath = null;
foreach (var path in pathsToSearch)
{
if (this.VirtualPathProvider.FileExists(path))
{
viewPath = path;
break;
}
}
if (viewPath != null)
{
string masterPath = null;
//try find the master
if (!string.IsNullOrEmpty(masterName))
{
string[] masterPathsToSearch = new string[]{
rel+"/"+masterName+".master",
"~/Views/"+ controller +"/"+ masterName+".master",
"~/Views/Shared/"+ masterName+".master"
};
foreach (var path in masterPathsToSearch)
{
if (this.VirtualPathProvider.FileExists(path))
{
masterPath = path;
break;
}
}
}
if (string.IsNullOrEmpty(masterName) || masterPath != null)
{
return new ViewEngineResult(
this.CreateView(controllerContext, viewPath, masterPath), this);
}
}
//try default implementation
var result = base.FindView(controllerContext, viewName, masterName);
if (result.View == null)
{
//add the location searched
return new ViewEngineResult(pathsToSearch);
}
return result;
}
Upvotes: 3