Tuesday, October 27, 2009

Codename 'DiscMaster' - Implementing a custom DataTypeAttribute

In MVC the default validation is provided by DataAnnotations (this is, of course, replaceable) and is fantastic if you ask me. Being able to define the validation principles at the scope of the Model instead of UI makes a much more friendly architecture and enables the controller to present it's data to any view imaginable while still holding together beneath it all.

The DataTypeAttributes available are great, but sometimes you might want to create your own, and so i hope you'll be pleased to learn it's actually really simple to do so:

The example code below is for a custom MaxLength attribute for a text property:

Usage:

  [MaxLength(Length=2, ErrorMessage="Text is to long")]
  public string sample { get; set; }

Code:

namespace DiscMaster.Web.Models.DataAnnotations
{
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
    public class MaxLengthAttribute : DataTypeAttribute
    {
        public int Length { get; set; }
        public MaxLengthAttribute() : base(DataType.Text)
        {
        }

        public override bool IsValid(object value)
       {
            string str = Convert.ToString(value, CultureInfo.CurrentCulture);
            if (string.IsNullOrEmpty(str))
                return true;

            if (str.Length > Length)
            {
                return false;
            }
            return true;
        }
    }
}

As always, Codename 'DiscMaster' can be found at: http://discmaster.codeplex.com/

Best regards,

P.

Codename 'DiscMaster' - Implementing custom HtmlHelpers

ASP.NET MVC has great support for writing your own HtmlHelpers, here's an example I wrote that takes a model of a player and outputs the profile picture (if any).

So now outputting the profile picture is trivial and from one source:
<%= Html.ProfileImage(Model, "", null, "70", new {style="float: left; padding-right: 10px; padding-bottom: 10px;"})  %>

The code below is the entire code that generates my helper. it's actually a extension method on the HtmlHelper class that uses a TagBuilder. diving further into that you could write class-specific helpers simply by adding the type to the param, like HtmlHelper<Player>..

using DiscMaster.Web.Models;
using DiscMaster.Web.Extensions;

namespace DiscMaster.Web.Helpers
{
    public static class ProfileImageHelper
    {
        public static string ProfileImage(this HtmlHelper helper, Models.Player player, string alternateText, string width, string height)
        {
            return ProfileImage(helper, player, alternateText, width, height, null);
        }

        public static string ProfileImage(this HtmlHelper helper, Models.Player player, string alternateText, string width, string height, object htmlAttributes)
        {
            // Create tag builder
            var builder = new TagBuilder("img");

            // Create valid id
            builder.GenerateId(player.UserName);

            // Add attributes
            string format = "{0}/{1}";
            if (!string.IsNullOrEmpty(width))
            {
                format = format + "&width={2}";
            }
            if (!string.IsNullOrEmpty(height))
            {
                format = format + "&height={3}";
            }

            builder.MergeAttribute("src", string.Format(format,"/Image.ashx?src=/Content/Media/Images/Players/",player.PlayerDetail.profilepicture.IfEmptyThen("noprofilepicture.png"),width,height));
            builder.MergeAttribute("alt", alternateText.IfEmptyThen(player.UserName));
            builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));

            // Render tag
            return builder.ToString(TagRenderMode.SelfClosing);
        }
    }
}

As always, the url to Codename 'DiscMaster': http://discmaster.codeplex.com/


Regards,

P.

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.

Tuesday, October 13, 2009

Codename 'DiscMaster' - now on CodePlex

I've published the project on CodePlex: http://discmaster.codeplex.com/ As of right now it's not in a stable, nor up to date, release. First scheduled publish of the current sourcecode is set to Thursday, 15th of October 2009. If you want to grab it it's there in it's current (somewhat fragile) state, but i suggest you wait until thursday before trying to run it locally since many changes will be uploaded on that day :) Regards, P.

Monday, October 12, 2009

Catch 22: The Branch that broke while you were standing on it

Using Team Foundation Server 2008 (TFS) is fantastic in many ways - it’s reliable, flexible and great for team development. It comes with countless of features right out of the box, and there’s even power tools for those that want more.

One of the (i hope) most widely used features of TFS is Branch & Merge (if you’re not familiar with branching strategies, i suggest you read this).

We branch.. constantly.
We merge.. constantly.

One time it got a little too hectic and one very interesting scenario occurred:

What if you want to escape/skip parts of the merge in a FI (forward integration) but still complete the merge?

If you’ve ever faced a situation like that i’m sure you’ve noticed that you can’t simply delete a changeset.. (yes, i know, if you use power tools there’s the rollback, but that’s actually a “destroy” command, not a rollback from what i’ve heard).

Now, to complicate things even more, imagine that scenario taking place from a “baseline” branch..

Picture this:

first

  • 1 – branch from a confused main-branch/baseline where the new dev-base is your hope of restoring the functionality and flexibility
  • 2 – branch a dev-track from your dev-base for a project that –might- become part of your dev-base later on (normal project branching)
  • 3 – start development on dev-branch
  • 4 – oh yes, there’s a “4” even though it’s not in the picture :)
  • 5 – normal FI planned
  • 6 – normal RI planned with new project and code incorporated into dev-base
  • 7 – RI the new and fixed “baseline”

so far so good, nothing –to- major there that will cause more problems than we can handle.. well, that is if you’re not counting the mysterious hidden/forgotten step 4 that we accidentally introduced..

halfway 

  • 1 – same as before
  • 2 – same as before
  • 3 – same as before
  • 4 – we notice that the project that was supposed to exist only in the branched dev-track is in dev base too, and so we delete it from the dev-base and check in, thinking that it’s the way it should be and our dev-base is intact.
  • 5 – time for a normal FI.. but wait! we’ve actually deleted the project in our dev-base, what happens when we try to complete a FI? the dreadful answer to that question is: it gets deleted..! (and that would make the people working on the project in the dev-track very sad.. and most likely very upset too) at this point we realize this step is not possible..
  • 6 – not possible
  • 7 – not possible

It’s quite the annoying situation we’ve ended up in, and it’s quite the mess trying to sort it out.

So, let’s focus on step 5: the FI.

instead of running it automatically we’ll choose the other merge option that allows us to merge specific changesets.

by merging the specific changesets up to, but not including, the changeset(s) containing the delete(s) we know we’re doing a safe merge and that so far all is well.. however, the changeset containing the delete(s) still exist and each time we try to merge it’s still there in the list (and that can create chaos at any given time in the future).

now, applying the same routine as above and merging specific changesets we make sure that any changesets after, but not including, the delete(s) are merged too, thus giving us the merge we set out to complete.

we could stop there.. that would be what we were after.. but there’s still that annoying sting-in-the-back-of-your-head changeset containing the actual delete constantly riding in the list of changes waiting to be merged.

here it comes, the beauty of it all is quite simple:

we use the command-line tf merge /discard option to sort it out.

without diving to much into tfs details the /discard flag tells the merge to keep our local version and not use the server changes (AcceptYours) while still respecting the merge and fully registering the merge as a successful merge in the history – thus removing the dreaded “delete changeset” we never wish to see again..

now, with those changes in place our workflow has returned to a somewhat normal state and we can move thru it with ease:

final

further reading regarding various parts of this article:

How to: Use tf merge /discard?
http://blogs.msdn.com/mohamedg/archive/2009/03/09/how-to-use-tf-merge-discard.aspx

How to: Port a specific changeset?
http://blogs.msdn.com/mohamedg/archive/2009/02/28/how-to-port-a-specific-changeset.aspx

UI Bug: resolving multiple merge conflicts
http://blogs.msdn.com/richardb/archive/2007/06/04/ui-bug-resolving-multiple-merge-conflicts.aspx

Regards,

P.

Wednesday, October 07, 2009

pex (Program EXplorer) - automated white box testing for .NET

just stumbled upon this and thought i'd share it with you - on DevLabs two guys have created what they call PEX - Program EXplorer - which is a Visual Studio tool that could make testing your (public) code a lot more friendly/helpful as well as intuitive, useful and fast. it's very interesting and is well worth the time going there and reading about it as well as watching their introduction video. way cool if you ask me! direct link: here P.

Tuesday, October 06, 2009

Codename ‘DiscMaster’ – MVC 2 migration and more maps!

After following the upgrade instructions in the release notes (see previous post) i’ve now updated the sourcecode so it’s now based on MVC 2 Preview 2.

First thing to try was the new support for Areas:

areas You’ll find pretty much all the information you need on MSDN and blog articles when it comes to defining your areas and creating the initial code.

I’m convinced the areas addition is one of the key features when it comes to using ASP.NET MVC in the enterprise scope. Scoping your application thru the use of Areas is great when you consider large-scale projects where x number of developers could be working on x number of things, it not only helps the developer but also even branching strategies could benefit from this approach.

After that i had some time to wire up another map, this time it’s a map showing all the registered courses:

courses

i've decided to release the entire code on codeplex once i'm done at the project space i've recently set up (discmaster.codeplex.com), figured that's the only decent thing to do to keep it alive since i'm right now without a hosting environment so hopefully someone will pick it up and (ab)use it when it's released :)

until the next post, take care..

P.

upgrading ASP.NET MVC to ASP.NET MVC 2 Preview 2

So, in case you missed it, the upgrade instructions as to how you go about moving from ASP.NET MVC to ASP.NET MVC 2 Preview 2 are available on page 2 in the release notes. i think they've done a great job and any reference as to how you go about is better viewed in the context of the actual document instead of any online copy.. so, once again: page 2 in the release notes for ASP.NET MVC 2 Preview 2 is where you should look. direct link to the download page (you'll find the release notes there to): here P.

morningstar sweden sports new look

i'm glad to see that one of my former employers -morningstar sweden - has updated their design to sport a more fresh and appealing look n' feel: by the looks of it they've incorporated quite a lot of jQuery/AJAX, enriched graphing components and also enhanced more on the SEO/meta side as well. congratulations on a nice remake, i'm sure it's been eagerly awaited by many! P.

Monday, October 05, 2009

Integrating weather from yr.no

UPDATED: there's a new implementation post here that uses MVC's built-in AJAX functionality.

a lot of places sell data, and often quite pricy too.. there are however a few that actually gives it away, i like that.

yr.no (the Norwegian Meteorological Institute and the NRK) exposes their weather data in xml format, both in norwegian as well as in english. I figured it was about time i tried it out since i really wanted to have some weather data on the discgolf site.

first, the basics:

the code i’m demonstrating is written in ASP.NET MVC, but it’s not anywhere near MVC-specific so you can easily take it all and run in your own solution.

for this example we’re gonna be using the following url:

http://www.yr.no/place/Sweden/Stockholm/J%c3%a4rva/forecast.xml

at yr.no it’s pretty easy to grab the url to the city of your choice (as long as they hold data for that city): just find it on their site and add forecast.xml in the end of the url.

so, after creating a partial view and setting some initial code up we get to consume the data from yr.no and output it to our site:

code:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<DiscMaster.Web.Models.Course>" %>
<%@ Import Namespace="System.Xml" %>
<%
    System.Xml.XmlDocument doc = new XmlDocument();

    if (Cache[Server.UrlPathEncode(Model.CourseDetail.yr_weather)]!=null)
    {
        doc = (XmlDocument)Cache[Server.UrlPathEncode(Model.CourseDetail.yr_weather)];
    }
    else
    {
        try
        {
            System.Xml.XmlTextReader reader = new XmlTextReader(Server.UrlPathEncode(Model.CourseDetail.yr_weather));
            doc.Load(reader);
            Cache.Add(Server.UrlPathEncode(Model.CourseDetail.yr_weather), doc, null, DateTime.Now.AddMinutes(60), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
        }
        catch (Exception ex)
        {
        
            throw;
        }
    }

    System.Xml.XmlNodeList nodelist = doc.SelectNodes("/weatherdata/forecast/tabular/time");
    for (int i = 0; i < 4; i++)
    {
     %>
     <p>
        <strong><%= DateTime.Parse(nodelist[i].Attributes["from"].Value).ToString("yyyy-mm-dd HH:MM") %> - <%= DateTime.Parse(nodelist[i].Attributes["to"].Value).ToString("HH:MM")%></strong>
        <br />
        <img src='<%= String.Format("/Image.ashx?src=/Content/Media/Icons/Weather/{0}.png&width=32", nodelist[i]["symbol"].Attributes["number"].Value.PadLeft(2,'0')) %>' />
        conditions: <%= nodelist[i]["symbol"].Attributes["name"].Value%>
        <br />
        temperature: <%= nodelist[i]["temperature"].Attributes["value"].Value%><sup>o</sup>C
        <br />
        precipitation: <%= nodelist[i]["precipitation"].Attributes["value"].Value%>
        <br />
        wind: <%= nodelist[i]["windSpeed"].Attributes["name"].Value%> - <%= nodelist[i]["windSpeed"].Attributes["mps"].Value%> mps <%= nodelist[i]["windDirection"].Attributes["code"].Value%>
    </p>
<%    }
     %>
<small>
    Weather forecast from <a href="http://www.yr.no" target="_blank">yr.no</a> delivered by the Norwegian Meteorological Institute and the NRK
</small>

view:

first nothing major really in that code.. the green-highlighted text in the example code is where the actual url to the xml data is entered.

a few minutes more and the code is more optimized to handle errors, linq and in the end i even had the time to throw in a little jQuery i found over at DynamicDrive called Featured Content Glider that i thought would do nicely for what i wanted this forecast to appear as..

so, now the final code:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<DiscMaster.Web.Models.Course>" %>
<%@ Import Namespace="System.Xml" %>
<script type="text/javascript" src="/scripts/featuredcontentglider.js">
    /***********************************************
    * Featured Content Glider script- (c) Dynamic Drive DHTML code library (www.dynamicdrive.com)
    * Visit http://www.dynamicDrive.com for hundreds of DHTML scripts
    * This notice must stay intact for legal use
    ***********************************************/
</script>
<script type="text/javascript">

    featuredcontentglider.init({
        gliderid: "forecast", //ID of main glider container
        contentclass: "glidecontent", //Shared CSS class name of each glider content
        togglerid: "p-select", //ID of toggler container
        remotecontent: "", //Get gliding contents from external file on server? "filename" or "" to disable
        selected: 0, //Default selected content index (0=1st)
        persiststate: false, //Remember last content shown within browser session (true/false)?
        speed: 500, //Glide animation duration (in milliseconds)
        direction: "downup", //set direction of glide: "updown", "downup", "leftright", or "rightleft"
        autorotate: true, //Auto rotate contents (true/false)?
        autorotateconfig: [5000, 2] //if auto rotate enabled, set [milliseconds_btw_rotations, cycles_before_stopping]
    })

</script>
<div id="forecast" class="glidecontentwrapper">
<%
    // used to read the forecast xml
    System.Xml.XmlDocument doc = new XmlDocument();
    // our soon-to-be forecast node collection
    System.Xml.XmlNodeList nodelist = null;

    // determine if the forecast is already stored in the cache..
    if (Cache[Server.UrlPathEncode(Model.CourseDetail.yr_weather)]!=null)
    {
        // .. if so, fetch it
        doc = (XmlDocument)Cache[Server.UrlPathEncode(Model.CourseDetail.yr_weather)];
    }
    else
    {
        // .. and if not, try to consume the source using a reader..
        try
        {
            System.Xml.XmlTextReader reader = new XmlTextReader(Server.UrlPathEncode(Model.CourseDetail.yr_weather));
            doc.Load(reader);
            // add entry to cache
            Cache.Add(Server.UrlPathEncode(Model.CourseDetail.yr_weather), doc, null, DateTime.Now.AddMinutes(60), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
        }
        catch (Exception ex)
        {
            // just in case something went wrong we allow this to be blank for now..
            throw;
        }
    }

    // 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)
        {
            // restruct the nodelist to a queryable list, then group by the day of the forecast, and iterate 'em..
            foreach (var day in nodelist.OfType<XmlNode>().GroupBy(n => DateTime.Parse(n.Attributes["from"].Value).ToString("yyyy-MM-dd")))
            {
                // iterate each forecast in the current group (day)
                foreach (XmlNode node in day)
                {
             %>
             <div class="glidecontent">
                 <p>
                    <strong><%= DateTime.Parse(node.Attributes["from"].Value).ToString("yyyy-MM-dd HH:MM") %> - <%= DateTime.Parse(node.Attributes["to"].Value).ToString("HH:MM")%></strong>
                    <br />
                    <img style="float: left;" src='<%= String.Format("/Image.ashx?src=/Content/Media/Icons/Weather/{0}.png&width=64", node["symbol"].Attributes["number"].Value.PadLeft(2,'0')) %>' />
                    conditions: <%= node["symbol"].Attributes["name"].Value%>
                    <br />
                    temperature: <%= node["temperature"].Attributes["value"].Value%><sup>o</sup>C
                    <br />
                    precipitation: <%= node["precipitation"].Attributes["value"].Value%>
                    <br />
                    wind: <%= node["windSpeed"].Attributes["name"].Value%> - <%= node["windSpeed"].Attributes["mps"].Value%> mps <%= node["windDirection"].Attributes["code"].Value%>
                </p>
             </div>
        <%    }
            }
        }
    }
             %>
</div>
<div id="p-select" class="glidecontenttoggler">
<a href="#" class="prev"><img border="0" title="previous forecast" src="/Image.ashx?src=/Content/Media/Icons/24x24/previous.png&width=32" /></a><br /><a href="#" class="next"><img border="0" title="next forecast" src="/Image.ashx?src=/Content/Media/Icons/24x24/next.png&width=32" /></a>
</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>

and the final view:

final

that’s really all there is to it.. some tweaking of the css and jQuery part to be more what i wanted it to be, but none the less it’s a fairly simple example and could hopefully give you some ideas or lend you some help.

external sources to this article:

Regards,

P.

Thursday, October 01, 2009

Codename 'DiscMaster' - weather and maps

the project keeps moving forward, and as stated earlier the project now has some initial integration with both maps (via Bing and some help from the great code in NerdDinner) and weather (via yr.no), here are some screenshots:

future enhancements will continue to be applied, but right now the next step is migrating the solution to ASP.NET MVC 2 Preview 2 that just came out.

example code and tutorials are coming too, as well as most of the source code.

stay tuned for further updates..

regards,

P.

Previous articles on CodeName 'DiscMaster':

Google Wave

just realized i wanna check out Google Wave, but as always it's on invite-only basis right now and sooo... anyone with an invite to Google Wave to spare? P.

ASP.NET MVC 2 Preview 2

As the title suggests, ASP.NET MVC 2 Preview 2 is now available, go download it here or read Phil Haack's post on it here. Preview 2 includes some nice features such as:
  • Client-side validation
  • Areas
  • Model validation providers
  • Metadata providers
Regards, P.