Posted by: lluppes | March 19, 2011

MVC3 Enabled for Mobile – a.k.a. MVVVVC

(Update – FYI – I’ve posted a followup video blog on this topic at https://blog.luppes.com/2011/12/30/mobile-web-apps/)

I’ve been doing a lot of work lately on websites that are configured to work nicely in smartphones and mobile devices. In the past, it’s been pretty tricky to create a site that changes nicely from a PC format into a mobile format. You’ll see lots of sites that use m.mysite.com or mobile.mysite.com, etc. The technique I’ve been developing here automatically decides what format to serve up based on the browser type you are using. This concept was inspired by a column from Scott Hanselmann. He had some great sample code posted, but it wasn’t completely fleshed out, so I’ve been working on a site that’s actually using these concepts.

Example

Here’s one site I’ve been working on for a local non-profit.  If you navigate to the site with a PC, you get the first picture – a fully functional standard website.  If you go with an iPhone, iPad, or Android device, you’ll get the second picture – a site that looks like a mobile application, with things that don’t work well stripped out.  Same URL, different views.

image

image

Start with the MVC Framework

To create this site, I have been using the MVC3 template from Microsoft. If you haven’t looked at it, you should. It combines the best of the new ASP.NET functionality with the old classic ASP programming simplicity. (I really miss programming in Classic ASP sometimes – it was very simple and straightforward!) The new Razor syntax is great and I’ve been very impressed with what you can do with it.

I start with a standard MVC3 Internet package (File-New-ASP.Net MVC3 Web Application-Internet Application(Razor)), and then I add in a couple of secret ingredients to spice up the recipe.

By default the project registers one view engine (the Razor engine), but to enable the “VVVV” part of this concept, I’m going to add a few more options.  The code looks at UserAgent (see line 62 below) and and then creates a Razor view engine with a slightly modified path. I’ll implement this by adding a CustomMobileViewEngine.cs file to the root of the project, which contains this code:

 1: using System;
 2: using System.Web.Mvc;
 3:
 4: namespace YourProject
 5: {
 6:   public class CustomMobileViewEngine : IViewEngine
 7:   {
 8:     public IViewEngine BaseViewEngine { get; private set; }
 9:     public Func<ControllerContext, bool> IsTheRightDevice { get; private set; }
 10:     public string PathToSearch { get; private set; }
 11:     public CustomMobileViewEngine(Func<ControllerContext, bool> isTheRightDevice, string pathToSearch, IViewEngine baseViewEngine)
 12:     {
 13:       BaseViewEngine = baseViewEngine;
 14:       IsTheRightDevice = isTheRightDevice;
 15:       PathToSearch = pathToSearch;
 16:     }
 17:     public ViewEngineResult FindPartialView(ControllerContext context, string viewName, bool useCache)
 18:     {
 19:       if (IsTheRightDevice(context))
 20:       {
 21:         return BaseViewEngine.FindPartialView(context, PathToSearch + "/" + viewName, useCache);
 22:       }
 23:       return new ViewEngineResult(new string[] { }); //we found nothing and we pretend we looked nowhere
 24:     }
 25:     public ViewEngineResult FindView(ControllerContext context, string viewName, string masterName, bool useCache)
 26:     {
 27:       if (IsTheRightDevice(context))
 28:       {
 29:         return BaseViewEngine.FindView(context, PathToSearch + "/" + viewName, masterName, useCache);
 30:       }
 31:       return new ViewEngineResult(new string[] { }); //we found nothing and we pretend we looked nowhere
 32:     }
 33:     public void ReleaseView(ControllerContext controllerContext, IView view)
 34:     {
 35:       throw new NotImplementedException();
 36:     }
 37:   }
 38:
 39:   public static class MobileHelpers
 40:   {
 41:     public static bool UserAgentContains(this ControllerContext c, string agentToFind)
 42:     {
 43:       return (c.HttpContext.Request.UserAgent.IndexOf(agentToFind, StringComparison.OrdinalIgnoreCase) > 0);
 44:     }
 45:     public static bool IsMobileDevice(this ControllerContext c)
 46:     {
 47:       return c.HttpContext.Request.Browser.IsMobileDevice;
 48:     }
 49:     public static void AddMobile<T>(this ViewEngineCollection ves, Func<ControllerContext, bool> isTheRightDevice, string pathToSearch)
 50:         where T : IViewEngine, new()
 51:     {
 52:       ves.Add(new CustomMobileViewEngine(isTheRightDevice, pathToSearch, new T()));
 53:     }
 54:     public static void AddMobile<T>(this ViewEngineCollection ves, string userAgentSubstring, string pathToSearch)
 55:         where T : IViewEngine, new()
 56:     {
 57:       ves.Add(new CustomMobileViewEngine(c => c.UserAgentContains(userAgentSubstring), pathToSearch, new T()));
 58:     }
 59:     public static void AddIPhone<T>(this ViewEngineCollection ves)
 60:         where T : IViewEngine, new()
 61:     {
 62:       ves.Add(new CustomMobileViewEngine(c => c.UserAgentContains("iPhone") || c.UserAgentContains("iPod"), "Mobile/iPhone", new T()));
 63:     }
 64:     public static void AddIPad<T>(this ViewEngineCollection ves)
 65:         where T : IViewEngine, new()
 66:     {
 67:       ves.Add(new CustomMobileViewEngine(c => c.UserAgentContains("iPad"), "Mobile/iPad", new T()));
 68:     }
 69:     public static void AddDroid<T>(this ViewEngineCollection ves)
 70:         where T : IViewEngine, new()
 71:     {
 72:       ves.Add(new CustomMobileViewEngine(c => c.UserAgentContains("Droid"), "Mobile/Android", new T()));
 73:     }
 74:     public static void AddBlackberry<T>(this ViewEngineCollection ves)
 75:         where T : IViewEngine, new()
 76:     {
 77:       ves.Add(new CustomMobileViewEngine(c => c.UserAgentContains("Blackberry"), "Mobile/Blackberry", new T()));
 78:     }
 79:     public static void AddGenericMobile<T>(this ViewEngineCollection ves)
 80:         where T : IViewEngine, new()
 81:     {
 82:       ves.Add(new CustomMobileViewEngine(c => c.IsMobileDevice(), "Mobile", new T()));
 83:     }
 84:   }
 85: }

In the Global.asax file, I then add in code to register those engines:

 1: protected void Application_Start()
 2: {
 3:   AreaRegistration.RegisterAllAreas();
 4:   RegisterGlobalFilters(GlobalFilters.Filters);
 5:   RegisterRoutes(RouteTable.Routes);
 6:
 7:   ViewEngines.Engines.Clear();
 8:   ViewEngines.Engines.AddIPhone<RazorViewEngine>();
 9:   ViewEngines.Engines.AddIPad<RazorViewEngine>();
 10:   ViewEngines.Engines.AddDroid<RazorViewEngine>();
 11:   ViewEngines.Engines.AddBlackberry<RazorViewEngine>();
 12:   ViewEngines.Engines.AddGenericMobile<RazorViewEngine>();
 13:
 14:   ViewEngines.Engines.Add(new RazorViewEngine());
 15: }
 16:

That’s really the magic that drives this whole process. The rest is just simple MVC programming techniques. You can use whatever other background things you want like LINQ to SQL or Entity Framework, etc. – it doesn’t really matter for purposes of this discussion.

Build Out Your Mobile Views

From this point, you go about building your MVC website just like you did before. Once you build out a view, you’ll create a series of Mobile folders within each view folder. In the example below, I’ve added in Mobile folders for the Home view. You’ll want the folders to match the custom view engines that you registered in the CustomMobileViewEngine.cs file above.  This keeps your mobile views right along-side of your normal views in your projects.

clip_image002

Since most of my mobile devices will behave almost exactly the same with only minor styling differences, I’m going to create shared partial views (the _Index.cshtml and _Details.cshtml) that will contain the guts of my pages, and then each actual mobile device file will simply set the layout file the device will use and then load in the partial view that contains the real contents of the page. If you don’t use this technique, you’ll end up replicating all your pages multiple times, which creates a maintenance headache. This way for each view you’ll have one file to maintain for rich browsers like a PC and one file to maintain for mobile browsers that is light and fast.

Here’s the Home/Mobile/Android/Index.cshtml.

 1: @{
 2: Layout = "../../../Shared/Mobile/Android/_Layout.cshtml";
 3: }
 4: @Html.Partial("../_Index")

and the Home/Mobile/Android/Details.cshtml.

 1: @{
 2: Layout = "../../../Shared/Mobile/Android/_PartialPage.cshtml";
 3: }
 4: @Html.Partial("../_Details")

Now Make Those Mobile Views Fit Look Nice

If you want to make your site look like a native mobile app, the easiest way I’ve found is to use an open source project called IUI. IUI makes your page look and behave like an iPhone type of interface. It’s based on the WebKit browser standards, so it will work on IOS and Android devices, and some but not all Blackberry devices. I include the IUI folder under the Content/Mobile folder, and have a slightly different themed CSS style file for Android pages and the iPhone pages.

Of course, there are a few things to watch out for here, the biggest of which has to do with how you set your layout pages. IUI will convert any links that you make into Ajax calls and fetch the HTML and load it right into a div in your document.  It will automatically keep track of the stack and put titles on the page and back buttons in the toolbar. The thing to keep in mind is that any subsequent page after that first one should NOT load in the javascript and stylesheets. If you use the standard layout page with everything on it, it will get ugly and you’ll start seeing two or three or more title bars on one screen.  Therefore, in the Shared Views folder, I’ve created a _Layout.cshtml and a _PartialLayout.cshtml. The first page I load (i.e. Home/Index) uses the _Layout.cshtml, but the other child pages use only the _PartialLayout.cshtml.

Here’s my Shared/Mobile/iPhone/_Layout.cshtml:

 1: <!DOCTYPE html>
 2: <html>
 3: <head>
 4:   <title>@ViewBag.Title</title>
 5:   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 6:   <script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
 7:   <link href="@Url.Content("~/Content/Mobile/iui/iui.css")" rel="stylesheet" type="text/css" media="screen" />
 8:   <link href="@Url.Content("~/Content/Mobile/iui/theme/default/default-theme.css")" rel="stylesheet" type="text/css" media="screen" />
 9:   <script src="@Url.Content("~/Content/Mobile/iui/iui.js")" type="text/javascript"></script>
 10:
 11:   <meta name="viewport" content="width=320; initial-scale=1.0; maximum-scale=3.0;" />
 12:   <meta name="apple-touch-fullscreen" content="YES" />
 13:   <meta name="apple-mobile-web-app-capable" content="yes" />
 14:   <link rel="apple-touch-icon" href="@Url.Content("~/Content/Mobile/iPhone/apple-touch-icon.png")" />
 15:   <link rel="apple-touch-startup-image" href="@Url.Content("~/Content/Mobile/iPhone/iPhone_Startup.png")" />
 16:
 17:   <link rel="shortcut icon" href="http://www.yourdomain.com/favicon.ico" type="image/x-icon" />
 18:   <link rel="icon" href="http://www.yourdomain.com/favicon.ico" type="image/x-icon" />
 19: </head>
 20: <body>
 21:   <div class="toolbar">
 22:     <a id="backButton" class="button" href="#"></a>
 23:     <h1 id="pageTitle">@ViewBag.Title</h1>
 24:   </div>
 25:   @RenderBody()
 26: </body>
 27: </html>

And here’s my Shared/Mobile/iPhone/_PartialPage.cshtml:

 1: @RenderBody()

Pretty simple, huh?

Make Your Website Behave Like a Native Application

If you want your app to play nicely in the iPhone/iPad world and look like an app, it’s really not too difficult – you just need to add a few meta tags to your layout pages (see the _Layout file above). The tags you are interested in are:

Desktop Icon:

Set “apple-touch-icon” to point to a PNG that is 57×57 pixels to look nice on an iPhone.

Mobile Web Application Behaviors:

Set “apple-touch-fullscreen”  and “apple-mobile-web-app-capable” to “yes”.  Adding the Mobile Web Capable meta tag does nothing for you when the user hits your site in the browser on their phone or when you test in a browser with UserAgent tags set to simulate an IOS device.  However – if the user creates desktop shortcut, then you will see totally different behavior. When the user clicks on that desktop shortcut, it looks and behaves just like an installed app. Your app should start in a full-screen mode with a splash screen without the browser windows appearing. It’s really still using a browser window and running like a website, but it doesn’t look like it.

Full Width Screen and Pinch to Zoom:

Set “viewport” to “initial-scale=1.0; maximum-scale=3.0;” or whatever settings you wish to support.

Application Splash Screen:

Set “apple-touch-startup-image” to a PNG.  The startup image for iPhone should be 320×460 pixels and the startup image for iPad should be 1004×768 pixels. You’ll be tempted to make them other sizes, but don’t do it. These devices are very picky and if you don’t make the startup images EXACTLY that size they simply won’t show up.

Testing

When I’m creating these websites, 90% of my testing is done on my PC, and then final testing is done on the devices. I use the Safari browser for most of my testing as it’s the closest thing to the IOS browsers. If you enable Developer mode in the browser, you should be able to go to the Developer menu and change the User Agent that the browser supplies. Since that’s what our Custom View Engine module looks at, the site will serve up the pages pretty much like an iPad or iPhone device would see them. It’s not perfect, but it is close and makes it very simple to debug and test your site.

The biggest thing to watch out for is that you HAVE to test on a IOS mobile device if you want your desktop shortcut to behave like an application. (Android doesn’t matter because you’ll still open the shortcut in the browser) Some types of links will break out of that application mode, and the “application” that was running full screen app mode will suddenly open a new browser window, then you’ll be right back in the browser world, so you’ll have to test each page/link for that. The best solution I’ve found so far is to change those problem links from a simple HREF to a javascript call like “window.location.href=’mynewpage’”.  If you do that, then it will keep it within the Mobile Web Application and not open a new browser window.

Wrapping It Up

That’s about it for this post – I’ve shown you the basics of how to create an MVC3 website that automatically morphs into a different mode when you browse to it with different devices. But this is really just the beginning – there are plenty of other topics you’ll want to explore to along these lines.

I created a VS2010 template file that encapsulates all of these ideas along with my standard base pages, stylesheets, images, etc., so that makes it very easy for me to create a new project with all of these things baked in.  (Create your own by doing a File – Export Template when you’ve got a working project).

I’ve also been working on a code generator that does essentially what the MVC scaffolding engines do, but which also scaffolds all of the Mobile pages at the same time. I can simply point it at a database table, and it will generate the Model, Controller, and all the resulting views automatically.

With those two things in hand, I can have the code base for a full MVC mobile-enabled website spun up in a few minutes, and then I can spend my time customizing that site to solve my business problem instead of creating all of the repetitive behind the scenes plumbing that you normally have to do.

References

Scott Gu’s Blog announcing MVC 3

http://weblogs.asp.net/scottgu/archive/2011/01/13/announcing-release-of-asp-net-mvc-3-iis-express-sql-ce-4-web-farm-framework-orchard-webmatrix.aspx

Scott Hanselman’s First Blog about MVC Mobile (Incomplete and updated later)

http://www.hanselman.com/blog/MixMobileWebSitesWithASPNETMVCAndTheMobileBrowserDefinitionFile.aspx

Scott Hanselman’s Second Blog about MVC Mobile (fixes problems with the first post, but still incomplete)

http://www.hanselman.com/blog/ABetterASPNETMVCMobileDeviceCapabilitiesViewEngine.aspx

Good Walkthroughs/Tutorials for MVC3 and Razor

http://www.asp.net/mvc/tutorials/getting-started-with-mvc3-part1-cs

http://www.asp.net/mvc/tutorials/mvc-music-store-part-1

http://nerddinnerbook.s3.amazonaws.com/Intro.htm

IUI Mobile Framework

http://code.google.com/p/iui/

Technorati Tags: ,,,,,,
Written by .
Advertisements

Responses

  1. This is a *great* idea and article! Wow, I really like this. Would you be willing to share the code used in this article and/or your template?

    Thank you!
    Kevin

    • If you check out the followup post on my blog, I put together a quick 12 minute demo of building a site with this technique. Attached that that post is a sample project with a simple version of the code located at http://www.luppes.com/source/MvcMobileWeb.zip.

  2. Thanks for the helpful article about grabbing UserAgent for mobile devices!

    • Thanks! I should go back and update this article to make it look more like the MVC4 style, but this does still work really well.


What do you think?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories

%d bloggers like this: