I have defined my custome view engine as below just to add more locations to search for.
public class CustomViewEngine : RazorViewEngine
{
private static string[] AdditionalViewLocations = new[]
{
"~/{1}/{0}.cshtml"
};
public CustomViewEngine() : base()
{
MasterLocationFormats = MasterLocationFormats.Union(AdditionalViewLocations).ToArray();
PartialViewLocationFormats = PartialViewLocationFormats.Union(AdditionalViewLocations).ToArray();
ViewLocationFormats = ViewLocationFormats.Union(AdditionalViewLocations).ToArray();
}
}
Now issue i am facing is, the layout is not getting applied for any of the views which was set in _ViewStart.cshtml page. Appreciates your help.
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);
});
//...
}
Some explanation about scenario but please be patient to end!!!
I have Implemented a pluggable MVC application which can register plugins which exist in Areas folder of main Project.
each plugin have some views and controller
I want to set layout for views in plugins(the plugins don't know anything about master layout in Main application)
So I investigated some ways to render views where I want to be rendered...
In Main Application my PluginBootstrapper will register all plugins in Areas folder is as:
public class PluginBootstrapper
{
public static readonly List<Assembly> PluginAssemblies = new List<Assembly>();
public static void Init()
{
var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");
foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll", SearchOption.AllDirectories))
PluginAssemblies.Add(Assembly.LoadFile(file));
PluginAssemblies.ForEach(BuildManager.AddReferencedAssembly);
// Add assembly handler for strongly-typed view models
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
}
private static Assembly AssemblyResolve(object sender, ResolveEventArgs resolveArgs)
{
var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
// Check we don't already have the assembly loaded
foreach (var assembly in currentAssemblies)
{
if (assembly.FullName == resolveArgs.Name || assembly.GetName().Name == resolveArgs.Name)
{
return assembly;
}
}
return null;
}
}
To call Init() and register plugin in assembly file:
[assembly: PreApplicationStartMethod(
typeof(PluginBootstrapper), "Init")]
In the other side each plugin can be developed in separate solution so developed plugin have it's own AreaRegistration implementation for example for one of them I have:
public class SettingPluginAreaRegistration : AreaRegistration
{
public override string AreaName
{
get { return "SettingPlugin"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"SettingPlugin",
"SettingPlugin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
I know how we can set layout in View but this is proper way for when all the views are in same project and Views knows where the main layout is, but in plugin views must know where the main Layout located in main application?
For example in plugin view:
#{
ViewBag.Title = "Plugin View Title";
Layout = "the address of main layout in main application";
}
As this link mentioned the other way is _ViewStart but this is also not proper way because each plugin have it's own _ViewStart.
So is there a good pattern to do this such as implementing WebViewPage to override Layout :
public abstract class SitePage<T> : System.Web.Mvc.WebViewPage<T>
{
public override string Layout
{
get
{
return base.Layout;
}
set
{
base.Layout = value;
}
}
}
or make a interface to set Layout and force the plugins view to implement that interface and change the layout by the reflections in Init() in PluginBootstrapper or something else ?
UPDATE1:
Is it possible or a good way to load all WebPageBase types while registering plugins in Init() method and set Layout for each of them by reflection ?
UPDATE2:
The bad way
public class BaseController : Controller
{
private string _masterName;
public string MasterLayout
{
get
{
return _masterName;
}
set
{
_masterName = value;
}
}
}
for controller:
public class SettingController : BaseController
{
public ActionResult Index()
{
var myView = View();
myView.MasterName = MasterLayout;
return myView;
}
}
and in Init() in PluginBootstrapper something like this:
var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");
foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll", SearchOption.AllDirectories))
PluginAssemblies.Add(Assembly.LoadFile(file));
foreach (Assembly plugin in PluginAssemblies)
{
BuildManager.AddReferencedAssembly(plugin);
var controllers = plugin.DefinedTypes.Where(x => x.BaseType.Name == "BaseController");
foreach (Type t in controllers)
{
// t.InvokeMember("MasterLayout", BindingFlags.SetProperty);
PropertyInfo propertyInfo = t.GetProperty("MasterLayout");
if (propertyInfo != null)
propertyInfo.SetValue(t, /*Convert.ChangeType(*/"~/Views/Shared/_Wrapper.cshtml"/*, propertyInfo.PropertyType)*/, null);
}
}
or hard code MasterLayout to always return specific layout address.
I believe UPDATE2 is not proper way...
thanks in advance.
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 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 ?
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.