Selecting a few things from a bunch of things: Using the SharePoint GroupedItemPicker control

SharePoint has a bunch of neat controls that are used in the built in application pages, and you can use them yourself if you know how they work. For example, selecting content types to attach to a list uses the following control:

This is a GroupedItemPicker control, which you can find in the Microsoft.SharePoint.WebControls namespace, in Microsoft.SharePoint.dll. It uses a select control for groups, two select boxes for items, two buttons to move items from the left side (candidate) to the right side (selected), and a span to show the description of any selected item. There's almost no good documentation on how to practically use this control, so let's take a look at how it's used here.

Markup

The application page that controls attaching content types to lists is at _layouts/AddContentTypeToList.aspx. You can examine the markup of the page just by looking at the file in the SharePoint hive under the Template/Layouts folder, i.e., c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS\AddContentTypeToList.aspx. The interesting bits look like the following:

<SharePoint:GroupedItemPicker id="Picker" runat="server" GroupControlId="SelectGroup" CandidateControlId="SelectCandidate" ResultControlId="SelectResult" AddButtonId="AddButton" RemoveButtonId="RemoveButton" DescriptionControlId="DescriptionControl" />
<table width="500px">
<tr>
<td class="ms-authoringcontrols" style="padding-right: 10px" colspan="3">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,addcontenttypetolist_select_section_text_select_from%>" EncodeMethod='HtmlEncode'/>
<br/>
<select id="SelectGroup" runat="server" title="<%$Resources:wss,ctypedit_select_group%>">
</select>
</td>
</tr>
<tr>
<td class="ms-authoringcontrols" valign="bottom" style="padding-right: 10px">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,addcontenttypetolist_select_section_text_available_templates%>" EncodeMethod='HtmlEncode'/>
</td>
<td>&#160;</td>
<td class="ms-authoringcontrols" valign="bottom" style="padding-right: 10px">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,addcontenttypetolist_select_section_text_content_types_on_list%>" EncodeMethod='HtmlEncode'/>
</td>
</tr>
<tr>
<td style="padding-right: 10px">
<SharePoint:SPHtmlSelect id="SelectCandidate" runat="server" multiple="true" title="<%$Resources:wss,fldpick_possible_flds%>"/>
</td>
<td align="center" valign="middle" class="ms-authoringcontrols" style="padding-right: 10px">
<button class="ms-ButtonHeightWidth " ID="AddButton" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,multipages_gip_add%>" EncodeMethod='HtmlEncode'/>
</button>
<br/>
<br/>
<button class="ms-ButtonHeightWidth " ID="RemoveButton" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,multipages_gip_remove%>" EncodeMethod='HtmlEncode'/>
</button>
</td>
<td class="ms-authoringcontrols" style="padding-right: 10px">
<SharePoint:SPHtmlSelect id="SelectResult" runat="server" multiple="true" title="<%$Resources:wss,fldpick_selected_flds%>"/>
</td>
</tr>
<tr>
<td class="ms-authoringcontrols" colspan="3">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,multipages_description%>" EncodeMethod='HtmlEncode'/><br/>
<span id="DescriptionControl" runat="server"> </span>&#160;
</td>
</tr>
</table>

One of the interesting things about it is that the control doesn't contain any complicated templates within the markup defining it. If you'll recall the implementation for a GridView control, for example, you'd put a bunch of tags inside the <asp:GridView> tag to define how to show rows and columns. No so, with the GroupedItemPicker control.

In this case, you'll give the control the ID of each of the elements that compose the functionality: GroupControlId, CandidateControlId, ResultControlId, AddButtonId, RemoveButtonId, DescriptionControlId. Then, anywhere else on the page (but most likely right after, as in this case), you'll add separate controls with the same IDs as in the parameters to the GroupedItemPicker control. You can see this in the sample above, which renders the controls in a standard table. Note a few things:

  1. The three selects are implemented differently in the markup: The group picker is a regular old <select> element, but the candidate list and selected lists are SPHtmlSelect elements. These are eventually rendered pretty much the same.
  2. There's no wiring up of the actions for the add and remove buttons. The under-the-covers action of the GroupedItemPicker control handles all of that. It's going to happen in JavaScript anyway, so there's no postback or AJAX wizardry to handle moving things back and forth.
  3. Because of the disconnected nature of the GroupedItemPicker control definition and the actual markup for the underlying controls, you can render this however you want! Go nuts branding a couple of awesome squiggly line images instead of just relying on plain looking buttons for add and remove actions, or rearrange the controls in a vertical orientation if you feel like it.
  4. The control does not define its own OK, Cancel, Revert, or any other buttons besides the two that move items between candidate and selected sides. You get to/have to do this yourself. In this case, the markup contains

    <asp:Button UseSubmitBehavior="false" runat="server" class="ms-ButtonHeightWidth"
    OnClick="Update" Text="<%$Resources:wss,multipages_okbutton_text%>" id="btnOK"
    accesskey="<%$Resources:wss,okbutton_accesskey%>"/>

    to handle that. This is your standard postback button definition that calls the Update method when clicked.

Initial Data Loading

OK, that's all fine and good, but how on earth do you get real data into and out of this thing? Here goes...

The code behind for this page is located in the Microsoft.SharePoint.ApplicationPages assembly, which is located in the ISAPI folder under the SharePoint hive. Open that up in .NET Reflector to see what the actual C# looks like. Or see below:

Some of this code relates to how content types work, so we'll skip that in favor of looking at how the SPGroupItemPicker code works. In the OnLoad method on this page, a couple of things are done to set up the page:

  1. Get references to data sources.
    SPContentTypeCollection availableContentTypes = base.Web.AvailableContentTypes;

  2. Call the AddItem method on the picker control with the id, name, description, and group.
    this.Picker.AddItem(type.Id.ToString(), name, ContentTypePageUtil.GetPickerAllGroupDescription(type), str2);
    this.Picker.AddItem(type.Id.ToString(), name, ContentTypePageUtil.GetPickerAllGroupDescription(type), group);
    
    • The id is a unique identifier that will be used by the control (behind the scenes, in JavaScript) to grab individual items and move them back and forth between candidate and selected lists. In this case, it's literally the content type ID from the system, e.g., 0x0108 for Task, 0x01 for Item.
    • The name is what will appear in the list for users to click.
    • The description is the text will appear in the description span when an item is clicked. In this case, the GetPickerAllGroupDescription method looks up the description from resource files.
    • The group is the entry in the groups dropdown that will display the item. Note that you don't have to define groups separately. The control will examine the collection of items and construct its own groups.

  3. The AddContentTypeToList page doesn't preselect content types that are already attached to the list, so if you have a list that already has a couple of content types attached, they don't appear in the right side of the control. There is a method called AddSelectedItem that will handle this for you if you need to show some items as preselected.

  4. The AddContentTypeToList page has a group containing all available content types in addition to the categorized groups. You can see how this is constructed by the two consecutive AddItem method calls. The first call adds the content type to the specific group, and the second adds the content type to the "All Content Types" group. Line 7, beginning
    string group = ...
    sets this up, initially, by loading the name of the "All Content Types" group for the active locale.

Handling Changes

Great, so now you have a page that renders items on the candidate side, categorizes them into groups, shows item descriptions when clicked, and supports moving them back and forth between the candidate and selected sides. Now you want to be able to do something meaningful with the selected items when the user clicks the OK button. Here's what the Update method looks like:

  1. Right off the bat, you can see how to get the items selected by the user.
    ICollection selectedIds = this.Picker.SelectedIds;
    This is a collection of strings containing the IDs of the selected items. Remember that the IDs are the same as you used when adding items in the previous section with the AddItem or AddSelectedItem method.

  2. Based on the IDs in the selected list, actual objects are looked up again and added to a List object.
    List<SPContentType> list = new List<SPContentType>();
    SPContentTypeCollection contentTypes = this.CurrentList.ContentTypes;
    SPContentTypeCollection availableContentTypes = base.Web.AvailableContentTypes;
    foreach (string str in selectedIds)
    {
            SPContentTypeId id = new SPContentTypeId(str);
            if ((contentTypes[id] == null) && (availableContentTypes[id] != null))
            {
                list.Add(availableContentTypes[id]);
            }
    }

  3. Glossing over some of the logic about content type order and whether they're allowed on the list, look at the code toward the bottom of the method
    List<SPContentType> list4 = new List<SPContentType>(uniqueContentTypeOrder);
    foreach (SPContentType type2 in list2)
    {
            if (type2.IsAllowedInContentTypeOrder)
            {
                list4.Add(type2);
            }
    }
    The list4 object contains the items that eventually will be attached or remain attached to the list.

  4. Attaching the content types
    this.CurrentList.RootFolder.UniqueContentTypeOrder = list4;
    this.CurrentList.RootFolder.Update();
    Finally, the content types are actually attached to the list.

Extending the Functionality

As I mentioned previously, the AddContentTypeToList page doesn't preselect the existing content types attached to the list, but I have implemented preselected items before. It's pretty easy to check whether an item is already attached in your data source and call AddSelectedItem instead of AddItem when you're setting things up. It does get a little tricky once the user makes some changes and clicks the OK button.

You have a couple of options for how to process the changes:

  1. Remove all attached items and then add each selected item, both new and remaining, to the attached items. You can do this if you don't care about preserving internal IDs of the item attachments, and you don't care about the order of the attachments.

  2. Make changes to the existing attachments. This is a tricky one, algorithmically. My algorithm is something like: 1. get a list of existing attachments, 2. delete the ones that don't appear in the new selection list, 3. add the items in the new selection list, but only the ones that didn't appear in the selected list before.

So, like I said, it's a little tricky.

Wrapping it Up

This control could be considered a little bit superfluous. You could implement similar functionality with a list of checkboxes and just let the user check the ones that should be added. But that's no fun! And you'd have to roll your own description field. And you'd have to handle your own groups. When you look at it, this isn't really much code, and you get most of the user interface niceties for free from the JavaScript the control contains.

Think about some other ways this this control could be used. Enroll students in a few of the courses your organization offers. Attach tags or categories to blog posts. Assign individuals from your work group to various task steps in a larger process. Any time you want to assign or attach a subset of a group to an item, this control is a good candidate to get the job done.

Resources