Archivo

Posts Tagged ‘ASP.NET’

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.

    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.

    Cómo identificar si el usuario autenticado pertenece a un grupo de SharePoint


    ¿Recuerdan que el viernes les comentaba sobre cómo identificar si un usuario de SharePoint pertenece a un grupo cualquiera? A grandes rasgos, la única forma de hacerlo era mediante un proceso manual de obtener el usuario y buscar en sus grupos, o bien obtener el grupo y buscar sus usuarios: si no encontrábamos el elemento, no existía.

    Pues bien, si ese usuario es el usuario actualmente autenticado, hay un camino más fácil: primero, obtenemos la referencia al SPGroup que queramos; y posteriormente, invocamos la propiedad SPGroup.ContainsCurrentUser. Esta propiedad nos devuelve true si el usuario autenticado pertenece a dicho grupo.

    El codiguillo sería algo así:

    private void MatchByGroup(HtmlTextWriter writer, string groupName, string userLogin)
    {
        SPGroup spgroup = SPContext.Current.Web.SiteGroups[groupName];
        bool match = spgroup.ContainsCurrentUser;
    
        writer.Write("El grupo '{0}' contiene al usuario '{1}': {2}<br/>",
            spgroup.Name, userLogin, match);
    }
    

    Mucho más sencillo, ¿no?