So I register all Areas in Global.asax:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
//...
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
But in my /Areas/Log/Controllers, when I try to find a PartialView:
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, "_LogInfo");
It fails, viewResult.SearchedLocations is:
"~/Views/Log/_LogInfo.aspx"
"~/Views/Log/_LogInfo.ascx"
"~/Views/Shared/_LogInfo.aspx"
"~/Views/Shared/_LogInfo.ascx"
"~/Views/Log/_LogInfo.cshtml"
"~/Views/Log/_LogInfo.vbhtml"
"~/Views/Shared/_LogInfo.cshtml"
"~/Views/Shared/_LogInfo.vbhtml"
And thus viewResult.View is null.
How can I make the FindPartialView search in my Area?
Update:
This is my custom view engine, which I have registered in Global.asax:
public class MyCustomViewEngine : RazorViewEngine
{
public MyCustomViewEngine() : base()
{
AreaPartialViewLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = new[]
{
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
// and the others...
}
}
But the FindPartialView doesn't use the AreaPArtialViewLocationFormats:
"~/Views/Log/_LogInfo.cshtml"
"~/Views/Shared/_LogInfo.cshtml"
I had exactly the same problem, I have a central Ajax controller I use, in which I return different partial views from different folders/locations.
What you are going to have to do is create a new ViewEngine deriving from a RazorViewEngine (I'm assuming your using Razor) and explicitly include new locations in the constructor to search for the partials in.
Alternatively you can override the FindPartialView method. By default the Shared folder and the folder from the current controller context are used for the search.
Here is an example which shows you how to override specific properties within a custom RazorViewEngine.
Update
You should include the path of the partial in your PartialViewLocationFormats array like this:
public class MyViewEngine : RazorViewEngine
{
public MyViewEngine() : base()
{
PartialViewLocationFormats = new string[]
{
"~/Area/{0}.cshtml"
// .. Other areas ..
};
}
}
Likewise if you want to find a partial in a Controller inside the Area folder then you will have to add the standard partial view locations to the AreaPartialViewLocationFormats array. I have tested this and it is working for me.
Just remember to add the new RazorViewEngine to your Global.asax.cs, e.g.:
protected void Application_Start()
{
// .. Other initialization ..
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MyViewEngine());
}
Here is how you may use it in an exemplary controller called "Home":
// File resides within '/Controllers/Home'
public ActionResult Index()
{
var pt = ViewEngines.Engines.FindPartialView(ControllerContext, "Partial1");
return View(pt);
}
I have stored the partial I'm looking for in the /Area/Partial1.cshtml path.
Related
I'm looking for a way to extend the AspNetCore MVC view discovery logic. I want to be able to inherit from a controller and have the new controller have access to the Actions of the base Controller. Is there a way to extend the view discovery logic so that you can tell a controller where to look for its vies, to look in the folder of the controller, look in a folder based on the name of the base controller, or even look in a folder based on the namespace of the controller?
~/Controllers/UserAccountController.cs
namespace App.Controllers.UserAccount
{
public class UserAccountController {
public virtual async Task<IActionResult> Action1()
{
return View();
}
}
}
~/Controllers/UserAccountExtController.cs
namespace App.Controllers.UserAccount
{
public class UserAccountExtController : UserAccountController {
public override async Task<IActionResult> Action1()
{
return View();
}
}
}
Is there a way that I can extend the view discovery logic so that it if it does not find the view in the view folder with the same name as the Controller name, that it will look in the folder based on an Attribute of the controller, or the folder of the inherited controller, the folder that the controller exists in, or a folder based on the namespace of the controller?
I ended up going with a IViewLocationExpander to solve the issue thanks to RandyBuchholz for the tip on casting the ActionContext to a ControllerActionContext, which allowed me to identify the BaseType of the controller. This allowed be to add the convention of checking the default location of the BaseController if a view didn't exist in the default location for the Controller.
public class MyViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
// list used for future extension
var alternateLocations = new List<string>();
if (context.ActionContext.ActionDescriptor is ControllerActionDescriptor descriptor)
{
var baseType = descriptor.ControllerTypeInfo.BaseType.Name;
if (!baseType.StartsWith("Controller"))
{
var baseLocation = baseType.Replace("Controller", string.Empty);
alternateLocations.Add("/Views/" + baseLocation + "/{0}.cshtml");
}
}
var locations = viewLocations.ToList();
locations.InsertRange(locations.IndexOf("/Views/Shared/{0}.cshtml") - 1, alternateLocations);
return locations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
}
Then just register the IViewLocationExpander in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//...
services.Configure<RazorViewEngineOptions>(options =>
{
var expander = new MyViewLocationExpander();
options.ViewLocationExpanders.Add(expander);
});
//...
}
In what way views are searched in route.config file. I want the order in which views are searched.
e.g.
~/Views//Home/Index
~/Views/Shared/Home/Index
By default, MVC view engine searches available view cshtml files in these locations in order from top to bottom:
~/Views/ControllerName/ActionName.cshtml
~/Views/Shared/ActionName.cshtml
~/Views/Shared/LayoutName.cshtml (for layout files)
Either changing or re-ordering view engine search method requires creating a new class like this one:
public class CustomViewSearch : RazorViewEngine
{
public CustomViewSearch()
{
MasterLocationFormats = new[]
{
"~/Views/Shared/{0}.cshtml"
};
ViewLocationFormats = new[]
{
// you can change view search order here
// {0} = action name, {1} = controller name
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{1}/{0}.cshtml"
};
PartialViewLocationFormats = ViewLocationFormats;
FileExtensions = new[]
{
"cshtml"
};
}
}
Then, place your custom view search method on Global.asax inside Application_Start method:
protected void Application_Start()
{
// remove all existing view search methods if you want
ViewEngines.Engines.Clear();
// add your custom view search method here
ViewEngines.Engines.Add(new CustomViewSearch());
}
Any suggestions welcome.
I'm trying to use areas at a custom path, and I'm having issues. I've been googeling a bunch, but havent found a solution.
My project is a EPiServer CMS project (which shouldn't have any effect I think, just wanna mention it, in case it does)
My structure is
Root
CompanyName
Areas
Commerce
Controllers
Models
Views
Cms
Controllers
HomePageController
Models
Views
HomePage
Index.cshtml
So I have a layer more to the tree then 'normal' which is the 'CompanyName'
I have this in global.asax.cs
protected void Application_Start()
{
ViewEngines.Engines.Add(new AreaTemplateViewEngineDynamic());
AreaRegistration.RegisterAllAreas();
...
}
I have a Custom RazorEngine (Could have just added more paths to the default, but have this solution as of now)
public class AreaTemplateViewEngineDynamic : RazorViewEngine
{
public AreaTemplateViewEngineDynamic()
{
this.PartialViewLocationFormats = this.ViewLocationFormats = this.MasterLocationFormats =
new string[]
{
"~/CompanyName/Views/{1}/{0}.cshtml", "~/CompanyName/Views/Shared/{0}.cshtml"
};
this.AreaMasterLocationFormats = this.AreaPartialViewLocationFormats = this.AreaViewLocationFormats =
new string[]
{
"~/CompanyName/Areas/{2}/Views/{1}/{0}.cshtml", "~/CompanyName/Areas/{2}/Views/Shared/{0}.cshtml"
};
}
}
Adding this area registration
public class CmsAreaRegistration: AreaRegistration
{
public override string AreaName
{
get { return "Commerce"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Cms_default",
"Cms/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "Root.CompanyName.Areas.Cms.Controllers" }
);
}
}
When I try to load the page, it seems it doesnt look at the Area paths, only the non-area paths.
The view 'index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/HomePage/index.aspx
~/Views/HomePage/index.ascx
~/Views/Shared/index.aspx
~/Views/Shared/index.ascx
~/Views/HomePage/index.cshtml
~/Views/HomePage/index.vbhtml
~/Views/Shared/index.cshtml
~/Views/Shared/index.vbhtml
~/CompanyName/Views/HomePage/index.cshtml
~/CompanyName/Views/Shared/index.cshtml
The path I want it to find is
~/CompanyName/Areas/Cms/Views/HomePage/index.cshtml
Also if I had to use
#{Html.RenderAction("MiniCart", "Cart", new { area = "Commerce"} );}
I would expect it to finde
~/CompanyName/Areas/Commerce/Views/Cart/MiniCart.cshtml
You are only setting the location for the AreaMasterLocation when you should also set the following locations:
AreaPartialViewLocationFormats
AreaViewLocationFormats
Find the following class in the object browser: VirtualPathProviderViewEngine for more properties and methods.
I wrote my own RazorViewEngine, where I added some custom codes to finding paths.
Could just use the URL, because the URL was controlled by the CMS, so the URL didnt represent the MVC path.
I have a layout page under
~/Areas/Admin/Shared/_Layout.cshtml
Now inside that I have a section where I was supposed to render a partial view . So what I did inside _layout.cshtml was to provide #Html.RenderAction("Sidebar")
The controller is actually basecontroller which is inherited to all other controllers . as
[OutputCache(Duration=60)]
public partial class BaseController : Controller
{
[ChildActionOnly]
public virtual ActionResult Sidebar()
{
return View();
}
}
Now this Controller is supposed to be interited by all X,Y , Z controllers so the childaction Sidebar would be available to all of them so that #Html.Renderaction("Sidebar") doesnt have the trouble to find the child action to be rendered .
Now the problem is the partialview path is under /Areas/Admin/Views/Shared/Partials/Sidebar/cshtml
I have also configured the razor view engine to find under that particular /Areas/Admin/Views/Shared/Partials/Sidebar.cshtml. And registered it under global.asax.
But its unable to find the partial view and giving the error as
~/Areas/Admin/Views/Admin/Sidebar.aspx
~/Areas/Admin/Views/Admin/Sidebar.ascx
~/Areas/Admin/Views/Shared/Sidebar.aspx
~/Areas/Admin/Views/Shared/Sidebar.ascx
~/Views/Admin/Sidebar.aspx
~/Views/Admin/Sidebar.ascx
~/Views/Shared/Sidebar.aspx
~/Views/Shared/Sidebar.ascx
~/Areas/Admin/Views/Admin/Sidebar.cshtml
~/Areas/Admin/Views/Admin/Sidebar.vbhtml
~/Areas/Admin/Views/Shared/Sidebar.cshtml
~/Areas/Admin/Views/Shared/Sidebar.vbhtml
~/Admin/Sidebar.cshtml
~/Views/Admin/Sidebar.vbhtml
~/Views/Shared/Sidebar.cshtml
~/Views/Shared/Sidebar.vbhtml
My custom razor view engine is
public class LocalizedViewEngine : RazorViewEngine
{
///{0} = View Name
///{1} = Controller Name
private static readonly string[] NewPartialViewFormats = new[] {
"~/Areas/Admin/Views/{1}/Partials/{0}.cshtml",
"~/Areas/Admin/Views/Shared/Partials/{0}.cshtml",
"~/Views/Shared/Partials/{0}.cshtml",
"~/Views/{1}/Partials/{0}.cshtml"
};
private static readonly string[] NewViewLocationFormats = new[] {
"~/Areas/Admin/Views/{1}/{0}.cshtml"
};
public LocalizedViewEngine()
{
base.ViewLocationFormats =
base.ViewLocationFormats.Union(NewViewLocationFormats).ToArray<string>();
base.PartialViewLocationFormats =
base.PartialViewLocationFormats.Union(NewPartialViewFormats).ToArray<string>();
}
}
And My global.asax contains
ViewEngines.Engines.Add(new LocalizedViewEngine());
But its unable to find the partial view under tha ~/Areas/Admin/Views/Shared/Partials/Sidebar.cshtml . Where am I going wrong ?
Create a controller:
public abstract class MyBaseController : Controller
{
public ActionResult MyAction(string id)
{
return View();
}
}
Than create another specific controller that inherit from MyBaseController:
public class MyController : MyBaseController
{
}
There is a view called MyAction.aspx in the Views/MyBaseController folder
Then, call MyController/MyAction method. Following exception will be generated:
The view 'MyAction' or its master
could not be found. The following
locations were searched:
~/Views/MyController/MyAction.aspx
~/Views/MyController/MyAction.ascx
~/Views/Shared/MyAction.aspx
~/Views/Shared/MyAction.ascx
Can I make MVC.NET to use the view from Views/MyBaseController folder?
you should wait for a more finesse answer but this work:
Create a new view engine based on the default one and override the FindViewMethod this way:
public class MyNewViewEngine : WebFormViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
var type = controllerContext.Controller.GetType();
//Retrieve all the applicable views.
var applicableViews = from m in type.GetMethods()
where typeof(ActionResult).IsAssignableFrom(m.ReturnType) & m.Name == viewName
select m;
//Save the original location formats.
var cacheLocations = ViewLocationFormats;
var tempLocations = cacheLocations.ToList();
//Iterate over applicable views and check if they have been declared in the given controller.
foreach(var view in applicableViews)
{
//If not, add a new format location to the ones at the default engine.
if (view.DeclaringType != type)
{
var newLocation = "~/Views/" + view.DeclaringType.Name.Substring(0, view.DeclaringType.Name.LastIndexOf("Controller")) + "/{0}.aspx";
if (!tempLocations.Contains(newLocation))
tempLocations.Add(newLocation);
}
}
//Change the location formats.
ViewLocationFormats = tempLocations.ToArray();
//Redirected to the default implementation
var result = base.FindView(controllerContext, viewName, masterName, useCache);
//Restore the location formats
ViewLocationFormats = cacheLocations;
return result;
}
}
Add the new view engine:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MyNewViewEngine());
RegisterRoutes(RouteTable.Routes);
}
}
hope this helps
You need to add it to shared because you are in the context of the subcontroller. If you want different behavior for different controllers, then you'll want to put a MyAction view in each of your subcontroller view folders.
To answer your question though, you probably could make it look in base controller folder, but it would require you to write your own request handler which looks in base controller folders. The default implementation only looks in the view folder for the current controller context, then it looks in the shared folder. It sounds like your view is shared however, so the shared folder seems like a good place for it anyway.
It is possible, but not very clean.
public class MyController : MyBaseController
{
public ActionResult MyAction(string id)
{
return View("~/Views/MyBaseController/MyAction.aspx");
}
}
However if your View (MyAction.aspx) contains a reference to a Partial View, ASP.NET MVC will look for it in the Views/MyController folder (and not find it there!).
If your view is shared across controllers, its best to place it in the Views/Shared folder as recommended by NickLarsen.