Friday, February 26, 2010

Web Part architecture - what I had to fix in ChartPart

An important aspect of web part architecture is "redeploy-ability".
Redeploy-ability is when you want to use the web part as part of a site template. When you develop a web part, you need to think of redeployment - since the web part will need to be deployed with a set of pre-configured settings into a new site - the web part's properties. If those properties cannot be set as part of the site template, the web part cannot be redeployed.

Let me use ChartPart as an example.
If you don't already know, there is a project called "ChartPart" in codeplex, that shows lists as charts:
chartpart2-1.png
The web part needs to store a lot of information in its properties when it gets configured. For example, it needs to store the X axis and the Y axis columns. For this, the developer created a property called "XAxisSourceColumns", of type "List<string>". That allowed him to add column names to the list easily in the code.
The problem with this, is that this property has to be stored with the web part, and is required for the web part to work - that means if I export the web part, then I expect that when I import it again, all the settings will be there. With a property of type "List<string>", this will not happen - because the web part system does not know how to serialize a "List<string>" object into XML (a .webpart file) and doesnt export those settings. Then, if you import the web part you just exported, you lost those settings. I recommend storing the information as a simple type (defining the properties as strings for example) for serialization purposes.

A second problem, and one that you can see with a lot of Microsoft web parts as well as ChartPart (for example the Content Query Web Part) is that when a web part is connected to a list, the property stores the list's ID - a GUID, instead of the list name. This is smart - because someone may rename the list, and the ID will remain the same, so the web part will keep working. On the other hand - using an ID means that you lose "redeploy-ability" - since a list's ID gets generated randomly every time the site gets created, if your template has the web part set with a specific ID, then when you create a new site off that template, the web part will be broken - since it will not find a list with that ID in the site.
I recommend you store in the web part both the list's ID and the list's name. This way, if there is a list with the ID that was specified, the web part uses it, and therefore does not break when the list's title changes. But if there is no list with that ID (or no ID was set - which is what we will do in a template), the web part will look for a list with the name that was set - which means it will support redeploy-ability.

Finally, a problem with many web parts that allow linking to objects in other sites is that the site url property often expects a value - and does not support a "this site" option (which I like to add as a blank value). This means again that we lose "redeploy-ability" - since there is no way to configure the web part to look in this new site that was created from the template - the web part expects a link to a site...
I recommend supporting a "leave site url empty for current site" as a minimum requirement for a web part, and if you want to be real good, add support for relative URLs.

So how do we resolve this?


Usually we implement "redeploy-ability" when we start writing the web part - we create properties of type string that are not browse-able, and then create ToolParts that populate them using a good user interface - that gets the list of columns (in this example) from the user, then serializes it into a string that gets saved in the web part. It does mean a bit more effort to serialize the list and deserialize the string - but the end result is that when you export the web part you get what you expected - the serialized property gets stored in the .webpart file.

Supporting "blank" as current site url
In ChartPart to make this change was easy - I looked for all the places that SiteUrl property was used, and changed the code to check if the siteurl property is empty then to use the current site url. This is not as effective as you can make it (a function that gets a SPWeb object would have done it better) but it saved me a lot of refactoring:

string url = this.SiteUrl;
if (string.IsNullOrEmpty(url))
   url = SPContext.Current.Web.Url;
using (SPSite site = new SPSite(url)) 
{
   ...
}

Supporting list names as well as IDs
In ChartPart I added a property called ListName and then made the following modifications to all locations that referred to the list ID:

if(this.ListId != null && this.ListId != Guid.Empty)
   list = web.Lists[this.ListId];
else if(!string.IsNullOrEmpty(this.ListName))
   list = web.Lists[this.ListName];
I also changed the toolpart so that when a user picks a list from the dropdown, both the list ID and the list name gets stored.

Serializing complex type properties

In ChartPart for example, since it was already developed and I had to keep supporting existing web parts that store the information as a list, I couldnt remove the existing properties. However, what I did is add a string property for each one of the "List<T>" properties, that return the serialized value of the "List<T>" property it reflects, and when set it sets the "List<T>" property. Here is an example:

//the original property
[Personalizable(PersonalizationScope.Shared)]
public List XAxisSourceColumns
{
    get;
    set;
}
//this is for supporting exporting and importing the web part (which does not serialize List)
//we will have these as pipe seperated strings.
[Personalizable(PersonalizationScope.Shared)]
public string XAxisSourceColumnNames
{
    get
    {
        if (this.XAxisSourceColumns != null)
        {
            string ret = "";
            foreach (string xAxisColumn in this.XAxisSourceColumns)
            {
                if (!string.IsNullOrEmpty(ret))
                    ret += "|";
                ret += xAxisColumn;
            }
            return ret;
        }
        else
            return "";
    }
    set
    {
        this.XAxisSourceColumns = new List();
        if(!string.IsNullOrEmpty(value))
        {                 
            string[] values = value.Split('|');
            this.XAxisSourceColumns.AddRange(values);
        }                
            
    }
}

No comments: