Adding client-side capabilities to our Webparts
A couple of weeks ago I was attending and presenting at Microsoft TechEd on Gold Coast, when I got a phone call from our Sales Manager. The mission: Develop a new webpart for an existing B2B portal which integrates SAP, MQ, CRM and webMethods through SharePoint. The challenge: Write some Javascript code to do some processing on the server-side based on the user’s input.
I’ve been doing some SharePoint development for a while and if you know how to do things on ASP.NET then is not that hard to leverage those skills and use them on SharePoint. The requirements were:
-
Allow user input and perform some validation on the browser-side
-
Add AJAX capabilities to deliver a responsive UI to the users
-
Grab information stored on the client-side (browser) from the server-side and do some processing against CRM
So let’s start from the beginning explaining how I got this done
Properties required
private DataTable SearchResults {
get {
return (ViewState["block_orders"] != null ?
(DataTable)ViewState["block_orders"] : null);
}
set {
ViewState["block_orders"] = value;
}
}
private string GridViewSortCondition {
get {
if (ViewState["SortCondition"] == null)
ViewState["SortCondition"] = "sapSearch ASC";
return ViewState["SortCondition"].ToString();
}
set {
ViewState["SortCondition"] = value;
}
}
private SortDirection GridViewSortDirection {
get {
if (ViewState["SortDirection"] == null)
ViewState["SortDirection"] = SortDirection.Ascending;
return (SortDirection)ViewState["SortDirection"];
}
set {
ViewState["SortDirection"] = value;
}
}
private string SortExpression {
get {
return (ViewState["sort_exp"] != null ?
ViewState["sort_exp"].ToString() : string.Empty);
}
set {
ViewState["sort_exp"] = value;
}
}
private string SelectedCheckBoxes {
get {
return (ViewState["selected_check_boxes"] != null ?
ViewState["selected_check_boxes"].ToString() : string.Empty);
}
set {
ViewState["selected_check_boxes"] = value;
}
}
I need to store the search results so the user can perform paging and sorting operations on the result set
Now, let’s proceed to the code required to add AJAX functionality, I usually break down the UI code into pieces, I mean, a method to generate the top, middle and bottom parts of a webpart. Since I’m implementing some AJAX I enclose everything inside a <DIV> so the code required looks like this
private void AddAJAXControls() {
Controls.Add(new Literal() { Text = "<div>", ID = "mainDiv" });
Controls.Add(new ScriptManager() { ID = "scriptManager_OrderBlock" });
Controls.Add(new Literal() { Text = InjectScriptToDisableDoublePostBack() });
Controls.Add(new UpdatePanel() { ID = "updatePanel_OrderBlock" });
}
Please note the call to “InjectScriptToDisableDoublePostback” method, it’s responsible for disabling the control which has triggered the postback (e.g.: a submit button) so the user won’t be able to “re-submit” the form more than once, the code is shown below
private string InjectScriptToDisableDoublePostBack() {
StringBuilder retval = new StringBuilder();
retval.AppendLine("<script type='text/javascript'>");
retval.AppendLine("var pbControl = null;");
retval.AppendLine("var prm = Sys.WebForms.PageRequestManager.getInstance();");
retval.AppendLine("prm.add_beginRequest(BeginRequestHandler);");
retval.AppendLine("prm.add_endRequest(EndRequestHandler);");
retval.AppendLine("function BeginRequestHandler(sender, args) {");
retval.AppendLine("//the control causing the postback");
retval.AppendLine("pbControl = args.get_postBackElement();");
retval.AppendLine("if (pbControl.id.indexOf('btnSearch') > -1 || ");
retval.AppendLine("pbControl.id.indexOf('btnUpdate') > -1 )");
retval.AppendLine("pbControl.disabled = true;");
retval.AppendLine("}");
retval.AppendLine("function EndRequestHandler(sender, args) {");
retval.AppendLine("pbControl.disabled = false;");
retval.AppendLine("pbControl = null;");
retval.AppendLine("}");
retval.AppendLine("</script>");
return retval.ToString();
}
The method responsible for putting all of the pieces together and rendering the webpart is
protected virtual void CreateUI() {
UpdatePanel panel = null;
// Add AJAX controls
AddAJAXControls();
if ((panel = FindControl("updatePanel_OrderBlock") as UpdatePanel) != null) {
// Top Table
panel.ContentTemplateContainer.Controls.Add(GetTopTable());
// Bottom Table
panel.ContentTemplateContainer.Controls.Add(GetBottomTable());
}
// Close Div containing AJAX controls
Controls.Add(new Literal() { Text = "</div>" });
}
This approach gives me a clean and tidy HTML when rendered plus it’s easy to follow, debug and maintain. Now, I have all of the objects required inside a <DIV> and it’s AJAX enabled… The only missing thing is our UpdateProgress template (code is attached) which uses an animated GIF bundled in SharePoint.
Table retval = new Table() {
ID = "tblTopTable", CellPadding = 1,
CellSpacing = 0, Width = Unit.Percentage(100)
};
retval.Rows.AddRange(new TableRow[] {new TableRow(), new TableRow(),
new TableRow(), new TableRow(),
new TableRow(), new TableRow(),
new TableRow(), new TableRow(),
new TableRow()});
retval.Rows[ 8 ].Cells.AddRange(new TableCell[] { new TableCell() {
Width=Unit.Percentage(20)},
new TableCell() {
Width=Unit.Percentage(60)}});
// Update Progress
retval.Rows[ 8 ].Cells[0].ColumnSpan = 2;
retval.Rows[ 8 ].Cells[0].HorizontalAlign = HorizontalAlign.Left;
retval.Rows[ 8 ].Cells[0].Controls.Add(new UpdateProgress() {
ID = "workProgress",
ProgressTemplate = new UpdateProgressTemplate()
});
Ok… so far we’ve got AJAX working but what’s special about that? Well.. not much really but now we need to synchronize user’s selection on the client-side and reflect these changes on the server-side and to accomplish that we need some scripts (code is attached) and a couple of hidden fields which are referenced from our JavaScript as well as from our webpart code and for the sake of keeping things tidy we add them to the controls collection right after creating everything else (as depicted below)
protected override void CreateChildControls() {
CreateUI();
Controls.Add(new HiddenField() { ID = "selected_checkboxes",
EnableViewState = true });
Controls.Add(new HiddenField() { ID = "checkboxes_cleared",
EnableViewState = true });
ChildControlsCreated = true;
}
We also need to add some Javascript to support client-side operations so we do this on the OnPreRender method (AddJavaScriptToWebpart method is attached to the post)
protected override void OnPreRender(EventArgs e) {
if (!Page.ClientScript.IsClientScriptBlockRegistered(JSCRIPT_NAME))
Page.ClientScript.RegisterClientScriptBlock(typeof(string),
JSCRIPT_NAME, AddJavaScriptToWebpart());
}
Up to now we’ve met some of the user’s requirements but what about the GridView? Let’s talk about it then. The GridView is created by the following method
private GridView GetSearchResultsGrid() {
GridView retval = new GridView() {
ID = "grdSearchResults", AutoGenerateColumns = false,
AllowPaging = true, AllowSorting = true, Width = Unit.Percentage(100),
PageSize = 100,
EmptyDataText="<font color='red'><b>No results found</b></font>"
};
// Add Columns to the GridView
retval.Columns.Add(new BoundField() { DataField = "sapSearch",
HeaderText = "SAP Search",
SortExpression = "sapSearch" });
retval.Columns.Add(new BoundField() { DataField = "accountNo",
HeaderText = "Account No.",
SortExpression = "accountNo" });
retval.Columns.Add(new BoundField() { DataField = "accountName",
HeaderText = "Account Name",
SortExpression = "accountName" });
retval.Columns.Add(new BoundField() { DataField = "state",
HeaderText = "State",
SortExpression = "state" });
retval.Columns.Add(new TemplateField() {
ItemTemplate = new CheckBoxTemplate(ListItemType.Item),
HeaderTemplate = new CheckBoxTemplate(ListItemType.Header)
});
// Let's align to the centre the Order Block and State Columns
retval.Columns[3].ItemStyle.HorizontalAlign = HorizontalAlign.Center;
retval.Columns[4].ItemStyle.HorizontalAlign = HorizontalAlign.Center;
// Subscribe to events
retval.Sorting += grdSearchResults_Sorting;
retval.RowCreated += grdSearchResults_RowCreated;
retval.PageIndexChanging += grdResults_PageIndexChanging;
retval.RowDataBound += grdSearchResults_RowDataBound;
return retval;
}
The ticking/unticking of the checkboxes is handled by a Javascript call from CheckBoxTemplate (attached to the post) as shown below
private CheckBox GetTemplateContents() {
CheckBox retval = null;
switch (_type) {
case ListItemType.Header:
retval = new CheckBox() { ID = "chkHeader",
Text = "Account Block",
EnableViewState = true };
retval.Attributes["onclick"] = "BLOCKED SCRIPTcheckUncheckHeader(this);";
break;
case ListItemType.Item:
retval = new CheckBox() {ID = "chkItem_", EnableViewState = true };
retval.Attributes["onclick"] = "BLOCKED SCRIPTcheckUncheckItem(this);";
break;
}
return retval;
As previously mentioned, our Gridview had to support paging and sorting plus display an indicator for the current sorting criteria so I had to handle the RowCreated event as depicted below
private void grdSearchResults_RowCreated(object sender,
GridViewRowEventArgs e) {
int rowIndex = 0;
DataView sorted = null;
string strSortedHeader = string.Empty;
// Header
if (e.Row.RowType == DataControlRowType.Header) {
foreach (TableCell tc in e.Row.Cells) {
if (tc.Controls.Count > 0 && tc.Controls[0].GetType().ToString() ==
"System.Web.UI.WebControls.DataControlLinkButton") {
strSortedHeader = ((LinkButton)tc.Controls[0]).Text;
// Sort indicator (Webdings font does the job for us)
if (((LinkButton)tc.Controls[0]).CommandArgument ==
SortExpression) {
if (GridViewSortDirection == SortDirection.Descending)
((LinkButton)tc.Controls[0]).Text =
strSortedHeader.Replace(strSortedHeader,
strSortedHeader + "<font face='Webdings'>5</font>");
else
((LinkButton)tc.Controls[0]).Text =
strSortedHeader.Replace(strSortedHeader,
strSortedHeader + "<font face='Webdings'>6</font>");
}
}
}
} else if (e.Row.RowType == DataControlRowType.DataRow) { // DataRow
if (e.Row.Cells[e.Row.Cells.Count - 1].Controls.Count > 0 &&
SearchResults != null && SearchResults.Rows.Count > 0) {
sorted = new DataView(SearchResults);
sorted.Sort = GridViewSortCondition;
rowIndex = e.Row.DataItemIndex;
((CheckBox)e.Row.Cells[e.Row.Cells.Count - 1].Controls[0]).ID +=
sorted[e.Row.DataItemIndex]["accountNo"].ToString();
((CheckBox)e.Row.Cells[e.Row.Cells.Count - 1].Controls[0]).
Checked = (bool) sorted[e.Row.DataItemIndex]["isSelected"];
// Is it checked? (Non user interaction)
if (((CheckBox)e.Row.Cells[e.Row.Cells.Count - 1].
Controls[0]).Checked)
SelectedCheckBoxes += string.Format("{0};",
((CheckBox)e.Row.Cells[e.Row.Cells.Count - 1].
Controls[0]).ClientID);
}
}
}
Please note that we make a difference when creating/rendering the rows based on their type, for instance, the header is going to display an arrow to indicate sorting direction, otherwise we grab the values retrieved from the database (CRM) and change the checkboxes’ ID on the fly, by adding the account number to it (this is required to keep track of the user selection).
At this moment you’ll be wondering about, where is this guy grabbing the values set already from the client side and calling CRM? I’m pretty sure that many of you know the answer… The button click event handler.
private void btnUpdate_Click(object sender, EventArgs e) {
int rowIndex = 0;
int selectedCell = 0;
DataView sorted = null;
string[] splitData = null;
string selectedControl = string.Empty;
HiddenField clientSideSelection = null;
StringBuilder userSelection = new StringBuilder();
GridView grdSearchResults = FindControl("grdSearchResults") as GridView;
HiddenField checkboxes_cleared = FindControl("checkboxes_cleared") as
HiddenField;
// Is there any data to continue?
if ((SearchResults != null && SearchResults.Rows.Count > 0) &&
(grdSearchResults != null && grdSearchResults.Rows.Count > 0)) {
// Let's create a view to deal with the data as it is displayed
sorted = new DataView(SearchResults);
sorted.Sort = GridViewSortCondition;
// Let's sync both server and client checkboxes selection
ManageDeselectedItemsOnClientSide();
// Should we combine client-side selection with data from the DB?
if ((clientSideSelection = FindControl("selected_checkboxes") as HiddenField) != null
&& !string.IsNullOrEmpty(clientSideSelection.Value)) {
splitData = clientSideSelection.Value.Split(';');
var includeQuery = from checkBoxName in splitData.ToList()
.Where(controlName => !string.IsNullOrEmpty(controlName) &&
SelectedCheckBoxes.IndexOf(controlName) == -1 &&
controlName.IndexOf("_REMOVED") == -1)
select checkBoxName;
// Is there any item we must include?
if (includeQuery.ToList().Count > 0)
includeQuery.ToList().ForEach(controlToInclude =>
userSelection.Append(string.Format("{0};",
controlToInclude.Substring(controlToInclude.IndexOf("chkItem")))));
// Let's combine it with the information coming from the DB
userSelection.Append(SelectedCheckBoxes);
} else if (checkboxes_cleared != null && // Has the user interacted with the UI?
string.IsNullOrEmpty(checkboxes_cleared.Value))
userSelection.Append(SelectedCheckBoxes);
// Accounts to update
splitData = userSelection.ToString().Split(';');
// We loop through the control names collection
// (by default, we unblock those unselected checkboxes)
foreach (string checkBoxId in splitData) {
rowIndex = 0;
foreach (GridViewRow selectedRow in grdSearchResults.Rows) {
selectedCell = selectedRow.Cells.Count - 1;
// Is it the right one?
if (selectedRow.Cells[selectedCell].Controls.Count > 0 &&
selectedRow.Cells[selectedCell].Controls[0] is CheckBox) {
selectedControl = ((CheckBox)selectedRow.Cells[selectedCell].Controls[0]).ID;
if (selectedControl.Equals(checkBoxId)) {
// Account to block
sorted[rowIndex]["isSelected"] = true;
break;
} else if (!splitData.Contains(selectedControl)) {
// Account to unblock (default behaviour)
sorted[rowIndex]["isSelected"] = false;
}
}
rowIndex++;
}
}
// Block/Unblock accounts based on the user's selection
var accountsToProcess = from currentSelection in sorted.Table
.AsEnumerable()
select new {
AccountId = currentSelection["accountId"].ToString(),
IsSelected = (bool)currentSelection["isSelected"]
};
accountsToProcess.ToList().ForEach(account =>
OrderBlockManagement.updateAccount(account.AccountId,
account.IsSelected));
// Let's clear DB selection information
SelectedCheckBoxes = string.Empty;
// Let's reflect the changes on the UI
GridBindingHelper();
}
}
The final result is shown below

Our webpart integrating different systems as mentioned at the beginning of this post plus providing a responsive UI to the user.
Regards,
Angel