How to dynamically change Themes and _Layout in ASP.Net MVC4 - asp.net-mvc

I want to be able to change the _Layout.cshtml view based on a setting in my database.
I understand that it is probably done in the _ViewStart.cshml view.
I am using EF 4.2 and want to adapt a solution that will not break any design pattern.
Not sure how to go about doing this in MVC.
In web forms, I could easily do this in the code-behind for the masterpage.
I am doing something like this in my base controller:
public abstract class BaseController : Controller
{
private IUserRepository _userRepository;
protected BaseController()
: this(
new UserRepository())
{
}
public BaseController(IUserRepository userRepository)
{
_userRepository = userRepository;
}
I have looked at FunnelWeb source as well but I am not quite getting how they are injecting things..

Add this code to in the RegisterBundles method of the BundleConfig class. Note that I am creating a separate bundle for each css so that I don't render each css to the client. I can pick which bundle I want to render in the HEAD section of the shared _Layout.cshtml view.
bundles.Add(new StyleBundle("~/Content/Ceruleancss").Include(
"~/Content/bootstrapCerulean.min.css",
"~/Content/site.css"));
bundles.Add(new StyleBundle("~/Content/defaultcss").Include(
"~/Content/bootstrap.min.css",
"~/Content/site.css"));
Then put some logic in the shared_Layout.cshtml to render the appropriate bundle. Since this layout view fires for every page, this is a good place to put it.
I think this approach could be used for branding if you support multiple corps for your app. It could also be used to provide a custom style by user I suppose.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>#ViewBag.Title - Contoso University</title>
#{
if (HttpContext.Current.User.Identity.Name == "MARK")
{
#Styles.Render("~/Content/defaultcss");
}
else
{
#Styles.Render("~/Content/Ceruleancss");
}
}

Old Question but for anyone coming across this question here is a nice solution using Action Filters Attributes
public class LoadUserLayoutAttribute : ActionFilterAttribute
{
private readonly string _layoutName;
public LoadUserLayoutAttribute()
{
_layoutName = MethodToGetLayoutNameFromDB();
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
var result = filterContext.Result as ViewResult;
if (result != null)
{
result.MasterName = _layoutName;
}
}
}
and then, you can add an attribute to your base controller (or action) with this custom attribute:
[LoadUserLayout]
public abstract class BaseController : Controller
{
...
}

Related

MVC Separate Area Project - Unable to find views

I'm working on setting up separate projects for my Areas in my main MVC project. I followed the guide here:
https://stackoverflow.com/a/12912161/155110
I set breakpoints during the launch and can see that the RazorGeneratorMvcStart is being hit as well as the AreaRegistration that sets up the routes. RazorGenerator.Mvc is installed and the Custom Tool on my cshtml pages is set to use it. When I access the Area URL after launching my main project, I can see that it hits the controller in the separate Area project, however, I cannot get it to find the view. I get the following which a huge list of locations:
[InvalidOperationException: The view 'Index' or its master was not
found or no view engine supports the searched locations. The following
locations were searched:
STARAreaRegistration.cs
using System.Web.Mvc;
namespace AreaSTAR.Areas.STAR
{
public class STARAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "STAR";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"STAR_default",
"STAR/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}
RazorGeneratorMvcStart.cs
using System.Web;
using System.Web.Mvc;
using System.Web.WebPages;
using RazorGenerator.Mvc;
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(AreaSTAR.RazorGeneratorMvcStart), "Start")]
namespace AreaSTAR {
public static class RazorGeneratorMvcStart {
public static void Start() {
var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly) {
UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal
};
ViewEngines.Engines.Insert(0, engine);
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
}
}
Areas/STAR/Controllers/DefaultController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace AreaSTAR.Areas.STAR.Controllers
{
public class DefaultController : Controller
{
// GET: STAR/Default
public ActionResult Index()
{
return View();
}
}
}
Areas/STAR/Views/Default/Index.cshtml:
#* Generator: MvcView *#
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>View1</title>
</head>
<body>
<div>
Index view
</div>
</body>
</html>
URL accessed when seeing the View not found error: http://localhost:53992/STAR/Default/Index
This was because I accidentally installed the RazorEngine.Generator extension and set the custom tool to RazorEngineGenerator instead of installing the Razer Generator extension and setting the custom tool to RazorGenerator.

Bind an interface using ninject in shared view

i use ninject to bind my interfaces to my repository as you can see :
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<CMSDataContext>().To<CMSDataContext>().InRequestScope();
kernel.Bind<IUserRepository>().To<UserRepository>().InRequestScope();
kernel.Bind<INewsRepository>().To<NewsRepository>().InRequestScope();
kernel.Bind<IConfigurationRepository>().To<ConfigurationRepository>().InRequestScope();
}
For example you can see here the structure of home controller :
public class HomeController : Controller
{
//
// GET: /fa/Home/
private IConfigurationRepository _configurationRepository;
public HomeController(IConfigurationRepository configurationRepository)
{
_configurationRepository = configurationRepository;
}
public ActionResult Index()
{
ViewBag.Configuration = _configurationRepository.GetConfiguration().First();
return View();
}
}
But i need to call an interface in my shared view i mean masterpage as you can see here:
<head>
#{
IConfigurationRepository _iconfigurationRepository;
}
<!-- Basic -->
<title>#ViewBag.Configuration.Title</title>
<!-- Define Charset -->
<meta charset="utf-8" />
My question is how can i bind this interface in view to its repository i mean configurationRepository using ninject ?
If you need to resolve a dependency in your view , you can use the dependencyResolver.
In your razor view:
#{
var config = DependencyResolver.Current.GetService<IConfigurationRepository >();
}
In this case, if ninject can resolve constructor-parameters then ninject is the current dependency resolver and you can use in views, filters, controllers,etc.

Setting the background in an MVC _Layout.cshtml page

Setting every page's background is rather simple in ASP.Net WebForms where you have access to the Page_Load event in the code-behind of a MasterPage but how is this best done in MVC? After spending several hours researching various alternatives I chose to assign the value to the ViewBag through a "base" controller, derive subsequent controllers from that base and then access that value in _Layout.cshtml.
Here is the base controller, in which I assign a url that points to a specific image:
public class BaseController : Controller
{
public BaseController()
{
ViewBag.url = BingImageLoader.getBingImageUrl();
}
}
The next step is to derive subsequent controllers, in this case the HomeController from that base class:
public class HomeController : BaseController
{
public ActionResult Index()
{
return View();
}
.
.
And finally, use the ViewBag in the head element of _Layout.cshtml to set the background-image style property.
.
.
<style type="text/css">
body {
background-image: url(#ViewBag.url);
background-repeat: no-repeat;
background-size: cover;
}
</style>
</head>
This did accomplish what I set out to do; however, along the way there were a number of alternatives indicated, including using ActionFilters. To be honest, creating a CustomActionFilter and using ActionFilterAttributes and overriding OnActionExecuting seems like overkill but sometimes the simplest way is not always the best.
Ultimately, the question comes down to "Is there a better way?" Are there side-effects from introducing an intermediary? If I override my ViewBag.url in the individual controller methods, the image changes accordingly. So I have yet to find any problems but there may be other issues resulting from this approach.
So again, "Is there a better way"?
One possible problem I can see with this approach is if the developer forgets to subclass hist controller from BaseController.
Using a global action filter would ensure that this will never happen and the property will be always available:
public class BackgroundImageFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
context.Controller.ViewBag.url = BingImageLoader.getBingImageUrl();
}
}
which will be registered only once in your Application_Start:
protected void Application_Start()
{
...
// Register global filter
GlobalFilters.Filters.Add(new BackgroundImageFilterAttribute());
}
If you find this filter approach cumbersome as an alternative I can suggest writing a custom Html helper that could be used in your _Layout.cshtml:
<style type="text/css">
body {
background-image: url(#Html.GetBackgroundImageUrl());
background-repeat: no-repeat;
background-size: cover;
}
</style>
which might be defined as a simple extension method:
public static class HtmlExtensions
{
public static IHtmlString GetBackgroundImageUrl(this HtmlHelper html)
{
string url = BingImageLoader.getBingImageUrl();
return new HtmlString(url);
}
}

Conditionally including stylesheets in the layout depending on the controller's name

I am learning the ASP.NET MVC 3 Framework. In my layout page (_Layout.cshtml), I would like to conditionally include some CSS stylesheets depending on the name of the controller. How do I do that?
You could obtain the current controller name using the following property:
ViewContext.RouteData.GetRequiredString("controller")
So based on its value you could include or not the stylesheet:
#if (ViewContext.RouteData.GetRequiredString("controller") == "somecontrollername")
{
<link href="#Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
}
Or use a custom helper:
public static class CssExtensions
{
public static IHtmlString MyCss(this HtmlHelper html)
{
var currentController = html.ViewContext.RouteData.GetRequiredString("controller");
if (currentController != "somecontrollername")
{
return MvcHtmlString.Empty;
}
var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
var link = new TagBuilder("link");
link.Attributes["rel"] = "stylesheet";
link.Attributes["type"] = "text/css";
link.Attributes["href"] = urlHelper.Content("~/Content/Site.css");
return MvcHtmlString.Create(link.ToString(TagRenderMode.SelfClosing));
}
}
and in layout simply:
#Html.MyCss()
I would use different approach. Define base controller instead and define method SetStyleSheet like:
public abstract class BaseController : Controller
{
protected override void Intialize(RequestContext requestContext)
{
base.Initialize(requestContext);
SetStyleSheet();
}
protected virtual void SetStyleSheet()
{ }
}
In derived classes you can override SetStyleSheet to set something like ViewData["styleSheet"] and use it for example in your master page (_Layout.cshtml).
Darin definitely answered your questions but an alternative would be use the controllers name as the id of some HTML element on your page, which would give you the flexibility of customizing controller-level views but keep your CSS in one file.
<body id="<%=ViewContext.RouteData.GetRequiredString("controller").ToLower() %>">
... content here
</body>
I did another extension method for ControllerContext because ViewContext is aleady derived from it and you can call your method directly.
For example :
public static class ControllerContextExtensions
{
public static string GetControllerName(this ControllerContext helper)
{
if (helper.Controller == null)
{
return string.Empty;
}
string[] fullControllerNames = helper.Controller.ToString().Split('.');
return fullControllerNames[fullControllerNames.Length-1].Replace("Controller",string.Empty);
}
}
And to use this in your _Layout :
#if(ViewContext.GetControllerName() == "MyControllerName")
{
//load my css here
}
You could also pass in your controller name as parameter and make this extension method return a bool.

How to set ViewBag properties for all Views without using a base class for Controllers?

In the past I've stuck common properties, such as the current user, onto ViewData/ViewBag in a global fashion by having all Controllers inherit from a common base controller.
This allowed my to use IoC on the base controller and not just reach out into global shared for such data.
I'm wondering if there is an alternate way of inserting this kind of code into the MVC pipeline?
The best way is using the ActionFilterAttribute. I'll show you how to use it in .Net Core and .Net Framework.
.Net Core 2.1 & 3.1
public class ViewBagActionFilter : ActionFilterAttribute
{
public ViewBagActionFilter(IOptions<Settings> settings){
//DI will inject what you need here
}
public override void OnResultExecuting(ResultExecutingContext context)
{
// for razor pages
if (context.Controller is PageModel)
{
var controller = context.Controller as PageModel;
controller.ViewData.Add("Avatar", $"~/avatar/empty.png");
// or
controller.ViewBag.Avatar = $"~/avatar/empty.png";
//also you have access to the httpcontext & route in controller.HttpContext & controller.RouteData
}
// for Razor Views
if (context.Controller is Controller)
{
var controller = context.Controller as Controller;
controller.ViewData.Add("Avatar", $"~/avatar/empty.png");
// or
controller.ViewBag.Avatar = $"~/avatar/empty.png";
//also you have access to the httpcontext & route in controller.HttpContext & controller.RouteData
}
base.OnResultExecuting(context);
}
}
Then you need to register this in your startup.cs.
.Net Core 3.1
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options => {
options.Filters.Add<Components.ViewBagActionFilter>();
});
}
.Net Core 2.1
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add<Configs.ViewBagActionFilter>();
});
}
Then you can use it in all views and pages
#ViewData["Avatar"]
#ViewBag.Avatar
.Net Framework (ASP.NET MVC .Net Framework)
public class UserProfilePictureActionFilter : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
filterContext.Controller.ViewBag.IsAuthenticated = MembershipService.IsAuthenticated;
filterContext.Controller.ViewBag.IsAdmin = MembershipService.IsAdmin;
var userProfile = MembershipService.GetCurrentUserProfile();
if (userProfile != null)
{
filterContext.Controller.ViewBag.Avatar = userProfile.Picture;
}
}
}
register your custom class in the global. asax (Application_Start)
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalFilters.Filters.Add(new UserProfilePictureActionFilter(), 0);
}
Then you can use it in all views
#ViewBag.IsAdmin
#ViewBag.IsAuthenticated
#ViewBag.Avatar
Also there is another way
Creating an extension method on HtmlHelper
[Extension()]
public string MyTest(System.Web.Mvc.HtmlHelper htmlHelper)
{
return "This is a test";
}
Then you can use it in all views
#Html.MyTest()
Since ViewBag properties are, by definition, tied to the view presentation and any light view logic that may be necessary, I'd create a base WebViewPage and set the properties on page initialization. It's very similar to the concept of a base controller for repeated logic and common functionality, but for your views:
public abstract class ApplicationViewPage<T> : WebViewPage<T>
{
protected override void InitializePage()
{
SetViewBagDefaultProperties();
base.InitializePage();
}
private void SetViewBagDefaultProperties()
{
ViewBag.GlobalProperty = "MyValue";
}
}
And then in \Views\Web.config, set the pageBaseType property:
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="MyNamespace.ApplicationViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
</namespaces>
</pages>
</system.web.webPages.razor>
Un-tried by me, but you might look at registering your views and then setting the view data during the activation process.
Because views are registered on-the-fly, the registration syntax doesn't help you with connecting to the Activated event, so you'd need to set it up in a Module:
class SetViewBagItemsModule : Module
{
protected override void AttachToComponentRegistration(
IComponentRegistration registration,
IComponentRegistry registry)
{
if (typeof(WebViewPage).IsAssignableFrom(registration.Activator.LimitType))
{
registration.Activated += (s, e) => {
((WebViewPage)e.Instance).ViewBag.Global = "global";
};
}
}
}
This might be one of those "only tool's a hammer"-type suggestions from me; there may be simpler MVC-enabled ways to get at it.
Edit: Alternate, less code approach - just attach to the Controller
public class SetViewBagItemsModule: Module
{
protected override void AttachToComponentRegistration(IComponentRegistry cr,
IComponentRegistration reg)
{
Type limitType = reg.Activator.LimitType;
if (typeof(Controller).IsAssignableFrom(limitType))
{
registration.Activated += (s, e) =>
{
dynamic viewBag = ((Controller)e.Instance).ViewBag;
viewBag.Config = e.Context.Resolve<Config>();
viewBag.Identity = e.Context.Resolve<IIdentity>();
};
}
}
}
Edit 2: Another approach that works directly from the controller registration code:
builder.RegisterControllers(asm)
.OnActivated(e => {
dynamic viewBag = ((Controller)e.Instance).ViewBag;
viewBag.Config = e.Context.Resolve<Config>();
viewBag.Identity = e.Context.Resolve<IIdentity>();
});
Brandon's post is right on the money. As a matter of fact, I would take this a step further and say that you should just add your common objects as properties of the base WebViewPage so you don't have to cast items from the ViewBag in every single View. I do my CurrentUser setup this way.
You could use a custom ActionResult:
public class GlobalView : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
context.Controller.ViewData["Global"] = "global";
}
}
Or even a ActionFilter:
public class GlobalView : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Result = new ViewResult() {ViewData = new ViewDataDictionary()};
base.OnActionExecuting(filterContext);
}
}
Had an MVC 2 project open but both techniques still apply with minor changes.
You don't have to mess with actions or change the model, just use a base controller and cast the existing controller from the layout viewcontext.
Create a base controller with the desired common data (title/page/location etc) and action initialization...
public abstract class _BaseController:Controller {
public Int32 MyCommonValue { get; private set; }
protected override void OnActionExecuting(ActionExecutingContext filterContext) {
MyCommonValue = 12345;
base.OnActionExecuting(filterContext);
}
}
Make sure every controller uses the base controller...
public class UserController:_BaseController {...
Cast the existing base controller from the view context in your _Layout.cshml page...
#{
var myController = (_BaseController)ViewContext.Controller;
}
Now you can refer to values in your base controller from your layout page.
#myController.MyCommonValue
If you want compile time checking and intellisense for the properties in your views then the ViewBag isn't the way to go.
Consider a BaseViewModel class and have your other view models inherit from this class, eg:
Base ViewModel
public class BaseViewModel
{
public bool IsAdmin { get; set; }
public BaseViewModel(IUserService userService)
{
IsAdmin = userService.IsAdmin;
}
}
View specific ViewModel
public class WidgetViewModel : BaseViewModel
{
public string WidgetName { get; set;}
}
Now view code can access the property directly in the view
<p>Is Admin: #Model.IsAdmin</p>
I have found the following approach to be the most efficient and gives excellent control utilizing the _ViewStart.chtml file and conditional statements when necessary:
_ViewStart:
#{
Layout = "~/Views/Shared/_Layout.cshtml";
var CurrentView = ViewContext.Controller.ValueProvider.GetValue("controller").RawValue.ToString();
if (CurrentView == "ViewA" || CurrentView == "ViewB" || CurrentView == "ViewC")
{
PageData["Profile"] = db.GetUserAccessProfile();
}
}
ViewA:
#{
var UserProfile= PageData["Profile"] as List<string>;
}
Note:
PageData will work perfectly in Views; however, in the case of a
PartialView, it will need to be passed from the View to
the child Partial.
I implemented the ActionFilterAttribute solution from #Mohammad Karimi. It worked well as I had the same scenario as the OP. I needed to add data to every view. The action filter attribute was executed for every Razor page request, but it was also called for every web API controller request.
Razor Pages offers a page filter attribute to avoid unnecessary execution of the action filter when a web API controller request is made.
Razor Page filters IPageFilter and IAsyncPageFilter allow Razor Pages to run code before and after a Razor Page handler is run.
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
namespace MyProject
{
// learn.microsoft.com/en-us/aspnet/core/razor-pages/filter?view=aspnetcore-6.0
// "The following code implements the synchronous IPageFilter"
// Enable the page filter using 'services.AddRazorPages().AddMvcOptions( ... )
// in the 'ConfigureServices()' startup method.
public class ViewDataPageFilter : IPageFilter
{
private readonly IConfiguration _config;
public ViewDataPageFilter(IConfiguration config)
{
_config = config;
}
// "Called after a handler method has been selected,
// but before model binding occurs."
public void OnPageHandlerSelected(PageHandlerSelectedContext context)
{
}
// "Called before the handler method executes,
// after model binding is complete."
public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
PageModel page = context.HandlerInstance as PageModel;
if (page == null) { return; }
page.ViewData["cdn"] = _config["cdn:url"];
}
// "Called after the handler method executes,
// before the action result."
public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
{
}
}
}
As per the sample in the filter methods for Razor Pages documentation, the page filter is enabled by:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages()
.AddMvcOptions(options =>
{
options.Filters.Add(new ViewDataPageFilter(Configuration));
});
}

Resources