Creating a custom ViewResult that displays a different view based on platform in ASP.NET MVC 3 - asp.net-mvc

I'm trying to make my Action return a different view for different platforms, respecting the routing config. If I create a custom ViewResult, will I override the FindView method? And if so, how can I modify the View that is automatically found?
For example: HomeController.About action would display View\Home\About.cshtml on computer, View\Home\AboutTablet.cshtml on a tablet, and View\Home\AboutMobile.cshtml on a cell phone

There's a NuGet for you: MobileViewEngines. ScottHa covered it in a blog post. It's spec compatible with ASP.NET MVC 4 where you can get rid of it easily because this functionality is built-in.

You could define an Actionfilter like this:
public class SetDeviceDependantView : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
// Only works on ViewResults...
ViewResultBase viewResult = filterContext.Result as ViewResultBase;
if (viewResult != null)
{
if (filterContext == null)
throw new ArgumentNullException("context");
// Default the viewname to the action name
if (String.IsNullOrEmpty(viewResult.ViewName))
viewResult.ViewName = filterContext.RouteData.GetRequiredString("action");
// Add suffix according to device type
if (IsTablet(filterContext.HttpContext))
viewResult.ViewName += "Tablet";
else if (IsMobile(filterContext.HttpContext))
viewResult.ViewName += "Mobile";
}
base.OnResultExecuting(filterContext);
}
private static bool IsMobile(HttpContextBase httpContext)
{
return httpContext.Request.Browser.IsMobileDevice;
}
private static bool IsTablet(HttpContextBase httpContext)
{
// this requires the 51degrees "Device Data" package: http://51degrees.mobi/Products/DeviceData/PropertyDictionary.aspx
var isTablet = httpContext.Request.Browser["IsTablet"];
return isTablet != null && isTablet.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase);
}
}
Then you can either annotate the required Actions / Controllers like this:
[SetDeviceDependantView]
public ActionResult About()
{
return View();
}
Or set it globally in the global.asax:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new SetDeviceDependantView());
}
Note, that I'm relying here on the 51degrees library to detect the tablet, you could consider using a different technique. However, that's a different topic.

If you want to do this pre-MVC4, check out this blog post from Christopher Bennage
http://dev.bennage.com/blog/2012/04/27/render-action/
I was specially interested in the ContentTypeAwareResult class, which looks like it might be what you are looking for.
https://github.com/liike/reference-application/blob/master/MileageStats.Web/ContentTypeAwareResult.cs

You'll have to create your own ViewEngine (probably deriving it from the one you're using) and override FindView and FindPartialView. You can provide fallbacks (i.e., use generic view if no tablet one is found).
Most trouble will be in defining the criteria to differentiate between different "modes".

Building a custom ViewEngine is a preferred approach in MVC3 for this requirement. For you info - MVC4 support this feature out of the box.
For more information about device specific view, a similar answer is posted here on StackOverflow itself https://stackoverflow.com/a/1387555/125651

Related

Restrict access to a controller based on certain criteria not user authentication

What is the correct way to restrict access to a controller?
For instance, I might have "ProductReviewController", I want to be able to check that this controller is accessible in the current store and is enabled. I'm not after the code to do that but am interested in the approach to stopping the user getting to the controller should this criteria not be met. I would like the request to just carry on as if the controller was never there (so perhaps throwing a 404).
My thoughts so far:
A data annotation i.e [IsValidController]. Which Attribute class would I derive from - Authorize doesn't really seem to fit and I would associate this with user authentication. Also, I'm not sure what the correct response would be if the criteria wasn't met (but I guess this would depend on the Attribute it's deriving from). I could put this data annotation against my base controller.
Find somewhere lower down in the page life cycle and stop the user hitting the controller at all if the controller doesn't meet my criteria. i.e Create my own controller factory as depicted in point 7 here: http://blogs.msdn.com/b/varunm/archive/2013/10/03/understanding-of-mvc-page-life-cycle.aspx
What is the best approach for this?
Note: At the moment, I am leaning towards option 1 and using AuthorizeAttribute with something like the code below. I feel like I am misusing the AuthorizeAttribute though.
public class IsControllerAccessible : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (!CriteriaMet())
return false;
return true;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary(
new
{
controller = "Generic",
action = "404"
})
);
}
}
I think you are confused about AuthorizeAttribute. It is an Action Filter, not a Data Annotation. Data Annotations decorate model properties for validatioj, Action Filter's decorate controller actions to examine the controller's context and doing something before the action executes.
So, restricting access to a controller action is the raison d'etre of the AuthorizeAttribute, so let's use it!
With the help of the good folks of SO, I created a customer Action Filter that restricted access to actions (and even controllers) based on being part of an Access Directory group:
public class AuthorizeADAttribute : AuthorizeAttribute
{
public string Groups { get; set; }
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (base.AuthorizeCore(httpContext))
{
/* Return true immediately if the authorization is not
locked down to any particular AD group */
if (String.IsNullOrEmpty(Groups))
return true;
// Get the AD groups
var groups = Groups.Split(',').ToList<string>();
// Verify that the user is in the given AD group (if any)
var context = new PrincipalContext(ContextType.Domain, "YOURADCONTROLLER");
var userPrincipal = UserPrincipal.FindByIdentity(context,
IdentityType.SamAccountName,
httpContext.User.Identity.Name);
foreach (var group in groups)
{
try
{
if (userPrincipal.IsMemberOf(context, IdentityType.Name, group))
return true;
}
catch (NoMatchingPrincipalException exc)
{
var msg = String.Format("While authenticating a user, the operation failed due to the group {0} could not be found in Active Directory.", group);
System.ApplicationException e = new System.ApplicationException(msg, exc);
ErrorSignal.FromCurrentContext().Raise(e);
return false;
}
catch (Exception exc)
{
var msg = "While authenticating a user, the operation failed.";
System.ApplicationException e = new System.ApplicationException(msg, exc);
ErrorSignal.FromCurrentContext().Raise(e);
return false;
}
}
}
return false;
}
}
Note this will return a 401 Unauthorized, which makes sense, and not the 404 Not Found you indicated above.
Now, the magic in this is you can restrict access by applying it at the action level:
[AuthorizeAD(Groups = "Editor,Contributer")]
public ActionResult Create()
Or at the controller level:
[AuthorizeAD(Groups = "Admin")]
public class AdminController : Controller
Or even globally by editing FilterConfig.cs in `/App_Start':
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new Code.Filters.MVC.AuthorizeADAttribute() { Groups = "User, Editor, Contributor, Admin" });
}
Complete awesome sauce!
P.S. You mention page lifecycle in your second point. There is no such thing in MVC, at least not in the Web Forms sense you might be thinking. That's a good thing to my mind, as things are greatly simplified, and I don't have to remember a dozen or so different lifecycle events and what the heck each one of them is raised for!

Accessing ControllerContext from within a Delegate within the Global.asax

Is there any way to gain access to the controller that is currently executing from within Global.asax?
I'd like to design an API with similar syntax to:
MyClass.RegisterComponents().When(IController => /* Some condition */)
Although I could move this code to a place where the controller is in context, I'd like to keep it centralised and portable.
So far, I have been unable to obtain the controller. Any ideas?
I have considered creating a base controller and extending all of my controllers from this base class, however, I'd like to make this library portable with the ability to be installed via NuGet. For this reason I am unable to take this approach.
You can do following in your global.asmx file.
private void Application_BeginRequest(object sender, EventArgs e)
{
string controllerName = Request.RequestContext.RouteData.Values.Where(p => p.Key =="controller").FirstOrDefault(p => p.Key);
}
I found a solution to this. Not a very good one but it solves my problem.
Register a global IActionFilter using an assembly start up method I found on David Ebbo's blog (http://blog.davidebbo.com/2011/02/register-your-http-modules-at-runtime.html).
The global action filter simply stores the action context in the current HttpContext.Items[] collection which is a per request collection.
public class GlobalActionFilter : System.Web.Mvc.IActionFilter {
internal static readonly object ActionExecutedFilterKey =
"__MvcResourceLoaderActionExecutedContext";
internal static readonly object ActionExecutingFilterKey =
"__MvcResourceLoaderActionExecutingContext";
static MvcResourceLoaderGlobalFilter __instance =
new MvcResourceLoaderGlobalFilter();
MvcResourceLoaderGlobalFilter() { }
public void OnActionExecuted(System.Web.Mvc.ActionExecutedContext filterContext) {
System.Web.HttpContext.Current.Items[ActionExecutedFilterKey] =
filterContext;
}
public void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext) {
System.Web.HttpContext.Current.Items[ActionExecutingFilterKey] =
filterContext;
}
public static void RegisterGlobalFilter() {
if (!System.Web.Mvc.GlobalFilters.Filters.Contains(__instance))
System.Web.Mvc.GlobalFilters.Filters.Add(__instance);
}
}
I can then access the context anywhere.

Route Parameter, Custom Model Binder or Action Filter?

Our ASP.NET MVC application allows an authenticated user to administer one or more "sites" linked to their account.
Our Urls are highly guessible since we use the site friendly name in the URL rather than the Id e.g:
/sites/mysite/
/sites/mysite/settings
/sites/mysite/blog/posts
/sites/mysite/pages/create
As you can see we need access to the site name in a number of routes.
We need to execute the same behaviour for all of these actions:
Look for a site with the given identifier on the current account
If the site returned is null, return a 404 (or custom view)
If the site is NOT null (valid) we can carry on executing the action
The current account is always available to us via an ISiteContext object. Here is how I might achieve all of the above using a normal route parameter and performing the query directly within my action:
private readonly ISiteContext siteContext;
private readonly IRepository<Site> siteRepository;
public SitesController(ISiteContext siteContext, IRepository<Site> siteRepository)
{
this.siteContext = siteContext;
this.siteRepository = siteRepository;
}
[HttpGet]
public ActionResult Details(string id)
{
var site =
siteRepository.Get(
s => s.Account == siteContext.Account && s.SystemName == id
);
if (site == null)
return HttpNotFound();
return Content("Viewing details for site " + site.Name);
}
This isn't too bad, but I'm going to need to do this on 20 or so action methods so want to keep things as DRY as possible.
I haven't done much with custom model binders so I wonder if this is a job better suited for them. A key requirement is that I can inject my dependencies into the model binder (for ISiteContext and IRepository - I can fall back to DependencyResolver if necessary).
Many thanks,
Ben
Update
Below is the working code, using both a custom model binder and action filter. I'm still not sure how I feel about this because
Should I be hitting my database from a modelbinder
I can actually do both the retrieving of the object and null validation from within an action filter. Which is better?
Model Binder:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (!controllerContext.RouteData.Values.ContainsKey("siteid"))
return null;
var siteId = controllerContext.RouteData.GetRequiredString("siteid");
var site =
siteRepository.Get(
s => s.Account == siteContext.Account && s.SystemName == siteId
);
return site;
}
Action Filter:
public class ValidateSiteAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var site = filterContext.ActionParameters["site"];
if (site == null || site.GetType() != typeof(Site))
filterContext.Result = new HttpNotFoundResult();
base.OnActionExecuting(filterContext);
}
}
Controller Actions:
[HttpGet]
[ValidateSite]
public ActionResult Settings(Site site)
{
var blog = site.GetFeature<BlogFeature>();
var settings = settingsProvider.GetSettings<BlogSettings>(blog.Id);
return View(settings);
}
[HttpPost]
[ValidateSite]
[UnitOfWork]
public ActionResult Settings(Site site, BlogSettings settings)
{
if (ModelState.IsValid)
{
var blog = site.GetFeature<BlogFeature>();
settingsProvider.SaveSettings(settings, blog.Id);
return RedirectToAction("Settings");
}
return View(settings);
}
This definitely sounds like a job for an action filter. You can do DI with action filters not a problem.
So yeah, just turn your existing functionality into a action filter and then apply that to each action OR controller OR a base controller that you inherit from.
I don't quite know how your site works but you could possibly use a global action filter that checks for the existence of a particular route value, e.g. 'SiteName'. If that route value exists, that means you need to follow through with checking that the site exists...
A custom model binder for your Site type sounds like a good idea to me.
You will probably also want an action filter as well to catch "null" and return not found.

Mock a web service used in an action filter

I have an external-to-my-solution web service that I'm using in an ActionFilter. The action filter grabs some basic data for my MasterPage. I've gone back and forth between using an action filter and extending the base controller class, and decided the action filter was the best approach. Then I started unit testing (Yeah, yeah TDD. Anyway... :D )
So I can't mock (using Moq, btw) a web service in an action filter because I can't inject my mock WS into the action filter, since action filters don't take objects as params. Right? At least that's what I seem to have come to.
Any ideas? Better approaches? I'm just trying to return a warning to the user that if the web service is unavailable, their experience might be limited.
Thanks for any help!
namespace MyProject.ActionFilters
{
public class GetMasterPageData : ActionFilterAttribute
{
public ThatWS ws = new ThatWS();
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpContextBase context = filterContext.HttpContext;
try {
DoStuff();
}
catch ( NullReferenceException e ) {
context.Session["message"] = "There is a problem with the web service. Some functionality will be limited.";
}
}
}
}
Here's a quick and dirty approach:
public class GetMasterPageData : ActionFilterAttribute
{
public Func<ISomeInterface> ServiceProvider = () => new ThatWS();
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var result = ServiceProvider().SomeMethod();
...
}
}
And in your unit test you could instantiate the action filter and replace the ServiceProvider public field with some mocked object:
objectToTest.ServiceProvider = () => new SomeMockedObject();
Of course this approach is not as clean as the one suggested by #Ryan in the comments section but it could work in some situations.

make sure each controller method has a ValidateAntiForgeryToken attribute?

Is there any way to centralize enforcement that every action method must have a "ValidateAntiForgeryToken" attribute? I'm thinking it would have to be done by extending one the "routing" classes.
Edit: Or maybe do some reflection at application startup?
Yes. You can do this by creating your own BaseController that inherits the Mvc Controller, and overloads the OnAuthorization(). You want to make sure it is a POST event before enforcing it:
public abstract class MyBaseController : Controller
{
protected override void OnAuthorization(AuthorizationContext filterContext)
{
//enforce anti-forgery stuff for HttpVerbs.Post
if (String.Compare(filterContext.HttpContext.Request.HttpMethod,
System.Net.WebRequestMethods.Http.Post, true) == 0)
{
var forgery = new ValidateAntiForgeryTokenAttribute();
forgery.OnAuthorization(filterContext);
}
base.OnAuthorization(filterContext);
}
}
Once you have that, make sure all of your controllers inherit from this MyBaseController (or whatever you call it). Or you can do it on each Controller if you like with the same code.
Sounds like you're trying to prevent "oops I forgot to set that" bugs. If so I think the best place to do this is with a custom ControllerActionInvoker.
Essentially what you want to do is stop MVC from even finding an action without a AntiForgery token:
public class MustHaveAntiForgeryActionInvoker : ControllerActionInvoker
{
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
var foundAction = base.FindAction(controllerContext, controllerDescriptor, actionName);
if( foundAction.GetCustomAttributes(typeof(ValidateAntiForgeryTokenAttribute), true ).Length == 0 )
throw new InvalidOperationException("Can't find a secure action method to execute");
return foundAction;
}
}
Then in your controller, preferably your base controller:
ActionInvoker = new MustHaveAntiForgeryActionInvoker();
Just wanted to add that custom Controller base classes tend to get "thick" and imo its always best practice to use MVC's brilliant extensibility points to hook in the features you need where they belong.
Here is a good guide of most of MVC's extensibility points:
http://codeclimber.net.nz/archive/2009/04/08/13-asp.net-mvc-extensibility-points-you-have-to-know.aspx
Ok, I just upgraded a project to MVC v2.0 here, and eduncan911's solution doesn't work anymore if you use the AuthorizeAttribute on your controller actions. It was somewhat hard to figure out why.
So, the culprit in the story is that the MVC team added the use of the ViewContext.HttpContext.User.Identity.Name property in the value for the RequestVerificationToken.
The overridden OnAuthorization in the base controller is executed before any filters on the controller action. So, the problem is that the Authorize attribute has not yet been invoked and therefore is the ViewContext.HttpContext.User not set. So the UserName is String.Empty whereas the AntiForgeryToken used for validation includes the real user name = fail.
We solved it now with this code:
public abstract class MyBaseController : Controller
{
protected override void OnAuthorization(AuthorizationContext filterContext)
{
//enforce anti-forgery stuff for HttpVerbs.Post
if (String.Compare(filterContext.HttpContext.Request.HttpMethod, "post", true) == 0)
{
var authorize = new AuthorizeAttribute();
authorize.OnAuthorization(filterContext);
if (filterContext.Result != null) // Short circuit validation
return;
var forgery = new ValidateAntiForgeryTokenAttribute();
forgery.OnAuthorization(filterContext);
}
base.OnAuthorization(filterContext);
}
}
Some references to the MVC code base:
ControllerActionInvoker#InvokeAuthorizationFilters() line 283. Same short circuiting.
AntiForgeryData#GetUsername() line 98. New functionality.
How about this?
[ValidateAntiForgeryToken]
public class MyBaseController : Controller
{
}

Resources