Angel Hernández

Agregando procesamiento del lado-cliente a nuestros Webparts

Hace un par de semanas atrás estaba atendiendo y presentando en  Microsoft TechEd en Gold Coast, cuando de repente recibí una llamada del Gerente de Mercadeo y Ventas. La misión: Desarrollar un nuevo webpart para un portal B2B existente que integra SAP, MQ, CRM y webMethods a través SharePoint. El reto: Escribir código en Javascript para luego realizar operaciones del lado del servidor en base a la selección realizada por el usuario. 

He estado desarrollando para SharePoint por un rato ya, pero si sabes como hacer las cosas con ASP.NET entonces no es díficil utilizar ese conocimiento y aplicarlos con SharePoint. Los requerimientos eran :

  • Permitir la entrada del usuario y validar del lado del navegador
  • Agregar AJAX para ofrecer una interfaz rápida a los usuarios 
  • Obtener información almacenada del lado del cliente (navegador) desde el lado del servidor para realizar operaciones contra CRM

Así que comencemos por el principio explicando como lo hice

Propiedades requeridas

        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;
            }
        }

Necesito guardar los resultados de la búsqueda para que el usuario pueda paginar y ordenar estos

Ahora, prosigamos con el código requerido para agregar AJAX, en mi caso siempre separo/distribuyo el código que genera la interfaz de usuario en partes, es decir, tengo un método para generar la parte superior, media e inferior del webpart y como estoy implementando AJAX me gusta encerrar todo dentro de un  <DIV> por lo que el código requerido es como éste

        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" });
        }

Por favor nótese la llamada al método “InjectScriptToDisableDoublePostback”, el cuál es responsable de deshabilitar el control que generó el  postback (por ejemplo, un botón enviar) así evito que el usuario lo presione más de una vez, el código es mostrado abajo

        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();
        }

El método responsable de poner todas las piezas juntas y dibujar el webpart es

        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>" });
        }

Este método me genera un HTML limpio y ordenado cuando el webpart es dibujado además es fácil de seguir, depurar y mantener. Ahora, todos los objetos requeridos están dentro del <DIV>  y ya agregué AJAX… La única cosa que me falta es nuestra plantilla de progreso (código adjunto) que utiliza un GIF animado que viene con  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()
            });
 

Bueno… hasta ahora tenemos el AJAX trabajando con nuestro webpart pero, ¿qué es lo especial de eso? Bien... no mucho en realidad pero ahora necesitamos sincronizar la selección realizada por el usuario del lado del cliente y reflejar estos cambios del lado del servidor, así que para lograr eso necesitamos escribir unos scripts (código adjunto) y un par de campos ocultos que son referenciados desde nuestro código en JavaScript y del webpart también, por lo que en aras de mantener las cosas ordenadas y simples, agregamos estos a la colección de controles del webpart una vez que ya hemos creado todo lo demás  (como se muestra abajo)

        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;
        }

También necesitamos agregar JavaScript para soportar operaciones del lado del cliente por lo que sobrescribimos el método  OnPreRender  (El método AddJavaScriptToWebpart está adjunto a este post)

        protected override void OnPreRender(EventArgs e) {
            if (!Page.ClientScript.IsClientScriptBlockRegistered(JSCRIPT_NAME))
                Page.ClientScript.RegisterClientScriptBlock(typeof(string),
                        JSCRIPT_NAME, AddJavaScriptToWebpart());
        }

Hasta ahora hemos satisfecho algunos de los requerimientos solicitados por el usuario, pero ¿qué pasó con el  GridView? Hablemos del GridView entonces. El GridView es creado por el siguiente método

        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;
        }

La selección/de-selección de las casillas de verificación es manejado por una llamada a una función en JavaScript, realizada desde  CheckBoxTemplate (adjunto al post) como es mostrado abajo

        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;

Como mencionamos previamente, nuestro GridVied tenía que soportar paginación y ordenamiento además de mostrar un indicador para el criterio de ordenamiento actual así que tuve que manejar el evento RowCreated event como se muestra a continuación

        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);
                }
            }
        }

Por favor, nótese la diferencia cuando se está creando/dibujando las filas basadas en su tipo, es decir, el encabezado va a mostrar una flecha que indica la dirección de ordenamiento, de lo contrario, tomamos los valores recuperados de la base de datos (CRM) y cambiamos el ID de las casillas de verificación en tiempo de ejecución, al agregar el número de cuenta a éste (esto es requerido para hacer seguimiento de la selección del usuario)

En este momento se estarán preguntando, ¿en donde se toman los valores almacenados del lado del cliente y se llama a CRM? Estoy seguro que muchos saben la respuesta… El manejador del evento click del botón.

        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();
            }
        }

El resultado final es mostrado abajo

Screenshot

Nuestro webpart integrando diferentes sistemas como mencioné al principio del post además de proveer una interfaz rápida al usuario.

Saludos,

Angel

Leave a Comment

(required) 

(required) 

(optional)

(required)