SharePoint: Enhanced ItemPicker
I was asked to develop a control based on the ItemPicker control, which, in addition to ability of choosing an external data item through BDC, brings it to client side without page postback. There was the following supposed sequence of actions:
- An user chooses a data item in the Picker Dialog;
- The identifier of the selected item is sent to the server through an Ajax-like technology;
- Using the received identifier, the server fetches the proper data out through BDC and sends it back to the client. By “the proper data” I mean the values of either all fields available in the selected data item or only fields defined in control declaration;
- On the client side, the received data is parsed and displayed in UI;
Additionally, the control should be free of bindings to SPField as it should be capable to reside within an independent aspx-page locating in the _layout folder.
*Note: for better understanding of BDC infrastructure, please read the following blog posts: SharePoint: Brief introduction to Business Data Catalog and SharePoint: Understanding BusinessData Column.
So, I developed the required control and made it as reusable as possible. Let’s call the control MyItemPicker (it’s so unusual, isn’t? :)). For sake of simplicity I decided to use the ASP.Net client callbacks applied through the ICallbackEventHandler interface. The ASP.Net client callbacks can be considered as a wrapper on XMLHTTP object. Also the MyItemPicker comprises and uses the standard ItemPicker.
Ok, let’s start with declaration of the control within page:
<MYCC:MyItemPicker id="myItemPicker" runat="server" LobSystemInstanceName="Products" EntityName="Product" PrimaryColumnName="Name" ClientCallback="MyClientCallback" ClientCallbackError="MyClientCallbackError" CallbackBDCFieldFilter="Price,Producer" />
The significant properties here are
- LobSystemInstanceName is the name of the Lob System Instance, through which data is provided to pick;
- EntityName is the type name of data items populating the picker;
- PrimaryColumnName is the name of the data item field, the value of which is used as a display value;
- ClientCallback is the name of the JavaScript function, which has to be present within the page. In case of success, the given function accepts and processes the server response containing fetched data;
- ClientCallbackError is the name of the JavaScript function, which can be within the page and is called, when server fails to fulfill request. This property is optional;
- CallbackBDCFieldFilter is the comma-separated string containing names of data item fields that should be included in server response. For example, if a BDC Entity has four fields – ID, Name, Price and Producer, you might want to have on client side only two of them – Price and Producer. If the CallbackBDCFieldFilter property is empty or not presented in the declaration, server response contains the values of all available fields of BDC Entity;
The sample of the JavaScript functions, which should be indicated in the ClientCallback and ClientCallbackError properties, is shown below. Note the functions’ signatures.
<script type="text/javascript"> function MyClientCallback(result, context) { alert("Result: " + result); if (result != null && typeof result != "undefined" && result.length != 0) { var res = eval("(" + result + ")"); alert('Price: ' + res.Price); alert('Producer: ' + res.Producer); // update UI with received data } } function MyClientCallbackError(result, context) { alert('Error: ' + result); } </script>
The server response looks like
{ 'Price' : '10.00', 'Producer' : 'Microsoft Corporation' }
Thus, the response is formatted in the manner to be easily turned into JavaScript object by means of the eval function.
So, it’s time to examine the source code of the MyItemPicker itself.
MyItemPicker Source
using System; using System.Collections.Generic; using System.Text; using Microsoft.SharePoint; using Microsoft.SharePoint.Portal.WebControls; using System.Web.UI; using Microsoft.Office.Server.ApplicationRegistry.Infrastructure; using Microsoft.Office.Server.ApplicationRegistry.MetadataModel; using Microsoft.Office.Server.ApplicationRegistry.Runtime; using System.Data; namespace MyControls { public class MyItemPicker : Control, ICallbackEventHandler { #region fields & properties protected ItemPicker _picker = null; protected string _callbackRequestedEntityId = string.Empty; public string LobSystemInstanceName { get; set; } public string EntityName { get; set; } public string PrimaryColumnName { get; set; } public string ClientCallback { get; set; } public string ClientCallbackError { get; set; } public string CallbackBDCFieldFilter { get; set; } #endregion #region public methods // Implementation of the ICallbackEventHandler interface // Generates response that will be sent to client public string GetCallbackResult() { return GetJSResult(); } // Implementation of the ICallbackEventHandler interface // Retrieves and preserves identifier of selected data item sent from client public void RaiseCallbackEvent(string eventArgument) { _callbackRequestedEntityId = eventArgument; } #endregion #region internal methods protected override void OnInit(EventArgs e) { base.OnInit(e); EnsureChildControls(); } protected override void CreateChildControls() { base.CreateChildControls(); if (_picker == null) { _picker = new ItemPicker(); _picker.MultiSelect = false; _picker.ID = ID + "_ItemPicker"; try { this.SetExtendedDataOnPicker(_picker); } catch (Exception exception) { _picker.ErrorMessage = exception.Message; _picker.Enabled = false; } this.Controls.Add(_picker); } } /// <summary> /// Initilizes main item picker's properties /// </summary> protected virtual void SetExtendedDataOnPicker(ItemPicker picker) { ItemPickerExtendedData data = new ItemPickerExtendedData(); BDCMetaRequest request = new BDCMetaRequest(LobSystemInstanceName, EntityName); data.SystemInstanceId = request.FoundLobSystemInstance.Id; data.EntityId = request.FoundEntity.Id; List<uint> list = new List<uint>(); FieldCollection fields = request.FoundEntity.GetSpecificFinderView().Fields; foreach (Field field in fields) if (string.Equals(field.Name, PrimaryColumnName, StringComparison.OrdinalIgnoreCase)) data.PrimaryColumnId = field.TypeDescriptor.Id; else list.Add(field.TypeDescriptor.Id); data.SecondaryColumnsIds = list.ToArray(); picker.ExtendedData = data; } protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); AddJSCallbackFunctions(); AddAdditionalJSFunctions(); } /// <summary> /// Generates and adds auxiliary JavaScript functions to the page /// </summary> protected void AddAdditionalJSFunctions() { if (_picker != null) { _picker.LoadPostData(null, null); // this line is required to force CreateChildControls() and to have HiddenEntityKey created Control upLevelDiv = FindControlRecursive(_picker, "upLevelDiv"); if (upLevelDiv != null) { string clearFuncName = "ClearItemPicker_" + ID; string clearFunc = "function " + clearFuncName + "() {" + "var upLevelDiv = document.getElementById('" + upLevelDiv.ClientID + "');" + "if (upLevelDiv != null) {" + "upLevelDiv.innerHTML = '';" + "updateControlValue('" + _picker.ClientID + "');" + "}" + "}"; Page.ClientScript.RegisterClientScriptBlock(GetType(), clearFuncName, clearFunc, true); } Control hiddenEntityDisplayTextControl = FindControlRecursive(_picker, "HiddenEntityDisplayText"); if (hiddenEntityDisplayTextControl != null) { string getDisplayTextFuncName = "GetDisplayText_" + ID; string getDisplayTextFunc = "function " + getDisplayTextFuncName + "() {" + "var hiddenEntityDisplayTextControl = document.getElementById('" + hiddenEntityDisplayTextControl.ClientID + "');" + "return hiddenEntityDisplayTextControl != null ? hiddenEntityDisplayTextControl.value : '';" + "}"; Page.ClientScript.RegisterClientScriptBlock(GetType(), getDisplayTextFuncName, getDisplayTextFunc, true); } } } /// <summary> /// Generates and adds the picker's AfterCallbackClientScript to the page /// </summary> protected void AddJSCallbackFunctions() { if (_picker != null) { string callbackFunc = null; if (!string.IsNullOrEmpty(ClientCallback) && !string.IsNullOrEmpty(ClientCallbackError)) callbackFunc = Page.ClientScript.GetCallbackEventReference(this, "arg", ClientCallback, "context", ClientCallbackError, true); else { if (!string.IsNullOrEmpty(ClientCallback)) callbackFunc = Page.ClientScript.GetCallbackEventReference(this, "arg", ClientCallback, "context", true); } if (!string.IsNullOrEmpty(callbackFunc)) { _picker.LoadPostData(null, null); // this line is required to force CreateChildControls() and to have HiddenEntityKey created Control pickerEntityKeyHidden = FindControlRecursive(_picker, "HiddenEntityKey"); if (pickerEntityKeyHidden != null) { string clientFuncName = "GetBdcFieldValuesAsync_" + ID; string clientFunc = "function " + clientFuncName + "(context)" + "{" + "var pickerEntityKeyHidden = document.getElementById('" + pickerEntityKeyHidden.ClientID + "');" + "if (pickerEntityKeyHidden != null)" + "{" + "var arg = pickerEntityKeyHidden.value;" + callbackFunc + ";" + "}" + "}"; Page.ClientScript.RegisterClientScriptBlock(GetType(), clientFuncName, clientFunc, true); _picker.AfterCallbackClientScript = clientFuncName + "('" + ID + "');"; } } } } /// <summary> /// Makes request to external data source and returns json-result /// </summary> protected string GetJSResult() { string res = string.Empty; try { if (!string.IsNullOrEmpty(_callbackRequestedEntityId)) { Dictionary<string, byte> bdcFieldFilter = GetBDCFieldFilter(CallbackBDCFieldFilter); BDCRequestById request = new BDCRequestById(LobSystemInstanceName, EntityName, _callbackRequestedEntityId); StringBuilder sb = new StringBuilder(); sb.Append("{"); foreach (Field field in request.FoundEntityInstance.ViewDefinition.Fields) { if (bdcFieldFilter.Count == 0 || bdcFieldFilter.ContainsKey(field.Name)) { if (sb.Length > 1) sb.Append(", ").AppendLine(); sb.Append("'").Append(field.Name).Append("' : "); sb.Append("'").Append(Convert.ToString(request.FoundEntityInstance.GetFormatted(field))).Append("'"); } } sb.Append("}"); res = sb.ToString(); } } catch (Exception ex) { // write error to log } return res; } /// <summary> /// Parses the user defined list of bdc fields, the values of which should be retrieved /// </summary> protected static Dictionary<string, byte> GetBDCFieldFilter(string commaSeparatedBdcFields) { Dictionary<string, byte> res = new Dictionary<string, byte>(StringComparer.OrdinalIgnoreCase); if (!string.IsNullOrEmpty(commaSeparatedBdcFields)) { string[] bdcFields = commaSeparatedBdcFields.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); foreach (string field in bdcFields) res.Add(field, 0); } return res; } #endregion } }
You probably noticed that the functions AddJSCallbackFunctions and AddAdditionalJSFunctions generate and add some JavaScript functions to the page. The exact names of these JavaScript functions depend on the id attribute defined in control declaration. For example, if control id is “myItemPicker“, the functions’ name will be GetBdcFieldValuesAsync_myItemPicker, ClearItemPicker_myItemPicker and GetDisplayText_myItemPicker.
Let’s take a look at the functions. The main function is GetBdcFieldValuesAsync_myItemPicker, which extracts the encoded id of selected item from ItemPicker and then makes the Ajax-like client callback to the server. The rest two functions are auxiliary, they are not used by MyItemPicker directly, but they are very useful for developing an interaction between user and MyItemPicker. As their names imply, the ClearItemPicker_myItemPicker clears the ItemPicker, and the GetDisplayText_myItemPicker returns the text displayed to user in ItemPicker. The listing below demonstrates the functions within page:
<script type="text/javascript"> function GetBdcFieldValuesAsync_myItemPicker(context) { var pickerEntityKeyHidden = document.getElementById('myItemPicker_ItemPicker_HiddenEntityKey'); if (pickerEntityKeyHidden != null) { var arg = pickerEntityKeyHidden.value; WebForm_DoCallback('myItemPicker', arg, ClientCallback, context, ClientCallbackError, true); } } function ClearItemPicker_myItemPicker() { var upLevelDiv = document.getElementById('myItemPicker_ItemPicker_upLevelDiv'); if (upLevelDiv != null) { upLevelDiv.innerHTML = ''; updateControlValue('myItemPicker_ItemPicker'); } } function GetDisplayText_myItemPicker() { var hiddenEntityDisplayTextControl = document.getElementById('myItemPicker_ItemPicker_HiddenEntityDisplayText'); return hiddenEntityDisplayTextControl != null ? hiddenEntityDisplayTextControl.value : ''; } </script>
The SetExtendedDataOnPicker and GetJSResult methods of MyItemPicker employ the classes BDCMetaRequest and BDCRequestById that are described in my post SharePoint: How to get value from BDC.
The FindControlRecursive method is mentioned in another my post.