In fact, there is no control with the name PeoplePicker in SharePoint, it’s a common name of the group of elements: a few controls, one aspx-page and a couple of JavaScript files. These elements are closely connected with each other, use each other and all together allow searching and picking out users and groups available in SharePoint and/or Active Directory.
Let’s take a closer look at each element of the People Picker.
PeopleEditor
PeopleEditor is a visual control providing an entry point to deal with People Picker. So, to leverage the People Picker functionality we need just to add the PeopleEditor control to our aspx-page as follows:
<%@ Register TagPrefix="wssawc" Namespace="Microsoft.SharePoint.WebControls"
Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
...
<wssawc:PeopleEditor id="peoplePicker" runat="server" SelectionSet="User,SecGroup,DL"
MultiSelect="true" Height="20px" Width="200px" />
...
Below is a class diagram demonstrating the ancestors of the PeopleEditor and supported interfaces:
The PeopleEditor usually consists of a composite Edit Box and two buttons: Check Names and Browse.
The Hml markup generated by the PeopleEditor is listed below. The listing is a relatively large bunch of the Html-tags but helps figure out what DOM elements are involved in and where in the markup they are located. The Html comments, indents and formatting are added for clarity.
<span id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker" editoroldvalue=""
removetext="Remove" value="" nomatchestext="<No Matching Names>"
moreitemstext="More Names..." prefercontenteditablediv="true"
showdatavalidationerrorborder="false" eeaftercallbackclientscript=""
invalidate="false" allowtypein="true" showentitydisplaytextintextbox="0">
<!-- [Begin] Hidden inputs of the composite Edit Box -->
<input type="hidden" value=""
name="ctl00$PlaceHolderMain$ctl00$ctl01$userPicker$hiddenSpanData"
id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_hiddenSpanData">
<input type="hidden" value="<Entities />"
name="ctl00$PlaceHolderMain$ctl00$ctl01$userPicker$OriginalEntities"
id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_OriginalEntities">
<input type="hidden"
name="ctl00$PlaceHolderMain$ctl00$ctl01$userPicker$HiddenEntityKey"
id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_HiddenEntityKey">
<input type="hidden"
name="ctl00$PlaceHolderMain$ctl00$ctl01$userPicker$HiddenEntityDisplayText"
id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_HiddenEntityDisplayText">
<!-- [End] Hidden inputs of the composite Edit Box -->
<table id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_OuterTable" class="ms-usereditor" cellspacing="0" cellpadding="0" border="0" style="border-collapse:collapse;">
<tr>
<td valign="top">
<table cellpadding="0" cellspacing="0" border="0" style="width:100%;table-layout:fixed;">
<tr>
<td id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_containerCell">
<!-- [Begin] Visible up level div of the composite Edit Box -->
<div id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_upLevelDiv" tabindex="0"
onfocus="StoreOldValue('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');
saveOldEntities('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');"
onclick="onClickRw(true, true,event,'ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');"
onchange="updateControlValue('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');"
onpaste="dopaste('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker',event);"
autopostback="0" rows="3"
ondragstart="canEvt(event);"
onkeyup="return onKeyUpRw('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');"
oncopy="docopy('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker',event);"
onblur="
if(typeof(ExternalCustomControlCallback)=='function'){
if(ShouldCallCustomCallBack('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker',event)){
if(!ValidatePickerControl('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker')){
ShowValidationError();
return false;
}
else
ExternalCustomControlCallback('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');
}
}"
title="People Picker"
onkeydown="return onKeyDownRw('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker', 3, true, event);"
aria-multiline="true" contenteditable="true" aria-haspopup="true" class="ms-inputuserfield"
style="word-wrap: break-word; overflow-x: hidden; background-color: window; color: windowtext; overflow-y: auto; height: 48px;"
prefercontenteditablediv="true" name="upLevelDiv" role="textbox">
</div>
<!-- [End] Visible up level div of the composite Edit Box -->
<!-- [Begin] Usually invisible down level textarea of the composite Edit Box -->
<textarea rows="3" cols="20" style="width:100%;display: none;position: absolute; "
name="ctl00$PlaceHolderMain$ctl00$ctl01$userPicker$downlevelTextBox"
id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_downlevelTextBox"
class="ms-inputuserfield" autopostback="0"
onkeyup="return onKeyUpRw('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');"
title="People Picker"
onfocus="StoreOldValue('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');
saveOldEntities('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');"
onblur="
if(typeof(ExternalCustomControlCallback)=='function'){
if(ShouldCallCustomCallBack('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker',event)){
if(!ValidatePickerControl('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker')){
ShowValidationError();
return false;
}
else
ExternalCustomControlCallback('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');
}
}"
onkeydown="return onKeyDownRw('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker', 3, true, event);"
renderascontenteditablediv="true"
onchange="updateControlValue('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');">
</textarea>
<!-- [End] Usually invisible down level textarea of the composite Edit Box -->
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<span id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_errorLabel" class="ms-error"></span>
</td>
</tr>
<tr style="padding-top:2;">
<td>
<table cellspacing="0" cellpadding="0" border="0" style="width:100%;border-collapse:collapse;">
<tr>
<td valign="top" style="width:88%;">
<span style="font-size:8pt;"></span>
</td>
<td valign="top" nowrap="true" style="padding-left:5px;padding-right:5px;">
<!-- [Begin] Check Names Button -->
<a id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_checkNames"
title="Check Names"
href="javascript:"
onclick="
if(!ValidatePickerControl('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker')){
ShowValidationError();
return false;
}
var arg=getUplevel('ctl00_PlaceHolderMain_ctl00_ctl01_userPicker');
var ctx='ctl00_PlaceHolderMain_ctl00_ctl01_userPicker';
EntityEditorSetWaitCursor(ctx);
WebForm_DoCallback('ctl00$PlaceHolderMain$ctl00$ctl01$userPicker',arg,
EntityEditorHandleCheckNameResult,
ctx,EntityEditorHandleCheckNameError,true);
return false;">
<img title="Check Names" src="/_layouts/images/checknames.png" alt="Check Names" style="border-width:0px;">
</a>
<!-- [End] Check Names Button -->
<!-- [Begin] Browse Button -->
<a id="ctl00_PlaceHolderMain_ctl00_ctl01_userPicker_browse"
title="Browse" href="javascript:"
onclick="__Dialog__ctl00_PlaceHolderMain_ctl00_ctl01_userPicker();
return false;">
<img title="Browse" src="/_layouts/images/addressbook.gif" alt="Browse" style="border-width:0px;">
</a>
<!-- [End] Browse Button -->
</td>
</tr>
</table>
</td>
</tr>
</table>
</span>
*Note that this markup corresponds to the empty PeopleEditor when nothing is typed in.
The PeopleEditor (like other classes derived from EntityEditor) uses functions defined in the entityeditor.js that located at 14\TEMPLATE\LAYOUTS.
Composite Edit Box
The composite Edit Box performs the following two functions:
- displays the already selected users and groups (I call them “resolved accounts”);
- allows typing names (or part of them) of users and groups (I call them “unresolved accounts”).
All names (resolved and unresolved) should be delimited by the so-called Entity Separator. Usually it’s a semicolon (“;“). So, typing two or more names we need to separate them from each other to let the validation know what entries should be resolved.
Being aggregative, the Edit Box usually comprises a few hidden inputs, one invisible textarea and one visible div element (see the PeopleEditor‘s Html markup above). The div element, so-called upLevelDiv, displays everything we see in the Edit Box and allows typing by handling key pressing. And, of course, there are a lot of JavaScript defined in the entityeditor.js and intended to apply all that rich functionality to the simple div. Besides the upLevelDiv another quite important element of the Edit Box is one of the hidden inputs namely the so-called hiddenSpanData. The value attribute of the hiddenSpanData at any moment contains the copy of the content (innerHtml) of the upLevelDiv. Whenever the content of the upLevelDiv is changed those changes are reflected in the hiddenSpanData by calling the copyUplevelToHidden function defined in the entityeditor.js. As you know, the content of div elements is never sent when submitting data to the server. That’s why we need the hiddenSpanData, which is an input (though invisible) and, therefore, takes part in form submitting. So, everything we typed in or selected in the upLevelDiv will be sent to the server by the hiddenSpanData. Also note that the hiddenSpanData keeps the copy of the upLevelDiv‘s inner Html as is, NO transformation is applied. That means that the very Html used to visualize the “resolved accounts” in the upLevelDiv is going to be posted to the server. That requires the server side to parse the Html (usually bunch of SPAN and DIV tags) to extract the entries. I have no idea why the Microsoft uses such a complex and excess format to pass the entries, but that’s a fact. Later in the article we’ll see an example of such Html-formatted entries sent to the server.
The usually invisible so-called downlevelTextBox textarea is used when browser doesn’t support content editable div. So, in case of legacy browser the downlevelTextBox gets visible while the upLevelDiv disappears. All changes of the text inside the downlevelTextBox are being reflected in the hiddenSpanData as well. Note that since textarea doesn’t support Html formatting inside, the typed entries are being sent to the server as a plain text.
The hidden input so-called OriginalEntities is aimed to keep the users and groups (“resolved accounts”) that were selected formerly. For example, if we are opening a list item to edit and the page contains the PeopleEditor bound to a field of the “Person or Group” type, the current value of the field will be persisted into the OriginalEntities. When data is submitted back to the server the original entities allow tracking whether the set of users and groups has been changed. Unlike the hiddenSpanData, the OriginalEntities input keeps the “resolved accounts” as a pure Xml string. The following two methods are used on the server side to serialize entities to and deserialize from the Xml stored in the OriginalEntities: PickerEntity.ConvertEntitiesToXmlData and PickerEntity.ParseEntitiesFromXml respectively. Below in the article we’ll see an example of such Xml-formatted entries as the same format is used to return the result of the validation process.
Two more hidden inputs, HiddenEntityKey and HiddenEntityDisplayText, make sense only if the MultiSelect property of the PeopleEditor is set to false (that’s true for other controls derived from the EntityEditor unless that behavior is overridden somehow). The inputs keep respectively the Key (ID) and DisplayText (readable alias) of the first and only resolved entity. Both look quite useless for the PeopleEditor, but you can learn the way they are employed for picking out a BDC entity through the Enhanced ItemPicker.
Check Names button
The Check Names button validates/resolves the typed names. If the typed names match real accounts, such accounts are displayed in the Edit Box as the resolved ones. If no matches are found or some typed name matches multiple accounts, the name becomes clickable, and clicking on it displays the drop-down menu that looks like the one depicted below:
The menu allows choosing an entity that, in user’s opinion, best conforms to the typed name. Clicking Remove in the menu deletes the typed name. While More Names… does the same what the Browse button does (described below).
The PeopleEditor implements the ICallbackEventHandler interface and therefore supports Client Callbacks. Clicking Check Names button ends up with sending an async request to the page that hosts the PeopleEditor. When on the server side the page and all its controls have been re-created the instance of the PeopleEditor handles the request by parsing input, validating/resolving entries and sending the result back to the browser, then on the client side the Edit Box’s content is updated. What is the input sent to the server? It’s an inner Html of the upLevelDiv (or text of the downlevelTextBox textarea in case of a legacy browser). The GetInnerHTMLOrTextOfUpLevelDiv function from the entityeditor.js is responsible for getting the current input. So, in case of the Client Callback the Html-like data is taken directly from the upLevelDiv/downlevelTextBox, while in case of the usual Form Submit the data of the same format is posted by the hiddenSpanData.
Ok, let’s take a look at possible inputs (__CALLBACKPARAM in the request) that come into the ICallbackEventHandler.RaiseCallbackEvent method of the PeopleEditor. For example, in case the “jira; student” is typed in the Edit Box, the input will be the same – “jira; student“. If, however, the Edit Box contains one or more of the “resolved accounts”, the input becomes much trickier. For example, the string “JIRA; jir” transforms to the following (indents and formatting are added for clarity):
<SPAN id=spanHQ\jira class=ms-entity-resolved
onmouseover=this.contentEditable=false;
title=HQ\jira tabIndex=-1
onmouseout=this.contentEditable=true;
contentEditable=true isContentType="true">
<DIV id=divEntityData description="HQ\jira"
isresolved="True" displaytext="JIRA" key="HQ\jira" style="DISPLAY: none">
<DIV data='<ArrayOfDictionaryEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<DictionaryEntry>
<Key xsi:type="xsd:string">AccountName</Key>
<Value xsi:type="xsd:string">HQ\jira</Value>
</DictionaryEntry>
<DictionaryEntry>
<Key xsi:type="xsd:string">Email</Key>
<Value xsi:type="xsd:string">jira@someservername.com</Value>
</DictionaryEntry>
<DictionaryEntry>
<Key xsi:type="xsd:string">PrincipalType</Key>
<Value xsi:type="xsd:string">User</Value>
</DictionaryEntry></ArrayOfDictionaryEntry>'>
</DIV>
</DIV>
<SPAN id=content tabIndex=-1 contentEditable=true
oncontextmenu='onContextMenuSpnRw(event,"ctl00_PlaceHolderMain_ctl00_ctl01_userPicker");'
onmousedown=onMouseDownRw(event);>
JIRA
</SPAN>
</SPAN>; jir ;
So, this listing demonstrates three things at once: the inner html of the upLevelDiv, the data stored in the hiddenSpanData and the data sent during the Client Callback. The EntityEditor.ParseSpanData is called on the server side to parse the input like that, while the ConvertEntityToSpan function from the entityeditor.js is used on the client side to turn the Client Callback result into such Html-like string.
To get the picture of how the PeopleEditor processes the input, see the code along with my comments that are listed below. The code is borrowed from the EntityEditor class (the ancestor of the PeopleEditor).
// eventArgument is an input similar to the ones shown above
private string InvokeCallbackEvent(string eventArgument)
{
// ensure that all child controls of the PeopleEditor are re-created
this.EnsureChildControls();
// remove all " ", i.e. Html spaces
string spans = StrEatUpNbsp(eventArgument);
// parse input, extract entries, convert them to instances of PickerEntity
// and then add to the Entities collection
this.ParseSpanData(spans);
// go through the collection and try resolving the entities
this.Validate();
// serialize the entities into the output xml string
return this.GenerateCallbackData(this.Entities, false);
}
During the validation process itself the following methods are used ultimately: SPUtility.ResolvePrincipal or SPUtility.ResolveWindowsPrincipal, or, in case of the claims-based authentication, SPClaimProviderOperations.Resolve. If a name can’t be resolved, the process will make an attempt to find suitable accounts by calling such methods as SPUtility.SearchWindowsPrincipals or SPUtility.SearchPrincipals. As regards the claims-based authentication, the SPClaimProviderOperations.Resolve itself is able to return suitable accounts if the only one couldn’t be found.
Ok, let’s take a look at what kind of result is returned to the browser in case the “jira” entity has been resolved while the “jir” has multiple matches:
<Entities Append="False" DoEncodeErrorMessage="True" Separator=";" MaxHeight="3"
Error="No exact match was found. Click the item(s) that did not resolve for more options.">
<Entity Key="HQ\jira" DisplayText="JIRA" IsResolved="True" Description="HQ\jira">
<ExtraData>
<ArrayOfDictionaryEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<DictionaryEntry>
<Key xsi:type="xsd:string">AccountName</Key>
<Value xsi:type="xsd:string">HQ\jira</Value>
</DictionaryEntry>
<DictionaryEntry>
<Key xsi:type="xsd:string">Email</Key>
<Value xsi:type="xsd:string">jira@someservername.com</Value>
</DictionaryEntry>
<DictionaryEntry>
<Key xsi:type="xsd:string">PrincipalType</Key>
<Value xsi:type="xsd:string">User</Value>
</DictionaryEntry>
</ArrayOfDictionaryEntry>
</ExtraData>
<MultipleMatches />
</Entity>
<Entity Key="jir" DisplayText="jir" IsResolved="False" Description="Multiple entries matched, please click to resolve.">
<MultipleMatches>
<Entity Key="HQ\jira-users" DisplayText="HQ\jira-users" IsResolved="True" Description="HQ\jira-users">
<ExtraData>
<ArrayOfDictionaryEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<DictionaryEntry>
<Key xsi:type="xsd:string">AccountName</Key>
<Value xsi:type="xsd:string">HQ\jira-users</Value>
</DictionaryEntry>
<DictionaryEntry>
<Key xsi:type="xsd:string">PrincipalType</Key>
<Value xsi:type="xsd:string">SecurityGroup</Value>
</DictionaryEntry>
</ArrayOfDictionaryEntry>
</ExtraData>
</Entity>
<Entity Key="HQ\locadmin" DisplayText="Jira Admin Local" IsResolved="True" Description="HQ\locadmin">
<ExtraData>
<ArrayOfDictionaryEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<DictionaryEntry>
<Key xsi:type="xsd:string">AccountName</Key>
<Value xsi:type="xsd:string">HQ\locadmin</Value>
</DictionaryEntry>
<DictionaryEntry>
<Key xsi:type="xsd:string">PrincipalType</Key>
<Value xsi:type="xsd:string">User</Value>
</DictionaryEntry>
</ArrayOfDictionaryEntry>
</ExtraData>
</Entity>
<Entity Key="HQ\jira" DisplayText="JIRA" IsResolved="True" Description="HQ\jira">
<ExtraData>
<ArrayOfDictionaryEntry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<DictionaryEntry>
<Key xsi:type="xsd:string">AccountName</Key>
<Value xsi:type="xsd:string">HQ\jira</Value>
</DictionaryEntry>
<DictionaryEntry>
<Key xsi:type="xsd:string">Email</Key>
<Value xsi:type="xsd:string">jira@someservername.com</Value>
</DictionaryEntry>
<DictionaryEntry>
<Key xsi:type="xsd:string">PrincipalType</Key>
<Value xsi:type="xsd:string">User</Value>
</DictionaryEntry>
</ArrayOfDictionaryEntry>
</ExtraData>
</Entity>
</MultipleMatches>
</Entity>
</Entities>
So, the server response is a Xml-based string where each resolved or unresolved name is presented by an Entity-node. The IsResolved attribute indicates if the name is resolved, i.e. whether the name matches a real user or group. Each Entity may contain the nested ones wrapped into MultipleMatches-tag in case the name matches multiple accounts. Those nested Entities will be enumerated in the drop-down menu when clicking the unresolved name.
Note that the OriginalEntities input contains the data in the same format.
Browse button
The Browse button opens the search dialog namely the dialog containing the picker.aspx page. Note that the static method PickerDialog.PickerActivateScript is called to get the appropriate JavaScript opening/activating the search dialog. The PickerDialog.PickerActivateScript, in turn, uses another static method PickerDialog.GetPickerDialogPage that constructs the url to the picker.aspx including all needed query string parameters. The PickerDialog is an ancestor of the PeoplePickerDialog class that along with the picker.aspx will be described in another article.
Related posts: