Tuesday, November 03, 2009

Using WCF to power a Custom DataTypeAttribute

In a previous post i demonstrated the simplicity in which you can create a custom DataTypeAttribute, this post aims to take it one step further, or totally off track depending on how you look at it.

the scenario

the rule our custom datatypeattribute applies when validating should be supplied from a service, making it available for several different layers all incorporating one and the same basic rule.

The sample project is complete with a MVC 2 (preview 2) web site, and a WCF service project. Note that it’s created with Visual Studio 2010 Ultimate Beta 2, so you’ll need that as well if you want to run it (you can always re-create it if you so choose).

Part 1 – simple WCF service

wcfThe WCF service in this example holds two simple methods, one for the rule and the other for the error message:

namespace ValidationServiceWCF
{
    public class Rules : IRules
    {
        private int maxlength = 2;
        public bool MaxShortTitleLength(string text)
        {
            if (string.IsNullOrEmpty(text))
            {
                return false;
            }
            return text.Length<=maxlength;
        }
 
        public string MaxShortTitleLengthErrorMessage()
        {
            return string.Format("{0} is invalid, can only be up to {1} chars", "{0}", maxlength);
        }
    }
}
Part 2 – the mvc 2 project

mvc2projThe rule created in the WCF service is then applied in a custom datatypeattribute class called MaxLengthAttribute:

namespace ValidationService.Models.DataAnnotations
{
    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
    public class MaxShortTitleLengthAttribute : DataTypeAttribute
    {
        private string errortext = string.Empty; 
        RulesService.IRules rulesengine = new RulesService.RulesClient();
 
        public MaxShortTitleLengthAttribute()
            : base(DataType.Text)
        {
            errortext = rulesengine.MaxShortTitleLengthErrorMessage();
        }
 
 
        public override bool IsValid(object value)
        {
            if (rulesengine.MaxShortTitleLength((string)value))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
 
        public override string FormatErrorMessage(string name)
        {
            return string.Format(errortext, name);
        }
    }
}

that attribute is then applied on our model using the DataTypeAttribute approach we’re now familiar with:

namespace ValidationService.Models
{
    [MetadataType(typeof(Product_MetaData))]
    public partial class Product
    {
    }
    internal class Product_MetaData
    { 
        [Required]
        [ScaffoldColumn(false)]
        public int ID { get; set; }
 
        [Required]
        public string Title { get; set; }
 
        [Required]
        [MaxShortTitleLength()]
        public string ShortTitle { get; set; }
    }
}

After that it was just a matter of creating some actions, wiring a few views, setting up a context and repository class and finally wrapping it all up.

Now when validation is run on the ShortTitle property of the model the custom attribute fires off to the service, calls the validation method and gets the errormessage in case we’ll need it:

errormessage

That’s it, source code available here (VS 2010 Beta 2).

Note: this is a simple example and therefore i’ve just used the HomeController to handle everything. The endpoint for the WCF is currently pointing to localhost:7556/rules.svc.

Regards,

P.

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.