Created
October 6, 2010 13:57
-
-
Save davidwhitney/613382 to your computer and use it in GitHub Desktop.
MVC2 view engine that detects devices
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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