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.
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 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.
I edited my whole question, so do not wonder :)
Well, I want to have an ActionResult that takes domain model data and some additional parameters, i.e page index and page size for paging a list. It decide itself if it returns a PartialViewResult or a ViewResult depending on the kind of web request (ajax request or not).
The reffered data shall be mapped automatically by using an IMappingService, which is responsible for transforming any domain model data into a view model.
The MappingService uses AutoMapper for simplicity.
MappingActionResult:
public abstract class MappingActionResult : ActionResult
{
public static IMappingService MappingService;
}
BaseHybridViewResult:
public abstract class BaseHybridViewResult : MappingActionResult
{
public const string defaultViewName = "Grid";
public string ViewNameForAjaxRequest { get; set; }
public object ViewModel { get; set; }
public override void ExecuteResult(ControllerContext context)
{
if (context == null) throw new ArgumentNullException("context");
var usePartial = ShouldUsePartial(context);
ActionResult res = GetInnerViewResult(usePartial);
res.ExecuteResult(context);
}
private ActionResult GetInnerViewResult(bool usePartial)
{
ViewDataDictionary viewDataDictionary = new ViewDataDictionary(ViewModel);
if (String.IsNullOrEmpty(ViewNameForAjaxRequest))
{
ViewNameForAjaxRequest = defaultViewName;
}
if (usePartial)
{
return new PartialViewResult { ViewData = viewDataDictionary, ViewName = ViewNameForAjaxRequest };
}
return new ViewResult { ViewData = viewDataDictionary };
}
private static bool ShouldUsePartial(ControllerContext context)
{
return context.HttpContext.Request.IsAjaxRequest();
}
}
AutoMappedHybridViewResult:
public class AutoMappedHybridViewResult<TSourceElement, TDestinationElement> : BaseHybridViewResult
{
public AutoMappedHybridViewResult(PagedList<TSourceElement> pagedList)
{
ViewModel = MappingService.MapToViewModelPagedList<TSourceElement, TDestinationElement>(pagedList);
}
public AutoMappedHybridViewResult(PagedList<TSourceElement> pagedList, string viewNameForAjaxRequest)
{
ViewNameForAjaxRequest = viewNameForAjaxRequest;
ViewModel = MappingService.MapToViewModelPagedList<TSourceElement, TDestinationElement>(pagedList);
}
public AutoMappedHybridViewResult(TSourceElement model)
{
ViewModel = MappingService.Map<TSourceElement, TDestinationElement>(model);
}
public AutoMappedHybridViewResult(TSourceElement model, string viewNameForAjaxRequest)
{
ViewNameForAjaxRequest = viewNameForAjaxRequest;
ViewModel = MappingService.Map<TSourceElement, TDestinationElement>(model);
}
}
Usage in controller:
public ActionResult Index(int page = 1)
{
return new AutoMappedHybridViewResult<TeamEmployee, TeamEmployeeForm>(_teamEmployeeRepository.GetPagedEmployees(page, PageSize));
}
So as you can see the IMappingService is hidden. The controller should not know anything about the IMappingService interface, when AutoMappedHybridViewResult is used.
Is the MappingActionResult with the static IMappingServer appropriate or am I violating the DI principle?
I think a better design is to have a ViewResultFactory that depends on IMappingService, then you can inject that into your controller. Then you call it like so:
public class MyController : Controller
{
IViewResultFactory _viewResultFactory;
ITeamEmployeeRepository _teamEmployeeRepository;
public MyController(IViewResultFactory viewResultFactory)
{
_viewResultFactory = viewResultFactory;
}
public ActionResult MyAction(int page, int pageSize)
{
return
_viewResultFactory.GetResult<TeamEmployee, TeamEmployeeForm>(
_teamEmployeeRepository.GetPagedEmployees(page, pageSize));
}
}
The implementation would like this (you would need to create overloads for each of your HybridViewResult constructors):
public HybridViewResult<TSourceElement, TDestinationElement> GetResult<TSourceElement, TDestinationElement>(PagedList<TSourceElement> pagedList)
{
return new HybridViewResult<TSourceElement, TDestinationElement>(_mappingService, pagedList);
}
That way you hide the implementation from your controllers, and you don't have to depend on the container.
There are a few different points that you could inject IMappingService. http://codeclimber.net.nz/archive/2009/04/08/13-asp.net-mvc-extensibility-points-you-have-to-know.aspx is a good site for help in picking the appropriate extensibility points for .NET MVC.
If you want to stick with having this functionality be a derived ActionResult, then I think you could put the dependency in the ActionInvoker if you want to, but the Controller makes more sense to me. If you don't want the IMappingService in the Controller, you could always wrap it in a HybridViewResultFactory, and access that object in the Controller. In that case your shortcut methods would look like:
public HybridViewResult<TSourceElement, TDestinationElement> AutoMappedHybridView<TSourceElement,TDestinationElement>(PagedList<TSourceElement> pagedList, string viewNameForAjaxRequest)
{
HybridViewResultFactory.Create<TSourceElement, TDestinationElement>(pagedList, viewNameForAjaxRequest);
}
etc.
I'm not sure why you need to use an ActionResult, but if there is no reason that makes it explicitly necessary, you could create a HybridViewModel class and a HybridViewModelBinder class that is injected with the mapping service dependency.
I am assuming you want to use constructor injection, but if you have the StructureMap dependency in your UI assembly, you could access a static dependency resolver class (like Clowers said).
This question would be easier to give a definite answer to if I understood why you using an ActionResult.
It seems like you are using the action result to handle two functionalities that do not necessarily go together all the time, and that could be used separately. Also, there is not a clear indication that it needs to be in an ActionResult.
Presumably, you could (a) leverage the Automapper functionality for results other than html (ViewResult) output, and (b) you could leverage the functionality of auto-detecting ajax requests without needing to automap the model.
It seems to me like the automapping of the view model could be used to inject the view model into the controller action directly, thus removing the controller's dependency on the IMappingService. What you would need is a ModelBinder class to be injected with your IMappingService (the implementation of which I assume contains a repository or datastore type dependency).
Here is a good article explaining how to leverage model binders: http://odetocode.com/blogs/scott/archive/2009/04/27/6-tips-for-asp-net-mvc-model-binding.aspx.
Then you can overwrite the DefaultModelBinder in the classes that need to be Automapped as follows:
public ActionResult DoItLikeThis([AutoMap(typeof(MyDomainModelClass))]MyViewModelClass viewModel){
//controller action logic
}
Now, regarding the HybridViewResult, I would suggest that you handle this with an Action Filter instead. So, you could just use ActionResult or ViewResultBase as the Result type of your action method and decorate it with an action filter, i.e.:
[AutoSelectViewResult]
public ViewResultBase AndDoThisLikeSo(){
//controller action logic
}
I think overall this will be a much better solution than coupling these two functionalities to an ActionResult.
I have this
public class HomeController{
public ActionResult Index()
{
//do stuff
return View();
}
Obviously this choses and renders Index.aspx in the Home folder.
What we really want is to chose another file - Index.ar.aspx - if the CurrentCulture is ar-AE. I don't want IF statements on every return View() call. Anyone help me find the best place to override the name of the view file that is selected?
Note, please don't tell me off :) I know that separate files are a bit hacky, and we ARE using RESX files, DIR directives and routes to change languages etc. But we need seperate files for layout reasons.
You should create your own ViewEngine. If you are using the WebFormViewEngine that is the default one with MVC, you could easily subclass it and then override the FindView(...) method.
In your overridden FindView(...) method you could easily look for a file based on a convention that includes the name of the current culture.
Take a look at Scott Hanselmans post about a ViewEngine that looks for different view files if the site is browsed using a mobile device.
Perhaps something like:
public class ExampleViewEngine : WebFormViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
ViewEngineResult result = null;
string conventionViewName = string.Format("{0}.{1}", viewName, System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName);
result = base.FindView(controllerContext, conventionViewName, masterName, useCache);
if (result == null || result.View == null)
{
result = base.FindView(controllerContext, viewName, masterName, useCache);
}
return result;
}
}
It sounds like you really want the View engine to be able to decide which view to return, rather than having the controllers be responsible for it.
Take a look at this tutorial, and google around for some others. It's pretty simple to override the default view engine, and you can add the language choosing logic there, removing the need for it at the controller level.
public class BaseController{
// Don't remember parameter type exactly
public void OnActionExecuted(ActionExecutedContext context)
{
// if view is returned, add culture suffix to its name
// also may need to do so for PartialViewResult
// One problem is if view is not named; Name is "";
// in this case use context.ActionContext.Name or RouteData["action"] for view name
if (context.Result is ViewResult)
{
var view = context.Result as ViewResult;
view.Name = view.Name + CurrentCultureSuffix;
}
}
}
public class HomeController: BaseController{
public ActionResult Index()
{
//do stuff
return View();
}
}