Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

MVC - Adaptive Rendering Device Views

0.00/5 (No votes)
17 Mar 2014 1  
I want to demonstrate an and-to-end tutorial on how to design an MVC project that caters for multiple devices but with only one respective controller and JavaScript file.

Introduction

I want to demonstrate an and-to-end tutorial on how to design an MVC project that caters for multiple devices but with only one respective controller and JavaScript file. The views will unfortunately most likely need an accompanying CSS file.

I want to show some tips and tricks on how to discover what device is connecting to your site and how to render the appropriate view for that device. All the time, adhering to the MVC design pattern.

The Scenario

Your manager has asked your team to develop a student dashboard for a local college, to inform the office administrator of statistics and metrics regarding degree courses. As your manager walks away, he turns and says, that our agile stories are to encompass tablet and mobile devices as a given!

Resulting Application

I have taken a screenshot of a desktop, tablet and mobile browsers being displayed on the monitor at the same time. Starting at the bottom right is the mobile view, tablet in the middle and the desktop behind the tablet. You will notice that the desktop has a lot more real-estate to play with and hosts more controls compared to the tablet and mobile – thus you have to design your views differently – but the trick is to use the same controller to give the user the same user experience as best as possible.

Technologies
  1. MVC design pattern
  2. C# 4.5
  3. Visual Studio 2013 (express)
  4. JQuery
  5. JQuery UI
  6. JQuery Mobile
  7. JPlot charts
  8. JQuery Blocking-UI

Testing Tools

  1. Opera Mobile Emulator
  2. Safari (able to change User Agent when posting)
  3. FireFox (UserAgent Switcher)

Project Structure

Below, you can see that the project is a plain old MVC application. The only thing I would bring to your attention is that the Home and Shared folders have multiple views and layout files. You have to remember that for every desktop view you will have to create at least two more views, one for mobile and tablet (others could be TV, for example). I have a separate layout file, for each view type (mobile or tablet).

Code

Bundles

Within the BundleConfig.cs file, I have added a couple new bundles. To reflect the new devices we are rendering against. For example, a JQuery UI (JavaScript or CSS) library really isn’t any good within the mobile environment, where you would use the mobile equivalent. So I have created a bundle for desktop, tablet and mobile – you will notice some common scripts or CSS files between the bundles, a smarter man would have separated these common files out into a separate bundle and referenced them in the views:

// mobile bundles
bundles.Add(new ScriptBundle("~/bundles/mobileJs").Include(
                      "~/Scripts/jquery.mobile-1.4.0.min.js",
                      "~/Scripts/jquery.jqplot.min.js",                   "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.pieRenderer.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.barRenderer.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.categoryAxisRenderer.min.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.pointLabels.min.js",
                      "~/Scripts/jquery.validate.js",
                      "~/Scripts/FooTable-2/dist/footable.all.min.js",
                      "~/Scripts/FooTable-2/dist/footable.filter.min.js",
                      "~/Scripts/FooTable-2/dist/footable.min.js",
                      "~/Scripts/FooTable-2/dist/footable.paginate.min.js",
                      "~/Scripts/FooTable-2/dist/footable.sort.min.js",
                      "~/Scripts/FooTable-2/demos/js/bootstrap-tab.js",
                      "~/Scripts/FooTable-2/demos/js/bootstrapSwitch.js"               
                      ));
 
            bundles.Add(new StyleBundle("~/Content/css/mobile").Include(
                     "~/Content/jquery.mobile-1.4.0.min.css",
                     "~/Content/jquery.mobile.icons-1.4.0.min.css",
                     "~/Content/jquery.mobile.theme-1.4.0.min.css",
                     "~/Content/jquery.mobile.structure-1.4.0.min.css",
                     "~/Content/jquery.mobile.inline-png-1.4.0.css",
                     "~/Content/jquery.mobile.external-png-1.4.0.min.css",
                     "~/Content/jquery.mobile.inline-svg-1.4.0.min.css",
                     "~/Scripts/FooTable-2/css/footable.core.min.css",
                     "~/Scripts/FooTable-2/css/footable.metro.min.css",
                     "~/Scripts/FooTable-2/css/footable.standalone.min.css",
                     "~/Scripts/jquery.jqplot.min.css"                  
                     ));
            // end mobile

 
            // desktop bundles
            bundles.Add(new ScriptBundle("~/bundles/desktopJs").Include(
                      "~/Scripts/jquery.jqplot.min.js",
                      "~/Scripts/jquery.jqGrid.min.js",
                      "~/Scripts/jquery.blockUI.min.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.pieRenderer.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.barRenderer.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.categoryAxisRenderer.min.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.pointLabels.min.js",
                      "~/Scripts/jquery.validate.js"
                      ));
 
            bundles.Add(new StyleBundle("~/Content/css/desktop").Include(
                     "~/Scripts/jquery.jqplot.min.css", 
                     "~/Scripts/jquery.jqplot.min.css",                    
                     "~/Content/jquery.jqGrid/ui.jqgrid.css",
                     "~/Content/themes/base/jquery.ui.all.css",
                     "~/Scripts/validateForm/validationEngine.jquery.css"                        
                     ));
            // end desktop

            // tablet bundles
            bundles.Add(new ScriptBundle("~/bundles/tabletJs").Include(
                      "~/Scripts/jquery.jqplot.min.js",
                      "~/Scripts/jquery.jqGrid.min.js",
                      "~/Scripts/jquery.blockUI.min.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.pieRenderer.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.barRenderer.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.categoryAxisRenderer.min.js",                      "~/Scripts/jquery.jqplot.1.0.8r1250/dist/plugins/jqplot.pointLabels.min.js",
                      "~/Scripts/jquery.validate.js",
                      "~/Scripts/jquery-ui-1.10.4.min.js"                    
                      ));
 
            bundles.Add(new StyleBundle("~/Content/css/tablet").Include(
                     "~/Scripts/jquery.jqplot.min.css",
                     "~/Content/jquery.mobile-1.4.0.min.css",
                     "~/Content/jquery.mobile.icons-1.4.0.min.css",
                     "~/Content/jquery.mobile.theme-1.4.0.min.css",
                     "~/Content/jquery.jqGrid/ui.jqgrid.css",
                     "~/Scripts/validateForm/validationEngine.jquery.css",
                     "~/Content/themes/base/jquery-ui.css"
                     ));
            // end tablet

A View Referencing a Different Layout

Mobile View Snippet

Below is the snippet of code that I have at the top of my mobile view, it will reference the mobile layout.

@{
    Layout = "~/Views/Shared/_LayoutMobile.cshtml";   
}

Tablet View Snippet

Again, the tablet view has its own layout. They layout files look very similar, but that is because I have only created an adaptive rendering for one (home) view. As your extend your application with more views, it will make it easier to edit the appropriate layout file and know that your changes will not affect they layout of the other device layouts.

@{
    Layout = "~/Views/Shared/_LayoutTablet.cshtml";      
}

Determine What Device has Connected to your Web Site

The main entry point to your site is through the Application Start method within your Global.asax file. The Start method will parse out the UserAgent header and determine what the device is. There are plugins out there that can aid your development in this section, namely:

  1. 51Degress (http://51degrees.codeplex.com/) or (http://51degrees.com/)
  2. WURFL

Tip: A roll-your-own approach would be to examine the device that has connected to your site, and if you cannot determine it – render the desktop view and log the device details – sending an email to your support. With this information, they can then update your device repository so that when the same device connects you will render the appropriate view.

The snippet of code below, basically inserts a new entry into the collection DisplayModeProvider, for mobile. So, when a device connects to your site and it is a mobile device, it will get the following page rendered - Index.mobile.cshtml for the index page:

DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("mobile")
{
ContextCondition = (context => GetDeviceType(context.GetOverriddenUserAgent()) == "mobile")
});

The helper method GetDeviceType will determine what the device type is - as best as possible – but nothing beats an up-to-date repository of device data to query against. But this method will certainly cover 90% of devices and if you employ a support factor for unknown devices you can easily bring that percentage up in time for your company.

 protected void Application_Start()
        {         
            DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("mobile")
            {
                ContextCondition = (context => GetDeviceType(context.GetOverriddenUserAgent()) == "mobile")
            }); 
 
            DisplayModeProvider.Instance.Modes.Insert(1, new DefaultDisplayMode("tablet")
            {
                ContextCondition = (context => GetDeviceType(context.GetOverriddenUserAgent()) == "tablet")
            });
 
            DisplayModeProvider.Instance.Modes.Insert(2, new DefaultDisplayMode("tv")
            {
                ContextCondition = (context => GetDeviceType(context.GetOverriddenUserAgent()) == "tv")
            });                       
 
            // otherwise it’s a desktop browser and defaults to normal view.

            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
 
        public string GetDeviceType(string ua)
        {
            string ret = "";
            // Check if user agent is a smart TV - http://goo.gl/FocDk
            if (Regex.IsMatch(ua, @"GoogleTV|SmartTV|Internet.TV|NetCast|NETTV|AppleTV|boxee|Kylo|Roku|DLNADOC|CE\-HTML", RegexOptions.IgnoreCase))
            {
                ret = "tv";
            }
            // Check if user agent is a TV Based Gaming Console
            else if (Regex.IsMatch(ua, "Xbox|PLAYSTATION.3|Wii", RegexOptions.IgnoreCase))
            {
                ret = "tv";
            }
            // Check if user agent is a Tablet
            else if ((Regex.IsMatch(ua, "iP(a|ro)d", RegexOptions.IgnoreCase) || (Regex.IsMatch(ua, "tablet", RegexOptions.IgnoreCase)) && (!Regex.IsMatch(ua, "RX-34", RegexOptions.IgnoreCase)) || (Regex.IsMatch(ua, "FOLIO", RegexOptions.IgnoreCase))))
            {
                ret = "tablet";
            }
            // Check if user agent is an Android Tablet
            else if ((Regex.IsMatch(ua, "Linux", RegexOptions.IgnoreCase)) && (Regex.IsMatch(ua, "Android", RegexOptions.IgnoreCase)) && (!Regex.IsMatch(ua, "Fennec|mobi|HTC.Magic|HTCX06HT|Nexus.One|SC-02B|fone.945", RegexOptions.IgnoreCase)))
            {
                ret = "tablet";
            }
            // Check if user agent is a Kindle or Kindle Fire
            else if ((Regex.IsMatch(ua, "Kindle", RegexOptions.IgnoreCase)) || (Regex.IsMatch(ua, "Mac.OS", RegexOptions.IgnoreCase)) && (Regex.IsMatch(ua, "Silk", RegexOptions.IgnoreCase)))
            {
                ret = "tablet";
            }
            // Check if user agent is a pre Android 3.0 Tablet
            else if ((Regex.IsMatch(ua, @"GT-P10|SC-01C|SHW-M180S|SGH-T849|SCH-I800|SHW-M180L|SPH-P100|SGH-I987|zt180|HTC(.Flyer|\\_Flyer)|Sprint.ATP51|ViewPad7|pandigital(sprnova|nova)|Ideos.S7|Dell.Streak.7|Advent.Vega|A101IT|A70BHT|MID7015|Next2|nook", RegexOptions.IgnoreCase)) || (Regex.IsMatch(ua, "MB511", RegexOptions.IgnoreCase)) && (Regex.IsMatch(ua, "RUTEM", RegexOptions.IgnoreCase)))
            {
                ret = "tablet";
            }
            // Check if user agent is unique Mobile User Agent
            else if ((Regex.IsMatch(ua, "BOLT|Fennec|Iris|Maemo|Minimo|Mobi|mowser|NetFront|Novarra|Prism|RX-34|Skyfire|Tear|XV6875|XV6975|Google.Wireless.Transcoder", RegexOptions.IgnoreCase)))
            {
                ret = "mobile";
            }
            // Check if user agent is an odd Opera User Agent - http://goo.gl/nK90K
            else if ((Regex.IsMatch(ua, "Opera", RegexOptions.IgnoreCase)) && (Regex.IsMatch(ua, "Windows.NT.5", RegexOptions.IgnoreCase)) && (Regex.IsMatch(ua, @"HTC|Xda|Mini|Vario|SAMSUNG\-GT\-i8000|SAMSUNG\-SGH\-i9", RegexOptions.IgnoreCase)))
            {
                ret = "mobile";
            }
            // Check if user agent is Windows Desktop
            else if ((Regex.IsMatch(ua, "Windows.(NT|XP|ME|9)")) && (!Regex.IsMatch(ua, "Phone", RegexOptions.IgnoreCase)) || (Regex.IsMatch(ua, "Win(9|.9|NT)", RegexOptions.IgnoreCase)))
            {
                ret = "desktop";
            }
            // Check if agent is Mac Desktop
            else if ((Regex.IsMatch(ua, "Macintosh|PowerPC", RegexOptions.IgnoreCase)) && (!Regex.IsMatch(ua, "Silk", RegexOptions.IgnoreCase)))
            {
                ret = "desktop";
            }
            // Check if user agent is a Linux Desktop
            else if ((Regex.IsMatch(ua, "Linux", RegexOptions.IgnoreCase)) && (Regex.IsMatch(ua, "X11", RegexOptions.IgnoreCase)))
            {
                ret = "desktop";
            }
            // Check if user agent is a Solaris, SunOS, BSD Desktop
            else if ((Regex.IsMatch(ua, "Solaris|SunOS|BSD", RegexOptions.IgnoreCase)))
            {
                ret = "desktop";
            }
            // Check if user agent is a Desktop BOT/Crawler/Spider
            else if ((Regex.IsMatch(ua, "Bot|Crawler|Spider|Yahoo|ia_archiver|Covario-IDS|findlinks|DataparkSearch|larbin|Mediapartners-Google|NG-Search|Snappy|Teoma|Jeeves|TinEye", RegexOptions.IgnoreCase)) && (!Regex.IsMatch(ua, "Mobile", RegexOptions.IgnoreCase)))
            {
                ret = "desktop";
            }
            // Otherwise assume it is a Mobile Device
            else
            {
                ret = "mobile";
            }
            HttpRuntime.Cache.Insert("DeviceType", ret);
                        
            return ret;
        }

One Controller

Below, is the single controller I am using for the multiple device view. Each view calls the same controller method (using Ajax) and the data that is returned is slightly processed differently – but essentially one JavaScript will also process and bind that data returned.

using DeskTabMobile.EntFramework;
using DeskTabMobile.Models;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace DeskTabMobile.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {         
            return View();
        }       
 
        public JsonResult GetPieGraphData()
        {
            // perform an EF action to retrieve data...dummy data below!!!
            List<PieChartData> dataArray = new List<PieChartData>();
            dataArray.Add(new PieChartData() { Name = "Physics", Value = 15 });
            dataArray.Add(new PieChartData() { Name = "Maths", Value = 10 });
            dataArray.Add(new PieChartData() { Name = "English Lt", Value = 25 });
            dataArray.Add(new PieChartData() { Name = "ICT", Value = 20 });
            dataArray.Add(new PieChartData() { Name = "History", Value = 30 });
            return this.Json(dataArray, JsonRequestBehavior.AllowGet);            
        }
 
        public JsonResult GetCourseResultsData()
        {
            // perform an EF action to retrieve data...dummy data below!!!
            BarChartData dataArray = new BarChartData();
            dataArray.ICTScores = new List<int>() { 200, 600, 700, 1000, 460, 820, 460, -210, 690, 820 };
            dataArray.MathsScores = new List<int>() { 460, -210, 690, 820, 200, 600, 700, -210, 690, 820 };
            dataArray.PhysicsScores = new List<int>() { -260, -440, 320, 200, 200, 600, 700, 1000, 460, 820 };
            dataArray.Months = new List<string>() { "Jan", "Feb", "Mar", "Apr", "May", "June", "Sept", "Oct", "Nov", "Dec"};
            
            return this.Json(dataArray, JsonRequestBehavior.AllowGet);                    
        }
 
        public JsonResult GetStudentsOnCourseData()
        {
            // perform an EF action to retrieve data...dummy data below!!!
            List<GridData> gridData = new List<GridData>();
            gridData.Add(new GridData() { StudentID = "JC200300001", Name = "Jessica Cohen", Course = "Applied Arts and Sciences", Year = 1 });
            gridData.Add(new GridData() { StudentID = "SF200800002", Name= "Shane Foster", Course= "Business Administration", Year= 2});
            gridData.Add(new GridData() {StudentID= "KK200800003", Name= "Kellan Kher", Course= "Social Sciences", Year= 4});
            gridData.Add(new GridData() { StudentID= "PS200100004", Name= "Patrick Shirazi", Course= "Education", Year= 1});
            gridData.Add(new GridData() { StudentID= "CB200400005", Name= "Constance Barry", Course= "Social Sciences", Year= 3});
            gridData.Add(new GridData() { StudentID= "DW200400006", Name= "Dustin Wallace", Course= "Philosophy", Year= 1  });
            gridData.Add(new GridData() { StudentID= "JC200300011", Name= "Jessica Cohen", Course= "Applied Arts and Sciences", Year= 1});
            gridData.Add(new GridData() { StudentID= "SF200800012", Name= "Shane Foster", Course= "Business Administration", Year= 2 });
            gridData.Add(new GridData() { StudentID= "KK200800013", Name= "Kellan Kher", Course= "Social Sciences", Year= 4 });
            gridData.Add(new GridData() {StudentID="PS200100014", Name= "Patrick Shirazi", Course= "Education", Year= 1 });
            gridData.Add(new GridData() { StudentID= "DW200400016", Name= "Dustin Wallace", Course= "Philosophy", Year= 1 });
            gridData.Add(new GridData() { StudentID= "JC200300101", Name= "Jessica Cohen", Course= "Applied Arts and Sciences", Year= 1});
            gridData.Add(new GridData() { StudentID= "SF200800102", Name= "Shane Foster", Course= "Business Administration", Year= 2});
            gridData.Add(new GridData() { StudentID= "KK200800103", Name= "Kellan Kher", Course= "Social Sciences", Year= 4});
            gridData.Add(new GridData() { StudentID= "PS200100104", Name= "Patrick Shirazi", Course= "Education", Year= 1 });
            gridData.Add(new GridData() { StudentID= "CB200400105", Name= "Constance Barry", Course= "Social Sciences", Year= 3 });
            gridData.Add(new GridData() { StudentID= "DW200400106", Name= "Dustin Wallace", Course= "Philosophy", Year= 1 });            
 
            return this.Json(gridData, JsonRequestBehavior.AllowGet);            
           
        }
 
        public ActionResult GetDeviceType()
        {           
            string deviceType = HttpRuntime.Cache["DeviceType"].ToString();
            return Json(new { deviceType }, JsonRequestBehavior.AllowGet);
        }
    }
}

One JavaScript

Also, I wanted to have one JavaScript per view device, as creating a JavaScript file per view per device will soon multiple in management issues!

A keynote was to determine what the device type was on the client side; this was simply achieved using an synchronous call to a controller method to return the device type. Then, when I called a JavaScript method and can determine what snippet of code to run based on the device.

The following snippet will get the device type for the client to hold onto, for later use:

$.getJSON('/home/GetDeviceType', function (result) {
        deviceType = result.deviceType;       
    });

The JavaScript file might have a bit of size to it, but you must remember that you are basically designing three applications in one application – the size isn't manageable – JavaScript is as complicated as you make it – maybe moving into TypeScript might be another approach!

Finally, in the JavaScript code, I determine what the device is before running the code using snippets like:

if (deviceType == 'mobile') {...}

JavaScript Code

$(document).ready(function () {
    var rowID; 
    var tableRowSelected;
    var isLoaded = false;
    var isGridLoaded = new Boolean(false);
    var deviceType;
 
    $.ajaxSetup({
        async: false
    });
 
    $.getJSON('/home/GetDeviceType', function (result) {
        deviceType = result.deviceType;       
    });
 
    $.ajaxSetup({
        async: true
    });
 
    if (deviceType == 'tablet') { $("#tabs").tabs(); }
 
    if (deviceType == 'mobile') {
        $(document).on("mobileinit", function () {
            $.mobile.loader.prototype.options.text = "loading";
            $.mobile.loader.prototype.options.textVisible = false;
            $.mobile.loader.prototype.options.theme = "a";
            $.mobile.loader.prototype.options.html = "";
        });
    }
 
    $('#btnUpdate').prop('disabled', true);
    $('#btnClear').prop('disabled', true);
 
    // load data into controls
    if (deviceType == 'desktop') {
      
        LoadBarChartChart();
        LoadGridData();
    }
 
    // all devices load pie chat & data
    LoadPieChartData();   
 
    function LoadBarChartChart()
    {
        if (deviceType == 'desktop') {
            $('#chartCtrl').block({
                message: '<h1>Loading...</h1>',
                css: {
                    border: 'none',
                    padding: '15px',
                    backgroundColor: '#000',
                    '-webkit-border-radius': '10px',
                    '-moz-border-radius': '10px',
                    opacity: .5,
                    color: '#fff'
                }
            });
        }
 
        if (deviceType == 'mobile') {
            $.mobile.loading("show", { text: "Loading......", textVisible: true });
 
            $('#one').hide();
            $('#three').hide();
            $('#four').hide();
            $('#two').show();
        }
 
        $.getJSON("/Home/GetCourseResultsData", null, function (data) {
            var s1 = data.MathsScores;
            var s2 = data.ICTScores;
            var s3 = data.PhysicsScores;
            var ticks = data.Months;
 
            var plot1 = $.jqplot('chartCtrl', [s1, s2, s3], {
                seriesDefaults: {
                    renderer: $.jqplot.BarRenderer,
                    rendererOptions: { fillToZero: true }
                },
                series: [
                    { label: 'Maths' },
                    { label: 'ICT' },
                    { label: 'Physics' }
                ],
                legend: {
                    show: true,
                    placement: 'outsideGrid'
                },
                axes: {
                    xaxis: {
                        renderer: $.jqplot.CategoryAxisRenderer,
                        ticks: ticks
                    },
                    yaxis: {
                        pad: 1.05,
                        tickOptions: { formatString: '#%d' }
                    }
                }
            });
 
            if (deviceType == 'mobile') {
                $.mobile.loading("hide");
            }
            else {
                $('#chartCtrl').unblock();
            }
 
           
 
        });
    }
 
    function LoadPieChartData() {
        if (deviceType == 'mobile') {
            $.mobile.loading("show", { text: "Loading......", textVisible: true });
        }
 
        // percentage of degree cources   
        
        if (deviceType == 'desktop') {
            if (deviceType == 'desktop') {
                $('#pieCtrl').block({
                    message: '<h1>Loading...</h1>',
                    css: {
                        border: 'none',
                        padding: '15px',
                        backgroundColor: '#000',
                        '-webkit-border-radius': '10px',
                        '-moz-border-radius': '10px',
                        opacity: .5,
                        color: '#fff'
                    }
                });
            }
        }
 
        $.getJSON("/Home/GetPieGraphData", null, function (data) {
            var pieChartData = [];
            for (var prop_name in data) {
                pieChartData.push([data[prop_name].Name, data[prop_name].Value])
            }
 
            var plot1 = $.jqplot('pieCtrl', [pieChartData], {
                gridPadding: { top: 0, bottom: 38, left: 0, right: 0 },
                seriesDefaults: {
                    renderer: $.jqplot.PieRenderer,
                    trendline: { show: false },
                    rendererOptions: { padding: 8, showDataLabels: true }
                },
                legend: {
                    show: true,
                    placement: 'inside',                   
                    rendererOptions: {
                        numberRows: 1
                    },
                    location: 's',
                    marginTop: '15px'
                }
            });
 
            if (deviceType == 'mobile') {
                $.mobile.loading("hide");
            }
            else {
                $('#pieCtrl').unblock();
            }
 
        });
    }
 
    function LoadGridData() {        
        if (deviceType == 'mobile') {
            $.mobile.loading("show", { text: "Loading......", textVisible: true });
 
            $('#one').hide();
            $('#two').hide();
            $('#four').hide();
            $('#three').show();
        }
 
        if (deviceType == 'desktop') {
            // make ajax call to controller
            if (deviceType == 'desktop') {
                $('#student-grid-array').block({
                    message: '<h1>Loading...</h1>',
                    css: {
                        border: 'none',
                        padding: '15px',
                        backgroundColor: '#000',
                        '-webkit-border-radius': '10px',
                        '-moz-border-radius': '10px',
                        opacity: .5,
                        color: '#fff'
                    }
                });
            }
        }
 
        $.getJSON("/Home/GetStudentsOnCourseData", null, function (data) {           
            if (deviceType == 'mobile') {
                $('#myTable tbody').html('<tr> <td></td><td>');
                var content = '';
                $.each(data, function () {
                    content += "<tr> <td>" + this['StudentID'] + "</td><td>" + this['Name'] + "</td><td>" + this['Course'] + "</td><td>" + this['Year'] + "</td> <td>Active</td></tr>";
                });
                $('#myTable tbody').html(content);
 
                $('#myTable').footable();
                $('#myTable').footable().bind({
                    'footable_row_expanded': function (e) {
                        tableRowSelected = e.row;
 
                        var elTableCells = e.row.getElementsByTagName("td"); // get the row cells

                        $('#txtID').val(elTableCells[0].innerText);
                        $('#txtName').val(elTableCells[1].innerText);
                        $('#txtCourse').val(elTableCells[2].innerText);
                        $('#txtYear').val(elTableCells[3].innerText);
                        $('#btnUpdate').prop('disabled', false);
                    }
                });
            }
            else {
                var pieChartData = [];
                for (var prop_name in data) {
                    pieChartData.push([data[prop_name].Name, data[prop_name].Value])
                }
 
                // studenst on courses
                $("#student-grid-array").jqGrid({
                    datatype: "local",
                    autowidth: true,
                    // height: $("#gridContainor").height()-55, // allow for scrollbar buttons etc.
                    colNames: ["Student ID", "Name", "Course", "Year"],
                    colModel: [
                                { name: "StudentID", index: "StudentID" },
                                { name: "Name", index: "Name" },
                                { name: "Course", index: "Course" },
                                { name: "Year", index: "Year", sorttype: "int" }
                    ],
                    multiselect: false,
                    shrinkToFit: true,
                    caption: "Student Degree List by Course",
                    onSelectRow: function (id) {
                        rowID = id;
                        var name = $('#student-grid-array').jqGrid('getCell', id, 'Name');
                        var course = $('#student-grid-array').jqGrid('getCell', id, 'Course');
                        var year = $('#student-grid-array').jqGrid('getCell', id, 'Year');
                        var studentID = $('#student-grid-array').jqGrid('getCell', id, 'StudentID');
 
                        $('#txtID').val(studentID);
                        $('#txtName').val(name);
                        $('#txtCourse').val(course);
                        $('#txtYear').val(year);
 
                        $('#btnUpdate').prop('disabled', false);
                        $('#btnClear').prop('disabled', false);
 
                    }
                });
 
                for (var x = 0; x <= data.length; x++) {
                    $("#student-grid-array").addRowData(x, data[x]);
                }
            }
 
            if (deviceType == 'mobile') {
                $.mobile.loading("hide");              
            }
            else {
                $('#student-grid-array').unblock();
            }
        });
    } 
 
    $('#btnUpdate').click(function () {
 
        if (deviceType == 'mobile') {
            $.mobile.loading("show", { text: "Loading......", textVisible: true });
 
            var elTableCells = tableRowSelected.getElementsByTagName("td"); // get the row cells
            elTableCells[0].innerText = $('#txtID').val(); // update ID
            elTableCells[1].innerText = $('#txtName').val(); // update name
            elTableCells[2].innerText = $('#txtCourse').val(); // update course
            elTableCells[3].innerText = $('#txtYear').val(); // update year

            $('#txtID').val('');
            $('#txtName').val('');
            $('#txtCourse').val('');
            $('#txtYear').val('');
            $('#btnUpdate').prop('disabled', true);
            $('#btnInsert').prop('disabled', true);
 
            $('#one').hide();
            $('#two').hide();
            $('#three').show();
            $('#four').hide();
 
            // make tab (appear) active
            $('#lnkStudentsOnCourse').addClass('ui-btn-active');
            $('#lnkUpdateStudent').removeClass('ui-btn-active');
 
            // refresh table
            $('#myTable').trigger('footable_redraw');
            $(".footable").trigger("footable_resize");
 
            $.mobile.loading("hide");
        }
        else {
 
 
            // update grid data (no validation)!!!
            var rowData = $('#student-grid-array').jqGrid('getRowData', rowID);
            rowData.Name = $('#txtName').val();
            rowData.Course = $('#txtCourse').val();
            rowData.Year = $('#txtYear').val();
            $('#student-grid-array').jqGrid('setRowData', rowID, rowData);
 
            $('#txtID').val('');
            $('#txtName').val('');
            $('#txtCourse').val('');
            $('#txtYear').val('');
            $('#btnUpdate').prop('disabled', true);
            $('#btnInsert').prop('disabled', true);
 
            if (deviceType == 'tablet') { $("#tabs").tabs("option", "active", 2); }
        }
 
    });
 
    $('#btnClear').click(function () {
        $('#txtID').val('');
        $('#txtName').val('');
        $('#txtCourse').val('');
        $('#txtYear').val('');
        $('#btnUpdate').prop('disabled', true);
        $('#btnClear').prop('disabled', true);
    });
 
    /*
    Mobile & Tablet tab methods
    */
 
    $("#lnkAverageMarks").click(function () {
 
        if (deviceType == 'mobile') {
            $.mobile.loading("show", { text: "Loading......", textVisible: true });
 
            $('#one').hide();
            $('#three').hide();
            $('#four').hide();
            $('#two').show();
        }
 
        LoadBarChartChart();
    });
 
    $("#lnkStudentsOnCourse").click(function () {
 
        if (deviceType == 'mobile') {
            $.mobile.loading("show", { text: "Loading......", textVisible: true });
 
            $('#one').hide();
            $('#two').hide();
            $('#four').hide();
            $('#three').show();            
        }
 
        if (isGridLoaded == false) {
            isGridLoaded = true;
            LoadGridData();
        }
 
        $.mobile.loading("hide");
    });
 
    $("#lnkUpdateStudent").click(function () {
        // fields will have been populated
        $('#one').hide();
        $('#two').hide();
        $('#three').hide();
        $('#four').show();
    });
 
    $("#lnkCourseAttend").click(function () {
        // data already loaded
        $('#four').hide();
        $('#two').hide();
        $('#three').hide();
        $('#one').show();
        $('#pieCtrl').unblock();
    });
});

View CSS

Because each view is different, there will be a different CSS applied to the respective views. To create a manageable structure for the style files, a folder structure representing the views in the Contents folder. I have just created a custom folder, which isn’t syntaxly correct, as you would be best to create a folder for each view (say Home for example), and then three subfolders for each device. So, because my application is only one pager – I just created a custom folder – but in reality a web application will have multiple pages, thus creating a folder for each view with three subfolders (for the three devices).

Running the Application

If you run the application from Visual Studio, you should be presented with the desktop version:

Desktop

Tablet

Notice that I have used a tab approach to displaying the various controls. Loading on demand the data when the respective tab header is clicked.

To test the application as a tablet, start Opera Emulator, and select one of the tablet devices, then enter the URL that was in the desktop browser – hit return and you should start to see how the application looks on a tablet device. I have used the Samsung Galaxy Tab device.

Mobile

Again the view has changed compared to the other devices. Some client-side logic also has to change, thus why I have to determine what device I am using in the JavaScript file. Again only loading data when I click on a tab button. I am using JQuery Mobile library and JQM CSS to get the look and feel. I have used the Opera Emulator again to view the page as a mobile device – selecting the LG Intuition device.

Conclusion

You can see that it possible to design for multiple devices using the MVC design pattern. I do agree that HTML5 is behind native device code (Android or IOS) performance wise and interacting with the device's features. But, if you are after a data\web oriented application, then this approach is very achievable!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here