Archive

Posts Tagged ‘Web Controls’

Crear menú de acciones de SharePoint programáticamente


Cuando trabajamos con Windows SharePoint Services 3.0 y Microsoft Office SharePoint Server 2007, uno de los requerimientos más comunes con los que contamos es el de extender la interfaz gráfica de usuario para añadir o quitar acciones a la barra de menús de una lista o biblioteca.

image

Hacer esto es relatívamente sencillo usando acciones personalizadas, usualmente basta con seguir estos pasos:

1.- crear un feature que enlace con un manifiesto personalizado.

2.- crear el manifiesto personalizado el cual contendrá una acción personalizada similar a la que se muestra a continuación.

<CustomAction Id="UserInterfaceCustomActions.DocLibNewToolbar"
    RegistrationType="List"
    RegistrationId="101"
    GroupId="NewMenu"
    Rights="ManagePermissions"
    Location="Microsoft.SharePoint.StandardMenu"
    Sequence="1000"
    Title="MY DOCLIB NEW MENU TOOLBAR BUTTON">
    <UrlAction Url="/_layouts/CustomActionsHello.aspx?NewMenu"/>
</CustomAction>

3.- Publicar el feature en la granja y activarlo para el sitio deseado.

Easy peasy (relativamente hablando). Puedes ver más información en este enlace de MSDN, o en este artículo de Joe Ferner.

Pero hay veces en las que por cualquier motivo no podemos usar las acciones personalizadas: bien sea porque estamos desarrollando una página de aplicación, bien sea porque estamos creando un front-end totalmente diferente. En estos casos, querríamos mostrar la barra de menú programáticamente. ¿Qué hacer?

El primer paso es que necesitamos crear una clase que herede de Microsoft.SharePoint.WebControls.ToolBarMenuButton. Esta clase representa un menú en una barra de herramientas (por ejemplo, “New”, “Actions” o “Settings” en la imagen de arriba).

Lo primero que querremos hacer  hacer es seleccionar una acción por default. Para ello nos creamos una propiedad (DefaultAction) o algo así. Si no queremos acción por default, no hacemos nada y ya. La magia, en cualquier caso, ocurre cuando sobreescribimos CreateChildControls, de esta forma:

[SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
protected override void CreateChildControls()
{
    base.CreateChildControls();
    if (!string.IsNullOrEmpty(this._defaultAction))
    {
        base.MenuControl.MenuFormat = MenuFormat.ArrowSplitButton;
        base.MenuControl.ClientOnClickScript = this._defaultAction;
        base.MenuControl.HoverCellInActiveCssClass = "ms-splitbutton";
        base.MenuControl.HoverCellActiveCssClass = "ms-splitbuttonhover";
    }
}

Lo único que hacemos es ver si la propiedad _defaultAction contiene algo, en cuyo caso creamos la acción por defecto (por ello asignamos los estilos de split-button. La acción por default debe ser un JavaScript y debe estar presente en la página donde se ejecute esta acción (ya sea a través de un ClientScript.RegisterClientScriptBlock o algún otro método que elijas). Si nunca tendrás una acción por default, no hagas nada en CreateChildControls.

Lo siguiente que nos interesa es añadir los hijos a nuestro menú. Por ejemplo, el menú “New” sólo muestra un hijo: “Add Item”, mientras que “Actions” muestra cuatro: “Edit in Datasheet”, “Export to spreadsheet”, “View RSS feed” y “Alert me”. En mi ejemplo, voy a crear tres hijos: “Vista previa”, “Editar tema”, “Eliminar tema”.

protected override void AddMenuItems()
{
    base.AddMenuItem(string.Format("{0}Preview", ID),
            "Vista previa",
            "/_layouts/images/searchsettings.gif",
            "Vista preliminar de un tema seleccionado.",
            "Preview.aspx",
            string.Empty);

    base.AddMenuItem(string.Format("{0}Edit", ID),
            "Editar tema",
            "/_layouts/images/MenuEditPictures.gif",
            "Edita las propiedades de un tema seleccionado.",
            string.Empty,
            "EditItems();");

    base.AddMenuItem(string.Format("{0}Delete", ID),
            "Eliminar temas",
            "/_layouts/images/delitem.gif",
            "Elimina los temas seleccionados. ",
            string.Empty,
            "DeleteItems();");
}

Cada llamada a AddMenuItem es quien en realidad añade el hijo. Esta llamada tiene varios parámetros.

1.- En primer lugar, pasamos un ID. Esta puede ser una cadena de texto cualquiera, el chiste es que identifique de forma única al comando.

2.- El segundo parámetro es el texto de nuestro menú.

3.- El siguiente es la URL hacia la imagen que queremos mostrar. Yo me basé en las que ya existen dentro de la carpeta de layouts.

4.- Si queremos que al hacer clic el menú navegue hacia cierta página, usamos el cuarto parámetro para poner dicha URL. El primer elemento del ejemplo navegará hacia la página Preview.aspx.

5.- Por último, si en lugar de navegar queremos ejecutar un JavaScript, usamos el último parámetro. En mi caso, nótese que en la segunda y tercera opción del menú mando llamar a las funciones EditItems y DeleteItems, respectivamente, por lo que la página donde se renderize este control deberá incluir esas funciones o el asunto no funcionará.

Por cierto, puedes sobreescribir la propiedad Text para establecer el texto del menú.

He aquí el código completo de mi clase de ejemplo.

using System;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;
using Microsoft.SharePoint.WebControls;

namespace Menus
{
    class ActionsThemeMenu : ToolBarMenuButton
    {
        private string _defaultAction;

        public ActionsThemeMenu()
        {
            _defaultAction = string.Empty;
        }

        public override string Text
        {
            get { return "Acciones"; }
        }

        protected override void AddMenuItems()
        {
            base.AddMenuItem(string.Format("{0}Preview", ID),
                    "Vista previa",
                    "/_layouts/images/searchsettings.gif",
                    "Vista preliminar de un tema seleccionado.",
                    "Preview.aspx",
                    string.Empty);

            base.AddMenuItem(string.Format("{0}Edit", ID),
                    "Editar tema",
                    "/_layouts/images/MenuEditPictures.gif",
                    "Edita las propiedades de un tema seleccionado.",
                    string.Empty,
                    "EditItems();");

            base.AddMenuItem(string.Format("{0}Delete", ID),
                    "Eliminar temas",
                    "/_layouts/images/delitem.gif",
                    "Elimina los temas seleccionados. ",
                    string.Empty,
                    "DeleteItems();");
        }

        [SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            if (!string.IsNullOrEmpty(this._defaultAction))
            {
                base.MenuControl.MenuFormat = MenuFormat.ArrowSplitButton;
                base.MenuControl.ClientOnClickScript = this._defaultAction;
                base.MenuControl.HoverCellInActiveCssClass = "ms-splitbutton";
                base.MenuControl.HoverCellActiveCssClass = "ms-splitbuttonhover";
            }
        }
    }
}

Y ya con esto tenemos nuestro menú definido. Ahora lo que sigue es crear una instancia de la clase Microsoft.SharePoint.WebControls.ToolBar y en su colección Buttons, añadir el control que hemos creado:

ToolBar toolbar = Page.LoadControl("/_controltemplates/ToolBar.ascx") as ToolBar;
toolbar.Buttons.Controls.Add(new ActionsThemeMenu());
Controls.Add(toolbar);

En este ejemplo, creamos un cotrol ToolBar y le añadimos nuestro recién creada clase, en mi caso, ActionsThemeMenu. Y con eso tenemos. Abajo muestro una imagen de cómo se ve en acción nuestro menú.

image

Cinco formas de usar el SPGridView


He publicado recientemente un código en la galería de MSDN. Éste contiene una solución hecha con Visual Studio 2010, la cual tiene cinco ejemplos sobre cómo utilizar el SPGridView de SharePoint.

¡Échale un ojo y descarga el código fuente!

Transcribo porciones del artículo que lo acompaña.

Introducción

La interfaz gráfica de SharePoint contiene diversos controles, los cuales están disponibles para ser usados mediante las librerías de SharePoint como Microsoft.SharePoint.dll.

En diversas ocasiones, al construir WebParts o páginas de aplicaciones, necesitamos mostrar datos de una u otra forma. Una particularmente útil es el empleo de vistas de reja (GridView). Por supuesto, podemos emplear el control GridView nativo de ASP.NET. Afortunadamente, sin embargo, SharePoint cuenta con su propia implementación de este control: SPGridView. Este control cuenta con ciertas configuraciones avanzadas, pero sobre todo, incorpora los estilos nativos de SharePoint, así como un poco de funcionalidad extra.

El código aquí contenido muestra cinco ejemplos sobre cómo utilizar este control:

1.- Cómo utilizarlo dentro de un WebPart.

2.- Cómo utilizarlo de forma declarativa en una página de aplicación.

3.- Cómo añadir un Edit Box Control a una columna del SPGridView.

4.- Cómo añadir paginadores, filtros y ordenamiento.

5.- Cómo usar el control en conjunto con un SPDataSource.

Construyendo el ejemplo

La solución de ejemplo cuenta con una solución para Visual Studio 2010. Al ser para SharePoint 2010, deberá abrirse en una máquina (virtual, por ejemplo) con SharePoint 2010 (Foundation, al menos) instalado. Compilar y desplegar la solución, sin embargo, es tan sencillo como seleccionar las opciones del Visual Studio 2010 y ya.

Ahora bien, dentro del directorio \Fermasmas.Labs.SPGridViewExample\bin\Debug está el archivo Fermasmas.Labs.SPGridViewExample.wsp. Para instalar la solución sin usar el Visual Studio 2010, basta instalar este WSP y luego hacer el deploy. Ejecutar las siguientes líneas de comando con los valores apropiados debe ser suficiente.

stsadm -o addsolution -filename "directorio-al-archivo\archivo.wsp"
stsadm" -o execadmsvcjobs
stsadm" -o deploysolution -name archivo.wsp -url http://tusitio -allowgacdeployment -immediate -force

Posteriormente, sólo será cuestión de activar el feature contenido en el WSP. Al hacerlo, se crearán las listas y páginas necesarias para mostrar los ejemplos, sobre el sitio seleccionado. También aparecerán, en el menú de Acciones del Sitio, cinco entradas, cada una direccionando a la página que contiene el ejemplo.

Descripción

Todos los ejemplos hacen uso de la lista "Asgard List", la cual contiene cierta información pre-cargada.

El primero de ellos muestra cómo crear el SPGridView dentro de un WebPart, llamado GridViewWebPart. El código dentro del método CreateChildControls muestra cómo se crea. Podrás observar que es muy similar a utilizar en GridView. Las diferencias en la interfaz gráfica, sin embargo, son notables. Así es como luce dentro de una página de WebParts:

Notarás que los campos se encuentran agrupados, y que añadimos un campo con una imagen, una flecha, la cual permite seleccionar una fila. La agrupación está habilitada y permite expander y contraer los elementos.

El segundo ejemplo muestra un SPGridView, pero éste se encuentra dentro de una página de aplicación, y como tal, está declarado solamente con marcas de ASP.NET. Para variar un poco respecto al anterior, este control permite editar los elementos al hacer clic en el botón de edición (los cambios se verán reflejados en la lista, por cierto).

El tercer ejemplo muestra el GridView, pero ahora añadimos una columna con un control EBC (Edit Box Control), que es un menú desplegable con diversas acciones. Es un control característico de SharePoint, y todas las listas personalizadas lo muestran en su campo Title. Adicionalmente, la columna de comentarios tiene un cambio. En lugar de mostrar simplemente el texto, ponemos un enlace, el cual al hacer clic abrirá una ventana desplegable (pop-up) donde se muestra el texto del comentario.

Y así es cómo luce el GridView con el EBC, y cómo luce al hacer clic sobre el enlace de comentarios.

En el cuarto ejemplo, la cosa se pone interesante: añadimos paginación, así como la capacidad de filtrar y ordenar, muy à la SharePoint. Y he aquí la imagen.

Ya por último, comentar que hasta el momento todos los ejemplos han hecho uso del ObjectDataSource para hacer el enlazado de los datos (quizás quieras revisar la clase AsgardSource, pero ahí no hay más que abrir la lista y obtener el DataTable de los elementos). En este último ejemplo mostramos cómo usar el SPGridView en conjunto con SPDataSource, un control que nos permite enlazar de manera fácil contra listas y otros elementos de SharePoint. No hay imagen, porque la vista es similar a las anteriores, pero sí hay fragmento de código a continuación.

Código fuente

    Hay bastantitos archivos, como cabría esperar en una solución para SharePoint. Veamos algunos de los importantes.

    • AsgardContentType\Elements.xml – define el tipo de contenido para la lista que usamos como fuente de datos.
    • AsgardList\Elements.xml – define la lista que usamos como fuente de datos.
    • AsgardList\Schema.xml – define la vista de la lista y su asociación con el content-type del primer punto.
    • AsgardList\ListInstance1\Elements.xml – define una instancia de la lista definida en el punto anterior y añade información pre-cargada.
    • AsgardPagesLibrary\Elements.xml – define una biblioteca de documentos donde podemos guardar páginas web. Aquí estará contenida la página de WebParts donde se muestra el ejemplo 1.
    • GridViewWebPart\Elements.xml – define un WebPart a utilizar en el ejemplo 1.
    • GridViewWebPart\GridViewWebPart.webpart – declara las propiedades iniciales del WebPart del punto anterior.
    • GridViewWebPart\GridViewWebPart.cs – el código C# del WebPart.
    • Layouts\Fermasmas.Labs.SPGridViewExample\GridPageFilterSortExample.aspx y GridPageFilterSortExample.cs  – página ASPX con su archivo de código para el ejemplo 4.
    • Layouts\Fermasmas.Labs.SPGridViewExample\GridPageSimpleExample.aspx y GridPageSimpleExample.cs  – página ASPX con su archivo de código para el ejemplo 2.
    • Layouts\Fermasmas.Labs.SPGridViewExample\GridPageWithDataSource.aspx y GridPageWithDataSource.cs  – página ASPX con su archivo de código para el ejemplo 5.
    • Layouts\Fermasmas.Labs.SPGridViewExample\GridPageWithEbcExample.aspx y GridPageWithEbcExample.cs  – página ASPX con su archivo de código para el ejemplo 3.
    • Layouts\Fermasmas.Labs.SPGridViewExample\ViewComments.aspx y ViewComments.cs – página que muestra un comentario pasado por parámetro de página.
    • Model\AsgardSource.cs – archivo C# que contiene una clase usada en los ObjectDataSource de diversos ejemplos.
    • Pages\AsgardWebPartPage.aspx – define una página de WebParts y carga de forma predeterminada el WebPart del ejemplo 1.
    • Pages\Elements.xml – define un módulo, el cual contiene la página de WebParts del punto anterior.
    • SiteActionMenu\Elements.xml – define los elementos añadidos al botón Acciones del Sitio, lo cual mejora la navegación.

    Mostrar una ventana emergente desde un WebPart


    En SharePoint 2010 introdujeron para muchas cosas ventanas emergentes, aprovechando que todo el sitio ahora cuenta con soporte para Ajax a través del UpdatePanel. Para que podamos hacer algo similar, basta usar un sencillo código en JavaScript.

    <script type="text/javascript" language="javascript">
        function openAsgardDialog() 
        {
            var options = {
                url: "/Pages/AsgardPantheon.aspx",
                width: 800,
                height: 650,
                title: "Hola mundo",
    	    };
    	    SP.UI.ModalDialog.showModalDialog(options);
        }
    </script>";
    

    Al ser invocada dicha función, aparecerá una ventana emergente con el título y dimensiones especificadas, y cargará la página /Pages/AsgardPantheon.aspx, o cualquier otra que especifiquemos.

    Ahora bien, para hacer esto desde un WebPart, tres cosas deben ocurrir.

    1.- Asegurarnos que el master page cuenta con un UpdatePanel. Esto es esencial, puesto que si no Ajax no funciona. Es el comportamiento por default en SharePoint, sólo una precaución por si alguien lo quitó.

    2.- El WebPart deberá registrar el script que permitirá mostrar la ventana emergente.

    private void RegisterOpenHelloWorldDialogScript()
    {
        string script =
            @"<script type=""text/javascript"" language=""javascript"">
                function openAsgardDialog() 
                {{
                    var options = {{
                        url: ""{0}/Pages/AsgardPantheon.aspx"",
                        width: 800,
                        height: 650,
                        title: ""ASGARD"",
    	            }};
    	            SP.UI.ModalDialog.showModalDialog(options);
                }}
                </script>";
        script = string.Format(script, SPContext.Current.Web.Url);
    
        Page.ClientScript.RegisterClientScriptBlock(GetType(),
                "OpenAsgardDialogScript", script);
    }
    
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
    
        RegisterOpenHelloWorldDialogScript();
    }
    

    3.- Llamamos a nuestro JavaScript desde algún control. En este caso, creamos una etiqueta <a> que haga lo propio.

    protected override void CreateChildControls()
    {
        Literal literal = new Literal();
        literal.Text = "<h3>Ejemplo Ventana Emergente</h3>" +
                               "Haga <a href="\&quot;#\&quot;" onclick="\"javascript:openAsgardDialog();\"">" +
                               "click aqu&iacute;</a> para abrir. ";
        Controls.Add(literal);
    }
    

    Y eso es todo. Una imagen de cómo luciría el WebPart…

    image

    …y la ventana emergente.

    image

    Agrupar elementos en un SPGridView


    Hace algunas lunas escribí sobre el componente SPGridView, de SharePoint, y vimos cómo sacarle provecho para mostrar datos e información. Un pequeño comentario entonces, que en aquella ocasión no mencioné: el control nos permite agrupar su propio contenido.

    Tomemos como ejemplo el WebPart con el SPGridView que hicimos en la entrada antes mencionada. Digamos que quisiéramos agrupar nuestro control por el género al que cada Asgard pertenece, y poder ver cuáles elementos pertenecen a los Æsir y cuáles son Ásynjur.

    Para lograrlo, tenemos que seguir los siguientes pasos.

    0.- Es indispensable que desactives el ViewState

    1.- Establecemos la propiedad AllowGrouping del SPGridView a true.

    2.- Establecemos la propiedad GroupField del SPGridView con el nombre del campo que queremos agrupar (obvio necesita haber un DataSource y así).

    3.- Opcionalmente establecemos la propiedad GroupFieldDisplayName para darle un nombre amigable al campo que queremos agrupar.

    4.- Opcionalmente establecemos la propiedad AllowGroupCollapse. Si la establecemos a true, permitirá expander y contraer las filas dentro de cada agrupación (como si fuera un árbol, un TreeView).

    5.- Opcionalmente podemos establecer la propiedad GroupDescriptionField al nombre de un campo que actúe como descripción.

    6.- También de manera opcional podemos establecer la propiedad GroupImageUrlField si queremos mostrar alguna imagen que describa al grupo.

    protected override void CreateChildControls()
    {
        base.CreateChildControls();
    
        SPList list = SPContext.Current.Web.Lists["Asgard Pantheon"];
        DataTable table = list.Items.GetDataTable();
        table.DefaultView.Sort = "Gender";
    
        _titleLiteral = new Literal();
        _titleLiteral.Text = string.Format("<br/><h2>{0}</h2>", list.Title);
        Controls.Add(_titleLiteral);
    
        _gridView = new SPGridView();
        _gridView.ID = "_gridView";
        _gridView.AutoGenerateColumns = false;
        _gridView.Width = new Unit(100, UnitType.Percentage);
        _gridView.DataSource = table.DefaultView;
        _gridView.AllowGrouping = true;
    
        // le quitamos el view state
        _gridView.EnableViewState = false;
        // agrupamos por el campo "Gender" del DataSource
        _gridView.GroupField = "Gender";
        // un nombre bonito para el campo
        _gridView.GroupFieldDisplayName = "Gender";
        // permitimos que los grupos generados colapsen
        _gridView.AllowGroupCollapse = true;
        // adicionalmente, podemos añadir algún campo descriptivo o una imagen
        // _gridView.GroupDescriptionField = "Mi Campo Descriptivo";
        // _gridView.GroupImageUrlField = "url a imagen";
    
        _gridView.Columns.Add(new CommandField {
            ButtonType = ButtonType.Image,
            ShowSelectButton = true,
            SelectImageUrl = "/_layouts/images/arrowright_light.gif"
        });
        _gridView.Columns.Add(new SPBoundField { 
            DataField = "Title", HeaderText = "Name" });
        _gridView.Columns.Add(new SPBoundField { 
            DataField = "Influence", HeaderText = "Influence" });
        _gridView.Columns.Add(new SPBoundField { 
            DataField = "Mate", HeaderText = "Mate" });
        _gridView.DataBind();
                
        Controls.Add(_gridView);
    }
    

    Si comparas este código con el del post anterior notarás una sutil diferencia respecto a la fuente de datos. En el escrito anterior simplemente obtenía el DataTable de la lista SharePoint, mientras que en esta ocasión obtengo el DataView por defecto y enlazo con éste. Esto es así para poder establecer la propiedad Sort del DataView, de tal suerte que los registros vengan ordenados, en este caso, por el género. Esto, porque el SPGridView ordena conforme va encontrando los registros. Por ejemplo, si tuvieras un conjunto de registros en donde el campo de ordenación viniera con los valores A, A, B, A, B, B, A, B, te los agruparía como A (2), B (1), A (1), B (2), A (1) y B (1) en lugar de lo esperado: A (4), B (4). Por eso, mejor que venga ordenado desde la fuente de datos y se acabó el problema.

    Así es como luciría nuestro WebPart, tras esta modificación.

    image

    Usa tipos de contenido MIME para exportar una tabla a Excel


    Este es el escenario: tienes un reporte sencillo, y se te ocurrió hacerlo con ASP.NET y alguno de los componentes que tiene para generar, digamos, una tabla HTML. Todo iba bien, el cliente feliz, tu jefe feliz, todos felices. Y de pronto, a alguien se le ocurre: hey, ¿por qué no exportarlo a Excel? Y aquí es donde te da el patatús.

    Vamos por partes. Primero creamos nuestro sitio web. El Visual Studio 2010 genera sitios iniciales bonitos, así que usemos ese. Añadimos un control Panel, y ahí dentro vendrá el HTML del reporte. Finalmente, añadamos dos botones: uno para generar el reporte y uno para exportar. El codiguín luciría algo así:

    <asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
        <h2>Reporte de productos</h2>
        <p>A continuaci&oacute;n generamos un reporte sencillo y toda la cosa. </p>
        <p>
            <asp:Panel ID="_panel" runat="server">
            </asp:Panel>
            <br /><hr /><br />
            <asp:Button ID="_generateButton" runat="server" Text="Generar" 
                OnClick="OnGenerate" />&nbsp;
            <asp:Button ID="_exportButton" runat="server" Text="Exportar" 
                OnClick="OnExport" />
        </p>
    </asp:Content>
    

    Luego, es cuestión de añadir un método, el cual nos genere el reporte. En la vida real sería algo mucho más complicado, pero este es un blog, no la vida real. Entonces hagamos algo más sencillo.

    private string GetReport()
    {
        Random price = new Random();
    
        StringBuilder html = new StringBuilder()
            .Append("<table border=\"0\" width=\"100%\">")
            .Append("<tr>")
            .Append("<th>ID</th>")
            .Append("<th>Producto</th>")
            .Append("<th>Precio</th>")
            .Append("<th>Cantidad</th>");
        for (int i = 0; i < 20; i++)
        {
            string color = i % 2 == 0 ? "black" : "white";
            string bg = i % 2 == 0 ? "white" : "gray";
                
            html.AppendFormat("<tr style=\"color:{0}; background-color:{1};\">", 
                        color, bg)
                .AppendFormat("<td>{0}</td>", i)
                .AppendFormat("<td>Producto {0}</td>", i)
                .AppendFormat("<td>{0:C}</td>", price.Next(1, 10000))
                .AppendFormat("<td>{0}</td>", price.Next(1, 1000))
                .Append("</tr>");
        }
        html.Append("</table>");
    
        return html.ToString();
    }
    

    Y ya por último, en el botón Generar, pues generamos el reporte llamando a la función GetReport y metiéndola dentro de un control Literal.

    protected void OnGenerate(object sender, EventArgs args)
    {
        Literal lit = new Literal();
        lit.Text = GetReport();
        _panel.Controls.Add(lit);
    }
    

    Esto nos daría una imagen similar a la siguiente.

    image

    Bueno, entonces ¿cómo generamos el Excel? Hay varias maneras. La primera, y usualmente la más obvia, sería añadir una referencia a los componentes COM que provee Excel. Una buena medida, sin duda, máxime que el .NET Framework 4, con la introducción de tipos de datos dinámicos ha hecho que trabajar con componentes COM sea mucho más fácil.

    Aunque usualmente el problema que hay con esto es un tema de licencias. Verás, para usar esos componentes el servidor web debe tener instalado Microsoft Office (o al menos, Excel). Y alguans empresas no están dispuestas a ello.

    Una alternativa es, por supuesto, usar algún otro componente de paga como ASPOSE, o alguno OpenSource como NPOI. Pero a veces restricciones en presupuesto, o políticas, hace que esto no sea posible. En fin. Una solución que reune las tres bes: bueno, bonito y barato, consiste en escribir directamente el HTML de la tabla, y luego cambiar el tipo MIME de la misma.

    El asunto es como sigue. MIME significa Multipurpose Internet Mail Extensions. No vamos a entrar en un tema tan largo, pero la idea es que el MIME tiene una propiedad, la cual describe el tipo de contenido que el servidor HTTP está sirviendo: HTML (text/html), JavaScript (text/javascript), etc. Una extensión es la de application/ms-excel, que define un archivo de Excel.

    Cambiando el tipo de contenido a application/ms-excel hace que el navegador pase el contenido al programa que lo interpreta, en este caso, a Microsoft Office Excel. Ahora bien, Excel es lo suficientemente inteligente como para interpretar una tabla HTML e incorporar el contenido, aún con colores y toda la cosa. Incluso en algunas ocasiones he visto que incorpore imágenes.

    Entonces, el asunto es simple. Para lograrlo hay que hacer varias cosas:

    1.- Limpiar el contenido del búfer que el servidor HTTP va a enviar.

    2.- Limpiar el encabezado y el contenido previamente establecido.

    3.- Establecer el tipo de contenido a application/ms-excel.

    4.- Añadir un encabezado que diga que el contenido debe ser tratado como un documento adjunto (y por tanto, descargable).

    5.- Escribir el HTML en cuestión y descargar el búfer.

    6.- Terminar la respuesta HTTP inmediatamente.

    Para hacer estos pasos, usamos el objeto de tipo HttpResponse que toda página ASP.NET expone mediante su propiedad Response.

    string html = GetReport();
    
    Response.Clear(); // paso 1
    Response.ClearHeaders(); // paso 2
    Response.ClearContent(); // paso 2
    Response.ContentType = "application/ms-excel"; // paso 3
    Response.AppendHeader("Content-Disposition", 
        "attachment; filename=Reporte.xls"); // paso 4
    Response.Write(html); // paso 5
    Response.Flush(); // paso 5
    Response.End(); // paso 6
    

    Y lixto, tenemos un hermoso reporte de Excel, en este caso, el mismo que regresa el método GetReport.

    image

    Sólo ten en cuenta que esta técnica sirve para reportes sencillos. Para exportar reportes más complicados vale la pena la licencia de Excel o algún otro componente, o incluso pensar en Reporting Services. Ten esta técnica, sin embargo, como tu as bajo la manga, para cuando se necesite algo bueno, bonito y barato.

    Categorías:.NET Framework, Apunte, C# Etiquetas: , ,

    Ejemplo sobre el uso de metadatos de SharePoint


    Ayer puse un pequeño tip sobre cómo utilizar SPListItem.Properties y SPWeb.AllProperties para guardar información y que ésta no se muestre al usuario, de forma accesible. Pues bien, quisiera mostrarles esto que he hecho a fecha reciente para que vean cómo puede usarse.

    Primero, les comento el requerimiento. Tengo una lista que contiene información sobre productos: código del material, nombre, descripción y una columna que indica los usuarios asignados a dicho producto. Posteriormente, es responsabilidad de cada usuario asignado a dicho producto, capturar diario un valor que refleje la cantidad de producto habido en el almacén.

    Entonces, primero tengo mi lista. Ésta luce así.

    image

    Como pueden ver, es una lista normal y de hecho el sitio no tiene ninguna otra lista. Hasta aquí easy peasy. Ahora, necesitamos crear un Application Page que muestre los productos por fecha y permita capturar una cantidad. Para ello, tomamos un control calendario y un SPGridView, al cual le creamos la columna “Cantidad”. El código ASP del grid luce algo así:

    <SharePoint:SPGridView ID="_grid" runat="server" 
        AutoGenerateColumns="false" DataSourceID="_mainSource">
        <Columns>
            <asp:CommandField ButtonType="Image" ShowEditButton="true" 
                EditImageUrl="/_layouts/images/edit.gif" 
                UpdateImageUrl="/_layouts/images/save.gif" 
                CancelImageUrl="/_layouts/images/delete.gif" />
            <SharePoint:SPBoundField HeaderText="Código de material" DataField="Code" />
            <SharePoint:SPBoundField HeaderText="Nombre del producto" DataField="Name" />
            <SharePoint:SPBoundField HeaderText="Descripción" DataField="Description" />
            <asp:TemplateField HeaderText="Cantidad">
                <ItemTemplate>
                    <asp:Label runat="server" 
                        Text='<%# ((double)Eval("Quantity")).ToString("0.00") %>' />
                </ItemTemplate>
                <EditItemTemplate>
                    <asp:TextBox ID="_quantityText" runat="server" Width="100px" 
                        Text='<%# Bind("Quantity") %>' />
                    <br />
                    <asp:RangeValidator runat="server" EnableClientScript="true" Type="Double"
                        MinimumValue="0" MaximumValue="9999999999" 
                        ControlToValidate="_quantityText" 
                        Text="Sólo se admiten números positivos" />
                </EditItemTemplate>
            </asp:TemplateField>
            <asp:TemplateField HeaderText="Fecha">
                <ItemTemplate>
                    <asp:Label runat="server" 
                        Text='<%# ((DateTime)Eval("Date")).ToShortDateString() %>' />
                </ItemTemplate>
            </asp:TemplateField>
            <asp:TemplateField HeaderText="">
                <ItemTemplate>
                    <asp:HiddenField ID="_idHiddenField" runat="server" 
                        Value='<%# Bind("ID") %>' />
                </ItemTemplate>
            </asp:TemplateField>
        </Columns>
        <EmptyDataTemplate>
            <h3>No hay información a mostrar.</h3>
        </EmptyDataTemplate>    
    </SharePoint:SPGridView>
    

    Utilizo un ObjectDataSource como proveedor de datos, el cual manda llamar un método que retorna un DataTable, filtrado por la fecha seleccionada en el calendario. Éste método realiza lo siguiente:

    1.- Obtiene una referencia a la lista (SPList) de los productos e itera sobre cada uno de ellos.

    2.- Para cada producto (SPListItem) verifica que el usuario actualmente autenticado (SPContext.Web.CurrentUser) esté dentro de los elementos de la columna “Usuarios”.

    3.- Crea una llave de texto con base en los siguientes datos: cuenta del usuario, guión bajo, la fecha del día seleccionado en el calendario.

    private string GetDatedProductKey(string user, DateTime date)
    {
        if (user == null)
            throw new ArgumentNullException("user");
    
        return string.Format("{0}_{1}{2}{3}", user, date.Year, date.Month, date.Day);
    }
    

    4.- Revisamos en el Hashtable “Properties” del producto (SPListItem) para ver si la clave existe. Si sí, obtenemos el valor y hacemos la conversión a un double. Si no, simplemente dejamos un 0.0.

    string key = GetDatedProductKey(user, date);
    if (product.Properties.Contains(key))
    {
        string value = product.Properties[key] as string;
        double quantity;
        double.TryParse(value, out quantity);
        row["Quantity"] = quantity;
    }
    else
    {
        bool allowUpdates = Web.AllowUnsafeUpdates;
        Web.AllowUnsafeUpdates = true;
        product.Properties.Add(key, "0.0");
        product.Update();
        Web.AllowUnsafeUpdates = allowUpdates;
    
        row["Quantity"] = 0.0;
    }
    

    5.- Añadimos una fila al DataTable, llenamos los datos y al final, retornamos la tabla. El SPGridView enlazará sin problemas.

    Hacer lo anterior nos garantiza que la cantidad se guarde por fecha y usuarios diferentes. Es decir, para el 9 de mayo de 2011, asgard\loki muestra una cantidad de 10 unidades mientras que asgard\heimdall muestra 20. De esta manera almacenamos la información en el mismo elemento SPListItem, pero sin comprometer la información.

    image

    El proceso de hacer la actualización de la información en el SPGridView es muy similar a cualquier otro GridView. Baste decir que dicho control está enlazado con un método Update en el ObjectDataSource. Este método termina por modificar el valor de forma relatívamente fácil, usando la misma llave generada por la fecha y la cuenta del usuario.

    public void UpdateDatedProducts(string user, DateTime date, int id, double quantity)
    {
        SPListItem product = List.Items.GetItemById(id);
        string key = GetDatedProductKey(user, date);
    
        bool allowUpdates = Web.AllowUnsafeUpdates;
        Web.AllowUnsafeUpdates = true;
                
        if (!product.Properties.ContainsKey(key))
            product.Properties.Add(key, quantity.ToString("0.00"));
        else
            product.Properties[key] = quantity.ToString("0.00");
        product.Update();
    
        Web.AllowUnsafeUpdates = allowUpdates;
    }
    

    Y listo, easy peasy.

    Usando el control de búsqueda de usuarios


    Cuando en una lista o biblioteca de documentos agregas un campo de tipo “Persona o Grupo”, al momento de editar el elemento verás un control como el que se muestra en la siguiente imagen, al lado de la fila “Cuenta”.

    image

    Este control cuenta con dos cosas útiles. La primera, cuando escribo algún nombre y hago clic en el primer iconito (el que muestra la palomita), el control valida si lo que escribí pertenece a algún usuario dentro de SharePoint / Directorio Activo. De ser así, lo muestra como tal, y en caso contrario lo marca como inválido.

    image

    La segunda funcionalidad es que, si no conocemos el nombre del usuario, podemos hacer clic en el segundo iconito (el del libro) y entonces un buscador aparecerá para ayudarnos a elegir alguno.

    image

    Sobra decir que este control es súmamente útil para buscar gente. Más aún, cuando hacemos algún componente para SharePoint, la gente esperará ver este control cuando tenga que ingresar gente, en lugar de un vil TextBox.

    Afortunadamente, el control que hace todas estas monerías está disponible para nosotros en la clase PeopleEditor, ubicado en el espacio de nombres Microsoft.SharePoint.WebControls.

    Utilizar esta clase es sencillo. Algunas propiedades son conocidas por nosotros, como AutoPostBack y Width. Otras son fáciles de adivinar: AllowEmpty nos permite determinar si el control debe quedar vacío o no (en caso de que sea false, si no ponemos algo el validador que lleva consigo nos lo hará saber); mientras que MultiSelect nos permite determinar si el control permitirá un solo elemento, o varios (el alto del control varía dependiendo de esto).

    He aquí el un código de ejemplo que muestra lo anterior. Es un fragmento de un WebPart creado a guisa de ejemplo, perteneciente al método CreateChildControls.

    _singleEditor = new PeopleEditor();
    _singleEditor.ID = "_singleEditor";
    _singleEditor.AutoPostBack = true;
    _singleEditor.Width = new Unit(300, UnitType.Pixel);
    _singleEditor.AllowEmpty = false;
    _singleEditor.MultiSelect = false;
    
    _multipleEditor = new PeopleEditor();
    _multipleEditor.ID = "_multipleEditor";
    _multipleEditor.AutoPostBack = true;
    _multipleEditor.Width = new Unit(300, UnitType.Pixel);
    _multipleEditor.AllowEmpty = true;
    _multipleEditor.MultiSelect = true;
    

    Todo más o menos sencillo, ¿no? Bueno, supongamos ahora que queremos pre-cargar nuestros controles con alguna información. Para ello, necesitamos el método UpdateEntities del control. Este método toma como parámetro un ArrayList (lo sé, lo sé, pero recordemos que WSS salió antes que .NET Frameowork 2.0), el cual debe contener cada uno de los usuarios a mostrar en el control. Cada entrada al arreglo debe ser de tipo PickerEntity, con su propiedad Key establecida al nombre del usuario o el de su cuenta. Por ejemplo:

    PickerEntity entity = new PickerEntity();
    entity.Key = "ASGARD\\freyja";
    ArrayList entities = new ArrayList();
    entities.Add(entity);
    _singleLoadedEditor.UpdateEntities(entities);
    

    En un control cuya propiedad MultiSelect sea true, podemos agregar varios elementos al arreglo. Por ejemplo, el siguiente código obtiene un grupo de SharePoint e itera sobre cada usuario, añadiéndolo al arreglo y por ende, al control PeopleEditor en cuestión.

    entities = new ArrayList();
    foreach (SPUser user in SPContext.Current.Web.SiteGroups["Asgard Æsir"].Users)
    {
        entity = new PickerEntity();
        entity.Key = user.Name;
        entities.Add(entity);
    }
    _multipleLoadedEditor.UpdateEntities(entities);
    

    ¿A poco no gana en la vida? Así es como luce mi WebPart.

    image

    Chidito, ¿no? ¡Y profesional! Este es el código completo del WebPart mostrado en la imagen anterior.

    using Microsoft.SharePoint;
    using Microsoft.SharePoint.WebPartPages;
    using Microsoft.SharePoint.WebControls;
    using System.Web.UI.WebControls;
    using System.Collections;
    
    namespace Fermasmas.Wordpress.Com.WebParts
    {
        public class PeopleEditorExample : WebPart
        {
            private PeopleEditor _singleEditor;
            private PeopleEditor _multipleEditor;
            private PeopleEditor _singleLoadedEditor;
            private PeopleEditor _multipleLoadedEditor;
    
            protected override void CreateChildControls()
            {
                base.CreateChildControls();
    
                _singleEditor = new PeopleEditor();
                _singleEditor.ID = "_singleEditor";
                _singleEditor.AutoPostBack = true;
                _singleEditor.Width = new Unit(300, UnitType.Pixel);
                _singleEditor.AllowEmpty = false;
                _singleEditor.MultiSelect = false;
    
                _multipleEditor = new PeopleEditor();
                _multipleEditor.ID = "_multipleEditor";
                _multipleEditor.AutoPostBack = true;
                _multipleEditor.Width = new Unit(300, UnitType.Pixel);
                _multipleEditor.AllowEmpty = true;
                _multipleEditor.MultiSelect = true;
    
                _singleLoadedEditor = new PeopleEditor();
                _singleLoadedEditor.ID = "_singleLoadedEditor";
                _singleLoadedEditor.AutoPostBack = true;
                _singleLoadedEditor.Width = new Unit(300, UnitType.Pixel);
                _singleLoadedEditor.AllowEmpty = false;
                _singleLoadedEditor.MultiSelect = false;
    
                _multipleLoadedEditor = new PeopleEditor();
                _multipleLoadedEditor.ID = "_multipleLoadedEditor";
                _multipleLoadedEditor.AutoPostBack = true;
                _multipleLoadedEditor.Width = new Unit(300, UnitType.Pixel);
                _multipleLoadedEditor.AllowEmpty = true;
                _multipleLoadedEditor.MultiSelect = true;
    
                Controls.Add(new Literal { Text = "Sencillo:<br/>" });
                Controls.Add(_singleEditor);
                Controls.Add(new Literal { Text = "<br/>Sencillo cargado:<br/>" });
                Controls.Add(_singleLoadedEditor);
                Controls.Add(new Literal { Text = "<br/>M&uacute;ltiple:<br/>" });
                Controls.Add(_multipleEditor);
                Controls.Add(new Literal { Text = "<br/>M&uacute;ltiple cargado:<br/>" });
                Controls.Add(_multipleLoadedEditor);
    
                PickerEntity entity = new PickerEntity();
                entity.Key = "ASGARD\\freyja";
                ArrayList entities = new ArrayList();
                entities.Add(entity);
                _singleLoadedEditor.UpdateEntities(entities);
    
                SPGroup group = SPContext.Current.Web.SiteGroups["Asgard Æsir"];
                entities = new ArrayList();
                foreach (SPUser user in group.Users)
                {
                    entity = new PickerEntity();
                    entity.Key = user.Name;
                    entities.Add(entity);
                }
                _multipleLoadedEditor.UpdateEntities(entities);
            }
        }
    }
    

    Para finalizar, solo comentar una cosa curiosa con la que me topé cuando escribía el ejemplo de este texto: las llamadas a UpdateEntities tienen que hacerse después de agregar los controles al WebPart (es decir, después de hacer el Controls.Add). Si no, el UpdateEntities no reconocerá a los usuarios y presentará un comportamiento extraño. No sé por qué ocurre así, pero lo hace (haz la prueba si gustas). Si alguien sabe la explicación, compártala con el mundo.

    ¡Buen fin de semana!