Dec 1 2009

A fluent HtmlHelper extension for using FusionCharts in ASP.NET MVC, Part 2

Category: ASP.NET | MVCMatt @ 08:44

Hey, it only took me nearly a month to write part 2 of this series!  Yeah, I’ve been neglecting this blog a lot lately.  There just aren’t enough hours in the day to write. 

In part 1, I discussed charting with ASP.NET MVC and why I decided to use a Flash solution (FusionCharts Free) instead of an ASP.NET control or a JavaScript solution.  Nearly a month later, I’m still very glad I made the switch. 

As I said in part 1, FusionCharts Free includes some methods for working with FusionCharts from ASP.NET, and those work fine in both WebForms and MVC applications.  They aren’t very MVC-like though, and they take quite a few parameters that I don’t want to deal with most of the time.  They also don’t help with building the XML that configures a FusionChart.  The markup the methods generate also suffers from the annoying Flash z-order glitch.  To overcome these limitations, I decided to create some HtmlHelper extensions.  I wanted something that was flexible yet simple.  While I think that fluent APIs have been abused by the .NET community, I do think there are times where they make sense.  When using an HtmlHelper extension, I want to be able to configure the extension without resorting to a code block, which is exactly why I chose to go the fluent route for the FusionCharts helpers.  Before digging into the implementation, let’s take a look at the API in action again.  This is a simple example using an array of numbers, but the API actually supports any object you want to work with:

<%=Html.FusionCharts().Column2D(new[] {1, 2, 3, 4, 5}, 300, 300, d => d)
        .Caption("Numbers")
        .SubCaption("(subcaption)")
        .Label(d => "Label " + d)
        .Hover(d => "Hover " + d)
        .Action(d => "javascript:alert(&apos;You clicked on " + d + "&apos;);")%>

This renders a 2D bar chart, like so:

barChart

All of the methods should be fairly self-explanatory, except perhaps the Action method.  Action allows you to create hyperlinks for the data items in your chart.  In this case, I’ve created a JavaScript link that will display an alert when a bar in the chart is clicked. 

As I said, this API is generic and supports any type.  This allows you to pass in complicated objects that expose values, labels, etc. that can be bound using the various methods exposed on the fluent API. 

Time for some code.  First is the HtmlHelper extension and the entry point to the FusionChart helpers:

/// <summary>
/// Container for the actual extension method.
/// </summary>
public static class FusionChartsHtmlHelper
{
    /// <summary>
    /// Gets a helper for building a fusion chart.
    /// </summary>
    /// <param name="helper"></param>
    /// <returns></returns>
    public static FusionChartsHelper FusionCharts(this HtmlHelper helper)
    {
        return new FusionChartsHelper(helper);
    }
}

/// <summary>
/// An HTML helper for FusionCharts. 
/// </summary>
public class FusionChartsHelper
{
    /// <summary>
    /// The HTML helper.
    /// </summary>
    private readonly HtmlHelper mHtmlHelper;

    /// <summary>
    /// The resolved path to the Fusion Charts SWF files.
    /// </summary>
    private readonly string mChartsFolderBase;

    /// <summary>
    /// Initializes the helper. 
    /// </summary>
    /// <param name="helper"></param>
    public FusionChartsHelper(HtmlHelper helper)
    {
        mHtmlHelper = helper;
        UrlHelper urlHelper = new UrlHelper(helper.ViewContext.RequestContext);

        mChartsFolderBase = urlHelper.Content("~/Charts/");
    }

    /// <summary>
    /// Gets a fusion chart builder that will create a 2D bar chart.
    /// </summary>
    /// <typeparam name="T">The type of the data items.</typeparam>
    /// <param name="data">The items to bind to the chart.</param>
    /// <param name="width">Width in pixels.</param>
    /// <param name="height">Height in pixels.</param>
    /// <param name="getValue">Delegate that extracts the numerical value from a data item.</param>
    /// <returns>A 2D chart builder.</returns>
    public FusionChartColumn2DBuilder<T> Column2D<T>(
        IEnumerable<T> data, 
        int width, 
        int height,  
        Func<T, double> getValue)
    {
        return new FusionChartColumn2DBuilder<T>(mHtmlHelper, mChartsFolderBase, data, getValue, width, height);
    }

    /// <summary>
    /// Creates a builder for 2D pie chart.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="data"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    /// <param name="getValue"></param>
    /// <returns></returns>
    public FusionChartPie2DChartBuilder<T> Pie2D<T>(
        IEnumerable<T> data, 
        int width, 
        int height,  
        Func<T, double> getValue)
    {
        return new FusionChartPie2DChartBuilder<T>(mHtmlHelper, mChartsFolderBase, data, getValue, width, height);
    }
}

So far I’ve only implemented two chart builders (one for bar charts, and one for pie charts), but FusionCharts supports a slew of charts that could easily be integrated into this API.

FusionChartsHelper doesn’t do much aside from instantiate the actual chart builders.  These are the fluent APIs for building and configuring a chart.  Let’s look at each of them:

/// <summary>
/// A builder for a 2D column chart.
/// </summary>
/// <typeparam name="T"></typeparam>
public class FusionChartColumn2DBuilder<T> : FusionChartBuilder<T>
{
    /// <summary>
    /// The filename of the chart.
    /// </summary>
    private const string CHART_NAME = "FCF_Column2D.swf";

    /// <summary>
    /// The label for the X axis.
    /// </summary>
    private string mXAxisLabel;

    /// <summary>
    /// The label for the Y axis.
    /// </summary>
    private string mYAxisLabel;

    /// <summary>
    /// Initializes the builder.
    /// </summary>
    /// <param name="helper"></param>
    /// <param name="baseUrl">The URL to the folder that contains the SWF files for Fusion Charts.</param>
    /// <param name="data"></param>
    /// <param name="valueExtractor"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    public FusionChartColumn2DBuilder(HtmlHelper helper, string baseUrl, IEnumerable<T> data, Func<T, double> valueExtractor, int width, int height) :
        base(helper, baseUrl + CHART_NAME, data, valueExtractor, width, height)
    {
    }

    /// <summary>
    /// Writes the X and Y axis labels.
    /// </summary>
    /// <param name="xml"></param>
    internal override void WriteGraphProperties(StringBuilder xml)
    {
        if (mXAxisLabel != null) xml.AppendFormat(" xAxisName='{0}'", mXAxisLabel);

        if (mYAxisLabel != null) xml.AppendFormat(" yAxisName='{0}'", mYAxisLabel);
    }

    /// <summary>
    /// Sets the label for the X Axis.
    /// </summary>
    /// <param name="xAxisLabel"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> XAxisLabel(string xAxisLabel)
    {
        mXAxisLabel = xAxisLabel;

        return this;
    }

    /// <summary>
    /// Sets the label for the Y Axis.
    /// </summary>
    /// <param name="yAxisLabel"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> YAxisLabel(string yAxisLabel)
    {
        mYAxisLabel = yAxisLabel;

        return this;
    }
}

/// <summary>
/// A chart builder for 2D pie charts.
/// </summary>
/// <typeparam name="T"></typeparam>
public class FusionChartPie2DChartBuilder<T> : FusionChartBuilder<T>
{
    /// <summary>
    /// The name of the Pie Chart SWF file.
    /// </summary>
    private const string CHART_NAME = "FCF_Pie2D.swf";

    /// <summary>
    /// Flag that controls whether or not labels are shown by pie slices.
    /// </summary>
    private bool mShowLables = true;

    /// <summary>
    /// Initializes the builder.
    /// </summary>
    /// <param name="helper"></param>
    /// <param name="chartUrl"></param>
    /// <param name="data"></param>
    /// <param name="valueExtractor"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    public FusionChartPie2DChartBuilder(HtmlHelper helper, string chartUrl, IEnumerable<T> data, Func<T, double> valueExtractor, int width, int height) 
        : base(helper, chartUrl + CHART_NAME, data, valueExtractor, width, height)
    {
    }

    /// <summary>
    /// Writes chart-specific XML settings. 
    /// </summary>
    /// <param name="xml"></param>
    /// <remarks>
    /// Derived classes should override this method to add any chart-specific markup to the
    /// &lt;graph&gt; element.  When called, the '&lt;graph ' markup will have been rendered already.  
    /// </remarks>
    internal override void WriteGraphProperties(StringBuilder xml)
    {
        if (mShowLables) xml.Append(" shownames='1'");
    }

    /// <summary>
    /// Hides the labels from the pie chart.
    /// </summary>
    /// <returns></returns>
    public FusionChartPie2DChartBuilder<T> HideLabels()
    {
        mShowLables = false;

        return this;
    }
}

Each class is fairly short and exposes only the functionality that is specific to its chart type.  All the common functionality and the core of the chart rendering is handled by the base class, FusionChartBuilder<T>: 

/// <summary>
/// Builds a chart.
/// </summary>
public abstract class FusionChartBuilder<T>
{
    ...
    
    /// <summary>
    /// Creates the builder.
    /// </summary>
    /// <param name="data">The data items to build a chart from.</param>
    /// <param name="valueExtractor">Used to get the value from data items.</param>
    /// <param name="helper"></param>
    /// <param name="chartUrl">The URL to the chart.</param>
    /// <param name="width">Chart width.</param>
    /// <param name="height">Chart height.</param>
    public FusionChartBuilder(HtmlHelper helper, string chartUrl, IEnumerable<T> data, Func<T,double> valueExtractor, int width, int height)
    {
        ...
    }

    /// <summary>
    /// Gets the next available color.
    /// </summary>
    /// <returns></returns>
    private string GetNextColor()
    {
        ...
    }

    /// <summary>
    /// Writes chart-specific XML settings. 
    /// </summary>
    /// <param name="xml"></param>
    /// <remarks>
    /// Derived classes should override this method to add any chart-specific markup to the
    /// &lt;graph&gt; element.  When called, the '&lt;graph ' markup will have been rendered already.  
    /// </remarks>
    internal abstract void WriteGraphProperties(StringBuilder xml);

    /// <summary>
    /// Adds an action link to each item. 
    /// </summary>
    /// <param name="actionLink"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> Action(Func<T, string> actionLink)
    {
        ...
    }

    /// <summary>
    /// Sets the ID of the generated chart.
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> Id(string id)
    {
        ...
    }

    /// <summary>
    /// Specify a callback that extracts a friendly label for each item.
    /// </summary>
    /// <param name="getLabel"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> Label(Func<T, string> getLabel)
    {
        ...
    }

    /// <summary>
    /// Enables debug mode.
    /// </summary>
    /// <returns></returns>
    public FusionChartBuilder<T> EnableDebugMode()
    {
        ...
    }

    /// <summary>
    /// Sets the number of decimal places to show.
    /// </summary>
    /// <param name="precision"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> DecimalPrecision(int precision)
    {
        ...
    }

    /// <summary>
    /// When enabled this will round numbers to millions or thousands and add the
    /// corresponding suffix (k for thousands and m for millions).
    /// </summary>
    /// <param name="enabled"></param>
    /// <returns></returns>
    /// <remarks>
    /// Setting this to true corresponds to setting FormatNumberScale='1' on the
    /// graph XML element in FusionCharts.
    /// </remarks>
    public FusionChartBuilder<T> UseDynamicSuffixes(bool enabled)
    {
        ...
    }

    /// <summary>
    /// Sets the value to prepend to all numeric values.
    /// </summary>
    /// <param name="prefix"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> NumberPrefix(string prefix)
    {
        ...
    }

    /// <summary>
    /// Sets the value to append to all numeric values.
    /// </summary>
    /// <param name="suffix"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> NumberSuffix(string suffix)
    {
        ...
    }

    /// <summary>
    /// Builds a string of text to show as a chart item's tooltip.
    /// </summary>
    /// <param name="hoverLabelBuilder"></param>
    /// <returns></returns>
    public FusionChartBuilder<T> Hover(Func<T, string> hoverLabelBuilder)
    {
        ...
    }

    /// <summary>
    /// Renders the chart.
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        //The real work happens here!
        ...
    }

    /// <summary>
    /// Sets the chart caption.
    /// </summary>
    /// <param name="caption"></param>
    public FusionChartBuilder<T> Caption(string caption)
    {
        ...
    }

    /// <summary>
    /// Sets the chart's subcaption.
    /// </summary>
    /// <param name="subCaption"></param>
    public FusionChartBuilder<T> SubCaption(string subCaption)
    {
        ...
    }
}

I’ve omitted the field and property definitions as they’re mundane (and available in the full source below).  The fluent methods are fairly simple and just do the standard “set and return”:

/// <summary>
/// Sets the chart caption.
/// </summary>
/// <param name="caption"></param>
public FusionChartBuilder<T> Caption(string caption)
{
    mCaption = caption;

    return this;
}

The real work occurs in the ToString method.  This method renders the FusionCharts markup and XML configuration data based on the data items and the configuration settings made with the fluent methods.  It also does a quick find-and-replace to correct the z-ordering problem with Flash:

/// <summary>
/// Renders the chart.
/// </summary>
/// <returns></returns>
public override string ToString()
{
    StringBuilder xml = new StringBuilder();

    xml.Append("<graph");

    WriteGraphProperties(xml);

    if (mDecimalPrecision >= 0) xml.AppendFormat(" decimalPrecision='{0}'", mDecimalPrecision);

    if (mUseDynamicSuffixes) xml.AppendFormat(" formatNumberScale='1'");

    if (mPrefix != null) xml.AppendFormat(" numberPrefix='{0}'", mPrefix);

    if (mSuffix != null) xml.AppendFormat(" numberSuffix='{0}'", mSuffix);

    if (mCaption != null) xml.AppendFormat(" caption='{0}'", mCaption);

    if (mSubCaption != null) xml.AppendFormat(" subCaption='{0}'", mSubCaption);

    xml.AppendLine(">");

    foreach (T item in Data)
    {
        xml.AppendFormat("<set value='{0}' color='{1}'", mValueExtractor(item), GetNextColor());

        if (mLabeler != null)
        {
            xml.AppendFormat(" name='{0}'", mHelper.UrlEncode(mLabeler(item)));
        }

        if (mLinkBuilder != null)
        {
            xml.AppendFormat(" link='{0}'", mHelper.UrlEncode(mLinkBuilder(item)));
        }

        if (mHoverLabelBuilder != null)
        {
            xml.AppendFormat(" hoverText='{0}'", mHelper.UrlEncode(mHoverLabelBuilder(item)));
        }

        xml.AppendLine("/>");
    }

    xml.AppendLine("</graph>");

    string markup = InfoSoftGlobal.FusionCharts.RenderChartHTML(mChartUrl, "", xml.ToString(), mChartId, 
                                                                Width.ToString(), Height.ToString(), mDebugEnabled);

    //We have to add another param to make sure the flash object doesn't shine through jQuery UI.
    markup = markup.Replace("<param name=\"quality\" value=\"high\" />",
                            "<param name=\"quality\" value=\"high\" /><param value=\"opaque\" name=\"wmode\" />")
        .Replace("<embed", "<embed wmode=\"opaque\"");

    return markup;
}

Also note that it calls the abstract WriteGraphProperties method, which allows derived classes to inject their own config settings.

Again, this is a pretty bare-bones API right now.  I’ve implemented just the charts and config options that I needed for the project I’m working on, but it could easily be extended with new settings and chart types.  If you’re going to use this in your own project, be sure you put the SWF files for FusionCharts in ‘/Charts’, or edit the hard-coded path in the code (or replace it with a config setting or static property that can be set from Global.asax). 

You can download the code here, graciously donated by InRAD.  Note that I have not tested this code outside of InRAD’s projects, so let me know if I missed a dependency. 

Thoughts?  Suggestions? 

Tags:

Nov 10 2009

A fluent HtmlHelper extension for using FusionCharts in ASP.NET MVC, Part 1

Category: ASP.NET | MVCMatt @ 10:26

We’ve been working like mad to get a (very) functional prototype of our new system running at work, which is why my posts have been rather sparse lately.  We’re doing interesting things, just not much that I can talk about.  Today is different though, I actually have something useful to share!

My current task revolves around rendering charts.  I’ve previously used a variety of charting libraries, mostly recently Telerik and dotnetCharting, but we wanted to go a different route this time.  Neither was a good fit for ASP.NET MVC the last time I looked.  So, I thought I’d pick up one of the many handy JavaScript charting libraries and be done with it.  Unfortunately, that didn’t pan out.  Flot is simply not flexible enough for my needs.  jqPlot was about the same.  Yeah, you can extend either of them, but I wanted something I could customize with less work (basically I wanted more built-in options).  I also looked at the Google solution, but that’s the exact opposite of flexible since it renders as a non-interactive image.  After lots of wailing around, I gave up on the JavaScript route and concluded that none of them are really mature.

I briefly considered Silverlight before deciding that I should see what Flash had available.  Much to my surprise, I found a great little charting library that works well enough with .NET: FusionCharts Free!  Out of all the charting APIs I’ve used over the years, this has quickly become my favorite.  You just feed in an XML definition of what the graph should look like, and it renders it.  It offers a slew of options, too, making it a good fit for what I needed.

Sadly the samples all deal with ASP.NET WebForms and not with ASP.NET MVC.  The same approach works, but I wanted something cleaner.  I decided to create an HtmlHelper extension that would allow me to build a FusionChart with a fluent interface.  Despite my aversion to fluent interfaces, I do think there are times where they make sense, and I think control builders for MVC are a good fit.  My approach when I make a fluent interface is simple: anything that’s required is a parameter of the head method in the fluent chain.  All other optional settings are exposed as fluent methods.  An alternative is to accept some sort of “settings” object that exposes all the settings as properties, and while this does allow for the same terse inline configuration, it’s less readable to me than the fluent version. 

Anyway, let’s look at the usage of this API.  You would call it from your view like any other HtmlHelper:

Html.FusionCharts().Column2D(Model.Data, 415, 247, d => d.TotalSales)
    .Label(d => d.ShortName)
    .Hover(d => d.LongName)
    .XAxisLabel("Widgets")
    .YAxisLabel("Sales")
    .DecimalPrecision(2)
    .UseDynamicSuffixes(true)
    .NumberPrefix("$")
    .Action(d => Html.BuildUrlFromExpression<WidgetController>(c => c.ViewWidget(d=>d.Id)))

Html.FusionCharts() returns a helper class that exposes methods for creating the various chart types (for now, I’ve only implemented the 2D column chart, but plan to do others).  Column2D is the head of the fluent chain and returns a FusionChartBuilder<T>:

/// <summary>
/// Gets a fusion chart builder that will create a 2D bar chart.
/// </summary>
/// <typeparam name="T">The type of the data items.</typeparam>
/// <param name="data">The items to bind to the chart.</param>
/// <param name="width">Width in pixels.</param>
/// <param name="height">Height in pixels.</param>
/// <param name="getValue">Delegate that extracts the numerical value from a data item.</param>
/// <returns></returns>
public FusionChartBuilder<T> Column2D<T>(
    IEnumerable<T> data, 
    int width, 
    int height,  
    Func<T, double> getValue)
{
    return new FusionChartBuilder<T>(mHtmlHelper, mUrlHelper.Content("~/Charts/FCF_Column2D.swf"),
            data, getValue, width, height);
}

The builder enables me to configure options on my charts cleanly from within my view.  I’m not exposing all the FusionChart options yet (and probably won’t), but so far I can control the chart labels, the tooltip that’s shown when you hover over a bar, the format of the numbers, and I can even add a link to each bar, enabling users to “drill-down” into the data or navigate to a corresponding details page.

Time is short, so I’m not going to get any further into the code today, but I will in a future post.  For now, feel free to tell me how you love/hate fluent APIs or what a brilliant/dumb developer I am for using Flash from ASP.NET.

Tags:

Sep 8 2009

Fluent wrapper for ASP.NET MVC TagBuilder

Category: ASP.NET | MVCMatt @ 07:35

I’m not a huge fan of the fluent-API.  I think it’s a pattern that’s been overused and is now applied like mayonnaise: people are putting it on things where it just doesn’t belong.  That said, there are times when it’s useful.  Case in point is building up an HTML snippet programmatically for use in ASP.NET MVC.  Unfortunately, the handy TagBuilder class actually isn’t fluent (at all).  Most of its methods return void.  Fortunately it is quite easy to adapt the TagBuilder into a fluent version.  Behold, FluentTagBuilder:

/// <summary>
/// Wrapper for <see cref="TagBuilder"/> that makes
/// it a fluent API.
/// </summary>
public class FluentTagBuilder
{
    private TagBuilder mBuilder;

    public FluentTagBuilder(string tagName)
    {
        mBuilder = new TagBuilder(tagName);
    }

    public FluentTagBuilder MergeAttribute(string key, string value)
    {
        mBuilder.MergeAttribute(key, value);

        return this;
    }

    public FluentTagBuilder AddCssClass(string cssClass)
    {
        mBuilder.AddCssClass(cssClass);

        return this;
    }

    public string InnerHtml
    {
        get
        {
            return mBuilder.InnerHtml;
        }
        set
        {
            mBuilder.InnerHtml = value;
        }
    }

    public override string ToString()
    {
        return mBuilder.ToString();
    }
}

Note that I haven’t exposed all the underlying TagBuilder functionality, just the bits I needed today.  Implementing the rest yourself should be trivial though.  Here’s an example use in an HtmlHelper extension:

public FluentTagBuilder SuperSecretButton(string url, string altText)
{
    FluentTagBuilder link = new FluentTagBuilder("a")
        .MergeAttribute("href", url)
        .MergeAttribute("target", "_blank");

    FluentTagBuilder image = new FluentTagBuilder("img")
        .MergeAttribute("src", mHelper.ResolveUrl(SEEKRET_URL))
        .MergeAttribute("alt", altText);

    link.InnerHtml = image.ToString();

    return link;
}

And the use of the helper in the view:

Html.MyHelper().SuperSecretButton("http://wherever", "I am a button").AddCssClass("analyzeCostResearch")

Note how the view can add a class (or potentially any other attribute) without clutering up the method signatures for the helper. 

Tags:

Aug 11 2009

Standardizing JSON Responses in ASP.NET MVC

Category: ASP.NET | MVC | liteGridMatt @ 04:05

ASP.NET MVC provides very nice support for returning JSON data, but my chief complaint with it is that it’s too flexible.  You can basically cram anything you want in it and trust that it will make it to the client script, which has lead to a complete lack of coherence among the various controllers in our big MVC application.  Some actions return a simple string that contains either “success” or “error” depending on whether or not the operation succeeded.  Others return true or false.  This is going to cause maintenance problems down the road, so we are taking a stab at standardizing things.  Right now, the idea is to create a class derived from JsonResult that exposes a few standard properties while at the same time maintaining the flexibility of the original JsonResult.  The common properties are “status”, a boolean that is true or false depending on success or failure of the requested action, and “message”, an optional string that can be set with additional details in the case of failure.  The custom result maintains the ability to insert additional properties into the JSON result via an object (anonymous or otherwise) as well as through adding key/value pairs explicitly.  The implementation is quite simple (commments removed for readability):

public class StandardJsonResult : JsonResult
{
    public string Message { get; set; }

    public bool Status { get; set; }

    public Dictionary<string, object> Properties { get; private set; }

    public StandardJsonResult()
    {
        Properties = new Dictionary<string, object>();
        Data = Properties;
        Status = true;
    }

    public StandardJsonResult(object data)
        : this()
    {
        IDictionary<string, object> properties = data.ToDictionary();

        foreach (var keyValue in properties)
        {
            Properties.Add(keyValue.Key, keyValue.Value);
        }
    }

    public StandardJsonResult(Exception ex) : this()
    {
        Status = false;
        Message = ex.Message;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        //Copy in standard properties
        Properties.Add("status", Status);

        if (Message != null)
        {
            Properties.Add("message", Message);
        }

        base.ExecuteResult(context);
    }
}

There are several constructors provided.  The one that takes an object as a parameter allows you to send anonymous objects or view models across the wire just like you can with a regular JsonResult object.  The values are copied from the object using the method I previous described, but it could also be done using a RouteDataDictionary.  The values are added to the Properties dictionary, which is actually assigned to the JsonResult’s Data property.  The key/value pairs in the dictionary are serialized to properties in the JSON output.  The ExecuteResult method is overridden so that the two standard properties can be added to the dictionary prior to JSON serialization.

This class can also be extended for other recurring scenarios.  For example, here is the custom result that feeds data into liteGrid:

public class LiteGridJsonResult : StandardJsonResult
{
    public Array DataItems { get; private set; }

    public LiteGridJsonResult(Array dataItems)
    {
        DataItems = dataItems;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        Properties.Add("dataItems", DataItems);

        base.ExecuteResult(context);
    }
}

This custom result simply extends the standard one with a collection of data items that will be rendered by the client. 

Thoughts or suggestions?

Tags:

May 13 2009

ASP.NET MVC HtmlHelper for Uploadify, Take One

Category: ASP.NET | MVC | JavaScriptMatt @ 06:56

As I’ve mentioned before, I really, really hate the way most people seem to be creating reusable UI “controls” with ASP.NET MVC.  I do not like emitting JavaScript, HTML, etc. from within C# code.  It’s cumbersome to create, difficult to really test, and just a real PITA in general.

Based on feedback I received from Rob after my attempts at creating a helper for jqGrid, I decided to take a completely different approach when it was time to wrap another jQuery plug-in: Uploadify.  My goal was to minimize the amount of tag-soup embedded in my C# code while still maintaining the ease-of-use of the jqGrid helper, which required only a single HtmlHelper call to go from nothing to full grid.

Well, one painful afternoon later, I think I’ve arrived at something that makes some sense.  First, I couldn’t completely eliminate the tag soup, but I did minimize it (I think) while still keeping the thing extremely simple to use and (hopefully) maintain.  Let’s start with how you would use it:

<asp:Content ContentPlaceHolderID="HeadContent" runat="server">
    <%=Html.Uploadify("fileInput", 
        new UploadifyOptions
           {
               UploadUrl = Html.BuildUrlFromExpression<SandboxController>(c => c.HandleUpload(null)),
            FileExtensions = "*.xls;*.xlsx",
            FileDescription = "Excel Files",
            AuthenticationToken = Request.Cookies[FormsAuthentication.FormsCookieName] == null ?
                string.Empty :
                Request.Cookies[FormsAuthentication.FormsCookieName].Value,
            ErrorFunction = "onError",
            CompleteFunction = "onComplete"
           }) %>
           
    <script type="text/javascript">
        function onError() {
            alert('Something went wrong.');
        }
        function onComplete() {
            alert('File saved!');
        }
    </script>                                                   
</asp:Content>

The first parameter is the name of the input control to convert to an uploadify control, the second contains all the optional settings you can customize.  I prefer to use an options class like this rather than provide 50,000 overloads.  By using a dedicated options class, I can add new settings without breaking existing code or having to create new overloads.  The options should be fairly self explanatory, but here they are:

/// <summary>
/// Defines all options for <see cref="HtmlHelperExtensions.Uploadify"/>.
/// </summary>
public class UploadifyOptions
{
    #region Public Properties

    /// <summary>
    /// The URL to the action that will process uploaded files.
    /// </summary>
    public string UploadUrl { get; set; }

    /// <summary>
    /// The file extensions to accept.
    /// </summary>
    public string FileExtensions { get; set; }

    /// <summary>
    /// Description corresponding to <see cref="FileExtensions"/>.
    /// </summary>
    public string FileDescription { get; set; }

    /// <summary>
    /// The ASP.NET forms authentication token.
    /// </summary>
    /// <example>
    /// You can get this in a view using:
    /// <code>
    /// Request.Cookies[FormsAuthentication.FormsCookieName].Value
    /// </code>
    /// You should check for the existence of the cookie before accessing
    /// its value.
    /// </example>
    public string AuthenticationToken { get; set; }

    /// <summary>
    /// The name of a JavaScript function to call if an error occurs
    /// during the upload.
    /// </summary>
    public string ErrorFunction { get; set; }

    /// <summary>
    /// The name of a JavaScript function to call when an upload
    /// completes successfully. 
    /// </summary>
    public string CompleteFunction { get; set; }

    #endregion
}

Next, we have the actual HtmlHelper extension method:

/// <summary>
/// Renders JavaScript to turn the specified file input control into an 
/// Uploadify upload control.
/// </summary>
/// <param name="helper"></param>
/// <param name="name"></param>
/// <param name="options"></param>
/// <returns></returns>
public static string Uploadify(this HtmlHelper helper, string name, UploadifyOptions options)
{
    string scriptPath = helper.ResolveUrl("~/Content/jqueryPlugins/uploadify/");

    StringBuilder sb = new StringBuilder();
    //Include the JS file.
    sb.Append(helper.ScriptInclude("~/Content/jqueryPlugins/uploadify/jquery.uploadify.js"));
    sb.Append(helper.ScriptInclude("~/Content/jqueryPlugins/uploadify/jquery.uploadify.init.js"));

    //Dump the script to initialze Uploadify
    sb.AppendLine("<script type=\"text/javascript\">");
    sb.AppendLine("$(document).ready(function() {");
    sb.AppendFormat("initUploadify($('#{0}'),'{1}','{2}','{3}','{4}','{5}',{6},{7});", name, options.UploadUrl,
                    scriptPath, options.FileExtensions, options.FileDescription, options.AuthenticationToken,
                    options.ErrorFunction ?? "null", options.CompleteFunction ?? "null");
    sb.AppendLine();
    sb.AppendLine("});");
    sb.AppendLine("</script");

    return sb.ToString();
}

The helper uses a StringBuilder (yeah, I hate them, and I’m open to suggestions) to include two JavaScript files.  The first is the standard uploadify script, but the second is something custom, which I’ll get to in just a second.    Finally, the helper outputs a call to initUploadify inside of the page load event, passing in all the options that were specified.

And that brings us to that second JavaScript include:

//This is used in conjunction with the HtmlHelper.Uploadify extension method.
function initUploadify(control, uploadUrl, baseUrl, fileExtensions, fileDescription, authenticationToken, errorFunction, completeFunction) {
    var options = {};

    options.script = uploadUrl;
    options.uploader = baseUrl + 'uploader.swf';
    options.cancelImg = baseUrl + 'cancel.png';
    //TODO: Make this an option?
    options.auto = true;
    options.scriptData = { AuthenticationToken: authenticationToken };
    options.fileExt = fileExtensions;
    options.fileDesc = fileDescription;

    if (errorFunction != null) {
        options.onError = errorFunction;
    }

    if (completeFunction != null) {
        options.onComplete = completeFunction;
    }

    control.fileUpload(options);
}

In here, I’ve created a simple JavaScript function that actually calls the uploadify JavaScript plug-in.  By using this method instead of using C# to emit the configuration code directly, I’m cutting out a fair amount of tag soup, and I’m wrapping things up in a way that will be easier to change in the future.  Hopefully.  The down side to this approach is that you have to create a new JavaScript method and include for every plug-in you want to use, but combining the scripts and correctly setting cache headers should reduce the request overhead.

I’m not claiming that this is the best way to do this.  In fact, I really hope it isn’t, because I still don’t like it.  But I think that I like it better than the approach I took for jqGrid.  If you have any suggestions or feedback, please share.  Feel free to tell me that I’m doing things completely wrong.

Tags:

May 13 2009

Using Flash with ASP.NET MVC and Authentication

Category: ASP.NET | MVCMatt @ 01:47

There is a well-known bug in Flash that causes it to completely ignore the browser’s session state when it makes a request.  Instead, it either pulls cookies from Internet Explorer or just starts a new session with no cookies.  GOOD CALL, ADOBE.  And when I say this bug is well-known, I mean it was reported in Flash 8.  It’s still sitting in the Adobe bug tracker.  It has been triaged, it seems to have high priority, yet it remains unfixed.  Again, GREAT job, Adobe. 

Anyway, why should you care?  Well, if you want to use Flash for anything, even something simple like AJAX file uploads with Uploadify, you better hope you don’t need authorization and authentication.  But really, why would you want to authenticate users before letting them upload stuff to your site, anyway?  There’s no possible way that could ever be exploited, right?

If you do decide that security is important (HINT: IT IS), there are some well-known hacks to work around it.  None of them fit well with ASP.NET MVC though.  Just when all seemed lost, I found this post from Ariel Popovsky that saved the day.  I have wrapped his solution up in an easy-to-apply custom AuthorizationAttribute that you can tag to a controller or action method.  Here’s the code:

/// <summary>
/// A custom version of the <see cref="AuthorizeAttribute"/> that supports working
/// around a cookie/session bug in Flash.  
/// </summary>
/// <remarks>
/// Details of the bug and workaround can be found on this blog:
/// http://geekswithblogs.net/apopovsky/archive/2009/05/06/working-around-flash-cookie-bug-in-asp.net-mvc.aspx
/// </remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class FlashCompatibleAuthorizeAttribute : AuthorizeAttribute
{
    /// <summary>
    /// The key to the authentication token that should be submitted somewhere in the request.
    /// </summary>
    private const string TOKEN_KEY = "AuthenticationToken";

    /// <summary>
    /// This changes the behavior of AuthorizeCore so that it will only authorize
    /// users if a valid token is submitted with the request.
    /// </summary>
    /// <param name="httpContext"></param>
    /// <returns></returns>
    protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
    {
        string token = httpContext.Request.Params[TOKEN_KEY];

        if (token != null)
        {
            FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(token);

            if (ticket != null)
            {
                FormsIdentity identity = new FormsIdentity(ticket);
                string[] roles = System.Web.Security.Roles.GetRolesForUser(identity.Name);
                GenericPrincipal principal = new GenericPrincipal(identity, roles);
                httpContext.User = principal;
            }
        }

        return base.AuthorizeCore(httpContext);
    }
}

The filter checks the request to see if the authentication ticket was submitted.  If so, it tries to decrypt it, then recreates the IPrincipal that is needed by the base AuthorizationAttribute to do its work.  Just apply it to your controller, make sure Flash is submitted the value of the Forms Authentication cookie, and BAM, everything works.

Tags:

May 7 2009

Simplified unit testing for ASP.NET MVC JsonResult

Category: MVC | .NETMatt @ 01:45

There are quite a few examples floating around on the web that describe how to test your JsonResult objects to make sure the data was correctly packaged.  They all follow the same basic pattern: mock out core ASP.NET objects (such as ControllerContext, HttpResponse, and HttpContext), call JsonResult.ExecuteResult, recover what was written to HttpResponse.Output, and deserialize it.  Sure, this approach works, but in the same manner as cleaning your house out by lighting it on fire.  It’s way overkill.  There’s a much easier way.  For simple objects, just cast JsonResult.Data:

   1: string value = "Hello, there!";
   2:  
   3: JsonResult result = new JsonResult { Data=value };
   4:  
   5: //SURPRISE!
   6: Assert.AreEqual(value, (string)result.Data);

Yeah, that seems fairly obvious.  You don’t even need the explicit cast there, I just threw it in to prove the point.  But what about anonymous types?  Easy:

   1: var value = new { Id=5, Something="Else" };
   2:  
   3: JsonResult result = new JsonResult { Data=value };
   4:  
   5: IDictionary<string,object> data = new RouteValueDictionary(result.Data);
   6:  
   7: Assert.AreEqual(5, data["Id"]);
   8: Assert.AreEqual("Else", data["Something"]);

See, easy! “But what about arrays of anonymous types?!?!?” Do not fret, LINQ to the rescue:

   1: var values = new[]
   2:                  {
   3:                      new { Id = 5, Something = "Else" },
   4:                      new { Id = 6, Something = "New" },
   5:                      new { Id = 7, Something = "Old" },
   6:                  };
   7:  
   8: JsonResult result = new JsonResult { Data = values };
   9:  
  10: IDictionary<string, object>[] data = ((object[]) result.Data).Select(o => new RouteValueDictionary(o)).ToArray();
  11:  
  12: Assert.AreEqual(5, data[0]["Id"]);
  13: Assert.AreEqual(6, data[1]["Id"]);
  14: Assert.AreEqual(7, data[2]["Id"]);
  15: Assert.AreEqual("Else", data[0]["Something"]);
  16: Assert.AreEqual("New", data[1]["Something"]);
  17: Assert.AreEqual("Old", data[2]["Something"]);

Again, easy!

Alright, I know what you’re thinking.  “But Matt, the other solutions are all way more complicated, plus you’re cheating, that isn’t what JsonResult.ExecuteResult is going to do!” Well, you’re half-right, the other solutions are way more complicated, but this is actually simulating precisely what ExecuteResult will do.  Don’t believe me?  Pop it open in Reflector, or just browse the source (man I love Subversion).  It isn’t doing anything magical, it’s just using JavaScriptSerializer.  My solution just cuts out the middle man and doesn’t require you to mock out a bunch of complicated objects.

Tags:

Feb 6 2009

ASP.NET MVC: Good in a lot of ways, bad in others

Category: ASP.NET | MVCMatt @ 10:06

I have spent the better part of a week now trying to encapsulate a jqGrid control into something that could be cleanly and easily reused from various ASP.NET views.  In some ways, I think I have met those goals, but in others, I think I have failed miserably.  On the plus side, actually creating a grid is quite easy:

   1: <%=Html.JQGrid("treeTabel",
   2:                 new JQGridOptions 
   3:                     { Caption="Components",
   4:                       DataUrl = Html.BuildUrlFromExpression<SandboxController>(c => c.JQGridTreeViewData((int)ViewData["EstimateId"], null)), 
   5:                       PagerId="PagerId", 
   6:                       IsTreeGrid = true,
   7:                       CellEditEnabled = true,
   8:                     },
   9:                 new[]
  10:                     {
  11:                         new JQGridColumn("id", "ID") { Visible = true, IsExpandColumn = true, Editable = false},
  12:                         new JQGridColumn("ComponentId", "ComponentId") { Visible = false, Editable = false},
  13:                         new JQGridColumn("Name", "Component"),
  14:                         new JQGridColumn("HistoricalCost", "Historical"), 
  15:                         new JQGridColumn("EstimatedCost", "Estimated"), 
  16:                         new JQGridColumn("TargetCost", "Target"),
  17:                         new JQGridColumn("Risk", "Risk") { EditType = "select" ,EditOptions = new[] { "Unassigned:Unassigned", "Low:Low", "Medium:Medium", "High:High"}}, 
  18:                     }) %>

That simple (well, sort-of simple), type-safe code produces this horrible mass of HTML and JavaScript:

   1: <script type='text/javascript'>
   1:  
   2: jQuery(document).ready(function(){
   3: jQuery('#treeTabel').jqGrid({
   4: url: '/Sandbox/JQGridTreeViewData/2',
   5: datatype: 'json',
   6: height: '475px',
   7: colNames:['ID','ComponentId','Component','Historical','Estimated','Target','Risk'],
   8: colModel:[
   9: {name:'id',index:'id',sortable:false,width:1},
  10: {name:'ComponentId',index:'ComponentId',sortable:false,width:1,hidden:true},
  11: {name:'Name',index:'Name',editable:true,sortable:false,width:1},
  12: {name:'HistoricalCost',index:'HistoricalCost',editable:true,sortable:false,width:1},
  13: {name:'EstimatedCost',index:'EstimatedCost',editable:true,sortable:false,width:1},
  14: {name:'TargetCost',index:'TargetCost',editable:true,sortable:false,width:1},
  15: {name:'Risk',index:'Risk',editable:true,sortable:false,width:1,edittype:'select',editoptions:{value:'Unassigned:Unassigned;Low:Low;Medium:Medium;High:High'}}
  16: ],
  17: pager: jQuery('#PagerId'),
  18: rowNum:25,
  19: rowList: [10,25,50,100],
  20: imgpath: '/Content/jQueryPlugins/JQGrid/themes/steel/images',
  21: width:(document.body.clientWidth)*(7/10),
  22: shrinkToFit: true,
  23: caption: 'Components',
  24: loadonce: false,
  25: treeGrid: true,
  26: ExpandColumn: 'id',
  27: treeGridModel: 'adjacency',
  28: cellEdit: true,
  29: cellsubmit: 'clientArray'
  30: }).navGrid('#PagerId',{edit:false,add:false,del:false,search:false,refresh:false})
  31: ;});
</script>
   2: <table id='treeTabel' class='scroll'></table>
   3: <div id='PagerId' class='scroll' style='text-align:center;'></div>

Obviously that's a step up.  But when you look under the covers, things are anything but clean and neat.  Basically, I have several methods that build up a bunch of JavaScript strings, then spit them back out.  It reminds me *so* much of old-school PHP, where you have this awful mix of presentation markup and PHP code interwoven together into a blanket that looks like someone threw up on it.  Here's an excerpt:

   1: if (options.IsTreeGrid)
   2: {
   3:     JQGridColumn expandColumn = columns.FirstOrDefault(c => c.IsExpandColumn);
   4:  
   5:     if (expandColumn == null)
   6:     {
   7:         throw new InvalidOperationException("IsTreeGrid is true, but no column found with IsExpandColumn set to true.");
   8:     }
   9:  
  10:     js.AppendLine("treeGrid: true,");
  11:     js.AppendFormat("ExpandColumn: '{0}',", expandColumn.Name).AppendLine();
  12:     js.AppendLine("treeGridModel: 'adjacency',");
  13: }
  14:  
  15: //If cell editing is enabeled, the changed rows are stored client-side,
  16: //and it is the responsibility of the page to provide a mechanism
  17: //for posting the data back.
  18: if (options.CellEditEnabled)
  19: {
  20:     js.AppendLine("cellEdit: true,");
  21:     js.AppendLine("cellsubmit: 'clientArray',");
  22: }

So, I'm torn.  Overall, I think the ASP.NET MVC approach is much, much better than the WebForms approach, but... is this it?  Is this really the best we can do?  There has to be a better way to do things like this.  There has to be a way to keep languages separate.  There has got to be a cleaner way to encapsulate and reuse "controls" in ASP.NET MVC.  I just haven't found it yet.

Tags:

Oct 13 2008

Creating a reusable GridTreeView with ASP.NET MVC and jQuery, take two

Category: MVCMatt @ 05:21

In my last post, I created a partial view page that rendered a collapsible gridview (which I now call a GridTreeView) using the MVCContrib Grid HtmlHelper extension and the jQuery  ActsAsTreeTable plug-in.  While the code works, there are a few drawbacks.  First, I completely forgot about having to link the CSS file in to the view.  That's doable using the view codebehind, but I don't like codebehinds in MVC.  The second drawback is that it doesn't follow the "standard" method of rendering controls in ASP.NET MVC.  All the built-in controls are available via the HtmlHelper object, and that's how I'd like to expose the GridTreeView as well.  Fortunately, it isn't too terribly difficult to do!

First, let's define the HtmlHelper extension method (the final version of this, which I will eventually post online, includes various overloaded versions of the method):

   1: /// <summary>
   2: /// Creates an MvcContrib Grid with the power of the jQuery table-as-tree plug-in.
   3: /// </summary>
   4: /// <param name="dataSource">The data source to display.</param>
   5: /// <param name="attributes">Any HTML attributes to add to the opening table tag.</param>
   6: /// <param name="helper">The helper.</param>
   7: /// <param name="gridId">The ID to assign to the generated table.</param>
   8: /// <param name="idSelector">An expression that selects the ID from an item.</param>
   9: /// <param name="parentIdSelector">An expression that selects the parent ID from an item.</param>
  10: /// <param name="columns">An expression that generates the columns.</param>
  11: /// <param name="sections">An expression that modifies how sections are emitted (such as changing the 
  12: /// opening tags for rows, columns, etc).</param>
  13: public static void GridTreeView<T>(this HtmlHelper helper, string gridId, IEnumerable<T> dataSource, 
  14:     Action<IRootGridColumnBuilder<T>> columns, Func<T, string> idSelector, Func<T, string> parentIdSelector,
  15:     IDictionary attributes, Action<IGridSections<T>> sections) where T : class
  16: {
  17:     GridColumnBuilder<T> builder = new GridColumnBuilder<T>();
  18:  
  19:     if (columns != null)
  20:     {
  21:         columns(builder);
  22:     }
  23:  
  24:     if (sections != null)
  25:     {
  26:         sections(builder);
  27:     }
  28:  
  29:     GridTreeView<T> grid = new GridTreeView<T>(gridId, dataSource, builder,
  30:                                                idSelector, parentIdSelector, attributes, 
  31:                                                helper.ViewContext.HttpContext.Response.Output,
  32:                                                helper);
  33:  
  34:     grid.Render();
  35: }

Let's talk through the parameters real quick, because some of them are fairly nasty.

  1. helper - By prefixing it with the "this" keyword, this method becomes an extension method for HtmlHelper instances, which is what we want. 
  2. gridId - This will be used as the "id" attribute for the table that's created by the control.
  3. dataSource - This is any enumerable type.  Items from this data source will be used to build the rows of the grid.
  4. columns - Ok, this one is complicated.  It is an Action delegate that takes an IRootGridColumnBuilder.  What that means is that the caller must specify a delegate (or lambda) that uses an IRootGridColumnBuilder to define the columns for the grid.  This is straight out of the Grid helper from MVCContrib, so go here if you want more info.
  5. idSelector - This is a delegate (or lambda) that will be called when the grid is being built.  The delegate will receive an object from the dataSource as input, and it must return a string representing the ID of the object as output.  This is half of what is used to tie parent/child rows together.
  6. parentIdSelector - This is the other half.  This delegate (or lambda) must return the parent ID of the instance that is passed to it.
  7. attributes - This is a simple key/value pair of attributes to assign to the <table> element of grid.
  8. sections - You can use this to override how the grid is rendered.  For more information, go here.

That's sounds pretty complicated, but it really isn't.  Here's how you could use it (I have omitted the markup code for clarity):

   1: Html.GridTreeView("MyGridTree", ViewData.Model,
   2:                       column =>
   3:                       {
   4:                           column.For(w => w.Name);
   5:                           column.For(w => w.Description);
   6:                           column.For(w => Html.TextBox("Description_" + w.Id, w.Description), "Editable").DoNotEncode();
   7:                       },
   8:                        w => w.Id.ToString(), w => w.ParentId.ToString(),
   9:                        new Hash(style => "width: 100%"),null
  10:                ); 

It may look a little intimidating, but it's actually quite simple.  An IEnumerable containing Widgets is passed in via the ViewData.Model property. Next, you can see the lambda expression that creates columns (one bound to the widget name, one bound to the description, and a text box that is also bound to the description), followed by two lambdas that deal with IDs.

Ok, so I've shown you how to define the extension method, and I've shown you how to call the method, now I need to show you how to implement the GridTreeView class.  Let's dive into the code:

   1: /// <summary>
   2: /// An extension of <see cref="Grid{T}"/> that adds
   3: /// jQuery ActsAsTree functionality.
   4: /// </summary>
   5: /// <typeparam name="T">The type of the data item being displayed in the grid.</typeparam>
   6: public class GridTreeView<T> : Grid<T> where T : class
   7: {
   8:     #region Const Fields
   9:  
  10:     /// <summary>
  11:     /// The index into the HttpContext.Items bag, it tracks whether or not
  12:     /// the includes for this control have already been written to the the
  13:     /// response.
  14:     /// </summary>
  15:     private const string mItemKey = "GridViewTree.Initialized";
  16:  
  17:     /// <summary>
  18:     /// The default path to the ActsAsTree javascript file.
  19:     /// </summary>
  20:     private const string mDefaultJavaScriptPath = "~/Content/jQueryPlugins/ActsAsTreeTable/jquery.acts_as_tree_table.js";
  21:  
  22:     /// <summary>
  23:     /// The default path to the ActsAsTree CSS file.
  24:     /// </summary>
  25:     private const string mDefaultCssPath = "~/Content/jQueryPlugins/ActsAsTreeTable/stylesheets/jquery.acts_as_tree_table.css";
  26:  
  27:     #endregion
  28:  
  29:     #region Private Delegates
  30:  
  31:     /// <summary>
  32:     /// The function that selects the parent ID from an item.
  33:     /// </summary>
  34:     private readonly Func<T, string> GetParent;
  35:  
  36:     /// <summary>
  37:     /// The function that selects the ID from an item.
  38:     /// </summary>
  39:     private readonly Func<T, string> GetId;
  40:  
  41:     #endregion
  42:  
  43:     #region Private Fields
  44:  
  45:     /// <summary>
  46:     /// The DOM ID to assign to the grid.
  47:     /// </summary>
  48:     private readonly string mGridId;
  49:  
  50:     /// <summary>
  51:     /// The HTML helper class, which is used to resolve URLs.
  52:     /// </summary>
  53:     private readonly HtmlHelper mHelper;
  54:  
  55:     #endregion
  56:  
  57:     #region Public Static Properties
  58:  
  59:     /// <summary>
  60:     /// The path to the JavaScript ActsAsTree file.
  61:     /// </summary>
  62:     /// <remarks>
  63:     /// This shouldn't need to be changed, but just in case, you can override it by changing this property.
  64:     /// </remarks>
  65:     public static string JavaScriptPath { get; set; }
  66:  
  67:     /// <summary>
  68:     /// The path to the CSS file that needs to be included for the tree to display correctly.
  69:     /// </summary>
  70:     /// <remarks>
  71:     /// This shouldn't need to be changed, but just in case, you can override it by changing this property.
  72:     /// </remarks>
  73:     public static string CssPath { get; set; }
  74:  
  75:     #endregion
  76:  
  77:     #region Static Constructors
  78:  
  79:     /// <summary>
  80:     /// Initializes the static fields to default values.
  81:     /// </summary>
  82:     static GridTreeView()
  83:     {
  84:         JavaScriptPath = mDefaultJavaScriptPath;
  85:         CssPath = mDefaultCssPath;
  86:     }
  87:  
  88:     #endregion
  89:  
  90:     #region Public Constructor
  91:  
  92:     /// <summary>
  93:     /// Creates a new GridTreeView class.
  94:     /// </summary>
  95:     /// <param name="dataSource"></param>
  96:     /// <param name="columnBuilder"></param>
  97:     /// <param name="htmlAttributes"></param>
  98:     /// <param name="output"></param>
  99:     /// <param name="helper"></param>
 100:     /// <param name="idSelector">A delegate that returns an ID from a T.</param>
 101:     /// <param name="parentIdSelector">A delegate that returns a parent ID from a T.</param>
 102:     /// <param name="gridId">The ID to assign to the grid.</param>
 103:     public GridTreeView(string gridId, IEnumerable<T> dataSource, GridColumnBuilder<T> columnBuilder, Func<T, string> idSelector, Func<T, string> parentIdSelector, IDictionary htmlAttributes, TextWriter output, HtmlHelper helper) : base(dataSource, columnBuilder, htmlAttributes, output, (helper == null ? null : helper.ViewContext.HttpContext))
 104:     {
 105:         GetParent = parentIdSelector;
 106:         GetId = idSelector;
 107:         mGridId = gridId;
 108:         mHelper = helper;
 109:  
 110:         //Override the ID if it has been set
 111:         HtmlAttributes["id"] = gridId;
 112:     }
 113:  
 114:     #endregion
 115:  
 116:     #region Private Methods
 117:  
 118:     /// <summary>
 119:     /// Renders the row with the default ActsAsTree functionality.
 120:     /// </summary>
 121:     /// <param name="item"></param>
 122:     /// <param name="isAlternate"></param>
 123:     private void RenderActsAsTreeRow(T item, bool isAlternate)
 124:     {
 125:         string row = string.Format("<tr class=\"{0} child-of-node-{1}\" id=\"node-{2}\">",
 126:                         isAlternate ? "gridrow_alternate" : "gridrow",
 127:                         GetParent(item), GetId(item));
 128:  
 129:         RenderText(row);
 130:     }
 131:  
 132:     /// <summary>
 133:     /// Writes the tags to include the required ActsAsTree javascript file.
 134:     /// </summary>
 135:     private void WriteJavaScriptInclude()
 136:     {
 137:         const string script = @"<script type=""text/javascript"" src=""{0}""></script>";
 138:  
 139:         RenderText(string.Format(script, ResolveUrl(JavaScriptPath)));
 140:     }
 141:  
 142:     /// <summary>
 143:     /// Writes the JavaScript to initialize the grid as an ActsAsTree grid.
 144:     /// </summary>
 145:     private void WriteActsAsTreeJavaScript()
 146:     {
 147:         const string script =
 148:             @"<script type=""text/javascript"">
 149:             $(document).ready(function()  {{
 150:                 $(""#{0}"").acts_as_tree_table();
 151:             }});
 152:             </script>";
 153:  
 154:         RenderText(string.Format(script, mGridId));
 155:     }
 156:  
 157:     /// <summary>
 158:     /// Writes some HTML to do a dynamic CSS include.
 159:     /// </summary>
 160:     private void WriteCssInclude()
 161:     {
 162:         const string script =
 163:             @"<script type='text/javascript'>
 164:         var link=document.createElement('link');  
 165:         link.setAttribute('rel', 'stylesheet');  
 166:         link.setAttribute('type', 'text/css');  
 167:         link.setAttribute('href', '{0}');
 168:         var head = document.getElementsByTagName('head')[0];  
 169:         head.appendChild(link);
 170:         </script>";
 171:  
 172:         RenderText(string.Format(script, ResolveUrl(CssPath)));
 173:     }
 174:  
 175:     /// <summary>
 176:     /// Resolves a URL if an HtmlHelper instance is available, otherwise
 177:     /// just returns the URL.
 178:     /// </summary>
 179:     /// <param name="url"></param>
 180:     /// <returns></returns>
 181:     private string ResolveUrl(string url)
 182:     {
 183:         return mHelper != null ? mHelper.ResolveUrl(url) : url;
 184:     }
 185:  
 186:     #endregion
 187:  
 188:     #region Protected Overrides
 189:  
 190:     /// <summary>
 191:     /// Renders the row.
 192:     /// </summary>
 193:     /// <param name="item"></param>
 194:     /// <param name="isAlternate"></param>
 195:     protected override void RenderRowStart(T item, bool isAlternate)
 196:     {
 197:         //If there's a custom delegate for rendering the start of the row, invoke that instead.
 198:         if (Columns.RowStartBlock != null)
 199:         {
 200:             Columns.RowStartBlock(item);
 201:         }
 202:         else if (Columns.RowStartWithAlternateBlock != null)
 203:         {
 204:             Columns.RowStartWithAlternateBlock(item, isAlternate);
 205:         }
 206:         else
 207:         {
 208:             RenderActsAsTreeRow(item, isAlternate);
 209:         }
 210:     }
 211:  
 212:     #endregion
 213:  
 214:     #region Public Methods
 215:  
 216:     /// <summary>
 217:     /// Renders the grid along with all required scripts and resources.
 218:     /// </summary>
 219:     public override void Render()
 220:     {
 221:         //Include the required CSS/JavaScript if this is the first tree we are rendering.
 222:         if (Context == null || Context.Items[mItemKey] == null)
 223:         {
 224:             //This checks for null first to enable unit testing.
 225:             if (Context != null) Context.Items[mItemKey] = true;
 226:  
 227:             WriteCssInclude();
 228:             WriteJavaScriptInclude();
 229:         }
 230:  
 231:         //Render the jQuery script to initialize the GridTree.
 232:         WriteActsAsTreeJavaScript();
 233:  
 234:         //Render the GridTree.
 235:         base.Render();
 236:     }
 237:  
 238:     #endregion
 239:  
 240: }

That's quite a bit of code, but most of it is straight forward.  There are static properties and fields that you can change based on where the jQuery plug-in is installed.  The constructor doesn't do much other than grab references to the parameters so that they can be used later.  The neat stuff starts in the overridden methods from Grid. 

First, Render checks to see if the necessary JavaScript and CSS files have already been included in the page.  If not, they are included using helper methods (more on those in a sec).  Next, the JavaScript to turn the grid into a GridTreeView is written.  Finally, the actual grid rendering is delegated back to the base class.  The only deviation from the standard behavior is in how the row start (tr) tags are written.  The RenderRowStart method is overridden, and for the most part, it behaves exactly like the base class.  If a caller has chosen to override how rows should be rendered, the specified delegates are called.  Otherwise, the helper RenderActsAsTreeRow method is called.  This method renders a tr tag with the required class and ID attributes.  It does this using the delegates that were passed in to the GridTreeView constructor. 

There are a couple of other helper methods that are worth mentioning.  First, there is a helper ResolveUrl method.  This exists to facilitate unit testing while keeping the code clean.  If an HtmlHelper instance was passed to GridTreeView, the call is routed to it, otherwise the method just returns the original URL.  Second, the WriteCssInclude method may appear unnecessarily complex upon first inspection.  Why does it write JavaScript that creates a link element instead of just emitting the link element?  Stylesheet includes are supposed to go in the HTML header of a page.  HtmlHelper extensions do not have access to the HTML header, so they cannot add CSS to the page correctly.  The JavaScript works around that limitation by inserting the link element into the head even though the JavaScript could potentially be written anywhere in the DOM.

So, that's basically it.  If there is sufficient interest, I'll wrap everything up in a standalone library (with source code) that can be reused.  If you want it, leave me a note in the comments.

Tags:

Oct 8 2008

Creating a reusable grid tree view with ASP.NET MVC and jQuery

Category: ASP.NET | JavaScript | MVCMatt @ 08:45

I think it is a safe assumption that every web developer has had to display tabular data at one point or another.  Tabular data is easy with ASP.NET: bind a GridView to a data source, and you're all set.  But with ASP.NET MVC, things are a little trickier.  We don't have access to all the nice WebForms controls.  Still, it's fairly easy to do: just write a for-loop, or better yet, use the grid helper from MvcContrib.

Things get a trickier though if your tabular data is also hierarchical.  Typically, we display hierarchical data in a tree of some kind, but trees really aren't great for tabular data.  What would be great is to combine the two somehow.  Fortunately, there's a nice plug-in for jQuery that does just that: ActsAsTreeTable.  It's easy enough to use; all you have to do is embed ID's and CSS class information in your table rows, and the JavaScript does everything else.  Here's a simple example from the docs:

   1: <link href="path/to/jquery.acts_as_tree_table.css" rel="stylesheet" type="text/css" />
   2: <script type="text/javascript" src="path/to/src/jquery.acts_as_tree_table.js"></script>
   1:  
   2: <script type="text/javascript">
   3:     
   4: $(document).ready(function()  {
   5:     $("#your_table_id").acts_as_tree_table();
   6: });
</script>
   3:  
   4: ...
   5:  
   6: <table id="tree">
   7:   <tr id="node-1">
   8:     <td>Parent</td>
   9:   </tr>
  10:   <tr id="node-2" class="child-of-node-1">
  11:     <td>Child</td>
  12:   </tr>
  13: </table>

We can now combine this with the grid from MvcContrib to produce a collapsable Grid Tree View.  This example is encapsulated inside a view user control so that it can be used on any page.  It displays imaginary "widgets" in a tree.  The widgets aren't really hierarchical, so I've fudged it by making it appear that each widget is a child of its predecessor in the table.

First, let's create the grid:

   1: <%
   1:  Html.Grid(GetWidgets(), new Hash(id => ClientID, style => "width:100%"), 
   2: column =>
   3:       {
   4:           column.For(w => w.Name);
   5:           column.For(w => w.Description);
   6:           column.For(w => Html.TextBox("Description_" + w.Id, c.Description), "Editable").DoNotEncode();
   7:       },
   8: sections =>
   9:     {
  10:         sections.RowStart(c =>
  11:                               {
%> <tr class="child-of-node-<%=w.Id - 1%>" id="node-<%=w.Id%>"> <%
   1:  
   2:                               });
   3:     }    ); 
%>

That probably looks horrendous, so let's walk through it.  GetWidgets() is a method on the view user control that grabs widgets from wherever (in practice, probably the model or view data).  Next, the Hash just contains key/value pairs that are embedded in the opening table tag; here, we've specified the table's ID (by using ClientID, it will have the name that ASP.NET gives to the user control), and we've specified that it should be 100% wide.  Next, we define the columns using lambda expressions.  The first two columns simply display the widget's name and description.  The last column is a little more complicated.  It creates a text box using the TextBox helper method.  Since the column contains HTML that shouldn't be encoded, we call DoNotEncode on it.  Finally, we use a lambda expression to override how rows are created.  The code here populates the row with the 'child-of-node-#' class and the id attribute, both of which are needed by ActsAsTreeTable.  It may look intimidating, but it's actually nice once get comfortable with the syntax. 

The last thing we need to do is spit out the JavaScript to turn our gird into an ActsAsTreeTable:

   1: <script type="text/javascript">
   2:         $(document).ready(function()  {
   3:             $("#<%=ClientID %>").acts_as_tree_table();
   4:         });
   5: </script>

If you set everything up correctly, you should now have a working "grid tree view".  In a future article, I'll introduce a new Html helper that does all the heavy lifting for you.

Tags: