Same view using different master pages - ASP.NET MVC - asp.net-mvc

I'm new to ASP.net MVC and I want to use a view but with a different Master Page depending on the user Role.
For now, i'm leaning to use one controller who return View1 if the user is in Role1 and View2 is in Role2. View1 and View2 contains the same partial view inside to render the content that is share by both but have a different master page.
I want to know if it's a good practice or if you recommend another design. My solution seems a little bit complicated to do something simple. Maybe I am missing something
Thanks !

You could have a function which returns the master name based on the user role and then write a custom action filter which will execute after the action and set the corresponding master page based on the currently connected user role:
public class MasterChooserAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
var result = filterContext.Result as ViewResult;
if (result != null)
{
var user = filterContext.HttpContext.User;
result.MasterName = GetMaster(user);
}
}
private string GetMaster(IPrincipal user)
{
// TODO: based on the current user roles return the proper master page
throw new NotImplementedException();
}
}
and then simply decorate your base controller with this action filter or if this is an ASP.NET MVC 3 application simply declare it as global filter.

Simply select the layout in your view.
You can dynamically change #{Layout = XXX} in mvc 3.
see: http://weblogs.asp.net/scottgu/archive/2010/10/22/asp-net-mvc-3-layouts.aspx
Your controller could check the roles, and assign the layout to use, which then is assigned to #Layout in your view, but you could just as well keep this code in your view to determine the layout to use, since after all it is 'view logic'

There are various ways to select the master page each having their merits.
The simplest would probably be to return the Master page name using the controller View method
public ViewResult Index() {
var masterName = getMasterPageNameForUser(); //get you master page/layout name here
return View("Index", masterName, model);
}
However this will lead to some repetitive code so an alternative could be to create a custom IViewEngine and set the master name there. Scott Hanselman's post and this coder journal post will give you an idea of how to create a custom view engine. From there it's a mater of setting the master name.

What happens if you come up with a third role? Fourth role? Instead of putting that kind of logic in your controller, what if the master page displays different things depending on their role? You could hide whole chunks of <div> or whatnot in the master. Then, you've only got one place to change it whenever the role dependency changes. Is the master page going to be that different based upon the role?

In your controller do
this.ViewBag.Layout = something
in your view
Layout = this.ViewBag.Layout

Related

MVC Areas & Shared Layouts

Hopefully I am making myself clear. I have an area in my mvc project with two shared layouts
_Application.cshtml (For Areas) _Layout.cshtml (For non-areas)
Reports and Favourites are for the area controller.
Roles and Users are for the default namespace controllers.
<li>#Html.ActionLink("Reports", "Reports", "Home")</li>
<li>#Html.ActionLink("Favourites", "Favourites", "Home")</li>
<li>#Html.ActionLink("System Roles", "Roles", "Support",new { Area = "" }, htmlAttributes: new { title = "System Roles" }) </li>
<li>#Html.ActionLink("Users", "Users", "Support", new { Area = "" }, htmlAttributes: new { title = "Users" })</li>
Defined in the Roles and Users View I am defining a different shared layout. That do not have links to Reports and Favourites (just totally different navigation menu).
Is it possible to assign the layout of Roles and Users to my _Application shared layout without losing the ability to call the actions from the default namespace controller?
if (this.ViewContext.RouteData.DataTokens["Area"] != null) {
Layout = "~/Views/Shared/_Application.cshtml";
}
else
{
Layout = "~/Views/Shared/_Layout.cshtml";
}
I've tried this but the DataToken never receives an Area route because it's not defined.
Edit: Sorry I am having trouble describing the issue (even with co-workers too...)
Area "Procurement": Controller : "Home" has three views Index, Reports and Favourites. Navigation Menu (_Applicaiton.cshtml) is custom to the Procurement Area only
Non Area : Controller "Support" has two view Roles and Users. Navigation Menu (_Layout.cshtml) is generic that contains links to navigate to other Areas of the project
The Action Links above reside in the Index View. If I click Reports or Favourites, it will navigate me to ApplicationName/Procurement/Home/Reports or ApplicationName/Procurement/Home/Favourites
If I click Roles or Users, it will navigate me to ApplicationName/Support/Roles or ApplicationName/SupportUsers
Because of how I have defined the layouts for Roles and Users, the navigation menu defaults back to the generic one instead of displaying the one specific to Procurement Area.
I am looking to see if it is possible that when I click Roles or Users for the navigation menu to stay on the Procurement Layout and reference the same Support controller. I don't want to have to create the Roles and Users page for each Area that I am creating.
If you desire to have specific layouts for some areas, there is a simple solution.
Use the _ViewStart file to specify the layout to use for each area.
~/Views/_ViewStart.cshtml file:
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
~/Areas/Xxx/Views/_ViewStart.cshtml file:
#{
Layout = "~/Views/Shared/_LayoutXxx.cshtml";
}
You may read ASP.NET MVC 3: Layouts with Razor
Don't forget to remove the Layout = "..." declarations from the views in order to let the ViewStart do its work.
Traditionally, you'd just use ViewContext.RouteData.Values["key"], rather than DataTokens. Not sure that makes a difference or not. Also, I think the key is "area", not "Area". Dictionary keys are case-sensitive, so that may very well be your issue.
EDIT
Okay. I think I might understand a little better now. To achieve what you want, you would need to re-implement the Roles and Users functionality in your Procurement area. However, that doesn't mean you have to just copy the code over. You can rely on the view conventions to load in the right views. For example, if you move your Roles view into Views\Shared, Razor can find it when searching for that view in different controllers or areas, since Views\Shared is always the last-result path searched. As for the actions themselves, you can utilize controller inheritance to implement the same controller/actions in one or more different areas, while not actually repeating code.
If you want to drive conditionally which view is used, you can inherit from your view engine(s) and override the view locations on a per-request basis.
This post illustrates a different static view resolution pattern.
You can override the ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) method to customize the available views for a given request. The controllerContext object will allow you to find the user and its roles so you can return the correct views.
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (/* user has role X */)
{
return new ViewEngineResult(new string[] { "Some View.cshtml" });
}
return base.FindView(controllerContext, viewName, masterName, useCache);
}
This article show how to inherit from the classic view engines and declare them. This article shows how to use the request object in the custom ViewEngine to select views depending on the user's roles.

What's the recommended pattern for handling control display logic in ASP.NET MVC?

I'm going through some of the MVC3 tutorials, namely the Pluralsight videos, and I'm thinking of how our existing application would function if it were overhauled to ASP.NET MVC (not the plan, but it gives me a frame of reference). We have quite a bit of code that looks something like this in our aspx code-behinds:
if (SomeBooleanCheck){SomeControl.Visible = true;}else {SomeControl.Visible = false;}
Granted that example is greatly simplified, but assuming the boolean logic is fairly complex and assuming multiple things need to happen as part of making the control visible (maybe changing color, size, text, etc.) what's the pattern for doing this in ASP.NET MVC? It seems like you'd have to do that same boolean checking in the view itself, which to me seems kind of ugly. Seems like there has to be a better way and this surely came up on MS's use case list, I'm just not seeing the answer.
The approach you may take will vary greatly depending on the specific scenario. A few options incude:
Doing as you say and adding the conditional in the view
Abstracting the conditional (if it is complex) into your view model so that the lines in the view are still simple (just accessing a preset boolean value on your view model).
Doing this conditional at the route or controller level and calling a different overall view (which may share a layout (razor) or master view (webforms mvc))
You don't mention explicitly how you would render the controls in the conditional. I assume you would be doing a RenderPartial. So the lines themselves in the view would be quite 'small'.
if(myViewModel.ComplexBoolean) // Boolean set in generation of view model
Html.RenderPartial('firstPartial')
else
Html.RenderPartial('secondPartial')
EDIT: If the item you are setting as 'visible' is simply a single control you may just output the control directly e.g.
if(myViewModel.ComplexBoolean) {
Html.DropDownListFor(m => m.Type, Model.Types /* An IEnumerable<SelectListItem>*/, new { #class = "myList" });
}
Additionally if you didn't want to set that 'Model.Types' property (to save a db hit for example) then the conditional could be in the location you create your view model (either the controller or some service/view model repo). The view could then just check for the properties existance instead:
if(Model.Types != null) {
Html.DropDownListFor(m => m.Type, Model.Types /* An IEnumerable<SelectListItem>*/, new { #class = "myList" });
}
If your controls does not use the data found in your View's ViewModel, you can also use Html.RenderAction to call Child Actions. For example, suppose you want to display a different menu to users with different roles. You can call #{Html.RenderAction("Menu", "Account");} in your View, which will call the "Menu" Action in your "Account" controller. Your complex Boolean logic and the logic to formulate your controllers’ settings will reside in the "Account" controller's "Menu" action. The "Menu" action will decide what Partial View/Controller to display.
// This goes in your View (clean single line!)
#{Html.RenderAction("Menu", "Account");}
// This goes in your controller
[ChildActionOnly]
public ActionResult Menu()
{
bool isAdmin = false;
// Your complex boolean logic goes here
// Set your controller settings here
string controllerSettings = ""; // Use class or array for multiple settings
if (isAdmin)
{
return PartialView("~/Views/Account/_AdminMenu.cshtml", controllerSettings);
}
else
{
return PartialView("~/Views/Account/_StandardMenu.cshtml", controllerSettings);
}
}

How to create different View depending on logged in user's role in ASP.NET MVC?

I am kind of new to ASP.NET MVC, so need your help getting me through a problem:
In my application the LogOn will be done using the Role of the user. I have my custom database schema (like User, Role, UserInRole etc.) and I am using my custom MembershipProvider and RoleProvider to achieve Logon.
BTW I am using the MVC default Account controller itself with some modification. What I am trying to achieve now is that depending upon the Role of logged in user I want to create a different View for the user.
Can I use the returnUrl parameter of LogOn action method in any way? (This parameter is set to null by default). If yes how can I construct and send returnUrl depending on Role?
Or is there any easier way to achieve this?
By default, in your controller methods which return an ActionResult, if you just return "View()" then the page that will be displayed is the page in your Views directory with the name of your controller method. However, you can specify the name of the View which will be returned, or pass on the call to another controller method to return the appropriate view.
As in the following contrived example:
public ActionResult Edit(string id)
{
// get the object we want to edit
IObjectDefinition definedObject = _objectManager.GetObjectById(id);
if (definedObject != null)
{
ViewData.Add("definition", definedObject );// add to view data collection so can display on page
IUser user = GetCurrentUser();// get from session/cookie/whatever
if (!user.IsAccountAdmin)
{
return View("Detail");// a readonly page as has no rights to edit
}
else
{
return View();// same object displayed in editable mode in another view
}
}
else
{
return New();// call to controller method below to allow user to create new record
}
}
public ActionResult New()
{
return View();
}
You decide what view to use (render) in the action - its not dictated directly by the URL
In the code you have access to the user object and from that can determine the roles that user is in - from that it should be straightforward enough to choose the view accordingly.
I think you can do a RedirectToAction on successful login and since you now have a valid login user, in the action method you can use IsInRole property of the of the LoggedIn User and render an appropriate partial view.

Can MVC routing be used to create a dynamic content management system with dynamic pages stored in db rather than in view pages

Are there any good examples of mvc routing wherein every 404 page not found request is routed to a standard view in MVC which basically pulls the content from the database.
Just add this route to the bottom of your RouteTable:
routes.MapRoute("DynamicPages", "{*page}", new { Controller = "DynamicPages", Action = "Show", Page = String.Empty });
And create a controller for displaying dynamic pages from db:
public class DynamicPagesController : Controller
{
public ActionResult Show(string page)
{
var pageContent = DB.GetContentForPage(page);
return Content(pageContent);
}
}
Here's one way to do this: In your global.asax file in Application_Start, you need to set the default controller factory. Override it with an instance of your own factory.
void Application_Start()
{
ControllerBuilder.Current.SetControllerFactory(new MyControllerFactory());
}
MyControllerFactory should inherit from DefaultControllerFactory and when selecting the controller to use, look in your database for the appropriate page you want to display. If the page exists, select the appropriate controller and override the action in the requestContext.RouteData collection to point at the appropriate action for displaying dynamic pages.
If the requested page doesn't exist, pass back a call to the base method and let it do what it would normally do.
There are other ways you could do it, but this one should work and allows you to intercept the request before you hit the 404 page.
modify the web.config file, you may Reference to this page and look at the setting custom error pages in web.config section.

Redirect to controller (but with a different master) using a catchall wildcard

I have a problem whereby I want to display a view differently (a different master page), depending on where it came from, but don't know where to start...
I have several routes which catch various different types of urls that contain different structures.
In the code snippet below, I have a product route, and then I have a partner site route which could also go to a product page, but let's say that this partner is Pepsi, and they want their branding on the master page, rather than our own default styling. Lets say I go to products/cola.htm. This should go to the same url as partners/pepsi/products/cola.htm, and the PartnerRedirect would be able to handle the url based on the wildcard, by translating the url wildcard (in this case, "products/cola.htm") into a controller action, and forward the user on, (but simply change the master page in the view).
routes.MapRoute(
"Product",
"products/{product}.htm",
new { controller = "Product", action = "ShowProduct" }
);
routes.MapRoute(
"ProductReview",
"products/{product}/reviews.htm",
new { controller = "Product", action = "ShowProductReview" }
);
routes.MapRoute(
"Partner",
"partners/{partner}/{*wildcard}",
new { controller = "Partners", action = "PartnerRedirect" }
);
Is this possible? And if so, how?
Many thanks in advance.
In your partners controller why don't you set a cookie that indicates which partner you want to show, and then redirects to the wildcard section of the route. That way you can show the same partner layout for all subsequent page views.
I don't know if this is what you're looking for, but it might be an option.
I had same issue
public class FriViewPage : ViewPage
{
public override string MasterPageFile
{
get
{
return "~/Views/Shared/Site.Master"; // base.MasterPageFile;
}
set
{
if (ViewData["agent"].ToString() == "steve")
base.MasterPageFile = "~/Views/Shared/Site.Master";
else
base.MasterPageFile = "~/Views/Shared/Site2.Master";
}
}
}
Then just ensure all the views inherit from FriViewPage instead of ViewPage
It may be the devils work but you could put some code in the Partner View's codebehind to look at the URL and then set the master page programmatically in there?
I'm not sure how you can programatically alter the master page, as I've never done that, but I'm sure it's possible (it's probably just a property on Page).
That might be worth asking as another question.
Acutally the MasterPageFile getter never appears to be called
You can change the MasterPage by modifying the ViewResult prior to rendering. For example, a controller action could do:
public ActionResult TestMP(int? id)
{
ViewData["Title"] = "MasterPage Test Page";
ViewData["Message"] = "Welcome to ASP.NET MVC!";
ViewResult result = View("Index");
if (id.HasValue)
{
result.MasterName = "Site2";
}
return result;
}
You could accomplish the same thing with an action filter for a more generic solution.

Resources