Is ViewPageBase the right place to decide what master page to load? - asp.net-mvc

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

Related

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.

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.

How do I pass value to MVC3 master page ( _layout)?

I have a custom modelbinder, its check the authentication cookie and return the value.
public class UserDataModelBinder<T> : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext.RequestContext.HttpContext.Request.IsAuthenticated)
{
var cookie =
controllerContext.RequestContext.HttpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
if (cookie == null)
return null;
var decrypted = FormsAuthentication.Decrypt(cookie.Value);
if (!string.IsNullOrWhiteSpace(decrypted.UserData))
return JsonSerializer.DeserializeFromString<T>(decrypted.UserData);
}
return null;
}
}
if I need to use it, I just need to pass it to the action. everything works.
public ActionResult Index(UserData userData)
{
AccountLoginWidgetVM model = new AccountLoginWidgetVM();
if (null != userData)
model.UserData = userData;
return View(userData);
}
However, I want to use it in my master page, because once user login, i want to display their info on the top on every page. I tried a few things, coudln't get it work
#Html.RenderPartial("LoginPartial", ???model here??)
We did it as follows:
Defined separate viewmodel for masterpages.
public class MasterPageViewModel
{
public Guid CurrentUserId { get; set; }
public string CurrentUserFullName { get; set; }
}
Added injection filter and filter provider.
public class MasterPageViewModelInjectorFilterProvider: IFilterProvider
{
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
return new [] {new Filter(new MasterPageViewModelInjectorFilter(), FilterScope.Action, null), };
}
private class MasterPageViewModelInjectorFilter: IResultFilter
{
public void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult == null)
return;
if (viewResult.ViewBag.MasterPageViewModel != null)
return;
//setup model whichever way you want
var viewModel = new MasterPageViewModel();
//inject model into ViewBag
viewResult.ViewBag.MasterPageViewModel = viewModel;
}
public void OnResultExecuted(ResultExecutedContext filterContext)
{
}
}
}
Configure filter provider:
//in Application_Start
FilterProviders.Providers.Add(new MasterPageViewModelInjectorFilterProvider());
Use in master:
ViewBag.MasterPageViewModel
This way you have fine uncoupled architecture. Of course you can combine it with Dependency Injection (we do, but I left it out for clarity) and configure your action filter for every action whichever way you want.
In this case you can use ViewBag.
public ActionResult Index(UserData userData)
{
AccountLoginWidgetVM model = new AccountLoginWidgetVM();
if (null != userData)
model.UserData = userData;
ViewBag.UserData = userData;
return View(userData);
}
#Html.RenderPartial("LoginPartial", ViewBag.UserData)
You have to make sure that userData is not null. If it'll be null the passed model will be default model of the view.

Limiting HTTP verbs on every action

Is it a good practice to limit the available HTTP verbs for every action? My code is cleaner without [HttpGet], [HttpPost], [HttpPut], or [HttpDelete] decorating every action, but it might also be less robust or secure. I don't see this done in many tutorials or example code, unless the verb is explicitly required, like having two "Create" actions where the GET version returns a new form and the POST version inserts a new record.
Personally I try to respect RESTful conventions and specify the HTTP verb except for the GET actions which don't modify any state on the server thus allowing them to be invoked with any HTTP verb.
Yes, I believe it's a good practice to limit your actions only to the appropriate HTTP method it's supposed to handle, this will keep bad requests out of your system, reduce the effectiveness of possible attacks, improve the documentation of your code, enforce a RESTful design, etc.
Yes, using the [HttpGet], [HttpPost] .. attributes can make your code harder to read, specially if you also use other attributes like [OutputCache], [Authorize], etc.
I use a little trick with a custom IActionInvoker, instead of using attributes I prepend the HTTP method to the action method name, e.g.:
public class AccountController : Controller {
protected override IActionInvoker CreateActionInvoker() {
return new HttpMethodPrefixedActionInvoker();
}
public ActionResult GetLogOn() {
...
}
public ActionResult PostLogOn(LogOnModel model, string returnUrl) {
...
}
public ActionResult GetLogOff() {
...
}
public ActionResult GetRegister() {
...
}
public ActionResult PostRegister(RegisterModel model) {
...
}
[Authorize]
public ActionResult GetChangePassword() {
...
}
[Authorize]
public ActionResult PostChangePassword(ChangePasswordModel model) {
...
}
public ActionResult GetChangePasswordSuccess() {
...
}
}
Note that this doesn't change the action names, which are still LogOn, LogOff, Register, etc.
Here's the code:
using System;
using System.Collections.Generic;
using System.Web.Mvc;
public class HttpMethodPrefixedActionInvoker : ControllerActionInvoker {
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName) {
var request = controllerContext.HttpContext.Request;
string httpMethod = request.GetHttpMethodOverride()
?? request.HttpMethod;
// Implicit support for HEAD method.
// Decorate action with [HttpGet] if HEAD support is not wanted (e.g. action has side effects)
if (String.Equals(httpMethod, "HEAD", StringComparison.OrdinalIgnoreCase))
httpMethod = "GET";
string httpMethodAndActionName = httpMethod + actionName;
ActionDescriptor adescr = base.FindAction(controllerContext, controllerDescriptor, httpMethodAndActionName);
if (adescr != null)
adescr = new ActionDescriptorWrapper(adescr, actionName);
return adescr;
}
class ActionDescriptorWrapper : ActionDescriptor {
readonly ActionDescriptor wrapped;
readonly string realActionName;
public override string ActionName {
get { return realActionName; }
}
public override ControllerDescriptor ControllerDescriptor {
get { return wrapped.ControllerDescriptor; }
}
public override string UniqueId {
get { return wrapped.UniqueId; }
}
public ActionDescriptorWrapper(ActionDescriptor wrapped, string realActionName) {
this.wrapped = wrapped;
this.realActionName = realActionName;
}
public override object Execute(ControllerContext controllerContext, IDictionary<string, object> parameters) {
return wrapped.Execute(controllerContext, parameters);
}
public override ParameterDescriptor[] GetParameters() {
return wrapped.GetParameters();
}
public override object[] GetCustomAttributes(bool inherit) {
return wrapped.GetCustomAttributes(inherit);
}
public override object[] GetCustomAttributes(Type attributeType, bool inherit) {
return wrapped.GetCustomAttributes(attributeType, inherit);
}
public override bool Equals(object obj) {
return wrapped.Equals(obj);
}
public override int GetHashCode() {
return wrapped.GetHashCode();
}
public override ICollection<ActionSelector> GetSelectors() {
return wrapped.GetSelectors();
}
public override bool IsDefined(Type attributeType, bool inherit) {
return wrapped.IsDefined(attributeType, inherit);
}
public override string ToString() {
return wrapped.ToString();
}
}
}
You don't need to specify the HttpGet, all others you do need

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