Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save davidwhitney/613382 to your computer and use it in GitHub Desktop.
Save davidwhitney/613382 to your computer and use it in GitHub Desktop.
MVC2 view engine that detects devices
using System.Collections.Generic;
using System.Web.Mvc;
using System;
using System.Globalization;
using System.Linq;
// Resolving view engine is reflected out from the default WebFormsViewEngine
// with extra hooks for a resolver
// device detection makes use of http://mdbf.codeplex.com/ and query string magic
namespace Resolving.Web.Mvc
{
public class ResolvingViewEngine : WebFormViewEngine
{
private const string CACHE_KEY_FORMAT = ":ViewCacheEntry:{0}:{1}:{2}:{3}:";
private const string CACHE_KEY_PREFIX_MASTER = "Master";
private const string CACHE_KEY_PREFIX_VIEW = "View";
private static readonly List<string> EmptyLocations = new List<string>();
private readonly ViewLocationResolver _resolver;
public ResolvingViewEngine()
: this(new ViewLocationResolver(new DeviceDetection()))
{
}
public ResolvingViewEngine(ViewLocationResolver viewLocationResolver)
{
_resolver = viewLocationResolver;
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (String.IsNullOrEmpty(viewName))
{
throw new ArgumentException("viewName");
}
List<string> viewLocationsSearched;
List<string> masterLocationsSearched;
var viewLocationsToSearch = _resolver.ResolvePossibleViewFileLocationsForRequest(controllerContext);
var masterLocationsToSearch = _resolver.ResolvePossibleMasterPageFileLocationsForRequest(controllerContext);
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string viewPath = GetPath(controllerContext, viewLocationsToSearch, viewName, controllerName, CACHE_KEY_PREFIX_VIEW, useCache, out viewLocationsSearched);
string masterPath = GetPath(controllerContext, masterLocationsToSearch, masterName, controllerName, CACHE_KEY_PREFIX_MASTER, useCache, out masterLocationsSearched);
if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
}
return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
List<string> strArray;
if (controllerContext == null) { throw new ArgumentNullException("controllerContext"); }
if (string.IsNullOrEmpty(partialViewName)) { throw new ArgumentException("Partial View Name is null or empty.", "partialViewName"); }
string requiredString = controllerContext.RouteData.GetRequiredString("controller");
string str2 = GetPath(controllerContext, PartialViewLocationFormats, partialViewName, requiredString, "Partial", useCache, out strArray);
if (string.IsNullOrEmpty(str2))
{
return new ViewEngineResult(strArray);
}
return new ViewEngineResult(CreatePartialView(controllerContext, str2), this);
}
private string GetPath(ControllerContext controllerContext, string[] locations, string name, string controllerName, string cacheKeyPrefix, bool useCache, out List<string> searchedLocations)
{
searchedLocations = EmptyLocations;
if (String.IsNullOrEmpty(name))
{
return String.Empty;
}
if (locations == null || locations.Length == 0)
{
throw new InvalidOperationException();
}
bool nameRepresentsPath = IsSpecificPath(name);
string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName);
if (useCache)
{
string result = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, cacheKey);
if (result != null)
{
return result;
}
}
if (nameRepresentsPath)
{
return GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations);
}
return GetPathFromGeneralName(controllerContext, locations, name, controllerName, cacheKey, ref searchedLocations);
}
private string GetPathFromGeneralName(ControllerContext controllerContext, string[] locations, string name, string controllerName, string cacheKey, ref List<string> searchedLocations)
{
string result = String.Empty;
searchedLocations = new List<string>();
for (int i = 0; i < locations.Length; i++)
{
string virtualPath = String.Format(CultureInfo.InvariantCulture, locations[i], name, controllerName);
if (FileExists(controllerContext, virtualPath))
{
searchedLocations = EmptyLocations;
result = virtualPath;
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
return result;
}
searchedLocations.Add(virtualPath);
}
return GetPathFromGeneralNameOfBaseTypes(controllerContext.Controller.GetType(), locations, name, controllerContext, cacheKey, result, ref searchedLocations);
}
private string GetPathFromGeneralNameOfBaseTypes(Type descendantType, string[] locations, string name, ControllerContext controllerContext, string cacheKey, string result, ref List<string> searchedLocations)
{
Type baseControllerType = descendantType;
if (baseControllerType == null
|| !baseControllerType.Name.Contains("Controller")
|| baseControllerType.Name == "Controller")
{
return result;
}
for (int i = 0; i < locations.Length; i++)
{
string baseControllerName = baseControllerType.Name.Replace("Controller", "");
string virtualPath = String.Format(CultureInfo.InvariantCulture, locations[i], name, baseControllerName);
if (!string.IsNullOrEmpty(virtualPath) &&
FileExists(controllerContext, virtualPath))
{
searchedLocations = EmptyLocations;
result = virtualPath;
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
return result;
}
searchedLocations.Add(virtualPath);
}
return GetPathFromGeneralNameOfBaseTypes(baseControllerType.BaseType, locations, name, controllerContext,
cacheKey, result, ref searchedLocations);
}
private string CreateCacheKey(string prefix, string name, string controllerName)
{
return String.Format(CultureInfo.InvariantCulture, CACHE_KEY_FORMAT,
GetType().AssemblyQualifiedName, prefix, name, controllerName);
}
private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref List<string> searchedLocations)
{
string result = name;
if (!FileExists(controllerContext, name))
{
result = String.Empty;
searchedLocations = new List<string> { name };
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
return result;
}
private static bool IsSpecificPath(string name)
{
char c = name[0];
return (c == '~' || c == '/');
}
}
public class ViewLocationResolver
{
private readonly DeviceDetection _deviceDetection;
private readonly List<string> _viewLocationFormats;
private readonly List<string> _masterLocationFormats;
private const string MobileRouteModifier = ".mobile";
private const string DestkopRouteModifier = ".desktop";
private const string TabletRouteModifier = ".tablet";
private const string DeviceModifierQueryStringKey = "targetdevice";
public ViewLocationResolver():this(new DeviceDetection())
{
}
public ViewLocationResolver(DeviceDetection deviceDetection)
{
_deviceDetection = deviceDetection;
_viewLocationFormats = new List<string>
{
"~/Views/{1}/{0}{2}.aspx",
"~/Views/{1}/{0}/default{2}.aspx",
"~/Views/{1}/{0}/index{2}.aspx",
"~/Views/{1}/{0}{2}.ascx",
"~/Views/Shared/{0}{2}.aspx",
"~/Views/Shared/{0}{2}.ascx",
};
_masterLocationFormats = new List<string> {"~/Views/{1}/{0}{2}.master", "~/Views/Shared/{0}{2}.master"};
}
public string[] ResolvePossibleViewFileLocationsForRequest(ControllerContext controllerContext)
{
return ResolvePossibleFileLocationsForRequest(_viewLocationFormats, controllerContext);
}
public string[] ResolvePossibleMasterPageFileLocationsForRequest(ControllerContext controllerContext)
{
return ResolvePossibleFileLocationsForRequest(_masterLocationFormats, controllerContext);
}
public string[] ResolvePossibleFileLocationsForRequest(List<string> locationFormats, ControllerContext controllerContext)
{
var potentialLocations = new List<string>();
if (HttpContextIsValid(controllerContext))
{
var searchModifier = CalculateViewSearchModifier(controllerContext);
if (!string.IsNullOrWhiteSpace(searchModifier) && searchModifier != DestkopRouteModifier)
{
AddLocations(potentialLocations, locationFormats, searchModifier);
}
if (_deviceDetection.IsTablet(controllerContext.HttpContext) && searchModifier != DestkopRouteModifier)
{
AddLocations(potentialLocations, locationFormats, TabletRouteModifier);
}
if (_deviceDetection.IsMobileDevice(controllerContext.HttpContext) && searchModifier != DestkopRouteModifier)
{
AddLocations(potentialLocations, locationFormats, MobileRouteModifier);
}
}
AddLocations(potentialLocations, locationFormats, string.Empty);
return potentialLocations.ToArray();
}
private static bool HttpContextIsValid(ControllerContext controllerContext)
{
return ((controllerContext != null && controllerContext.HttpContext != null) &&
controllerContext.HttpContext.Request != null) && controllerContext.HttpContext.Request.Browser != null;
}
private static string CalculateViewSearchModifier(ControllerContext controllerContext)
{
if (controllerContext.RequestContext.HttpContext.Request.QueryString[DeviceModifierQueryStringKey] != null)
{
return "." + controllerContext.RequestContext.HttpContext.Request.QueryString[DeviceModifierQueryStringKey];
}
return string.Empty;
}
private static void AddLocations(List<string> potentialLocations, IEnumerable<string> locationFormats, string modifier)
{
potentialLocations.AddRange(locationFormats.Select(location => string.Format(location, "{0}", "{1}", modifier)));
}
}
public class DeviceDetection
{
public bool IsMobileDevice(HttpContextBase context)
{
return HttpContextIsValid(context)
&& (context.Request.Browser.IsMobileDevice || UserAgentContains(context, "ipad"));
}
public bool IsIosDevice(HttpContext context)
{
return IsIosDevice(new HttpContextWrapper(context));
}
public bool IsIosDevice(HttpContextBase context)
{
return DeviceVendorIs(context, "Apple")
&& (UserAgentContains(context, "ipad") || UserAgentContains(context, "iphone"));
}
public bool IsAndroidDevice(HttpContext context)
{
return IsAndroidDevice(new HttpContextWrapper(context));
}
public bool IsAndroidDevice(HttpContextBase context)
{
return UserAgentContains(context, "android");
}
public bool IsTablet(HttpContext context)
{
return IsTablet(new HttpContextWrapper(context));
}
public bool IsTablet(HttpContextBase context)
{
return UserAgentContains(context, "ipad")
|| (IsAndroidDevice(context)
&& context.Request.Browser.ScreenPixelsWidth > 640);
}
private static bool UserAgentContains(HttpContextBase context, string userAgentContainsThis)
{
return HttpContextIsValid(context)
&& context.Request.UserAgent != null
&& context.Request.UserAgent.ToLower().Contains(userAgentContainsThis.ToLower());
}
private static bool DeviceVendorIs(HttpContextBase context, string vendorName)
{
return HttpContextIsValid(context)
&& context.Request.Browser.MobileDeviceManufacturer.ToLower().Contains(vendorName.ToLower());
}
private static bool HttpContextIsValid(HttpContextBase context)
{
return context != null && context.Request != null && context.Request.Browser != null;
}
public static string MapTargetDeviceToUrl(NameValueCollection incomingQueryParameters, string urlToProcess)
{
if (incomingQueryParameters.AllKeys.Contains("targetdevice") && !urlToProcess.ToLower().Contains("targetdevice="))
{
if (!urlToProcess.Contains("?"))
{
urlToProcess += "?";
}
urlToProcess += "&targetdevice=" + incomingQueryParameters["targetdevice"];
}
return urlToProcess;
}
public static RedirectToRouteResult MapTargetDeviceToUrl(NameValueCollection incomingQueryParameters, RedirectToRouteResult redirectResult)
{
if (incomingQueryParameters.AllKeys.Contains("targetdevice")
&& !redirectResult.RouteValues.ContainsKey("targetdevice"))
{
redirectResult.RouteValues.Add("targetdevice", incomingQueryParameters["targetdevice"]);
}
return redirectResult;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment