Icono del sitio Programando a medianoche

Editores de WebParts contraibles

Hace un tiempo desarrollamos un tablero de control para un cliente, el cual nos pedía que éste fuera personalizable por el usuario, tanto en contenido y estética, como en la posición de los controles a visualizar, motivo por el cual nos decidimos a construirlo con WebParts. La primera versión tuvo buena aceptación y gustó mucho, por lo cual en la segunda versión nos pidieron que hubieran más componentes y que éstos tuvieran muchas más opciones de configuración. Hasta acá todo iba bien, pero nos dimos cuenta que al agregar muchos WebParts al catálogo éste se hacía incómodo de utilizar (el catálogo tiene más de 200 componentes), y al agregarle muchas opciones de edición a éstos componentes era incómoda la edición, ya que esta se hacía muy extensa y el usuario debía “scrollear” mucho la página. Para resolver estos inconvenientes hicimos dos nuevos controles que utilizamos en ese desarrollo: un catálogo de componentes que utiliza un árbol (del cual hablaré en otro artículo), y una zona de edición donde cada EditorPart es “contraíble” para ocupar menos espacio en la página. Sobre éste último hablaremos en este artículo.

La idea será hace una zona de edición que se asemeje a la imagen a continuación:

Para modificar la forma en que se van a ver los editores de los componentes hay que crear una nueva “zona de edición”, la cual debe heredar de EditorZone y llamaremos CollapsibleEditorZone. En esta clase cambiaremos el método que devuelve el “chrome” a utilizar en esta zona, el cual modificaremos para que le agregue la opción de contraer y expandir como se ve en la imagen anterior. A nuestro “chrome” lo vamos a llamar EditorPartChrome, por lo tanto, el código de la clase CollapsibleEditorZone debería quedar como se muestra en el código que se encuentra a continuación:

using System.Web.UI.WebControls.WebParts;

/// <summary>
/// Nuevo editor de webparts
/// </summary>
public class CollapsibleEditorZone : EditorZone
{
    /// <summary>
    /// Escribe el control
    /// </summary>
    /// <param name="writer"/>Objeto sobre el cual escribir
    protected override void Render(System.Web.UI.HtmlTextWriter writer)
    {
        writer.Write("<span class="CollapsibleEditorZone">");
        base.Render(writer);
        writer.Write("</span>");
    }
    
    /// <summary>
    /// Obtiene una referencia a un nuevo objeto <see cref="CollapsibleEditorChrome"></see>
    /// que se utiliza para representar los editores
    /// </summary>
    /// <returns>Un objeto <see cref="CollapsibleEditorChrome"></see></returns>
    protected override EditorPartChrome CreateEditorPartChrome()
    {
		return new CollapsibleEditorChrome(this);
    }
}

Ahora que tenemos la nueva zona tenemos que crearle el “chrome” del que hablamos antes. Esta nueva clase debe heredar de EditorPartChrome y debe tener el constructor (para que utilice el de la clase base) y dos métodos: uno para escribir el contenido de cada editor a utilizar y otro para dibujar el título de cada uno de éstos, en donde van a estar los botones para contraer y expandir el contenido. Estos botones van a ejecutar un JavaScript que modifica el estilo “display” de un DIV que envuelve el editor, para evitar hacer postbacks innecesarios.

Pero no todo es tan sencillo como parece, supongamos que el usuario expande algunos editores, modifica algunos parámetros del componente y luego pulsa sobre “aplicar” para ver los cambios, la aplicación va a hacer un postback y aplicará los cambios sobre las propiedades de los WebParts, pero al volver a dibujar los editores éstos se verán contraídos nuevamente. Para solucionar esto agregamos un INPUT HIDDEN por cada editor donde guardamos del lado del cliente el último estado del editor, así cuando hacemos un postback simplemente tenemos que verificar el valor de este campo para saber si estaba expandido o contraído.

A continuación muestro el código de esta clase:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;

/// <summary>
/// PartChrome de editores para hacerlos contraibles
/// </summary>
public class CollapsibleEditorChrome : EditorPartChrome
{
    /// <summary>
    /// Constructor del chrome
    /// </summary>
    /// <param name="Zone"/>Zona
    public CollapsibleEditorChrome(CollapsibleEditorZone Zone)
        : base(Zone)
    {
    }

    /// <summary>
    /// Representa un control <see cref="EditorPart"></see> completo con todas sus secciones.
    /// </summary>
    /// <param name="Writer"/>Objeto <see cref="HtmlTextWriter"></see> que recibe el contenido de editorPart.
    /// <param name="EditorPart"/>Control que se está procesando en la actualidad.
    public override void RenderEditorPart(HtmlTextWriter Writer, EditorPart EditorPart)
    {
        if (EditorPart == null)
            throw new ArgumentNullException("EditorPart");

        PartChromeType enuPartChromeType = Zone.GetEffectiveChromeType(EditorPart);
        Style objPartChromeStyle = CreateEditorPartChromeStyle(EditorPart, enuPartChromeType);

        objPartChromeStyle.AddAttributesToRender(Writer, Zone);
        Writer.RenderBeginTag(HtmlTextWriterTag.Fieldset);

        if (enuPartChromeType == PartChromeType.TitleAndBorder || enuPartChromeType == PartChromeType.TitleOnly)
            RenderTitle(Writer, EditorPart);

        Style objPartStyle = Zone.PartStyle;
        objPartStyle.AddAttributesToRender(Writer, Zone);
        Writer.AddAttribute(HtmlTextWriterAttribute.Id, EditorPart.ClientID + "_div");
        if (EditorPart.Page.Request.Form.Get(EditorPart.ClientID + "_state") == "1")
            Writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "inline");
        else
            Writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "none");
        Writer.RenderBeginTag(HtmlTextWriterTag.Div);

        RenderPartContents(Writer, EditorPart);

        Writer.RenderEndTag(); // div
        Writer.RenderEndTag();// fieldset
    }

    /// <summary>
    /// Representa el título de un control <see cref="EditorPart"></see>.
    /// </summary>
    /// <param name="Writer"/>Objeto <see cref="HtmlTextWriter"></see> que recibe el contenido de editorPart.
    /// <param name="EditorPart"/>Control que se está procesando en la actualidad.
    private void RenderTitle(HtmlTextWriter Writer, EditorPart EditorPart)
    {
        string strDisplayTitle = EditorPart.DisplayTitle;

        if (!String.IsNullOrEmpty(strDisplayTitle))
        {
            Style objTitleTextStyle;

            TableItemStyle objPartTitleStyle = this.Zone.PartTitleStyle;
            Style objStyle = new Style();
            objStyle.CopyFrom(objPartTitleStyle);
            objTitleTextStyle = objStyle;

            objTitleTextStyle.AddAttributesToRender(Writer, Zone);

            string strDescription = EditorPart.Description;
            if (!String.IsNullOrEmpty(strDescription))
                Writer.AddAttribute(HtmlTextWriterAttribute.Title, strDescription);

            string strAccessKey = EditorPart.AccessKey;
            if (!String.IsNullOrEmpty(strAccessKey))
                Writer.AddAttribute(HtmlTextWriterAttribute.Accesskey, strAccessKey);

            Writer.RenderBeginTag(HtmlTextWriterTag.Legend);

            if (EditorPart.Page.Request.Form.Get(EditorPart.ClientID + "_state") == "1")
                Writer.AddAttribute(HtmlTextWriterAttribute.Src, EditorPart.ResolveClientUrl("~/Images/Minus.gif"));
            else
                Writer.AddAttribute(HtmlTextWriterAttribute.Src, EditorPart.ResolveClientUrl("~/Images /Plus.gif"));

            Writer.AddAttribute(HtmlTextWriterAttribute.Onclick, "collapseExpandEditor('" + EditorPart.ClientID + "');");
            Writer.AddAttribute(HtmlTextWriterAttribute.Width, "9px");
            Writer.AddAttribute(HtmlTextWriterAttribute.Height, "9px");
            Writer.AddAttribute(HtmlTextWriterAttribute.Align, "middle");
            Writer.AddStyleAttribute(HtmlTextWriterStyle.Cursor, "hand");
            Writer.AddAttribute(HtmlTextWriterAttribute.Id, EditorPart.ClientID + "_img");
            Writer.RenderBeginTag(HtmlTextWriterTag.Img);
            Writer.RenderEndTag(); // img
            Writer.AddAttribute(HtmlTextWriterAttribute.Name, EditorPart.ClientID + "_state");
            Writer.AddAttribute(HtmlTextWriterAttribute.Type, "hidden");
            Writer.AddAttribute(HtmlTextWriterAttribute.Value, EditorPart.Page.Request.Form.Get(EditorPart.ClientID + "_state"));
            Writer.RenderBeginTag(HtmlTextWriterTag.Input);
            Writer.RenderEndTag(); // input

            Writer.WriteFullBeginTag("B");
            Writer.Write(' ');
            Writer.Write(strDisplayTitle);
            Writer.WriteEndTag("B");
            Writer.RenderEndTag(); // legend
        }
    }
}

Cabe mencionar que para mostrar los botones de “más” y “menos” de cada editor la aplicación web debe tener las imágenes “plus.gif” y “minus.gif” dentro de la carpeta “images”.
Ahora faltaría mostrar el JavaScript que utiliza estas clases del lado del cliente para ocultar y mostrar los editores:

function collapseExpandEditor(id) {
    var editor = $get(id + '_div');
    var img = $get(id + '_img');
    var input = $get(id + '_state');
    if (editor.style.display == 'none') {
        editor.style.display = 'inline';
        img.src = 'Images/Minus.gif';
        input.value = '1';
    } else {
        editor.style.display = 'none';
        img.src = 'Images/Plus.gif';
        input.value = '';
    }
}

En este ejemplo nosotros utilizamos el ScriptManager de ASP.NET, por eso se ve que usamos el método $get(), pero si prefieren no usarlo simplemente lo tienen que cambiar por document.getElementById().
Por si no se dieron cuenta, en la clase CollapsibleEditorZone creamos un SPAN donde metemos todos los editores, el cual tiene un estilo llamado igual que la clase. Este estilo lo utilizamos para agregarle el borde a cada uno de los editores:

.CollapsibleEditorZone FIELDSET
{
border: 1px solid #777777;
padding: 2px 2px 2px 2px;
}
.CollapsibleEditorZone FIELDSET FIELDSET
{
border: 1px solid #aaaaaa;
margin-bottom: 6px;
}

Lo último que quedaría hacer es agregar esta clase en alguna página, la cual debería quedar algo como lo que se muestra a continuación:

<wp:collapsibleeditorzone ID="cezEdit" runat="server" VerbButtonType="Button"
    HeaderCloseVerb-Visible="false" Width="100%">
    <applyverb Text="Aplicar"></applyverb>
    <okverb Text="Aceptar"></okverb>
    <cancelverb Text="Cancelar"></cancelverb>
    <zonetemplate>
        <asp:propertygrideditorpart runat="server"></asp:propertygrideditorpart>
    </zonetemplate>

Nótese que el prefijo “wp” debe apuntar al namespace donde se encuentra la clase CollapsibleEditorZone.

Espero que este control les sea de utilidad y como siempre los invito a dejar sus opiniones y comentarios.

Salir de la versión móvil