February 2009 Entries

Well I am heading out to Myrtle Beach tonight at 4am, so I spent today finishing up some cleanup so I could release DoddleImport on CodePlex. It should be pretty feature complete and ready for use, and as always, please let me know if it helps you out!

Objective

Flexible importing of data via a fluid and fully extensible API to easily add support for new Import Sources and Import Destinations.

I will update the documentation and provide some better examples when I get back. Here you can see a quick example of me importing some Excel data into a SharePoint list. As you can see it supports a variety of validation including missing fields and data type conversion problems.

image 

image

Key Components

Importing relies on 2 distinct (and entirely extensible) mechanisms:

  • IImportSource - The import framework requires a set of row/field data provided via an IImportSource
  • IImportDestination - The import framework requires a destination to write the data to via an IImportDestination
Note: Most "importable" classes implement both IImportSource as well as IImportDestination, as such, can be used as a source of import rows and a destination to import into

Import Sources and Destinations

DoddleImport ships with some basic Import Sources, but just like all other Doddle Projects, you can easily create your own by implementing a simple interface.
  • ImportableCollection - provides importing to and from a generic list
  • ImportableDictionary - provides importing to and from a basic dictionary
  • ImportableSPList - provides importing to and from a SharePoint List
  • Spreadsheet - provides importing from an Excel Spreadsheet

Usage

// Basic import from one type of in-memory collection to another
ImportableDictionary source = new ImportableDictionary();
source.Fields.Add("ProductID", typeof(int));
source.Fields.Add("ProductName", typeof(string));

IDictionary row1 = source.AddRow();
row1["ProductID"] = 1;
row1["ProductName"] = "My Product";

List products = new List<Product>();
IImportDestination destination = new ImportableCollection<Product>(products);

Importer importer = new Importer();
importer.Import(source, destination);
// More realistic example, importing an Excel Spreadsheet into a SharePoint list
Stream uploadedFile = fileUpload.PostedFile.InputStream;
Spreadsheet spreadsheet = new Spreadsheet(uploadedFile);

SPList myList = Web.Lists["My List"];
IImportDestination destination = new ImportableSPList(myList);

Importer importer = new Importer();
importer.Import(spreadsheet, destination);

Validation

DoddleImport will provide automatic validation of your Import Source against the specified destination. This ensures that the entire import contents will be successfully imported as well as provide helpful error messages to your user so they can easily correct errors.

Validation Rules

The validation mechanism works by automatically evaluating rules against each row being imported. This is handled through the IValidationRule interface.
  • The default rules with DoddleImport are as follows:
    • RequiredFieldsRule - validates a row to make sure that data was provided for all required fields
    • MissingHeadersRule - validates that the source contains all required fields that the destination expects
    • DataTypeValidationRule - valdiates data type mis-match errors

Configuration

If you want to change some of th default behavior of DoddleImport then using your application configuration file is the best place to start.

To use configuration, be sure to register the <section> node between <configSections> in your app.Config or web.Config

From there, you can add or remove validation rules that will be automatically applied to every import. You are also able to customize the default validation messages. Below is a quick sample of some of the changes that can be made.

<configuration>
  <configSections>
    <section name="doddleImport" 
          type="Doddle.Import.Configuration.ImportSection, Doddle.Import, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6f5f0fd458d019c9" />
  </configSections>
  <doddleImport>
    <validation>
      <rules>
        <remove name="MissingHeadersRule" />
        <add name="MyCustomRule" type="MyName.Importing.CustomValidationRule, MyName.Importing" />
      </rules>
      <messages>
        <remove rule="RequiredFieldsRule" />
        <add rule="RequiredFieldsRule" message="Unable to locate field '{0}'"/>
      </messages>
    </validation>
  </doddleImport>
</configuration>

That's all for now, check back for updates on this and some other projects.

Get the Source and Try it out!


Check it out at CodePlex

Technorati Tags: ,

A project I am now working on is using the Web Client Software Factory (WCSF) from the patterns & practices team. It is basically an elaborate MVP-based framework that currently sits on top of ASP.NET Webforms.

One of the components it ships with is the ObjectContainerDataSource which is a lot like the ObjectDataSource that ASP.NET provides. I admittedly have little experience with either of these controls, since I am not a huge fan of declarative data binding in larger applications. It does however offer better integration with the MVP pattern, simplifying the “bubbling” of DataSource events up to the Presenter to handle them. The Presenter is of course where all of the logic for the view and its services belongs.

At first everything was working great: selects, server-side paging, server-side sorting, even inserting. But when I tried some Updates and Deletes with the GridView and the ObjectContainerDataSource I was encountering a frustrating exception I hadn’t seen before.

Row not found or changed.

The exception was being thrown on the dataContext.SubmitChanges(). After some quick digging I found that this error is due to our Timestamp column which is used by the LinqDataContext for optimistic concurrency. Before the Data Source fires its Updated event, it re-creates an instance of the object and populates the properties with the values in the data control using reflection. However, the ObjectContainterDataSource was not setting the Timestamp property when it was re-constructing the object for updating. This was causing the LinqDataContext to throw the “Row not Found” exception because it could not find a row WHERE ContractorId = 1 AND Timestamp = NULL. I needed some way to persist the Timestamp property within the DataSource so it would re-populate it on the Updated event.

exec sp_executesql N'UPDATE [dbo].[Contractors] 
SET [ContractorName] = @p2 WHERE ([ContractorId] = @p0) AND ([Timestamp] = @p1) 

@p0=1,
@p1=NULL,
@p2='My Updated Contractor'

The solution I found, was to include the Timestamp column in the DataKeyNames property. This ensured that the data source would correctly populate the property even though it did not appear as a column in my GridView.

<asp:GridView ID="ContractorGrid" runat="server" 
    AutoGenerateEditButton="true" AutoGenerateDeleteButton="true"
    DataSourceID="ContractorDataSource" DataKeyNames="ContractorId, Timestamp"
    PageSize="5" AllowPaging="true" AllowSorting="true" AutoGenerateColumns="false">

This may be a well-known problem with LINQ to SQL and the DataSources, but as I mentioned I don’t use them that much for declarative data binding. Either way, hopefully this saves someone frustration down the road.

Technorati Tags: ,

Update 2/10/2009: I have updated the helper code a little bit to include an ID parameter as well as to inject the jQuery treeview script code automatically. Please modify to your desire, or make sure to include the jQuery TreeView plugin script to your page before running.

The following helper will make it easy to create a tree view from a recursive self-referencing table. Below you are seeing a tree of “Locations” where each Location can contain X number of child locations.

Dependencies

 jQuery TreeView Plugin


Rendered Tree

image

Table Definition

The table itself is extremely simple, each Location has a ParentLocationId which is a relationship to the same table. If the ParentLocationId is null then it is a root location.

 image image


Usage

Simple

<%= Html.TreeView("locations", 
    Model.Locations, 
    l => l.ChildrenLocations, 
    l => l.Name) %>

Wrapping a div around each list item

<%= Html.TreeView("dropTree", 
    Model.Locations, 
    l => l.ChildrenLocations, 
    l => "<div class='dropZone'>" + l.Name + "<div>")

Making each list item an ActionLink

<%= Html.TreeView("dropTree", 
    Model.Locations, 
    l => l.ChildrenLocations, 
    l => Html.ActionLink("MyController", "MyAction", l.Name, new { id = l.Name })

The Code

public static class TreeViewHtmlHelper
{
    /// <summary>
    /// Create a TreeView of nodes starting from a root element
    /// </summary>
    /// <param name="treeId">The ID that will be used when the ul is created</param>
    /// <param name="rootItems">The root nodes to create</param>
    /// <param name="childrenProperty">A lambda expression that returns the children nodes</param>
    /// <param name="itemContent">A lambda expression defining the content in each tree node</param>
    public static string TreeView<T>(this HtmlHelper html, string treeId, IEnumerable<T> rootItems, Func<T, IEnumerable<T>> childrenProperty, Func<T, string> itemContent)
    {
        return html.TreeView(treeId, rootItems, childrenProperty, itemContent, true, null);
    }

    /// <summary>
    /// Create a TreeView of nodes starting from a root element
    /// </summary>
    /// <param name="treeId">The ID that will be used when the ul is created</param>
    /// <param name="rootItems">The root nodes to create</param>
    /// <param name="childrenProperty">A lambda expression that returns the children nodes</param>
    /// <param name="itemContent">A lambda expression defining the content in each tree node</param>
    /// <param name="includeJavaScript">If true, output will automatically render the JavaScript to turn the ul into the treeview</param>    
    public static string TreeView<T>(this HtmlHelper html, string treeId, IEnumerable<T> rootItems, Func<T, IEnumerable<T>> childrenProperty, Func<T, string> itemContent, bool includeJavaScript)
    {
        return html.TreeView(treeId, rootItems, childrenProperty, itemContent, includeJavaScript, null);
    }

    /// <summary>
    /// Create a TreeView of nodes starting from a root element
    /// </summary>
    /// <param name="treeId">The ID that will be used when the ul is created</param>
    /// <param name="rootItems">The root nodes to create</param>
    /// <param name="childrenProperty">A lambda expression that returns the children nodes</param>
    /// <param name="itemContent">A lambda expression defining the content in each tree node</param>
    /// <param name="includeJavaScript">If true, output will automatically render the JavaScript to turn the ul into the treeview</param>
    /// <param name="emptyContent">Content to be rendered when the tree is empty</param>
    /// <param name="includeJavaScript">If true, output will automatically into the JavaScript to turn the ul into the treeview</param>    
    public static string TreeView<T>(this HtmlHelper html, string treeId, IEnumerable<T> rootItems, Func<T, IEnumerable<T>> childrenProperty, Func<T, string> itemContent, bool includeJavaScript, string emptyContent)
    {
        StringBuilder sb = new StringBuilder();

        sb.AppendFormat("<ul id='{0}'>\r\n", treeId);

        if(rootItems.Count() == 0)
        {
            sb.AppendFormat("<li>{0}</li>", emptyContent);
        }

        foreach (T item in rootItems)
        {
            RenderLi(sb, item, itemContent);
            AppendChildren(sb, item, childrenProperty, itemContent);
        }

        sb.AppendLine("</ul>");

        if (includeJavaScript)
        {
            sb.AppendFormat(
                @"<script type='text/javascript'>
                    $(document).ready(function() {{
                        $('#{0}').treeview({{ animated: 'fast' }});
                    }});
                </script>", treeId);
        }

        return sb.ToString();
    }

    private static void AppendChildren<T>(StringBuilder sb, T root, Func<T, IEnumerable<T>> childrenProperty, Func<T, string> itemContent)
    {
        var children = childrenProperty(root);
        if(children.Count() == 0)
        {
            sb.AppendLine("</li>");
            return;
        }

        sb.AppendLine("\r\n<ul>");
        foreach (T item in children)
        {
            RenderLi(sb, item, itemContent);
            AppendChildren(sb, item, childrenProperty, itemContent);
        }

        sb.AppendLine("</ul></li>");
    }

    private static void RenderLi<T>(StringBuilder sb, T item, Func<T, string> itemContent)
    {
        sb.AppendFormat("<li>{0}", itemContent(item));            
    }
}

Technorati Tags: ,

I encountered a rather frustrating problem with jQuery UI tabs this week. The problem occurs when trying to tabify some content after using jQuery’s $().load method to load the tabs HTML via an ajax request.

The following code works perfectly in all browsers except IE6 and IE7 (It works fine in IE8 as well), as can be seen in the screenshot below.

$('#deviceDetailsWrapper').load('/device/details/294', null, function() {
    $('#tabs').tabs();
});

image

However, here is what renders in IE6/7. For some reason, the jQuery tabs are triggering a GET request of the current URL and loading the content into the tab itself. Below you are actually seeing the entire page loaded inside the tab. I discovered this by inspecting the DOM tree within the tab in Firebug, as well as loading up Fiddler and watching the tab call an entirely separate GET request to the current URL as soon as the tabs were loaded. Strange behavior to me, since I am not entirely familiar with the jQuery tabs.

image

The Solution

The only thing I discovered was different regarding the .tabs() DOM manipulation in the different browsers was the ID of the tab – instead of being the #tab-id that I named it, the element was being assigned the full URL+#id to the container. So “#tab1” was becoming “http://localhost/device#tab1” – which I believe was triggering the tabs to behave in their “ajax” mode, which is a built-in feature of the jQuery tabs.

The solution I found was opening up jquery.ui.all.js and locating the following code:

this.$tabs.each(function(i, a) {
var href = $(a).attr('href');

// inline tab
if (fragmentId.test(href))
    self.$panels = self.$panels.add(self._sanitizeSelector(href));
Replace it with the following:
this.$tabs.each(function(i, a) {
var href = $(a).attr('href');
            
// Fix tab IDs in IE6/7
href = href.substring(href.indexOf("#"));

// inline tab
if (fragmentId.test(href))
    self.$panels = self.$panels.add(self._sanitizeSelector(href)); 

Hopefully this helps someone in their googling of this problem. I will be logging this as a bug with the jQuery developers.