Angel Hernández

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

Screenshot

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

Regards,

Angel

Leave a Comment

(required) 

(required) 

(optional)

(required)