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 ?
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);
});
//...
}
I have a _LoginPartial View and want to send data to it by ViewBag, but the Controller that I'am sending data from, doesn't have a View.
public PartialViewResult Index()
{
ViewBag.sth = // some data
return PartialView("~/Views/Shared/_LoginPartial.cshtml");
}
This code didn't work for me.
It seems you're expecting this Index action to be called when you do: #Html.Partial('_LoginPartial'). That will never happen. Partial just runs the partial view through Razor with the current view's context and spits out the generated HTML.
If you need additional information for your partial, you can specify a custom ViewDataDictionary:
#Html.Partial("_LoginPartial", new ViewDataDictionary { Foo = "Bar" });
Which you can then access inside the partial via:
ViewData["Foo"]
You can also use child actions, which is generally preferable if working with a partial view that doesn't need the context of the main view. _LoginPartial seems like a good candidate, although I'm not sure how exactly you're using it. Ironically, though, the _LoginPartial view that comes with a default MVC project with individual auth uses child actions.
Basically, the code you have would already work, you would just need to change how you reference it by using Html.Action instead of Html.Partial:
#Html.Action("Index")
Notice that you're calling the action here and now the view.
You can always pass data directly to the partial view.
public PartialViewResult Index()
{
var data = // some data
return PartialView("~/Views/Shared/_LoginPartial.cshtml", data);
}
Pass multiple pieces of data
public class MyModel
{
public int Prop1 { get; set; }
public int Prop2 { get; set; }
}
public PartialViewResult Index()
{
var data = new MyModel(){ Prop1 = 5, Prop2 = 10 };
return PartialView("~/Views/Shared/_LoginPartial.cshtml", data);
}
I passed viewBag data to my partial view like below, and I converted that viewBag data object to JSON in my partial view by using #Html.Raw(Json.Encode(ViewBag.Part));
my code sample is given below.
public async Task<ActionResult> GetJobCreationPartialView(int id)
{
try
{
var client = new ApiClient<ServiceRepairInspectionViewModel>("ServiceRepairInspection/GetById");
var resultdata = await client.Find(id);
var client2 = new ApiClient<PartViewModel>("Part/GetActive");
var partData = await client2.FindAll();
var list = partData as List<PartViewModel> ?? partData.ToList();
ViewBag.Part = list.Select(x => new SelectListItem() {Text = x.PartName, Value = x.Id.ToString()});
return PartialView("_CreateJobCardView" ,resultdata);
}
catch (Exception)
{
throw;
}
}
Here i have passed both model and viewBag .
First off, the code in your question does not run. When you do #Html.Partial("_SomeView") the Index() method you have there does not run. All #Html.Partial("_SomeView") does is render _SomeView.cshtml in your current view using the current view's ViewContext.
In order to get this to work you need a bit of functionality that's common to all the controllers in your project. You have two options: extension method for ControllerBase or a BaseController that all the controllers in your project inherit from.
Extension method:
Helper:
public static class ControllerExtensions
{
public static string GetCommonStuff(this ControllerBase ctrl)
{
// do stuff you need here
}
}
View:
#ViewContext.Controller.GetCommonStuff()
BaseController
Controller:
public class BaseController : Controller
{
public string GetCommonStuff()
{
// do stuff you need here
}
}
Other controllers:
public class SomeController : BaseController
...
...
View:
#((ViewContext.Controller as BaseController).GetCommonStuff())
I've got an ASP.NET MVC4 project with standard controllers and views. I have to different master pages I use, depending on a global variable I can reach out and get based on the Request.Url.Host. I've written the code below but it is getting kind of bulky to put in every controller. I've gotten it pretty short but was hoping for a suggestion to make it much cleaner.
private ActionResult IndexBase(string year)
{
var data = null; // real data here for model
var localConfig = LocalConfig.GetLocalValues(Request.Url.Host, null, year);
ViewResult view = localConfig.EventType == "svcc"
? View("Index", "~/Views/Shared/_Layout.cshtml", data)
: View("Index", "~/Views/Shared/_LayoutConf.cshtml", data);
return view;
}
I don't know if this solution works for you, but I would solve it with ViewModel's and a common base controller.
One of the nice things with Layouts is you can pass a base ViewModel with the properties common to all your pages (the users name, for example). In your case, you could store the path to the Layout.
First, the base class every ViewModel derives from:
public class MasterViewModel
{
public string Name { get; set; }
public string Layout { get; set; }
}
I prefer to use a 1:1 mapping of ViewModels to Views. That is, each action gets it's own ViewModel. For example: HomeIndexViewModel for /Home/Index, ProfileEditViewModel for /Profile/Edit, etc.
public class HomeIndexViewModel : MasterViewModel
{
// properties you need for /Home/Index
}
To simplify creating the ViewModels, I add a generic method on a base controller that handles setting all these the common properties:
public class BaseController : Controller
{
protected T CreateViewModel<T>() where T : MasterViewModel, new()
{
User user = db.GetUser(User.Identity.Name);
var localConfig = LocalConfig.GetLocalValues(Request.Url.Host, null, year);
return new T()
{
Name = user.Name,
Layout = localConfig.EventType == "svcc" ? "~/Views/Shared/_Layout.cshtml"
: "~/Views/Shared/_LayoutConf.cshtml"
}
}
}
And finally, just use CreateViewModel() in each of your Actions and things should work:
public class HomeController : BaseController
{
public ActionResult Index()
{
HomeIndexViewModel viewModel = CreateViewModel<HomeIndexViewModel>();
return View(viewModel);
}
}
Inside the Views, you can just set
#model HomeIndexViewModel
#{
Layout = Model.Layout;
}
There's no need to duplicate the path anywhere, and changing the logic on which Layout to show requires you only change it in one place.
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.
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.