How to change View & partial view default location - asp.net-mvc

i am new in MVC and very curious about to know that how could i change view & partial view location.
we know that view & partial view store in view folder. if my controller name is home then view must be store in home folder inside view folder and all parial view store in shared folder. i like to know how can i change View & partial view default location ?
1) suppose my controller name is product but i want to store the corresponding view in myproduct folder.......guide me what i need to do to make everything works fine.
2) i want to store all my partial view in partial folder inside view folder and want to load all partial view from there. so guide me what i need to do to make everything works fine.
basicall how could i instruct controller to load view & partial view from my folder without mentioning path. looking for good discussion. thanks

If you want to have a special views locations for specific controllers, in your case you want ProductController views to go to MyProduct folder, you need to to override FindView and FindPartialView methods of RazorViewEngine:
public class MyRazorViewEngine : RazorViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext.Controller is ProductController)
{
string viewPath = "/Views/MyProduct/" + viewName + ".cshtml";
return base.FindView(controllerContext, viewPath, masterName, useCache);
}
return base.FindView(controllerContext, viewName, masterName, useCache);
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
if (controllerContext.Controller is ProductController)
{
string partialViewPath = "/Views/MyProduct/Partials/" + partialViewName + ".cshtml";
return base.FindPartialView(controllerContext, partialViewPath, useCache);
}
return base.FindPartialView(controllerContext, partialViewName, useCache);
}
}
And if you maybe want to prepend "My" to every controller views folder, your view engine should look like this
public class MyRazorViewEngine : RazorViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
string viewPath = "/Views/My" + GetControllerName(controllerContext) + "/" + viewName + ".cshtml";
return base.FindView(controllerContext, viewPath, masterName, useCache);
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
string partialViewPath = "/Views/My" + GetControllerName(controllerContext) + "/Partials/" + partialViewName + ".cshtml";
return base.FindPartialView(controllerContext, partialViewPath, useCache);
}
private string GetControllerName(ControllerContext controllerContext)
{
return controllerContext.RouteData.Values["controller"].ToString();
}
}
And than in your Global.asax
protected void Application_Start()
{
//remove unused view engines, for performance reasons as well
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MyRazorViewEngine());
}

You can modify RazorViewEngine's ViewLocationFormats and PartialViewLocationFormats properties in your Global.asax startup code. Something around the lines below should work:
protected void Application_Start(object obj, EventArgs e)
{
var engine = ViewEngines.Engines.OfType<RazorViewEngine>().Single();
var newViewLocations = new string[] {
"~/SomeOtherFolder/{1}/{0}.cshtml",
"~/GlobalFolder/{0}.cshtml"
};
engine.ViewLocationFormats = newViewLocations;
engine.PartialViewLocationFormats = newViewLocations;
}
IIRC, {1} would correspond to controller and {0} to view name, you can look at existing properties to make sure.
If you want to keep existing search locations you need to copy them into your new array.

Related

Failover to an alternate View when Partial View is not found?

I have an MVC app that uses dynamic business objects that are inherited from a parent object type. For example the base class Client might have two sub classes called Vendor and ServiceProvider, and these are all handled by the same controller. I have a partial view that I load on the right side of the page when viewing the client's details called _Aside.cshtml. When I load the client I try to look for a specific Aside first and failing that I load a generic one. Below is what the code looks like.
#try
{
#Html.Partial("_" + Model.Type.TypeName + "Aside")
}
catch (InvalidOperationException ex)
{
#Html.Partial("_Aside")
}
The TypeName property would have the word "Vendor" or "ServiceProvider" in it.
Now this works fine but the problem is I only want it to fail over if the view is not found, It's also failing over when there is an actual InvalidOperationException thrown by the partial view (usually the result of a child action it might call). I've thought about checking against Exception.Message but that seems a bit hackish. Is there some other way I can get the desired result without having to check the Message property or is that my only option at this point?
ex.Message = "The partial view '_ServiceProviderAside' was not found or no view
engine supports the searched locations. The following locations were
searched: (... etc)"
UPDATE: This is the class with extension methods I have currently in my project based off of Jack's answer, and Chao's suggestions as well.
//For ASP.NET MVC
public static class ViewExtensionMethods
{
public static bool PartialExists(this HtmlHelper helper, string viewName)
{
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
return view.View != null;
}
public static bool PartialExists(this ControllerContext controllerContext, string viewName)
{
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = ViewEngines.Engines.FindPartialView(controllerContext, viewName);
return view.View != null;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName) : HtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
return OptionalPartial(helper, viewName, fallbackViewName, null);
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, object model)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName, model) : MvcHtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName, model);
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
{
if (PartialExists(helper, viewName))
{
helper.RenderPartial(viewName);
}
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
}
UPDATE: If you happen to be using ASP.NET Core MVC, swap the PartialExists() methods for these three methods, and change all of the usages of HtmlHelper for IHtmlHelper in the other methods. Skip this if you're not using ASP.NET Core
//For ASP.NET Core MVC
public static class ViewExtensionMethods
{
public static bool PartialExists(this IHtmlHelper helper, string viewName)
{
var viewEngine = helper.ViewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(helper.ViewContext, viewName, false);
return view.View != null;
}
public static bool PartialExists(this ControllerContext controllerContext, string viewName)
{
var viewEngine = controllerContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(controllerContext, viewName, false);
return view.View != null;
}
public static bool PartialExists(this ViewContext viewContext, string viewName)
{
var viewEngine = viewContext.HttpContext.RequestServices.GetService<ICompositeViewEngine>();
if (string.IsNullOrEmpty(viewName)) throw new ArgumentNullException(viewName, "View name cannot be empty");
var view = viewEngine.FindView(viewContext, viewName, false);
return view.View != null;
}
}
In my view...
#Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside")
//or
#Html.OptionalPartial("_" + Model.Type.TypeName + "Aside", "_Aside", Model.AsideViewModel)
Came across this answer while trying to solve the problem of nested sections as I wanted to include styles and scripts in an intermediate view. I ended up deciding the simplest approach was convention of templatename_scripts and templatename_styles.
So just to add to the various options here is what I'm using based on this.
public static class OptionalPartialExtensions
{
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName)
{
return PartialExists(helper, viewName) ? helper.Partial(viewName) : MvcHtmlString.Empty;
}
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
return helper.Partial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName)
{
if (PartialExists(helper, viewName))
{
helper.RenderPartial(viewName);
}
}
public static void RenderOptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName)
{
helper.RenderPartial(PartialExists(helper, viewName) ? viewName : fallbackViewName);
}
public static bool PartialExists(this HtmlHelper helper, string viewName)
{
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentNullException(viewName, "View name cannot be empty");
}
var view = ViewEngines.Engines.FindPartialView(helper.ViewContext, viewName);
return view.View != null;
}
}
This brings the my most common use cases in to the extension methods helping keep the views that bit cleaner, the RenderPartials were added for completeness.
I had a similar requirement. I wanted to keep the view markup cleaner and also to avoid generating the dynamic view name twice. This is what I came up with (modified to match your example):
Helper extension:
public static string FindPartial(this HtmlHelper html, string typeName)
{
// If you wanted to keep it in the view, you could move this concatenation out:
string viewName = "_" + typeName + "Aside";
ViewEngineResult result = ViewEngines.Engines.FindPartialView(html.ViewContext, viewName);
if (result.View != null)
return viewName;
return "_Aside";
}
View:
#Html.Partial(Html.FindPartial(Model.Type.TypeName))
or with access to the Model within the partial :
#Html.Partial(Html.FindPartial(Model.Type.TypeName), Model)
You could try the FindPartialView method to check if the view exists. Something along these lines might work (untested):
public bool DoesViewExist(string name)
{
string viewName = "_" + Model.Type.TypeName + "Aside";
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName , null);
return (viewResult.View != null);
}
Info on the FindPartialView method for ASP MVC 3
Bug fix to handle null viewName or null fallbackViewName (replace appropriate code in OP):
public static MvcHtmlString OptionalPartial(this HtmlHelper helper, string viewName, string fallbackViewName, object model)
{
string partialToRender = null;
if (viewName != null && PartialExists(helper, viewName))
{
partialToRender = viewName;
}
else if (fallbackViewName != null && PartialExists(helper, fallbackViewName))
{
partialToRender = fallbackViewName;
}
if (partialToRender != null)
{
return helper.Partial(partialToRender, model);
}
else
{
return MvcHtmlString.Empty;
}
}
I have edited the OP's code (which combines code from multiple answers), but my edit is pending peer review.

Custom ViewEngine with custom pages extension

i created a custom View Engine for handling mobile requests. These views must have ".mobile" extension and have to be placed under /ViewsMobile root folder:
public class MobileViewEngine : RazorViewEngine
{
public MobileViewEngine()
{
MasterLocationFormats = new string[] { "~/ViewsMobile/Shared/{0}.mobile" };
ViewLocationFormats = new string[] { "~/ViewsMobile/{1}/{0}.mobile", "~/ViewsMobile/Shared/{0}.mobile" };
PartialViewLocationFormats = new string[] { "~/ViewsMobile/Widgets/{1}/{0}.mobile" };
FileExtensions = new string[] { "mobile" };
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
ViewEngineResult result = null;
var request = controllerContext.HttpContext.Request;
if (request.Browser.IsMobileDevice)
{
result = base.FindView(controllerContext, viewName, masterName, false);
}
return null;
}
}
i inserted this ViewEngine in ViewEngines.Engines at position 0 (to be the top engine) in Application_Start event.
ViewEngines.Engines.Insert(0, new MobileViewEngine());
After i added this line into web.config in order to recognize the .mobile extension:
<buildProviders>
<add extension=".mobile" type="System.Web.WebPages.Razor.RazorBuildProvider, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
</buildProviders>
Now, if im accessing a home page (controller=pages, action=main) from a mobile platform im getting the following exception:
Could not determine the code language for "~/ViewsMobile/Pages/Main.mobile". Crashes on line base.FindView(controllerContext, viewName, masterName, false);
Here's the stack trace:
[InvalidOperationException:Could not determine the code language for
"~/ViewsMobile/Pages/Main.mobile]
System.Web.WebPages.Razor.WebPageRazorHost.GetCodeLanguage()+24401
System.Web.WebPages.Razor.WebPageRazorHost..ctor(String virtualPath,
String physicalPath) +136
System.Web.Mvc.MvcWebRazorHostFactory.CreateHost(String virtualPath,
String physicalPath) +43 ....
Do you know how i can use a custom extension for my views like ".mobile" and use Razor inside each one?
Thanks in advance.
Kind Regards.
Jose.

Question about ViewEngines.Engines.FindView method and the masterName parameter

This method works great if I pass null to the last param masterName, My views setup in my class derivved from RazorViewEngine work and all is good. Out of curiosity what is the masterName parameter used for? I first thought maybe it was for a layout.cshtml, however; when I pass it a layout it throws an exception.... Any ideas on how this is supposed to be used, what is it looking for?
Custom View Engine (Hardly LOL)
public class CustomRazorViewEngine : RazorViewEngine
{
private readonly string[] NewViewFormats = new[]
{
"~/Views/Messaging/{0}.cshtml"
};
public CustomRazorViewEngine()
{
base.ViewLocationFormats = base.ViewLocationFormats.Union(NewViewFormats).ToArray();
}
}
public string RenderViewToString(string viewName, object model, ControllerContext controllerContext,
string masterName)
{
if (string.IsNullOrEmpty(viewName))
viewName = controllerContext.RouteData.GetRequiredString("action");
controllerContext.Controller.ViewData.Model = model;
using (var stringWriter = new StringWriter())
{
ViewEngineResult viewEngineResult = ViewEngines.Engines.FindView(controllerContext, viewName, masterName);
var viewContext = new ViewContext(controllerContext, viewEngineResult.View,
controllerContext.Controller.ViewData,
controllerContext.Controller.TempData,
stringWriter);
viewEngineResult.View.Render(viewContext, stringWriter);
return stringWriter.GetStringBuilder().ToString();
}
}
So after some more debugging I have found what appears to be the correct answer. First let me state that the masterName parameter is the name of the "Layout" so to say that the view being rendered will use. The catch here is that layout must be able to be located. So instead of the code for the ViewEngine in my original post the following code works as desired.
public string RenderViewToString(string viewName, object model, ControllerContext controllerContext,
string masterName)
{
if (string.IsNullOrEmpty(viewName))
viewName = controllerContext.RouteData.GetRequiredString("action");
controllerContext.Controller.ViewData.Model = model;
using (var stringWriter = new StringWriter())
{
ViewEngineResult viewEngineResult = ViewEngines.Engines.FindView(controllerContext, viewName, masterName);
var viewContext = new ViewContext(controllerContext, viewEngineResult.View,
controllerContext.Controller.ViewData,
controllerContext.Controller.TempData,
stringWriter);
viewEngineResult.View.Render(viewContext, stringWriter);
return stringWriter.GetStringBuilder().ToString();
}
}
public class CustomRazorViewEngine : RazorViewEngine
{
private readonly string[] NewMasterViewFormats = new[]
{
"~/Views/Messaging/Layouts/{0}.cshtml"
};
private readonly string[] NewViewFormats = new[]
{
"~/Views/Messaging/{0}.cshtml"
};
public CustomRazorViewEngine()
{
base.ViewLocationFormats = base.ViewLocationFormats.Union(NewViewFormats).ToArray();
base.MasterLocationFormats = base.MasterLocationFormats.Union(NewMasterViewFormats).ToArray();
}
}
Now when calling
string returnViewToString = _viewUtils.RenderViewToString("RegistrationEmail", new RegistrationEmailModel
{ UserName = userName
},
this.ControllerContext,"_RegistrationEmailLayout");
Everything is happy and my layout for the passed in view, if it exists in the folder gets used. This was the highlight of my day... LOL

Is ViewPageBase the right place to decide what master page to load?

As you can tell from the title, I'm a n00b to MVC. I'm trying to decide what Master to load based on my route configuration settings. Each route has a masterpage property (in addition to the usual url, controller and action properties) and I'm setting the masterpage in the OnPreInit event of a ViewPageBase class (derived from ViewPage). However, I'm not sure if this is the MVC way of doing it? Do I need a controller for this that supplies the masterpage info to the view?
Here's my code snippet.
public class ViewPageBase : ViewPage
{
protected override void OnPreInit(EventArgs e)
{
RouteElement currentRoute = MvcRoutes.GetCurrentRoute();
//Set master page
this.MasterPageFile = string.IsNullOrEmpty(currentRoute.MasterPage) ?
MvcConfiguration.DefaultMasterPage : currentRoute.MasterPage;
base.OnPreInit(e);
}
}
I'm a huge fan of ignoring anything that seems webformish and trying to always find the right MVC hook. In this case creating a custom view engine is the correct extensibility hook for this. If you think about it the engine that decides what .aspx file to render should also decide what mater page that aspx file uses. Here is some semi-psuedo ( I've never compiled it ) code that should work.
public class DynamicMasterViewEngine: VirtualPathProviderViewEngine
{
public DynamicMasterViewEngine()
{
/* {0} = view name or master page name
* {1} = controller name */
MasterLocationFormats = new[] {
"~/Views/Shared/{0}.master"
};
ViewLocationFormats = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/Shared/{0}.aspx"
};
PartialViewLocationFormats = new[] {
"~/Views/{1}/{0}.ascx",
};
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
throw new NotImplementedException();
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
return new WebFormView(viewPath, masterPath );
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
RouteElement currentRoute = MvcRoutes.GetCurrentRoute();
var masterName = string.IsNullOrEmpty(currentRoute.MasterPage) ?
MvcConfiguration.DefaultMasterPage : currentRoute.MasterPage;
return base.FindView(controllerContext, viewName, masterName, useCache);
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
return base.FileExists(controllerContext, virtualPath);
}
}
ported from this answer

How to group partial shared views for specified controllers?

Is it possible to tell ViewEngine to look for partial shared views in additional folders for specified controllers (while NOT for others)?
I'm using WebFormViewEngine.
This is how my PartialViewLocations looks at the moment.
public class ViewEngine : WebFormViewEngine
{
public ViewEngine()
{
PartialViewLocationFormats = PartialViewLocationFormats
.Union(new[]
{
"~/Views/{1}/Partial/{0}.ascx",
"~/Views/Shared/Partial/{0}.ascx"
}).ToArray();
}
Sure. Don't change PartialViewLocationFormats in this case; instead, do:
public override ViewEngineResult FindPartialView(
ControllerContext controllerContext,
string partialViewName,
bool useCache)
{
ViewEngineResult result = null;
if (controllerContext.Controller.GetType() == typeof(SpecialController))
{
result = base.FindPartialView(
controllerContext, "Partial/" + partialViewName, useCache);
}
//Fall back to default search path if no other view has been selected
if (result == null || result.View == null)
{
result = base.FindPartialView(
controllerContext, partialViewName, useCache);
}
return result;
}

Resources