Mobile redirection is simple stuff but what happens when you need to deep link into the mobile application?
On a recent project we needed to produce mobile (iPad specifically) equivalent. The desktop app itself was developed using ExtJS (3.3.1) and had three primary entry points,
- Logon Screen
- Home Screen
- Product Screen (New/View/Edit)
Due to the way the project was implemented these became as 3 distinct pages/controllers/actions. Users could receive emails with links to a particular product and they would go directly to that view (with a login redirect if not previously authenticated). The mobile solution, however, written using Sencha Touch (consistent development experience, native-esque UI with little effort) is a single page application. This presents a problem when the user is on a compatible mobile device and they receive a link to a particular placement - how do we push that sort of deep linking into a single page app. Well on the client side frameworks such as Backbone.js, jQuery Mobile and Sencha Touch [anyone got more please?] all offer history support using hash navigation. That's the client side sorted but how do we translate, say, /Product/Show/12345 into /Mobile#placement/12345?
MobileRedirectAttribute
Firstly I created an extension of the AuthorizationAttribute that will act as an interim redirection and request parser between the mobile and desktop solutions. Here's the code (usage follows),
/// <summary>
/// Redirects to the mobile view if on a supported device
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class MobileRedirectAttribute : AuthorizeAttribute
{
private string _clientFragment;
/// <summary>
/// Default Constructor
/// </summary>
public MobileRedirectAttribute()
{
_clientFragment = string.Empty;
}
/// <summary>
/// Constructor that takes an argument
/// </summary>
/// <param name="clientUrl">The url fragment we should append to the url</param>
public MobileRedirectAttribute(string clientFragment)
{
_clientFragment = clientFragment;
}
/// <summary>
/// Tests if this request originates from a supported mobile device
/// and redirects as appropriate
/// </summary>
/// <param name="ctx">The action execution context</param>
public override void OnAuthorization(AuthorizationContext ctx)
{
if (ctx.HttpContext.Request.Browser.IsMobileDevice)
{
// parse the fragment with request parameters
string fragment = ParseClientFragment(ctx);
// construct the redirect url
UrlHelper urlHelper = new UrlHelper(ctx.RequestContext);
string url = string.Format("{0}#{1}", urlHelper.Action("Index", "Mobile"), fragment);
// return redirect result to prevent action execution
ctx.Result = new RedirectResult(url);
}
}
/// <summary>
/// Parses the client fragment and replaces :[token] with the request parameter
/// </summary>
/// <param name="ctx">The controller context</param>
/// <returns>The parsed fragment</returns>
private string ParseClientFragment(ControllerContext ctx)
{
string parsedFragment = _clientFragment ?? string.Empty;
if (!string.IsNullOrEmpty(parsedFragment))
{
NameValueCollection @params = ctx.HttpContext.Request.Params;
MatchCollection matches = Regex.Matches(_clientFragment, ":[a-zA-Z]+");
RouteData routeData = RouteTable.Routes.GetRouteData(ctx.HttpContext);
// check each token and replace with param or route values
foreach (Match match in matches)
{
string token = match.Value.TrimStart(':');
string value = @params[token];
// if we haven;t got a parameter here we must check the route values
if (string.IsNullOrEmpty(value) && routeData.Values.ContainsKey(token))
{
value = routeData.Values[token] as string;
}
// perform the replace
parsedFragment = parsedFragment.Replace(match.Value, value);
}
}
return parsedFragment;
}
}
Usage
So for our 3 entry points into our application we attribute the controller actions with the MobileRedirectAttribute and give it a client fragment.
public class ProductController : Controller
{
[MobileRedirect("[product/:id")]
public ActionResult Index(Nullable<long> id)
{
// perform action
}
}
public class HomeController : Controller
{
[MobileRedirect("home")]
public ActionResult Index()
{
return View();
}
}
public class AuthenticationController : Controller
{
[MobileRedirect("home")]
public ActionResult Login()
{
}
}
The client fragment is capable of translating tokens embedded within it ( as :<token_name>) and replacing the token with a matching route value or request parameter. The ProductController Index action is a good example of this. A request to /Product/Index/12345 on a mobile device would translate to /Mobile/#product/12345
How It Works
Pretty simple really.
- The attribute checks if the device is a compatible/mobile device.
- If it is the retrieves the client fragment and extracts the tokens - :<token_name>
- It attempts to match the token names against request parameters first
- If there is no parameter it then looks into the route values (eg. :id in the above url isn;t a parameter but rather a route value)
- It replaces the token with the real value
- It performs a redirect to /Mobile#<client_fragment> which cancels the execution of the action.
Other Points
- It's probably not the most robust solution in that more complex scenarios may not work as expected but it's a decent base that can be extended.
- I have hardcoded the mobile route as it fitted my needs so I think that should be externalised as well.
- The determination of whether a device is a compatible device is facilitated through Browser.IsMobileDevice. This is for demonstration purposes only. In the project we use a different solution but it is a bit more long winded to explain here.
- The "redirect to logon" handling is performed within the app itself so that is why the Logon view's fragment is simply "home".
Demo
I've pushed a very quick and dirty demo of this onto GitHub for anyone interested - https://github.com/kouphax/mobileredirect-mvc/ . It uses a really quick UserAgent.Contains("iPad") check for "mobile" detection so use and iPad or set your User Agent to try it out.
Any use to anyone out there? Any problems with it? Let me know.