Sunday, October 18, 2009

Codename ‘DiscMaster’ – Implementing MVC’s AJAX functionality

Seeing that i didn’t really finish the site 100% i figured i might as well go ahead and implement some additional features.

One of those features on top of my agenda was to implement some of the built-in Ajax features available in ASP.NET MVC.

So, now sporting a fresh new approach, the weather forecast from yr.no (read this post for history) is now AJAX enabled and more ‘MVC’-ish:

mvcajax-final This tutorial covers 8 steps:

1: creating a forecast model 2: moving the forecast fetch routine to the repository 3: extending the courses model 4: adding a Forecast action to the CoursesController 5: adding a Forecast partial view 6: enabling Ajax 7: updating the course details view to use AJAX calls 8: adding some fancy jQueryUI functionality

step 1: creating a forecast model

the model for our Forecast object is simple and easy, only thing to note are the three properties in the end that we’ll later use for navigational purposes.

public class Forecast
{
    public string DateFrom { get; set; }
    public string DateTo { get; set; }
    public string SymbolNumber { get; set; }
    public string Conditions { get; set; }
    public string Temperature { get; set; }
    public string Precipitation { get; set; }
    public string Wind { get; set; }
    public string WindSpeed { get; set; }
    public string WindDirection { get; set; }
    public int ForecastsCount { get; set; }
    public int ForecastIndex { get; set; }
    public int NextForecastIndex { get; set; }
}

step 2: moving the forecast fetch routine to the repository

If you’ve followed the previous posts you’ll have seen that earlier we wrote a fetch routine in the partial view that did pretty much everything.. now we’re going to update it, give it some additional features and make it more available throughout the site. The following methods go in the repository layer:

public Forecast GetForecast(Guid courseid, int forecastindex)
{
    return GetForecast(courseid.ToString(), forecastindex);
}
public Forecast GetForecast(string courseid, int forecastindex)
{
    List<Forecast> forecasts = GetForecasts(courseid);

    if (forecasts!=null)
    {
        return forecasts[forecastindex];
    }

    return null;
}
public List<Forecast> GetForecasts(string courseid)
{
    var course = GetCourse(courseid);

    if (!String.IsNullOrEmpty(course.CourseDetail.yr_weather))
    {
        List<Forecast> forecasts = null;

        // determine if the forecast is already stored in the cache..
        if (System.Web.HttpContext.Current.Cache[System.Web.HttpContext.Current.Server.UrlPathEncode(course.CourseDetail.yr_weather)] != null)
        {
            // .. if so, fetch it
            forecasts = (List<Forecast>)System.Web.HttpContext.Current.Cache[System.Web.HttpContext.Current.Server.UrlPathEncode(course.CourseDetail.yr_weather)];
        }
        else
        {
            // .. and if not, try to consume the source using a reader..
            try
            {

                // used to read the forecast xml
                System.Xml.XmlDocument doc = new XmlDocument();
                // our soon-to-be forecast node collection
                System.Xml.XmlNodeList nodelist = null;

                System.Xml.XmlTextReader reader = new XmlTextReader(System.Web.HttpContext.Current.Server.UrlPathEncode(course.CourseDetail.yr_weather));
                doc.Load(reader);

                // if all is loaded ok and data is there, simple xpath query to get the forecast data..
                if (doc != null)
                {
                    if (doc.HasChildNodes)
                    {
                        nodelist = doc.SelectNodes("/weatherdata/forecast/tabular/time");
                    }
                }

                if (nodelist != null)
                {
                    if (nodelist.Count > 0)
                    {
                        // iterate each forecast in the current group (day)

                        var forecastscol = from node in nodelist.OfType<XmlNode>()
                                    select new Forecast
                                    {
                                        DateFrom = DateTime.Parse(node.Attributes["from"].Value).ToString("yyyy-MM-dd HH:MM"),
                                        DateTo = DateTime.Parse(node.Attributes["to"].Value).ToString("yyyy-MM-dd HH:MM"),
                                        Conditions = node["symbol"].Attributes["name"].Value,
                                        SymbolNumber = node["symbol"].Attributes["number"].Value.PadLeft(2, '0'),
                                        Precipitation = node["precipitation"].Attributes["value"].Value,
                                        Temperature = node["temperature"].Attributes["value"].Value,
                                        Wind = node["windSpeed"].Attributes["name"].Value,
                                        WindDirection = node["windDirection"].Attributes["code"].Value,
                                        WindSpeed = node["windSpeed"].Attributes["mps"].Value
                                    };
                        forecasts = forecastscol.ToList();

                        foreach (Forecast forecast in forecasts)
                        {
                            forecast.ForecastsCount = forecasts.Count();
                            forecast.ForecastIndex = forecasts.IndexOf(forecast);
                            if (forecast.ForecastIndex == forecast.ForecastsCount-1)
                            {
                                forecast.NextForecastIndex = 0;
                            }
                            else
                            {
                                forecast.NextForecastIndex = forecast.ForecastIndex + 1;
                            }
                        }

                        // add entry to cache
                        System.Web.HttpContext.Current.Cache.Add(System.Web.HttpContext.Current.Server.UrlPathEncode(course.CourseDetail.yr_weather), forecasts, null, DateTime.Now.AddMinutes(60), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Default, null);
                    }
                }
            }
            catch (Exception ex)
            {
                // just in case something went wrong we allow this to be blank for now..
            }
        }
        if (forecasts != null)
        {
            return forecasts;
        }
    }
    return null;
}

step 3: extending the Course model

Now we’ll add a simple method to our Course model that takes care of fetching the Forecasts no matter where we are calling it from:

public List<Forecast> Forecasts
{
    get
    {
        return new DiscMasterRepository().GetForecasts(this.courseid.ToString());
    }
}

step 4: adding a Forecast action to the CoursesController

Now it’s time to add the action method that we’ll use in our AJAX-enabled forecast routine:

//
// GET: /Courses/Forecast/5

public ActionResult Forecast(string courseid, int forecastindex)
{
    var forecast = repository.GetForecast(courseid, forecastindex);
    if (forecast!=null)
    {
        return PartialView("Forecast", forecast);
    }
    else
    {
        return PartialView("ForecastNotFound");
    }
}

step 5: adding a Forecast partial view

Now we’ll add a new Forecast.ascx partial view. Compared to the previous version (weather.ascx) this one is more clean and strongly typed to our Forecast model, thus enabling a much more controlled design:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<DiscMaster.Web.Models.Forecast>" %>
<div id="forecast">
     <p>
        <strong><%= DateTime.Parse(Model.DateFrom).ToString("dddd, dd MMM hh:mm") %> - <%= DateTime.Parse(Model.DateTo).ToString("dddd, dd MMM hh:mm")%></strong>
        <br />
        <img alt='<%= Model.Conditions %>' style="float: left;" src='<%= String.Format("/Image.ashx?src=/Content/Media/Icons/Weather/{0}.png&width=64", Model.SymbolNumber) %>' />
        conditions: <%= Model.Conditions%>
        <br />
        temperature: <%= Model.Temperature%><sup>o</sup>C
        <br />
        precipitation: <%= Model.Precipitation%>
        <br />
        wind: <%= Model.Wind%> - <%= Model.WindSpeed%> mps <%= Model.WindDirection%>
       <%= Html.Hidden("newforecastindex", Model.NextForecastIndex) %>
    </p>
    <p>
    <em>viewing forecast <%= Model.ForecastIndex+1 %> of <%= Model.ForecastsCount %></em>
    </p>
</div>
<div class="forecastfooter">
    <small>
        Weather forecast from <a href="http://www.yr.no" target="_blank">yr.no</a> delivered by<br />the Norwegian Meteorological Institute and the NRK
    </small>
</div>

step 6: enabling AJAX

Enabling AJAX in the project is really easy – all we have to do is to add the following two lines in the site.Master file:

<script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>

step 7: updating the course details view to use AJAX calls

Making an AJAX call from our ASP.NET MVC view (the built-in way) we’ll be using Ajax.BeginForm:

<% using (Ajax.BeginForm("Forecast", new AjaxOptions() {
                   UpdateTargetId = "forecastContainer",
                   LoadingElementId = "forecastLoading",
                   OnSuccess="forecastLoaded",
                   InsertionMode = InsertionMode.Replace
               })){ %>
Below is the entire code part that handles the forecast rendering. The code for our updated Details view now holds the AJAX wrapped request, and a function we’re calling in the end to update forecast index:
<script type="text/javascript">
    function forecastLoaded() {
        $("#forecastindex").get(0).value = $("#newforecastindex").get(0).value;
    }
</script>
    <h2><%= Model.CourseDetail.city %>, <%= Model.CourseDetail.country %></h2>
    <% if (!String.IsNullOrEmpty(Model.CourseDetail.yr_weather)) {
           %>
        <% using (Ajax.BeginForm("Forecast", new AjaxOptions() {
               UpdateTargetId = "forecastContainer",
               LoadingElementId = "forecastLoading",
               OnSuccess="forecastLoaded",
               InsertionMode = InsertionMode.Replace
           })){ %>
           <%= Html.Hidden("courseid", Model.courseid) %>
           <%= Html.Hidden("forecastindex", 1) %>
                <h3>Weather forecast</h3><input type="image" title="next forecast" src="/Image.ashx?src=/Content/Media/Icons/24x24/next.png&width=32" />
        <% } %>
        <div id="forecastContainer">
            <% Html.RenderPartial("~/Areas/Courses/Views/Courses/Forecast.ascx", Model.Forecasts[0]); %>
        </div>
        <div id="forecastLoading" style="display: none;"><img src="/Content/Media/Icons/load.gif" alt="loading.." /></div>
       <%} %>

step 8: adding some fancy jQueryUI functionality

At jQueryUI you can generate a custom file with the contents you wish to implement, i went with some of their effects since i think it’s a great library and the implementation is superb.

In the site.Master we’ll now need to add another line to the script includes:

<script src="/Scripts/jquery-ui-1.7.2.custom.min.js" type="text/javascript"></script>
Now we’ll update the Details view with some animations – i chose the .hide and .show effects in a horizontal mode. The code below is the complete code for the forecast part of our course details view:
<script type="text/javascript">
    function forecastOnLoad() {
        $("div#forecastContainer").hide("slide", { direction: "horizontal" }, 1000);
    }
    function forecastLoaded() {
        $("#forecastindex").get(0).value = $("#newforecastindex").get(0).value;
        $("div#forecastContainer").show("slide", { direction: "horizontal" }, 1000);
    }
</script>
    <h2><%= Model.CourseDetail.city %>, <%= Model.CourseDetail.country %></h2>
    <% if (!String.IsNullOrEmpty(Model.CourseDetail.yr_weather)) {
           %>
        <% using (Ajax.BeginForm("Forecast", new AjaxOptions() {
               UpdateTargetId = "forecastContainer",
               LoadingElementId = "forecastLoading",
               OnBegin="forecastOnLoad",
               OnSuccess="forecastLoaded",
               InsertionMode = InsertionMode.Replace
           })){ %>
           <%= Html.Hidden("courseid", Model.courseid) %>
           <%= Html.Hidden("forecastindex", 1) %>
                <h3>Weather forecast</h3><input type="image" title="next forecast" src="/Image.ashx?src=/Content/Media/Icons/24x24/next.png&width=32" />
        <% } %>
        <div id="forecastContainer">
            <% Html.RenderPartial("~/Areas/Courses/Views/Courses/Forecast.ascx", Model.Forecasts[0]); %>
        </div>
        <div id="forecastLoading" style="display: none;"><img src="/Content/Media/Icons/load.gif" alt="loading.." /></div>
       <%} %>

and now the finished update will be Ajax enabled, fetching one forecast at a time using the built-in AJAX functionality available in ASP.NET MVC with some pretty jQueryUI effects:

mvcajax-final2 As always, the source code for the entire project can be downloaded at http://discmaster.codeplex.com

Take care,

P.

No comments: