Monday, December 26, 2011

Mobile & Global with HTML5, MVC & Windows Azure, Step 3: Dynamic Content

In this series of posts we’re progressively demonstrating a mobile and global sample, Responsive Tours. The source code for all 7 steps is on CodePlex at http://responsivetours.codeplex.com.

Here in Step 3 of 7 we’re going to implement some dynamic content for the site—specifically, dynamic promotional item content and map integration. In this step we will add:

• Server-side dynamic content for promotional items driven by a database
• Client-side dynamic content for Bing Maps integration using device current location


Server-side Dynamic Content for Promotional Items

The bottom area of our web pages is for promotional items. There is room for three items each with an image, title, and description. We would like all of this to be dynamic so that new offers can be easily advertised.

There are several ways we might store content (database, primitive storage, content management system). In this case we’ll use a SQL Server database for the content and create a Promotions table that holds the image URL, title, and description for each promotional item. We add a database project to the solution for this purpose. We define three records of promotional content.

Now that we have this data, we need to implement retrieving it in our web project on the server side. The right place to do that is in our MVC controllers, by querying our database table and storing a collection of Promo items in the ViewBag for our views.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Data.SqlClient;

namespace html5_mvc_razor.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            LoadPromos();
            return View();
        }

        private void LoadPromos()
        {
            Dictionary<string, Promo> Promos = new Dictionary<string, Promo>();

            try
            {
                using (SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["Tours"].ConnectionString))
                {
                    conn.Open();

                    using (SqlCommand cmd = new SqlCommand("SELECT * FROM Promotions ORDER BY Id", conn))
                    {
                        using (SqlDataReader reader = cmd.ExecuteReader())
                        {
                            while (reader.Read())
                            {
                                Promos.Add(Convert.ToString(reader["Id"]), new Promo
                                    {
                                        Title = Convert.ToString(reader["Title"]),
                                        Text = Convert.ToString(reader["Text"]),
                                        ImageURL = Convert.ToString(reader["ImageURL"])
                                    }
                                );
                            }
                        }
                    }
                    conn.Close();
                }
            }
            catch (SqlException)
            {
                // TODO: log exception
            }

            ViewBag.Promos = Promos;
        }

    }

    public class Promo
    {
        public string Title;
        public string Text;
        public string ImageURL;
    }
}

One way to set dynamic content in MVC is with Razor. We can set the image URLs based on what’s in the ViewBag using the @ syntax:
<!-- begin - homepage promos -->
<div class="home_promo_container">
 <div class="home_promo">
  <div class="home_promo_content" style="background-image:url(images/@(ViewBag.Promos["1"].ImageURL));">
   <h2>...</h2>
   <p>...</p>
   <a class="button" href="#">Learn more &raquo;</a>
  </div>
 </div>
 <div class="home_promo">
  <div class="home_promo_content" style="background-image:url(images/@(ViewBag.Promos["2"].ImageURL));">
   <h2>...</h2>
   <p>...</p>
   <a class="button" href="#">Learn more &raquo;</a>
  </div>
 </div>
 <div class="home_promo">
  <div class="home_promo_content" style="background-image:url(images/@(ViewBag.Promos["3"].ImageURL));">
   <h2>...</h2>
   <p>...</p>
   <a class="button" href="#">Learn more &raquo;</a>
  </div>
 </div>
 <div class="clear_both"></div>
</div>


Of course the images aren’t really dynamic yet—they’re still static files that are part of our web project—but once we change that in a later step we have a way to indicate the URL dynamically from our database.

Now let’s look at a more modern and versatile way to pass dynamic content from an MVC web back end to an open standards web client: data binding with Knockout.js. The Knockout library lets us apply the Model-View-ViewModel pattern to JavaScript. To do this, we need to:

• Use Razor to generate JavaScript code to create a ViewModel (JavaScript object) with content.

• Annotate HTML DOM elements with data-bind attributes to denote where data binding should occur.
• Initialize and call the Knockout library to apply data binding to the HTML.
You can see all three of these activities in the Home view page (Views/Home/Index.cshtml):


@{
    ViewBag.Title = "Responsive Tours";
}

@* Optional : Include additional stylesheets *@
@section StylesTop
{
<link rel="stylesheet" type="text/css" href="~/../css/stylesheet.css" />
<link rel="stylesheet" type="text/css" media="only screen and (min-width:50px) and (max-width:550px)"  href="~/../css/screen_small.css">
<link rel="stylesheet" type="text/css" media="only screen and (min-width:551px) and (max-width:800px)" href="~/../css/screen_medium.css">
<!--[if lt IE 9]>
 <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
 <link rel="stylesheet" type="text/css" href="~/../css/stylesheet_ie.css" />
<![endif]-->
}

@* Optional : Include additional immediately executed script references *@
@section ScriptsTop
{

}

@* Optional : Header content *@
@section Header
{
<meta name="description" content="This site was created from a template originally designed and developed by Codify Design Studio. Find more free templates at http://www.adobe.com/devnet/author_bios/chris_converse.html" />
<meta http-equiv="X-UA-Compatible" content="IE=9" />
}

@* Required : Render body container here *@
  <div class="page">
   <header><a class="logo" href="#"></a></header>
   <div class="page_content">

     
     <!-- begin - homepage promo -->
     <div class="marquee_container">
      <div class="marquee_photos"><br/><br/>loading...</div>
      <div class="marquee_caption">
       <div class="marquee_caption_content"></div>
       
      </div>
      <div class="marquee_nav"></div>
      <div class="marquee_panel_data"></div>
     </div>
     <div class="marquee_smallscreen"><br/><br/>loading...</div>
     <!-- end - homepage promo -->
     
     <!-- begin - homepage promos -->
     <div class="home_promo_container">
      <div class="home_promo">
       <div class="home_promo_content" style="background-image:url(images/@(ViewBag.Promos["1"].ImageURL));">
        <h2 data-bind="text: PromoTitle1"></h2>
        <p  data-bind="text: PromoText1"/>
        <a class="button" href="#">Learn more &raquo;</a>
       </div>
      </div>
      <div class="home_promo">
       <div class="home_promo_content" style="background-image:url(images/@(ViewBag.Promos["2"].ImageURL));">
        <h2 data-bind="text: PromoTitle2"></h2>
        <p  data-bind="text: PromoText2"/>
        <a class="button" href="#">Learn more &raquo;</a>
       </div>
      </div>
      <div class="home_promo">
       <div class="home_promo_content" style="background-image:url(images/@(ViewBag.Promos["3"].ImageURL));">
        <h2 data-bind="text: PromoTitle3"></h2>
        <p  data-bind="text: PromoText3"/>
        <a class="button" href="#">Learn more &raquo;</a>
       </div>
      </div>
      <div class="clear_both"></div>
      <!-- begin - homepage promos -->


    </div>
    <nav>
     <a href="">Home</a>
     <a href="Map">Walking Map</a>
     <a href="#">About Us</a>
    </nav>
   </div>
   <footer>&copy; 2011 &bull; Responsive Tours (a fictitious company)</footer>
  </div>

@* Optional : Include fooder content *@
@section Footer
{
}

@* Optional : Include additional deferred script references, or page initialisation *@
@section ScriptsBottom
{
<script type="text/javascript" src="~/../scripts/farinspace/jquery.imgpreload.min.js"></script>
<script type="text/javascript" src="~/../scripts/farinspace/template.js"></script>
<script type="text/javascript">
    var timeToChange = 6;     //seconds
    var transitionTime = 1.5; //seconds
</script>
<script>
$(document).ready(function () {

    // Knockout view model.
        
    var viewModel = {
        PromoTitle1: "@(ViewBag.Promos["1"].Title)",
        PromoTitle2: "@(ViewBag.Promos["2"].Title)",
        PromoTitle3: "@(ViewBag.Promos["3"].Title)",
        PromoText1: "@(ViewBag.Promos["1"].Text)",
        PromoText2: "@(ViewBag.Promos["2"].Text)",
        PromoText3: "@(ViewBag.Promos["3"].Text)"
    };

    // Activates knockout.js
    ko.applyBindings(viewModel);
});
</script>
<!-- Knockout data binding -->
<script type="text/javascript" src="~/../scripts/libs/knockout-1.2.1.min.js"></script>
}

With all of this in place, when we run the project we now see the promotional item content is loaded from the database on the server side and bound to the web page on the client side. Voila! -- dynamic content.
Client-side Map Integration
The other area of dynamic content is the map view. When a user clicks the Walking Map link, they are supposed to get an interactive map that is initially centered on their current location. This is an integration we can handle on the client side. We’ll do it using Bing Maps and HTML5’s geolocation feature.

With a Bing Maps key in hand, a small amount of JavaScript is all that is needed to render an interactive map, obtain the current location (if the user grants it), and center a map on that point. The code selects one of two map elements (“map1” or “map2”) based on which is visible for thr active device layout.
<!-- Bing Maps -->
<script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
<script type="text/javascript">
    var map = null;

    function getMap() {

        var mapOptions = {
            credentials: 'PucpG1...BING-MAPS-KEY...ByFfkTmeGP',
            mapTypeId: Microsoft.Maps.MapTypeId.automatic,
            zoom: 17
        }

        var mapElement = document.getElementById("map1");
        if (!isVisible(mapElement)) {
            mapElement = document.getElementById("map2");
        }
        map = new Microsoft.Maps.Map(mapElement, mapOptions);
    }

    function setMapZoom(zoomLevel) {
        map.setView({ zoom: zoomLevel });
    }

    function getCurrentLocation() {
        var geoLocationProvider = new Microsoft.Maps.GeoLocationProvider(map);
        geoLocationProvider.getCurrentPosition({ errorCallback: errorCallback, successCallback: displayCenter });
    }

    function displayCenter(args) {
        setMapZoom(17);
    }

    function errorCallback(object) {
        alert('Error callback invoked, error code ' + object.errorCode);
    }

    function isVisible(obj) {
        if (obj == document) return true

        if (!obj) return false
        if (!obj.parentNode) return false
        if (obj.style) {
            if (obj.style.display == 'none') return false
            if (obj.style.visibility == 'hidden') return false
        }

        //Try the computed style in a standard way
        if (window.getComputedStyle) {
            var style = window.getComputedStyle(obj, "")
            if (style.display == 'none') return false
            if (style.visibility == 'hidden') return false
        }

        //Or get the computed style using IE's silly proprietary way
        var style = obj.currentStyle
        if (style) {
            if (style['display'] == 'none') return false
            if (style['visibility'] == 'hidden') return false
        }

        return isVisible(obj.parentNode)
    }
</script>
When we run the project, select the map view, and give permission to use our location we are greeted with an interactive map initially showing our current location. We can use this in a walking tour of a city to see where we are, find points of interest, bookmark favorite spots, and find others in our party.



The final result of all this integration is a web site that has both client-side and server-side dynamic content: an interactive map for walking tours, and promotional items that can be easily changed via a database.
Summary
In Step 3 we enabled server-side dynamic content for promotional items using a database, MVC Razor, and Knockout. We also enabled client-side dynamic content via Bing Maps. Our site now has the following functionality:
• Uses HTML5 and open standards on the web client
• Embodies responsive web design and runs on desktops, tablets, and phones.
• Provides server-side dynamic content (promotional items)
• Provides client-side dynamic content (Bing Maps)

In the next step, we'll prepare the application for cloud computing on Windows Azure.

No comments: