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.
Testing Tools
- Opera Mobile Emulator
- Safari (able
to change User Agent when posting)
- 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:
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"
));
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"
));
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"
));
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:
- 51Degress (http://51degrees.codeplex.com/) or (http://51degrees.com/)
- 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")
});
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
public string GetDeviceType(string ua)
{
string ret = "";
if (Regex.IsMatch(ua, @"GoogleTV|SmartTV|Internet.TV|NetCast|NETTV|AppleTV|boxee|Kylo|Roku|DLNADOC|CE\-HTML", RegexOptions.IgnoreCase))
{
ret = "tv";
}
else if (Regex.IsMatch(ua, "Xbox|PLAYSTATION.3|Wii", RegexOptions.IgnoreCase))
{
ret = "tv";
}
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";
}
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";
}
else if ((Regex.IsMatch(ua, "Kindle", RegexOptions.IgnoreCase)) || (Regex.IsMatch(ua, "Mac.OS", RegexOptions.IgnoreCase)) && (Regex.IsMatch(ua, "Silk", RegexOptions.IgnoreCase)))
{
ret = "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";
}
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";
}
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";
}
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";
}
else if ((Regex.IsMatch(ua, "Macintosh|PowerPC", RegexOptions.IgnoreCase)) && (!Regex.IsMatch(ua, "Silk", RegexOptions.IgnoreCase)))
{
ret = "desktop";
}
else if ((Regex.IsMatch(ua, "Linux", RegexOptions.IgnoreCase)) && (Regex.IsMatch(ua, "X11", RegexOptions.IgnoreCase)))
{
ret = "desktop";
}
else if ((Regex.IsMatch(ua, "Solaris|SunOS|BSD", RegexOptions.IgnoreCase)))
{
ret = "desktop";
}
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";
}
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()
{
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()
{
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()
{
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);
if (deviceType == 'desktop') {
LoadBarChartChart();
LoadGridData();
}
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 });
}
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') {
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");
$('#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])
}
$("#student-grid-array").jqGrid({
datatype: "local",
autowidth: true,
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"); elTableCells[0].innerText = $('#txtID').val(); elTableCells[1].innerText = $('#txtName').val(); elTableCells[2].innerText = $('#txtCourse').val(); elTableCells[3].innerText = $('#txtYear').val();
$('#txtID').val('');
$('#txtName').val('');
$('#txtCourse').val('');
$('#txtYear').val('');
$('#btnUpdate').prop('disabled', true);
$('#btnInsert').prop('disabled', true);
$('#one').hide();
$('#two').hide();
$('#three').show();
$('#four').hide();
$('#lnkStudentsOnCourse').addClass('ui-btn-active');
$('#lnkUpdateStudent').removeClass('ui-btn-active');
$('#myTable').trigger('footable_redraw');
$(".footable").trigger("footable_resize");
$.mobile.loading("hide");
}
else {
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);
});
$("#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 () {
$('#one').hide();
$('#two').hide();
$('#three').hide();
$('#four').show();
});
$("#lnkCourseAttend").click(function () {
$('#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!