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.
Related
I have a container in my layout where I'd like to display a list of menu items, and I'd like for additional menu items to be displayed based on which area of the site we are viewing.
I know that I could do this with a #section, but that means I would have to copypaste the section contents in every single view within the area, which would be a maintenance mess and a nasty violation of DRY. Multiple per-area layouts would also be undesirable code duplication.
It would be good to do it in the _ViewStart partial, but apparently MVC doesn't allow #sections to be defined in partials. What else can I do?
I would consider making an action method in some generic controller that returns the correct partial view with the proper menu items.
[ChildActionOnly]
public PartialViewResult GetSubMenu(){
var areaName = ViewContext.RouteData.DataTokens["area"];
switch(areaName){
case "Admin":
return PartialView("_adminSubMenu");
....
case default:
//not sure on how to return nothing exactly
return null;
}
}
In your layout
#{Html.RenderAction("GetSubMenu","GenericControllerName");}
Create a Controller and Action to cater for this, I generally use something like NavigationController with a MainMenu action or similar.
In your action:
public ActionResult MainMenu()
{
return PartialView();
}
The from any where on your site layouts or views you can use:
#{ Html.RenderAction("MainMenu", "Navigation"); }
This also means that you can include any business logic in your action and pass a model to your MainMenu, maybe for checking roles etc.
Very handy.
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);
}
}
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
I am running out of ideas here. Maybe you can advice me what pattern or method(s) to use.
User should be able to log in and change the appearance only for his/her profile.
The difference (AFAIK) with personalization is that personalized layout are seen only for the editor (him-/herself).
The difference between skinning, I guess, is that Skins are predefined but users should be able to change the settings themselves.
I need to be able to display the customized layout to everyone who visit author`s page.
The good solution would be to keep the layout info in a DB table. Also it should be cached I guess to take load off the DB and used in CSS.
Thanks
Edit:
OK I have done some research now. Came up with this kind of idea.
In a View get a userId (Guid type) from a DB and set it to the ViewData:
ViewData["userId"] = profile.userId;
That View uses the following MasterPage called 'Profile.Master' and links to the dynamic CSS file:
<link href="<%= Url.Action("Style", "Profile",
ViewData["userId"]) %>" rel="stylesheet" type="text/css" />
</head>
In the ProfileController get the CSS data from DB and return it to the dynamic CSS View:
public ActionResult Style(Guid userId)
{
var styles = (from s in Db.UserStyleSet.OfType<UserStyle>()
where s.aspnet_Users.UserId == userId
select s);
return View("Style", styles);
}
The problem is that the UserId is never passed to the dynamic CSS link:
The parameters dictionary contains a null entry for parameter 'userId' of non-nullable type 'System.Guid' for method 'System.Web.Mvc.ActionResult Style(System.Guid)' in 'Project.Controllers.ProfileController'.
Any advice is welcome, thank you.
Very neat layout customization features you can find in Kona project developed by Rob Conery. When you run source code which you can find here, you will see layout management UI which allows you to change the position of each component on the screen.
The approach used there is as follows:
When page is rendered our customized view engine check which master page should present (this way we are able to switch themes based on current settings)
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) {
ViewEngineResult result = null;
var request = controllerContext.RequestContext;
if (controllerContext.Controller.GetType().BaseType == typeof(KonaController)) {
var orchardController = controllerContext.Controller as KonaController;
string template = orchardController.ThemeName;
View engine uses master page and renders view which was defined by specific controller action resolved using route tables. For instance, we typed main url of the site which pointed to Home Controller, Index method. This method returned Index.aspx view which was rendered by View engine.
While view engine is rendering the Index.aspx page it launches helper methods like
<%this.RenderWidgets("sidebar1"); %>.
This method is truely responsible for rendering specific widdgets per each div in the aspx page. This way, if your user changes the layout of the widgets they will be correctly presented on the screen.
public static void RenderWidgets(this ViewPage pg, Kona.Infrastructure.Page page, bool useEditor, string zone) {
if (page != null) {
foreach (IWidget widget in page.Widgets.Where(x => x.Zone.Equals(zone, StringComparison.InvariantCultureIgnoreCase))) {
string viewName = useEditor ? widget.EditorName : widget.ViewName;
if (widget.ViewName != null) {
if (widget.IsTyped) {
var typedWidget = widget as Widget<IList<Product>>;
pg.Html.RenderPartial(viewName, typedWidget);
} else {
pg.Html.RenderPartial(viewName, widget);
}
} else if (!string.IsNullOrEmpty(widget.Title)) {
pg.Html.RenderPartial("TitleAndText", widget);
} else {
pg.Html.RenderPartial("TextOnly", widget);
}
}
}
}
How user is able to change the layout? Kona has very neat javascript which is used together with Ajax and user simply drag&drop widgets from one panel to another to reorder the layout.
You could use a CMS framework. See this question for suggestions
You could dynamically build a CSS file and save the css name in the user's db entry.
How much customisation do you need? Storing an entire css in the database 1 style at a time seems a little overkill, are you sure your users really need / want that level of customisation?
Wouldn't it be simpler to present a list of themes, allow the user to select the one they want and then store that information with the user profile so that when you retrieve the profile details you also retrieve the theme. This information can then be used to select the appropriate master as well as passed to the view to render the correct stylesheet(s).
If you really want to allow extreme customisation down to the individual style level, I would use a default css and then when the user customises their layout, copy the default and alter as necessary, creating a custom css for the user. Each time the user updates their profile layout, simply update the css file with the changes. To get around css caching, record an incrementing version number for each change and append that to the end of the url for the css e.g. <link rel="stylesheet" href="user001.css?v=2>.
I've just been bitten by a problem where I have a view (FindUser.aspx) trying to render a partial view (FindUser.ascx). The default search paths for views look for a file named after the view in a variety of folders. Rather surprisingly, for views, it looks for a file with the extensions of .aspx or .ascx. And the partial views use the same list.
Because I've got the two files named the same, the view resolution repeatedly finds the page first, and falls into an endless loop.
I know I can fix this either by calling the view and the partial view different names, or by changing my search locations to be .aspx only for views and .ascx only for partial views.
My question is why does MVC default to looking at both extensions? It seems to make more sense that a view == a page == .aspx and a partial view == a control == .ascx. So why muddy the waters?
Because partial or not, a view is still a view. Having FindUser.aspx and FindUser.ascx is the same as having two regular views with the same name.
I think that the way to avoid the problem you're having is to use different view names. You probably shouldn't have two views whose file name differs only in extension. However, if you really want a strict Page = View, Control = Partial mapping, just create your own ViewEngine by inheriting from WebFormViewEngine and change the view location formats:
public class MyWebFormViewEngine : WebFormViewEngine {
public MyWebFormViewEngine() {
base.ViewLocationFormats
= new string[] {"~/Views/{1}/{0}.aspx", "~/Views/Shared/{0}.aspx" };
base.PartialViewLocationFormats
= new string[] { "~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.ascx" };
}
}
Then configure it as your View Engine in Application_Start():
// Call this method during Application_Start to setup your view engine
internal static void SetupViewEngines() {
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MyWebFormViewEngine());
}
For what it's worth I append "Control" to the name of all of my .ascx ViewUserControls. So I would have FindUser.aspx and FindUserControl.ascx. Doesn't solve the problem but it helps you to avoid it by avoiding naming collisions.
You can give MVC the direct path when rendering Views. Say I have a Foo.aspx in my Home folder and a Foo.ascx partial view in Shared. In your action method you can do either:
return View("~/Views/Shared/Foo.ascx"); // or
return View("~/Views/Home/Foo.aspx");
And it will get the proper one you're looking for.
Reason
View == UserControl in ASP.NET MVC.
Fix
Use different names.
Tip
It`s common convention to name usercontrols with underscore prefix.
If you're using Areas, you'll have to add additional LocationFormats in the constructor:
public class ExtensionBasedWebFormViewEngine : WebFormViewEngine
{
public ExtensionBasedWebFormViewEngine()
{
ViewLocationFormats = new[] {"~/Views/{1}/{0}.aspx", "~/Views/Shared/{0}.aspx"};
AreaViewLocationFormats = new[] {"~/Areas/{2}/Views/{1}/{0}.aspx", "~/Areas/{2}/Views/Shared/{0}.aspx"};
PartialViewLocationFormats = new[] {"~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.ascx"};
AreaPartialViewLocationFormats = new[] { "~/Areas/{2}/Views/{1}/{0}.ascx", "~/Areas/{2}/Views/Shared/{0}.ascx" };
}
}