Web Designer Friendly Web Parts, Take 2

April 12 2010 3 comments

We discussed an approach for creating Web Parts backed by a single user control in a previous post. While the approach works, it’s not quite perfect yet.

First, it requires repeating a bunch of boilerplate code that should be neatly abstracted away, if possible. Second, the part where the user control is configured should preferably be separated from the infrastructure code. Finally, we noticed that it doesn’t actually work quite right if you need to pass properties from the Web Part to the user control in edit mode.

That last part deserves some clarification. It turns out that the correct place to initialize the control is actually the OnPreRender event of the Web Part. The consequences of doing it in the wrong place aren’t that dire, just annoying: you have to click “apply” twice before the configuration changes actually kick in.

To address these issues, we can write a base class which I’ll opt to call VisualWebPart. The name was inspired by the Visual Studio 2010 template that generates code very similar to our previous approach.

Let’s start by assuming that this web part will always be backed by a single user control:

public abstract class VisualWebPart<TControl>
    : WebPart where TControl : UserControl
{
    private TControl childControl;
    protected TControl Control
    {
        get
        {
            EnsureChildControls();

            return childControl;
        }
        private set { childControl = value; }
    }
}

The first problem I’d like to tackle is the repetitive control loading code, so let’s introduce a method that does that:

private const string TEMPLATE_PATH = "~/_CONTROLTEMPLATES/";

private TControl LoadControl()
{
    var loadedControl = Page.LoadControl(
        Path.Combine(
            Path.Combine(TEMPLATE_PATH, controlRootPath),
            Path.ChangeExtension(typeof(TControl).Name, ".ascx")));
    if (loadedControl is PartialCachingControl)
    {
        loadedControl = ((PartialCachingControl)loadedControl).CachedControl;
    }

    if (loadedControl is TControl)
    {
        return (TControl)loadedControl;
    }

    throw new InvalidOperationException(
        "Attempted to load a control template " +
        "that doesn't match the control class.");
}

The snippet above loads the control with the assumption that a control with a class name of XxxUserControl will have a control template named XxxUserControl.ascx, a convention that should sit well with pretty much everyone. :-) It’s not quite ready yet, though, as it references controlRootPath, a field we haven’t defined yet. We decided to favor convention over configuration, and have the path to the control template be the namespace of the control by default. Since the convention might not work for every scenario, we also added the option of specifying the path via an alternate constructor:

private readonly string controlRootPath;

protected VisualWebPart() : this(typeof(TControl).Namespace) { }

protected VisualWebPart(string controlRootPath)
{
    this.controlRootPath = controlRootPath;
}

OK, so now we’ve got code to load the control. Let’s put it to use:

protected sealed override void CreateChildControls()
{
    Control = LoadControl();
    Controls.Add(Control);
}

This makes sure we will always load the control at the right time. Now, there’s one more thing to take care of: providing a way to configure the control at the correct point in the lifecycle of the Web Part:

protected event EventHandler<EventArgs> ControlConfiguring = delegate { };

protected sealed override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    ControlConfiguring(this, new EventArgs());
}

We define an event specifically for the purpose of setting control properties. Notice the initializer that adds an empty delegate as the first event handler — this ensures that we won’t have to perform a null check when invoking the event.

The jury is still out on whether we should seal the two lifecycle methods above or not — feel free to do otherwise, and please express your opinion in the comments! In the meantime, our shiny new base class is ready for use. Here’s the updated version of the previous Web Part example, using our new base class:

public class XxxWebPart : VisualWebPart<XxxUserControl>
{
    public XxxWebPart()
    {
        ControlConfiguring  += (sender, e) => {
            Control.MaxItems = 10;
            Control.SearchScope = "All sites";
        };
    }
}

Crisp, clean and no boilerplate code. Lovely!

If your control is good to go as soon as it’s loaded and requires no additional configuration, the code for the web part becomes even simpler:

public class XxxWebPart : VisualWebPart<XxxUserControl>
{

}

If the convention we chose for the control template paths doesn’t suit you, you can use the alternate constructor for the base class:

public class XxxWebPart : VisualWebPart<XxxUserControl>
{
    public XxxWebPart() : base(@"My\Control\Path") { }
}

That about wraps it up. Enjoy!

Popularity: 1% [?]

3 comments to “Web Designer Friendly Web Parts, Take 2”

  1. [...] Web Designer Friendly Web Parts, Take 2 | SharePoint Blues says: April 12, 2010 at 08:10 [...]

  2. [...] This post was mentioned on Twitter by Lauri Kotilainen. Lauri Kotilainen said: My first #sharepoint article is up at http://www.sharepointblues.com/2010/04/12/web-designer-friendly-web-parts-take-2/ [...]

  3. ophi says:

    Was googling so much for the problem “click apply twice” but could not find workable solution as none described it with reference to a user control. A simple tip by you to add user control in OnPreRender did the trick. Thanks a lot!

Leave a Reply