Archivo

Archive for 30 mayo 2011

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

Anuncios

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.

Apóyate en los metadatos de SharePoint


SharePoint tiene una herramienta peculiar para varios elementos, dentro del modelo de objetos. Muchos elementos tienen una propiedad a través de la cual es posible acceder a los metadatos de ese elemento. En particular, hay dos de éstos que son de mucha utilidad: SPWeb y SPListItem.

SPWeb tiene la propiedad AllProperties, mientras que SPListItem tiene la propiedad Properties. Ambas son de tipo Hashtable, así que es hora de desempolvar mi post sobre esa colección, si tienes dudas.

Ambas colecciones guardan metadatos, es decir, información adicional. SharePoint las guarda en la base de datos, como todo, y las carga al obtener el objeto dueño de los mismos. Esto los convierte en un perfecto lugar para guardar información relacionada o de configuración. Muchas veces esto es mejor que crear otras listas o guardarlas en el web.config.

La regla es así: si quieres guardar información a nivel del sitio, utiliza SPWeb.AllProperties. Por ejemplo:

SPContext.Current.Web.AllProperties.Add("Mi propiedad", "Mi valor");
SPContext.Current.Web.Update();
...
string value = SPContext.Current.Web.AllProperties["Mi propiedad"] as string;
Console.WriteLine(value); // imprime "Mi valor"

Para guardar información sobre un elemento de cualquier lista (incluyendo bibliotecas de documentos y galerías) usa SPListItem.Properties. Por ejemplo:

SPList list = SPContext.Current.Web.Lists["Mi lista"];
foreach (SPListItem item in list.Items)
{
    item.Properties.Add("Mi propiedad", "ID " + item.ID);
    item.Update();
}
...
foreach (SPListItem item in list.Items)
{
    string value = item.Properties["Mi propiedad"] as string;
    Console.WriteLine(value); // imprime "ID 1", etc
}

En general, es una buena idea usar SPWeb.AllProperties para cuestiones de configuración sobre el sitio (en lugar del web.config, por ejemplo) y usar SPListItem.Properties sobre información que no deberían ver los usuarios en la lista. Esto último, por ejemplo, es mejor que crear una columna en el SPList y ocultarla en las vistas.

Juega un rato con esto y verás qué tan útil resulta esto.