SharePoint picker ToolPart
SharePoint 2007 has built in pop-up pickers for selecting lists and sites as used in the Context Quey web part, however these are not automatically exposed to other web parts. Web parts which operate on list data often need to be able to allow users to select one or more lists or sites via their tool panes. Ideally, the default tool part would recognise SPSite and SPList properties and provide pop-up pickers automatically. Sadly this is not the case. However, it is still possible to leverage most functionality used by built in web parts such as the Content Query Web Part from your own custom tool pane.
Essentially what we’re going to do is create a tool part which is generic enough to be bound to any web part and allow selection of lists and sites using the built in picker functions in PickerTreeDialog.js. When completed simply overriding the GetToolParts method in your web part will be enough to create an extra tool part with as many list or site selectors as are required.
public override ToolPart[] GetToolParts()
{
List<ToolPart> parts = new List<ToolPart>();
PickerToolPart picker = new PickerToolPart();
parts.AddRange(base.GetToolParts());
picker.Items.Add(new PickerItem("Site to query", "TargetURL", PickerType.Site));
picker.Title = "Select data source";
parts.Add(picker);
return parts.ToArray();
}
In the example above we are mapping two custom properties- TargetURL and ListURL- to a site picker and list picker respectively. We are also retaining all other default tool parts because the generic PickerToolPart class we are going to write only deals with selection of sites and lists. Because we are retaining the default tool parts though we need to be careful to mark the properties we are setting with the Browsable(false) attribute to ensure that they do not appear twice (i.e. as a normal text box and as our custom picker).
[Browsable(false), Category("Miscellaneous"), DefaultValue(""),
WebPartStorage(Storage.Shared)]
public string TargetURL
{
get;
set;
}
First we need to create a PickerItem class. When the tool pane is set up, an instance of PickerItem is created for each mapping of a list or site to a property. These are passed into the Items collection of our PickerToolPart. The PickerItem class is accompanied by an enumeration which is used to determine which type of object (site or list) is being selected.
public enum PickerType
{
Site,
List
}
public class PickerItem
{
public string Title { private set; get; }
public string PropertyName { private set; get; }
public PickerType PickerType { private set; get; }
public PickerItem(string title, string propertyName, PickerType pickerType)
{
Title = title;
PropertyName = propertyName;
PickerType = pickerType;
}
}
Next we create our custom tool pane PickerToolPart. This tool part simply creates a text box and a button for each of the PickerItem instance in it’s Items collection. The button is used to call the built in SharePoint picker windows to allow the user to select a list or site. Once a selection has been made the associated text box is populated with the URL of the selected item.
public class PickerToolPart : ToolPart, INamingContainer
{
public List<PickerItem> Items { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PickerToolPart"/> class.
/// </summary>
public PickerToolPart()
: base()
{
Items = new List<PickerItem>();
}
/// <summary>
/// Registers the script to load the web selector.
/// </summary>
void RegisterPopupScript()
{
StringBuilder sb = new StringBuilder();
sb.Append("function cust_launchPickerSite(inputID)\r\n {\r\n if (!document.getElementById) return;");
sb.Append("var targetTextBox = document.getElementById(");
sb.Append("inputID");
sb.Append(");");
sb.Append("if (targetTextBox == null) return;");
sb.Append("var serverUrl = '");
sb.Append(SPContext.Current.Web.ServerRelativeUrl);
sb.Append("';var callbackSite = function(results){if (results == null || results[1] == null) return;targetTextBox.value = results[1];};\r\n");
sb.Append("LaunchPickerTreeDialog('CbqPickerSelectSiteTitle','CbqPickerSelectSiteText','websOnly','',serverUrl, '','','','/_layouts/images/smt_icon.gif','', callbackSite);}");
this.Page.ClientScript.RegisterClientScriptBlock(typeof(PickerToolPart), "PickerSite", sb.ToString(), true);
sb = new StringBuilder();
sb.Append("function cust_launchPickerList(inputID)\r\n {\r\n if (!document.getElementById) return;");
sb.Append("var targetTextBox = document.getElementById(");
sb.Append("inputID");
sb.Append(");");
sb.Append("if (targetTextBox == null) return;");
sb.Append("var serverUrl = '");
sb.Append(SPContext.Current.Web.ServerRelativeUrl);
sb.Append("';var callbackList = function(results){if (results == null || results[1] == null || results[2] == null) return;targetTextBox.value = results[1]+(results[1]=='/' ? '' : '/')+results[2];};\r\n");
sb.Append("LaunchPickerTreeDialog('CbqPickerSelectListTitle','CbqPickerSelectListText','listsOnly','',serverUrl, '','','','/_layouts/images/smt_icon.gif','', callbackList);}");
this.Page.ClientScript.RegisterClientScriptBlock(typeof(PickerToolPart), "PickerList", sb.ToString(), true);
this.Page.ClientScript.RegisterClientScriptInclude("PickerTreeDialog", "/_layouts/1033/PickerTreeDialog.js");
}
/// <summary>
/// Raises the <see cref="E:System.Web.UI.Control.Load"/> event.
/// </summary>
/// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param>
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
RegisterPopupScript();
}
/// <summary>
/// Gets a property value of an object by reflection.
/// </summary>
/// <param name="target">Target object.</param>
/// <param name="name">Name of the property.</param>
/// <returns>The property value.</returns>
object GetPropertyValue(object target, string name)
{
try
{
PropertyInfo pi = target.GetType().GetProperty(name);
return pi.GetValue(target, null);
}
catch
{
return "##error##";
}
}
/// <summary>
/// Sets a property value of an object by reflection.
/// </summary>
/// <param name="target">Target object.</param>
/// <param name="name">Name of the property.</param>
/// <param name="value">The value to set.</param>
/// <returns>The property value.</returns>
void SetPropertyValue(object target, string name, object value)
{
PropertyInfo pi = target.GetType().GetProperty(name);
pi.SetValue(target, value, null);
}
/// <summary>
/// Sends the tool part content to the specified HtmlTextWriter object, which writes the content to be rendered on the client.
/// </summary>
/// <param name="output">The HtmlTextWriter object that receives the tool part content.</param>
protected override void RenderToolPart(HtmlTextWriter output)
{
WebPart parent;
int i = 0;
parent = this.ParentToolPane.SelectedWebPart;
if (Items.Count > 0)
{
output.Write("<table cellspacing=\"0\" border=\"0\" style=\"border-width:0px;width:100%;border-collapse:collapse;\">");
foreach (PickerItem pi in Items)
{
output.Write("<tr><td>");
output.Write("<div class=\"UserSectionHead\">");
output.Write(pi.Title);
output.Write("</div>");
output.Write("<div class=\"UserControlGroup\"><nobr>");
output.Write("<input type=\"text\" ");
output.Write("value=\"" + GetPropertyValue(parent, pi.PropertyName) + "\"");
output.Write(" name=\"picker_" + i.ToString() + "\" id=\"picker_" + i.ToString() + "\"/>");
output.Write("<input type=\"button\" onclick=\"" + (pi.PickerType == PickerType.Site ? "cust_launchPickerSite" : "cust_launchPickerList"));
output.Write("('picker_" + i.ToString() + "'); return false;\" value=\"...\"/>");
output.Write("</nobr></div>");
output.Write("</td></tr>");
i++;
}
output.Write("</table>");
}
}
/// <summary>
/// Called when the user clicks the OK or the Apply button in the tool pane.
/// </summary>
public override void ApplyChanges()
{
WebPart parent;
int i = 0;
parent = ParentToolPane.SelectedWebPart;
if (Items.Count > 0)
{
foreach (PickerItem pi in Items)
{
SetPropertyValue(parent, pi.PropertyName, Page.Request.Form["picker_" + i.ToString()]);
i++;
}
}
}
}
The tool part notifies the parent web part of changes when the OK or Apply button is clicked through the ApplyChanges method. Reflection is used to set the properties named in each PickerItem object. The resulting view looks similar to the following.

Clearly providing this kind of selector in your custom web parts helps to make them more user friendly and doesn’t really take all that much effort. I do think though that more of the built in types such as SPSite and SPList should be inherently supported by the built in tool part editors. Maybe in the next version! Note though that this code sample only works with SharePoint 2007 (not WSS) as PickerTreeDialog.js is not included with WSS.

Comment by Daniel Reed on 18 July 2008:
Excellent post. Been considering writing something like this - now I don’t have to!
Comment by Mike Morawski on 13 August 2008:
Good implementation. I’ve never liked seeing js or tags right in c# code, if it were put into an external or resource file it would be easier to learn from IMO.
Also do note that theres a BIG limitation with this. If the user setting up the webpart tries to navigate anywhere where he doesnt have AT LEAST contribute permissions it will throw a 404 error. So even if the user has view, it still does it. It should be used for administrative purposes only. (I understand this is for a webpart property generally for admins but really helpful to keep that in mind)
Cheers
Comment by darren on 13 August 2008:
Hi Mike. Thanks for the feedback and the info about the 404 error. Well worth noting.
Cheers,
Darren
Comment by Dave on 23 September 2008:
Hi, I’m looking for something very similar to this. The Default Image Webpart only has a Textinputbox for the Image URL and users have to lookup the image URL manually. It would be much more user friendly if you could pick the Image URL from a dialog as you can do this for any Image-Websitecolumn.
Can anyone push me into the right direction how to accomplish this?
Thanks,
Dave
Comment by darren on 23 September 2008:
Hi Dave,
This isn’t too difficult to do. If I get a chance I’ll try and find an example for you.
Will you be using MOSS or WSS?
Cheers,
Darren
Comment by Sven on 30 October 2008:
@Dave
Have a look at the Microsoft.SharePoint.Publishing.WebControls.AssetUrlSelector class.
Should provide what you are looking for.
Rgds
Sven
Comment by Sai Krishna on 11 December 2008:
Darren,
Very good post.
I tried to map your post with one of my requirement. In the toolpart, i need to show two drop drown lists DL1 and DL2. DL1 for showing list names in the current site. DL2 is for the list views for the selected list in DL1.
During toolport loading, DL1 will be populated with all the list names in the current site. On selection of a list from DL1, I need to populate the names of all the views into DL2.
Did you ever do something like this? Please share your thoughts.
Comment by TDC on 7 February 2009:
Excellent..thanks much.
Comment by Brijesh Patil on 27 February 2009:
I have a check box list in the toolpart which is populated with the User profile properties and in the edit mode I select few check box and apply it to the web part. My requirement is, I need to maintain the checked values on the next visit to the property window. I mean the check boxes should be persistant for the webpart.
Please let me know what can be done to achieve this.