我可以在 ASP.NET MVC 中指定一个自定义位置来“搜索视图”吗?

我的 mvc 项目的布局如下:

  • 控制器
    • /示范
    • /Demo/DemoArea 1控制器
    • /Demo/DemoArea 2控制器
    • 等等。
  • /意见
    • /示范
    • /Demo/DemoArea 1/Index.aspx
    • /Demo/DemoArea 2/Index.aspx

然而,当我为 DemoArea1Controller准备这个的时候:

public class DemoArea1Controller : Controller
{
public ActionResult Index()
{
return View();
}
}

我得到“查看‘索引’或其主服务器无法找到”错误,与通常的搜索位置。

如何在“ Demo”视图子文件夹中的“ Demo”命名空间搜索中指定控制器?

76617 次浏览

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;
}

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());
}

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

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.

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.

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

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();
}

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());
});
}

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.

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