Custom ViewEngine with custom pages extension - asp.net-mvc

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.

Related

Search for view files in a custom location only for Specified Area in MVC 5

I'm looking to override the 'ViewEngine' for MVC5 in a way that first, it find my pages.. which already i failed.
Second, It only operate on a single Area {root}/{area}/{controller}/{action}/{etc.}
as so far as i googled it, i found several topic and answers, but they didn't fit my need. so i rather to ask it here, maybe i'm wrong with something...
public class CustomAreaViewEngine:RazorViewEngine
{
public CustomAreaViewEngine()
{
var viewLocations = new[]
{
"~/App/pages/{1}/{0}.cshtml",
"~/App/pages/{1}/{0}.vbhtml"
};
AreaMasterLocationFormats = viewLocations;
AreaPartialViewLocationFormats = viewLocations;
AreaViewLocationFormats = viewLocations;
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
var viewEngineResult = base.FindPartialView(controllerContext, partialViewName, useCache);
return viewEngineResult;
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
controllerContext.RouteData.Values["controller"] = controllerContext.RouteData.Values["controller"].ToString().ToLower();
var viewEngineResult = base.FindView(controllerContext, viewName, masterName, useCache);
return viewEngineResult;
}
}
The method 'FindView' returns empty [first problem]
Global Config:
AreaRegistration.RegisterAllAreas();
ViewEngines.Engines.Add(new CustomAreaViewEngine()); // Look View Inside App/Pages/{1}/{0}.cshtml/.vbhtml
My Hierarchy
Root
--/App
--/--/pages
--/--/shared (but not that shared)
--/...
--/Area
--/--/View
--/--/--/Controller
--/--/--/View(MVC BASED HIERARCHY, I Don't want to use in most case, and redirect to App)
--/--/--/...
--/--/...
--/Controller
--/View
--/--/...(MVC BASED HIERARCHY)
--/...
EDITS:
EDIT1:
Changes i did due to #James Ellis-Jones answers:
Images:
My Route Config:
Area Provided Route Config:
Global Config:
My View Engine:
still when i use http://localhost:1422/view/home/index i receive an error, which exist in my other home (View, related to the main controller, not the area controller.) it bring a wrong file.
Another Issue I Figured Out, Which Didn't Worked Too
My namespaces was wrong in last edit, and i changed them, but it didn't worked out too.
namespaces: new[] { "RavisHSB.Areas.View.Controllers" }
EDIT2:
Changes i did due to #CarlosFernández answers:
I add ViewEngines.Engines.Clear(); it somehow went one step ahead. but still doesn't work.
New Global.aspx:
And i face this new error:
Try to customize this code for your engine
Update #1
private static readonly List<string> EmptyLocations;
public MyEngine()
{
ViewLocationFormats = new[]
{
"~/App/Pages/{1}/{0}.cshtml",
"~/App/Pages/{1}/{0}.vbhtml"
};
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
try
{
return System.IO.File.Exists(controllerContext.HttpContext.Server.MapPath(virtualPath));
}
catch (HttpException exception)
{
if (exception.GetHttpCode() != 0x194)
{
throw;
}
return false;
}
catch
{
return false;
}
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
var strArray = new List<string>();
var strArray2 = new List<string>();
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException("viewName must be specified.", "viewName");
}
var controllerName = controllerContext.RouteData.GetRequiredString("controller");
var viewPath = "";
var viewLocation = ViewLocationFormats;
var masterLocation = MasterLocationFormats;
var area = "";
if (controllerContext.RouteData.DataTokens.Keys.Any(x => x.ToLower() == "area"))
{
area = controllerContext.RouteData.DataTokens.FirstOrDefault(x => x.Key.ToLower() == "area").Value.ToString();
viewLocation = AreaViewLocationFormats;
masterLocation = AreaMasterLocationFormats;
}
viewPath = GetPath(controllerContext, viewLocation, area, viewName, controllerName, "TubaSite_View", useCache, strArray);
var masterPath = GetPath(controllerContext, masterLocation, area, masterName, controllerName, "TubaSite_Master", useCache, strArray2);
if (!string.IsNullOrEmpty(viewPath) && (!string.IsNullOrEmpty(masterPath) || string.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(this.CreateView(controllerContext, viewPath, masterPath), this);
}
if (string.IsNullOrEmpty(viewPath))
{
throw new Exception(String.Format("Page Not Found - {0} {1}", masterName, masterPath));
}
return new ViewEngineResult(strArray.Union<string>(strArray2));
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
var strArray = new List<string>();
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(partialViewName))
{
throw new ArgumentException("partialViewName must be specified.", "partialViewName");
}
var requiredString = controllerContext.RouteData.GetRequiredString("controller");
var partialViewLocation = PartialViewLocationFormats;
var area = "";
if (controllerContext.RouteData.DataTokens.Keys.Any(x => x.ToLower() == "area"))
{
area = controllerContext.RouteData.DataTokens.FirstOrDefault(x => x.Key.ToLower() == "area").Value.ToString();
partialViewLocation = AreaPartialViewLocationFormats.Union(PartialViewLocationFormats).ToArray();
}
var partialViewPath = "";
partialViewPath = GetPath(controllerContext, partialViewLocation, area, partialViewName, requiredString, "TubaSite_Partial", false, strArray);
return string.IsNullOrEmpty(partialViewPath) ? new ViewEngineResult(strArray) : new ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), this);
}
private string GetPath(ControllerContext controllerContext, string[] locations, string area,
string name, string controllerName,
string cacheKeyPrefix, bool useCache, List<string> searchedLocations)
{
searchedLocations = EmptyLocations;
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
if ((locations == null) || (locations.Length == 0))
{
throw new InvalidOperationException("Path not found.");
}
var flag = IsSpecificPath(name);
var key = CreateCacheKey(cacheKeyPrefix, name, flag ? string.Empty : controllerName);
if (useCache)
{
var viewLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, key);
if (viewLocation != null)
{
return viewLocation;
}
}
if (!flag)
{
return
GetPathFromGeneralName(controllerContext, locations, area, name, controllerName, key,
searchedLocations);
}
return GetPathFromSpecificName(controllerContext, name, key, searchedLocations);
}
private static bool IsSpecificPath(string name)
{
var ch = name[0];
if (ch != '~')
{
return (ch == '/');
}
return true;
}
private string CreateCacheKey(string prefix, string name, string controllerName)
{
return string.Format(CultureInfo.InvariantCulture,
":ViewCacheEntry:{0}:{1}:{2}:{3}", GetType().AssemblyQualifiedName, prefix, name, controllerName);
}
private string GetPathFromGeneralName(ControllerContext controllerContext, IEnumerable<string> locations, string area, string name,
string controllerName, string cacheKey, List<string> searchedLocations)
{
if (locations == null) throw new ArgumentNullException("locations");
if (searchedLocations == null) searchedLocations = new List<string>();
var virtualPath = string.Empty;
var locationData =
locations.Select(
t =>
string.Format(CultureInfo.InvariantCulture, t, new object[] { name, controllerName })).ToList();
foreach (var str2 in locationData)
{
if (FileExists(controllerContext, str2))
{
virtualPath = str2;
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
return virtualPath;
}
searchedLocations.Add(str2);
}
return virtualPath;
}
private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, List<string> searchedLocations)
{
var virtualPath = name;
if (!FileExists(controllerContext, name))
{
virtualPath = string.Empty;
searchedLocations = new List<string>() { name };
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
return virtualPath;
}
}
and also for the last error you've get you can use this config
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Web.Optimization" />
</namespaces>
</pages>
</system.web.webPages.razor>
Try this: in RouteConfig.cs where you are setting up the route which links to a controller which you want to find the custom view location, add this:
var route = routes.MapRoute(<route params here>);
route.DataTokens["area"] = "AreaName";
This will tell MVC that when you follow this route, you are going into area 'AreaName'. Then it will subsequently look for views in that area. Your custom ViewEngine will only affect view locations when MVC is looking for them in some area. Otherwise it won't have any effect, as the location format lists for areas are the only ones it overrides.

How to change View & partial view default location

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.

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