Archivo

Archive for the ‘Cómo hacer’ Category

Cómo guardar contenido cuando el usuario no tiene permisos en una lista de SharePoint


Cómo guardar contenido cuando el usuario no tiene permisos en una lista

He observado con mi equipo de trabajo, colegas y sharepointeros en general que una de los problemas que tenemos cuando comenzamos a usar el modelo de objetos de SharePoint es el tema de la seguridad. De hecho a mí mismo me pasaba que tenía que crear un WebPart, el cual obtendría datos de una lista o biblioteca, los renderiza, y al final presenta alguna acción que causa una actualización en dicha fuente. Por supuesto, en ambiente de desarrollo todo jala de pelos, porque usamos la cuenta del administrador. Pero tan pronto publicamos para que el cliente pruebe, ¡boom! Un SecurityException es nuestra recompensa.

Por supuesto, el problema en estos casos suele ser que el usuario que ha ingresado no tiene permisos para acceder a una biblioteca. El caso suele ser sencillo: darle permisos a dicho usuario, pues si no los tiene, su razón ha de tener. En otras palabras, si el usuario no tiene permiso es por la lógica de seguridad implementada para la lista o biblioteca en cuestión.

Sin embargo hay ocasiones en las que el cliente quiere compartir sólo cierta información. Por ejemplo, imagina una lista donde se guarda información sobre los proveedores de una empresa y la tarifa que se les paga a cada uno por unidad de servicio. Por otro lado, el jefe de compras decide que quiere exponer una forma para que los usuarios de otras áreas busquen información de los proveedores, pero no quiere que la tarifa quede expuesta. Además, si el proveedor no buscado no existe, el usuario debería poder darlo de alta (nuevamente, sin capturar la tarifa). ¿Qué podemos hacer?

No podemos simplemente dar permisos de lectura a los usuarios generales porque entonces podrían visualizar la página AllItems.aspx de la lista y les mostraría la tarifa. Luego, lo que tenemos que hacer es un WebPart que consulte la lista y haga la búsqueda ahí, aunque el usuario autenticado no tenga permisos de lectura en la misma. Y lo mismo ocurre con el alta de un proveedor: el usuario deberá ingresar la información sin tener permisos para hacerlo.

Afortunadamente, el modelo de objetos de SharePoint nos proporciona una forma de hacer el “by-pass” de seguridad. La clase SPSecurity (ubicada en el espacio de nombres Microsoft.SharePoint) contiene un método llamado RunWithElevatedPrivileges. Éste toma un sólo parámetro: un delegado de un método sin parámetros que regresa void.

protected override void CreateChildControls()
{ 
    SPGridView myView = new SPGridView();
    ...

    SPSecurity.RunWithElevatedPrvileges( () => {

        // aunque el usuario autenticado no tenga
        // permisos para leer o modificar la lista "Proveedores",
        // al ejecutarse este delegado anónimo sí que
        // los tiene. 
        SPList list = SPContext.Current.Lists["Proveedores"];
        myView.DataSource = list.Items;
        myView.DataBind();
    });

    Controls.Add(myView);
}

El código anterior muestra la forma básica de utilizar dicho método. Como ves, le pasamos un método anónimo. Si el usuario que carga el WebPart no tiene permisos para ingresar, éste código le garantizará el pase. Lo mismo para actualizar datos.

protected void OnOK(object sender, EventArgs args)
{
    SPSecurity.RunWithElevatedPrivileges(() => {

        SPList list = SPContext.Current.Lists["Proveedor"];
        SPListItem item = list.Items[0];
        item["Título"] = "Nuevo título";
        item.Update();
    });
}

Ahora bien, hay que aclarar un asunto. SPSecurity.RunWithElevatedPrivileges no es que de un paso libre así tal cual. Lo que hace en realidad es darle los privilegios del usuario que corre el proceso. Así, si corremos el código anterior dentro de un WebPart, éste se ejecutará bajo los permisos que tenga la cuenta asociada al Application Pool (y usualmente será el administrador de la granja). Pero por otro lado, si corriésemos un código similar en, digamos, un temporizador de SharePoint o un Workflow, la cuenta que tendrá será la asociada a estos procesos (digamos, al servicio SPTimerv4).

Sin embargo, por lo anterior, hay que tener cuidado ya que no siempre funcionará. Si por ejemplo, ejecutamos desde un temporizador y la cuenta asociada a ésta no tiene permiso en el recurso que queremos consultar (i.e. la lista “Proveedor” en nuestro ejemplo) tendremos un SPException como si no tuviésemos permiso. Lo mismo ocurre si ejecutamos dicho código desde una aplicación (de consola o Windows Forms): el usuario que ejecuta la aplicación será quien deba tener permisos en la lista.

Así que ya sabes, no siempre será garantía su empleo. Sin embargo, para WebParts, páginas de aplicación y controles, siempre será una muy buena opción.

Anuncios

Leer los valores de un campo de opción múltiple


SharePoint cuenta con los campos de opción múltiple, los cuales nos permiten seleccionar un valor a partir de una lista de valores predeterminados.

Cuando hacemos algún desarrollo, como –digamos- un WebPart, quizás querramos mostrar las opciones que presenta uno de estos campos. Es algo sencillo, pero pensé que sería bueno publicarlo de todas formas. Un “quickie”.

El campo de opción múltiple es representado por la clase SPFieldChoice. Y esta clase tiene una propiedad, Choices, la cual es una colección de cadena de texto. Soooo, easy peasy, sólo tenemos que conseguir una referencia a dicha clase.

SPWeb web = SPContext.Web;
SPList list = web.Lists["Mi Lista"];
SPFieldChoice field = list.Fields["Mi Campo"] as SPFieldChoice;

DropDownList list = new DropDownList();
list.DataSource = field.Choices;
list.DataBind();

El ejemplo anterior muestra cómo obtener dicha referencia. Si el campo Mi Campo es de opción múltiple, la conversión será un éxito. Luego, simplemente hacemos un enlazado de datos contra una lista desplegable de ASP.NET.

Easy peasy.

Cómo obtener todos los usuarios de un sitio


Si estás haciendo un desarrollo para SharePoint y de pronto quisieras saber cuáles son los usuarios que hay en un sitio, ¿qué hacer? Easy peasy. Comenzamos por referenciar nuestro Microsoft.SharePoint.dll de siempre e importamos el espacio de nombres Microsoft.SharePoint. Luego obtenemos un objeto SPWeb (ya sea mediante el SPContext.Current o creando un nuevo SPSite).

Si te das cuenta, SPWeb cuenta con al menos tres propiedades interesantes, todas las cuales regresan un objeto SPUserCollection (una colección de objetos SPUser), a saber:

    ¿Cuál es la diferencia entre estas tres propiedades? Pensando un poquito, SiteUsers salta a la vista y en efecto es lo que sospechas: esa propiedad regresa a todos los usuarios existentes en la colección de sitios. ¿Cuál será la diferencia entre las otras dos? Pues, en esencia, que Users te regresa todos los usuarios registrados en el sitio, mientras que AllUsers te regresa no nada más los usuarios registrados en el sitio, sino todos aquellos que hayan entrado al sitio alguna vez, o incluso que hayan sido referenciados en alguna lista (digamos, alguna alerta o algo por el estilo).

    Puede obtenerse un usuario específico por el índice de la colección, o bien por el nombre de usuario (de la forma “dominio\\nombreUsuario”), usando el indizador. O bien alguno de los métodos expuestos, como GetByID o GetByEmail.

    SPWeb web = SPContext.Current.Web;
    
    // propiedades de SPUser
    foreach (SPUser user in web.AllUsers)
    {
        string email = user.Email;
        string name = user.Name;
        // etc
    }
    
    // Obtener un usuario específico por correo electrónico
    SPUser user = web.AllUsers.GetByEmail("fernando.gomez@dominio.com");
    
    // Buscar todos los usuarios que comiencen con "Fer"
    var query = from SPUser user in web.AllUsers
                         where user.Name.BeginsWith("Fer")
                         select user;
    SPUser[] ferUsers = query.ToArray();
    
    // Easy Peasy
    

¿Y qué pasa si queremos consultar la información del perfil, desde un lugar fuera del servidor (i.e. una aplicación externa o desde Silverlight)? Pues usamos un servicio web, en concreto: UserGroup.asmx.

Este servicio web cuenta con tres métodos que te resultarán familiares:

  • UserGroup.GetAllUserCollectionFromWeb.
  • UserGroup.GetUserCollectionFromSite.
  • UserGroup.GetUserCollectionFromWeb.

¿Te suenan? Pues claro, son los equivalentes a SPWeb.AllUsers, SPWeb.SiteUsers y SPWeb.Users. Los métodos regresan un objeto tipo XmlNode, el cual contiene un XML más o menos con este esquema:

<GetUserCollectionFromWeb xmlns="http://schemas.microsoft.com/sharepoint/soap/directory/">
    <Users>
        <User ID="190" Sid="S-1-5-21-944133582-289434890-317593308-21562" 
            Name="Fernando Gómez" LoginName="dominio\fernando.gomez" 
            Email="fernando.gomez@dominio.com" Notes="" IsSiteAdmin="False" 
            IsDomainGroup="False" Flags="0" /> 
        <User ID="111" Sid="S-1-5-21-944133582-289434890-317593308-22617" 
            Name="Fulano De Tal" LoginName="dominio\fulano.detal" 
            Email="fulano.detal@dominio.com" Notes="" IsSiteAdmin="False" 
            IsDomainGroup="False" Flags="0" /> 
        <!-- etc -->
    </Users>
</GetUserCollectionFromWeb>

En esencia, el XML devuelto tiene un primer nodo, cuyo nombre corresponde al nombre del método (por ejemplo, de haber llamado a GetAllUserCollectionFromWeb, el nodo habría tenido este otro nombre), seguido de un nodo Users, el cuál contiene una colección de nodos User. Éstos últimos contienen atributos, cada uno representando información sobre el usuario. En el ejemplo te muesto algunos, como el ID interno, el nombre, la cuenta (LoginName) y el correo electrónico.

Cargar esta información, pues, es relativamente sencilla, dado que es el mismo esquema. Puedes cargarlo iterando sobre el nodo, o bien mediante LINQ to XML. Pero a mí me gusta usar XPath.

Es relativamente sencillo, de hecho, usar XPath. A primera instancia, puede que pienses en llamar a SelectNodes sobre el XmlNode, quizás algo así:

UserGroup service = new UserGroup();
service.Url = "http://labwf";
service.Credentials = CredentialCache.DefaultCredentials;

XmlNode rootNode =  service.GetAllUserCollectionFromWeb();
string xpath = "GetAllUserCollectionFromWeb/Users/descendant::User";
XmlNodeList nodes = rootNode.SelectNodes(xpath);
foreach (XmlNode node in nodes)
{
    string loginName = node.Attributes["LoginName"];
    string email = node.Attributes["Email"];
    // etc
}

service.Dispose();

Pero pues toma chango tu banano, lo anterior te regresará un XmlNodeList vacío. La razón es que el XML devuelto por el servicio web contiene un espacio de nombres especificado. Pero no uno cualquiera, sino el que se usa por default: xmlns.

Para solventar este problema tenemos que llamar SelectNodes con un gestionador de espacio de nombres (XmlNamespaceManager), al cual le añadiremos el espacio de nombres que viene en el XML junto con un prefijo con el que lo queramos identificar (en mi caso, he escogido “defns”). Pero el gestionador necesita de una tabla de nombres en su constructor, y la forma más sencilla de obtenerla, según pude ver, es crear un XmlDocument y usar su propiedad NameTable.

Una vez hecho esto, podremos usar el SelectNodes sober el XmlDocument, pero tendremos que cambier el XPath para que referencie el prefijo que añadimos al gestionador de espacios de nombres. Por ejemplo, en mi caso, el XPath quedaría como:

defns:GetAllUserCollectionFromWeb/defns:Users/descendant::defns:User

Así luciría nuestro código.

// preparar el servicio web
UserGroup service = new UserGroup();
service.Url = "http://labwf";
service.Credentials = CredentialCache.DefaultCredentials;

// generar el documento xml con los usuarios
XmlNode rootNode =  service.GetAllUserCollectionFromWeb();
XmlDocument doc = new XmlDocument();
doc.LoadXml(rootNode.OuterXml);

// crear el gestionador de espacios de nombres 
XmlNamespaceManager manager = new XmlNamespaceManager(doc.NameTable);
manager.AddNamespace("defns", "http://schemas.microsoft.com/sharepoint/soap/directory/");

// ejecutar nuestro xpath...
string xpath = "defns:GetAllUserCollectionFromWeb/defns:Users/descendant::defns:User";
XmlNodeList nodes = rootNode.SelectNodes(xpath);

//... y ahora sí iterar sobre los resultados
foreach (XmlNode node in nodes)
{
    string loginName = node.Attributes["LoginName"];
    string email = node.Attributes["Email"];
    // etc
}

service.Dispose();

Bien, pues eso es todo por el momento. Ya veremos más cosas de usuarios en el siguiente post…

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

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!

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


Habrá ocasiones en las que querremos realizar determinada acción sólo si un usuario pertenece a cierto grupo. Por ejemplo, mostrar controles, permitir ediciones, regular la seguridad, etc. En esta entrada veremos como solucionar este problema.

Existen dos formas de enfocar el problema: a) podemos obtener el grupo en cuestión y ver si el usuario en cuestión pertenece a él, o bien b) obtenemos el usuario y revisamos todos sus grupos, hasta encontrar el que buscamos. Exploraremos las dos opciones.

En SharePoint, la clase SPUser identifica a un usuario, mientras que la clase SPGroup identifica a un grupo. Un usuario cualquiera cuenta con una propiedad llamada Groups, de tipo SPGroupCollection, la cual contiene una referencia a los grupos del usuario. Adicionalmente, un grupo cuenta con una propiedad llamada Users, de tipo SPUserCollection, la cual contiene todos los usuarios pertenecientes al grupo. Así, vemos que para resolver nuestro problema con cualquiera de los dos enfoques, sólo tenemos que iterar sobre un SPUserCollection o SPGroupCollection, según sea el caso. Tristemente, no exsite un método llamado “ContainsGroup” o “ContainsUser” para estas colecciones, así que tendremos que iterar. Nos podemos apoyar en LINQ, sin embargo.

Vamos a hacer un ejemplo. En nuestro sitio, creemos un par de grupos y añadámosle algunos usuarios. En mi sitio, yo he creado el grupo “Asgard Æsir”, al cual pertenecen los usuarios ASGARD\odin, ASGARD\thor y ASGARD\tyr; y el sitio “Asgard Vanir”, al cual pertenecen los usuarios ASGARD\freyja, ASGARD\skadi y ASGARD\loki. La siguiente imagen lo ilustra.

image

Ahora, creemos un simple WebPart y añadámosle dos métodos: MatchByGroup, el cual tome como parámetros el nombre de algún grupo y escriba si un usuario cualquiera pertenece a éste o no; y MatchByUser, el cual tome como parámetro el nombre de algún usuario y escriba si pertenece a un grupo cualquiera.

El siguiente es el código de MatchByGroup.

private void MatchByGroup(HtmlTextWriter writer, string groupName, string userLogin)
{
    SPGroup spgroup = SPContext.Current.Web.SiteGroups[groupName];
    var query = from SPUser user in spgroup.Users
                where user.LoginName.Equals(userLogin)
                select user;
    bool match = query.Count() > 0;

    writer.Write("El grupo '{0}' contiene al usuario '{1}': {2}<br/>", 
        spgroup.Name, userLogin, match);
}

Como podemos ver, primero obtenemos el grupo y hacemos una consulta LINQ a la propuedad SPGroup.Users en busca de aquellos usuarios cuyo LoginName concuerde. Si la consulta nos devuelve más de un elemento, entonces sí lo hace.

Así, el algoritmo para el método MatchByUser es muy similar, solo que hacemos la consulta LINQ sobre SPUser.Groups. Aquí está el código.

private void MatchByUser(HtmlTextWriter writer, string groupName, string userLogin)
{
    SPUser user = SPContext.Current.Web.AllUsers[userLogin];
    var query = from SPGroup spgroup in user.Groups
                where spgroup.Name.Equals(groupName)
                select spgroup;
    bool match = query.Count() > 0;

    writer.Write("El usuario '{0}' pertenece al grupo '{1}': {2}<br/>",
        user.LoginName, groupName, match);
}

Easy peasy. En nuestro WebPart, solo falta sobreescribir el método RenderContents e invocar a nuestros métodos. Por ejemplo:

protected override void RenderContents(HtmlTextWriter writer)
{
    base.RenderContents(writer);

    writer.Write("<h2>Por grupo</h2>");
    MatchByGroup(writer, "Asgard Vanir", "ASGARD\\thor");
    MatchByGroup(writer, "Asgard Vanir", "ASGARD\\freyja");
    MatchByGroup(writer, "Asgard Vanir", "ASGARD\\loki");
    writer.Write("<h2>Por usuario</h2>");
    MatchByUser(writer, "Asgard Vanir", "ASGARD\\thor");
    MatchByUser(writer, "Asgard Vanir", "ASGARD\\freyja");
    MatchByUser(writer, "Asgard Vanir", "ASGARD\\loki");
}

Con este código, así es como luce nuestro WebPart.

image

Y ya por último, para los flojos que gustan de hacer copy-paste, les dejo el código completo del WebPart.

using System;
using System.Linq;
using System.Web.UI;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;

namespace Fermasmas.Wordpress.Com.WebParts
{
    public class UserGroupExample : WebPart
    {
        public UserGroupExample()
        {
        }

        private void MatchByGroup(HtmlTextWriter writer, string groupName, string userLogin)
        {
            SPGroup spgroup = SPContext.Current.Web.SiteGroups[groupName];
            var query = from SPUser user in spgroup.Users
                        where user.LoginName.Equals(userLogin)
                        select user;
            bool match = query.Count() > 0;

            writer.Write("El grupo '{0}' contiene al usuario '{1}': {2}<br/>",
                spgroup.Name, userLogin, match);
        }

        private void MatchByUser(HtmlTextWriter writer, string groupName, string userLogin)
        {
            SPUser user = SPContext.Current.Web.AllUsers[userLogin];
            var query = from SPGroup spgroup in user.Groups
                        where spgroup.Name.Equals(groupName)
                        select spgroup;
            bool match = query.Count() > 0;

            writer.Write("El usuario '{0}' pertenece al grupo '{1}': {2}<br/>",
                user.LoginName, groupName, match);
        }

        protected override void RenderContents(HtmlTextWriter writer)
        {
            base.RenderContents(writer);

            writer.Write("<h2>Por grupo</h2>");
            MatchByGroup(writer, "Asgard Vanir", "ASGARD\\thor");
            MatchByGroup(writer, "Asgard Vanir", "ASGARD\\freyja");
            MatchByGroup(writer, "Asgard Vanir", "ASGARD\\loki");
            writer.Write("<h2>Por usuario</h2>");
            MatchByUser(writer, "Asgard Vanir", "ASGARD\\thor");
            MatchByUser(writer, "Asgard Vanir", "ASGARD\\freyja");
            MatchByUser(writer, "Asgard Vanir", "ASGARD\\loki");
        }
    }
}

Buen fin de semana.