Archivo

Posts Tagged ‘SharePoint’

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

Leyendo columnas de búsqueda (Lookup) en SharePoint


Buen inicio de año a todos, feliz 2012, feliz nuevo b’ak’tun… una disculpa por mi silencio de noviembre y diciembre, pero bueno, ya saben: miles de proyectos, todo urge… yada yada yada. Pero ya vine con un post pequeño pero que es útil en tu programación para SharePoint run-of-the-mill.

Una característica padre que tiene SharePoint es que te permite crear campos de búsqueda en una lista cualquiera. Estos campos hacen referencia a campos dentro de otra lista o biblioteca, de tal suerte que permiten crear una relación padre/hijo.

Veamos un ejemplo jocoso. Supongamos que estamos creando un SharePoint para el colegio inglés Hogwarts. Pensemos que estamos armando un catálogo que contiene información sobre los cursos que daría el famoso auror Alastor Moody. Cada curso tiene asociados diversos temas, y cada tema tiene asociadas las preguntas del examen que el profesor practicará a los estudiantes.

image

Creamos nuestra lista de preguntas: una lista personalizada sencilla. Por ejemplo:

image

Posteriormente, creamos nuestra lista de temas. Pero cada tema debe tener asociada una o más preguntas, por lo que podemos sacar provecho el campo búsqueda: añadimos uno, le decimos que busque en la lista “Preguntas” y que permita valores múltiples. La siguiente imagen muestra cómo luce la lista cuando estamos creando un nuevo tema, y cómo se ve la vista de tabla.

image

image

Y algo muy similar para los cursos: creamos un campo de búsqueda y lo asociamos a los temas.

image

Ahora bien, como administradores eso está padre, y todo, pero nada nuevo bajo el sol. Lo interesante ocurre cuando tenemos que crear algún control en el cual tengamos que leer dicha información.

Afortunadamente SharePoint nos provee con una clase que nos hace la vida más fácil: SPFieldLookupValueCollection. Las columnas de búsqueda con opción múltiple (como la de Temas en la imagen anterior) pueden convertirse en esta clase, y ahí podemos iterar para obtener los objetos SPFieldLookupValue, los cuales cuentan con dos propiedades importantes: LookupId y LookupValue. De esta forma podemos buscar relaciones fácilmente.

SPWeb web = SPContext.Current.Web;

SPList cursos = web.List["Cursos"];
SPList temas = web.List["Temas"];

foreach (SPListItem curso in cursos.Items)
{
    Console.WriteLine("Curso: {0}", curso.DisplayName);

    var temasEnCursos = curso["Temas"] as SPFieldLookupValueCollection;
    foreach (var temaLookup in temasEnCursos)
    {
        var tema = temas.Items.GetItemById(temaLookup.LookupId);
        Console.WriteLine("Tema: {0}", tema.DisplayName);
    }
    Console.WriteLine("=====\n");
}

Dado que SPFieldLookupValueCollection hereda de List<SPFieldLookupValue>, tenemos disponible toda la funcionalidad de List<T>. Por ejemplo, podemos usar el método Find para obtener todos los elementos que comienzan con A:

SPWeb web = SPContext.Current.Web;

SPList cursos = web.List["Cursos"];
SPList temas = web.List["Temas"];

foreach (SPListItem curso in cursos.Items)
{
    Console.WriteLine("Curso: {0}", curso.DisplayName);

    var temasEnCursos = curso["Temas"] as SPFieldLookupValueCollection;
    var cursosFiltrados = temasEnCursos.Find(x => x.StartsWith("A"));

    foreach (var temaLookup in cursosFiltrados)
    {
        Console.WriteLine("Tema: {0}", temaLookup.LookupValue);
    }
}

Por otra parte, SPFieldLookupValueCollection tiene el método ToString sobrecargado. Al invocarlo, nos regresa una cadena de texto con el formato: ID;#Texto;ID;#Texto. Por ejemplo:

var temasEnCursos = curso["Temas"] as SPFieldLookupValueCollection;
Console.WriteLine(temasEnCursos.ToString());

Nos imprime algo como:

2;#Cruciatus;#3;#Imperio;#1;#Avada Kedavra

 

Si quisiéramos crear un nuevo objeto, podemos usar el constructor que toma un parámetro: una cadena con el formato anterior. Por ejemplo:

var texto = "2;#Cruciatus;#3;#Imperio;#1;#Avada Kedavra";
var nuevoTema = new SPFieldLookupValueCollection(texto);
curso["Temas"] = nuevoTema;
curso.Update();

Como podemos ver, es muy sencillo utilizar este tipo de campos en nuestros desarrollos para SharePoint. Y esto gana en la vida, porque usar este tipo de columnas nos permite establecer relaciones maestro-hijo de forma muy sencilla. La alternativa sería usar una tercera lista que enlace ambas entidades. Pero eso es muy de base de datos, y no tanto de SharePoint.

Tschüss!

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.

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

Consulta, descarga y exportación de perfiles de usuarios en SharePoint


Puedes consultar mi último código en la galería de código de MSDN: consulta, descarga y exportación de perfiles de usuarios en SharePoint. Algunos extractos a continuación.

Introducción

El presente ejemplo muestra cómo utilizar los servicios web para construir una aplicación que permita consultar, descargar y exportar los perfiles de usuario de SharePoint Server 2010, así como información suplemental.

Construyendo el ejemplo

El código de ejemplo es una solución hecha con Visual Studio 2010 para .NET Framework 3.5. No tiene alguna otra dependencia. Hace referencia a dos servicios web, cuyas clases proxy se generaron a partir de los servicios _vti_bin/UserGroup.asmx y _vti_bin/UserProfileService.asmx. El primer servicio está disponible en SharePoint Foundation 2010 y el segundo en SharePoint Server 2010. Nota: este programa funciona también con versiones anteriores de SharePoint, aunque es posible que algunas características no estén disponibles.

Descripción

El programa cuenta con un componente llamado ProfileSource. Éste actúa como fuente de datos para el programa, y contiene los métodos que obtienen la información de los perfiles mediante consultas hechas a los servicios web. Este componente implementa gran parte de la lógica de la aplicación. En particular el método OnDoWork es quien se encarga de descargar la información de los perfiles. A continuación se muestran extractos del mismo.

_worker.ReportProgress(0, Resources.ProfileSource_StartingServices); 
userGroup = new UserGroup(); 
PrepareService(userGroup, _connectionProperties.UserGroupWebService); 
userProfile = new UserProfileService(); 
PrepareService(userProfile, _connectionProperties.UserProfileWebService); 
 
_worker.ReportProgress(0, Resources.ProfileSource_DownloadSchematics);  
Table = CreateTable(userProfile);

El método PrepareService establece algunas propiedades básicas del servicio web, como la URL de donde se encuetra y las credenciales. Nota que éstas últimas varían dependiendo de si el usuario ha seleccionado usar las credenciales por defecto o no. Por otra parte, el método CreateTable utiliza el servicio UserProfileService y su método GetUserProfileSchema para descargar todos los campos disponibles desde el servidor, y con base en ellos generar el esquema de un objeto DataTable.

XmlNode mainNode = null; 
if (ConnectionProperties.AllSiteUsers) 
{ 
    _worker.ReportProgress(0, Resources.ProfileSource_DownloadAllUserInfo); 
    mainNode = userGroup.GetAllUserCollectionFromWeb(); 
} 
else 
{ 
    _worker.ReportProgress(0, Resources.ProfileSource_DownloadUserInfo); 
    mainNode = userGroup.GetUserCollectionFromWeb(); 
} 
_worker.ReportProgress(0, Resources.ProfileSource_UserInfoDownloaded);

El extracto anterior obtiene los usuarios disponibles del sitio (todos los registrados, o bien cualquiera que haya ingresado), mediante la llamada a los métodos GetAllUserCollectionFromWeb o GetUserCollectionFromWeb del servicio web UserGroup, según sea el caso.

XmlDocument doc = new XmlDocument(); 
doc.LoadXml(mainNode.OuterXml); 
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); 
nsmgr.AddNamespace("defns", "http://schemas.microsoft.com/sharepoint/soap/directory/"); 
 
string xpath; 
if (ConnectionProperties.AllSiteUsers) 
    xpath = "defns:GetAllUserCollectionFromWeb/defns:Users/descendant::defns:User"; 
else 
    xpath = "defns:GetUserCollectionFromWeb/defns:Users/descendant::defns:User"; 
XmlNodeList userNodes = doc.SelectNodes(xpath, nsmgr); 

Una vez descargados los usuarios, necesitamos analizar el documento XML regresado. Para ello hacemos uso de XPath. El extracto anterior muestra el código que genera la consutla XPath, y que nos devuelve una lista con todos los nodos que contienen la información de los usuarios.

Posteriormente, recorremos cada elemento de la lista y llamamos al método GetUserProfile, el cual usará al método GetUserProfileByName del servicio web UserProfileService. Los resultados obtenidos los insertamos como una fila en nuestra tabla, como se muestra en el siguiente extracto.

foreach (XmlNode userNode in userNodes) 
{ 
    double stepDouble = ((double)++count / (double)maxCount) * 100.0; 
    int stepInt32 = Math.Min((int)Math.Round(stepDouble, 0), 100); 
    string loginName = userNode.Attributes["LoginName"].Value; 
    _worker.ReportProgress(stepInt32, string.Format(Resources.ProfileSource_DownloadingProfile, loginName, stepInt32)); 
    DataRow row = GetUserProfile(userProfile, loginName); 
    Table.Rows.Add(row); 
 
    if (count >= maxCount) 
        break; 
}

Esta porción de código muestra cómo el método GetUserProfile obtiene el perfil de un usuario.

PropertyData[] allData = service.GetUserProfileByName(loginName); 
foreach (PropertyData data in allData) 
{ 
    if (row.Table.Columns.Contains(data.Name)) 
    { 
        if (data.Values.Length > 0 && data.Values[0].Value != null) 
            row[data.Name] = data.Values[0].Value.ToString(); 
        else 
            row[data.Name] = "[Empty]"; 
    } 
}

Adicional a los métodos mencionados, el componente cuenta con los siguientes miembros.

  • Table. Un objeto DataTable que contiene la información descargada de los perfiles.
  • ConnectionProperties. Un componente ConnectionProperties, el cual contiene la información necesaria para establecer la conexión y descarga de elementos (i.e. usuario, contraseña, URLs, etc.).
  • GetColleagues. Consulta al servicio web UserProfileService por los colegas de un usuario determinado.
  • GetInCommon. Consulta al servicio web UserProfileService por información que el usuario actual tenga en común con otro usuario determinado.
  • GetLinks. Consulta al servicio web UserProfileService por los enlaces de un usuario determinado.
  • GetMemberships. Consulta al servicio web UserProfileService por las membrecías de un usuario determinado.
  • GetOrganizations. Consulta al servicio web UserProfileService por las organizaciones a las que pertenece un usuario determinado.
  • GetPinnedLinks. Consulta al servicio web UserProfileService por los enlaces marcados de un usuario determinado.
  • SaveAs. Exporta los perfiles de usuario hacia un archivo de valores separados por coma (CSV) o a un XML.
  • Loaded. Evento que se dispara cuando el componente ha terminado de descargar la información de perfiles.
  • LoadProgress. Evento que se dispara cuando hay un avance significativo durante la descarga de información de perfiles.
  • ProfileFailure. Evento que se dispara cuando se intenta obtener el perfil de un usuario y éste falla (i.e. por cuestiones de permisos, porque no existe, etc.), en cuyo caso se puede continuar con el siguiente perfil (comportamiento por defecto) o bien terminar con el proceso.
  • Failure. Evento que se dispara cuando la carga de información de usuarios falla y lanza una excepción.

Como última precisión, comentar que el programa está localizado y puede visualizarse en inglés y en español.

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…

Perfiles de usuario e información social


En esta era donde las redes sociales inundan nuestras vidas, es cada vez más común ver empresas y corporaciones que piden características de éstas en sus propios sitios corporativos.

SharePoint 2010 ha dado pasos significativos en esa dirección mediante la provisión de mecanismos para habilitar características similares a las encotnradas en redes sociales. Así pues, cuando un cliente nos pida desarrollar tal o cual característica social, sabremos que podemos apoyarnos en lo que SharePoint nos ofrece.

Quizás lo primero que viene a la mente, al respecto, sean los perfiles de usuario. Es decir, dado un usuario cualquiera en SharePoint, quisiéramos obtener su información particular: nombre, teléfonos, departamentos, móviles, jefe, direcciones, etc. En SharePoint 2010 es posible obtener esta información, si se tienen los permisos necesarios. Existen dos formas de hacerlo: mediante el modelo de objetos de SharePoint, o bien mediante los servicios web que se exponen en /_vti_bin/ de cada sitio. En esta ocasión usaremos los servicios web.

O mejor dicho, usaremos un servicio web en particular: UserProfileService.asmx. Este lo podemos referenciar desde http://misitio/_vti_bin/UserProfileService.asmx. Al hacerlo, nos generará varias clases proxy, entre las que se encuentra UserProfileService.

Vamos por partes. Lo primero que nos interesa es obtener el perfil dado un usuario determinado. Pues bien, es cuestión de llamar a UserProfileService.GetUserProfileByName. Este método toma como parámetro el nombre de la cuenta de un usuario de la forma “dominio\cuenta” (digamos, “ASGARD\thor”) y regresa una colección de objetos PropertyData (esta clase, por cierto, es una clase proxy generada al momento de referenciar el servicio web). Cada PropertyData representa una propiedad del usuario, así que para obtener todas hay que iterar sobre el array.

El siguiente ejemplo muestra cómo obtener la propiedad PreferredName.

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

PropertyData[] properties = service.GetUserProfileByName("LABWF\\thor");
service.Dispose();

var query = from property in properties
                     where propety.Name.Equals("PreferredName")
                         && property.Values.Length > 0
                         && property.Values[0] != null
                         && property.Values[0].Value != null
                     select property.Values[0].Value.ToString();

string preferredName = query.FirstOrDefault();

                     

Algunas de las propiedades más interesantes (i.e. PropertyData.Name) son las siguientes: AccountName, ADGuid, CellPhone, Department, FirstName, HomePhone, LastName, Manager, PictureURL, PreferredName, SPSBirthday, SPSHireDate, SPSJobTitle, Title, UserName, WebSite, WorkEmail y WorkPhone. Pero hay otras, ¡muchas más!

Si estamos en una aplicación en consola, podemos iterar sobre cada elemento de forma más o menos genérica:

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

PropertyData[] properties = service.GetUserProfileByName("LABWF\\thor");
service.Dispose();

foreach (PropertyData property in properties)
{
    string name = property.Name;
    string value;
    if (property.Values.Length > 0
        && property.Values[0] != null
        && property.Values[0].Value != null)
        value = property.Values[0].Value.ToString();
    else
        value = string.Empty;

    Console.WriteLine("Propiedad: {0} Valor: {1}", name, value);
}

Ahora bien, aunque es mucha información relacionada al usuario, y ciertamente reviste importancia, no lo es todo en una red social. En particular, nos interesará ver otros elementos que estén asociados a un usuario particular. SharePoint provee, a través del mismo WebService, información adicional. Veamos una por una.

Quizá una de las más usadas es ver qué colegas tiene un usuario determinado. Un colega es un usuario que es marcado como conocido de otro usuario. Así, un usuario determinado puede tener a varios colegas. Esa información se obtiene usando el método UserProfileService.GetUserColleagues, pasando como parámetro la cuenta del usuario en cuestión. Easy peasy:

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

ContactData[] allData = service.GetUserColleagues("LABWF\\thor");
service.Dispose();

foreach (ContactData data in allData)
{
    Console.WriteLine("Colega: {0}", data.AccountName);
    Console.WriteLine("*** Email: {0}", data.Email);
    Console.WriteLine("*** Group: {0}", data.Group);
    Console.WriteLine("*** ID : {0}", data.ID);
    Console.WriteLine("*** IsInWorkGroup : {0}", data.IsInWorkGroup);
    Console.WriteLine("*** Name : {0}", data.Name);
    Console.WriteLine("*** Privacy : {0}", data.Privacy);
    Console.WriteLine("*** Title : {0}", data.Title);
    Console.WriteLine("*** Url : {0}", data.Url);
    Console.WriteLine("*** UserProfileID : {0}", data.UserProfileID);
    Console.WriteLine();
}

El método GetUserColleagues regresa una colección de ContactData. Ésta clase (igual, proxy generada al referenciar el servicio web) tiene varias propiedades, relacionadas con el colega. Cada elemento de la colección, obvio, representa uno.

Otro aspecto importante que SharePoint ofrece es la posibilidad de que cada usuario tenga un enlace (link), digamos, como favorito. O que otro usuario le envíe un enlace como referencia. Bueno, pues UserProfileService cuenta con dos métodos para llevar a cabo lo anterior: GetUserLink y GetUserPinnerLinks. La diferencia entre ambos es que GetUserPinnerLniks son aquellos enlaces que el usuario ha marcado como favoritos, de manera explícita. El procedimiento es muy similar: ambos métodos regresan una colección de clases proxy, QuickLinkData y PinnedLinkData, respectivamente.

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

QuickLinkData[] quickLinks = service.GetUserLinks("LABWF\\thor");
foreach (QuickLinkData data in quickLinks)
{
    Console.WriteLine("Enlace: {0}", data.Name);
    Console.WriteLine("*** Group: {0}", data.Group);
    Console.WriteLine("*** ID : {0}", data.ID);
    Console.WriteLine("*** Privacy : {0}", data.Privacy);
    Console.WriteLine("*** Url : {0}", data.Url);
    Console.WriteLine();
}

PinnedLinkData[] pinnedLinks = service.GetUserPinnedLinks("LABWF\\thor");
foreach (PinnedLinkData data in pinnedLinks)
{
    Console.WriteLine("Enlace marcado: {0}", data.Name);
    Console.WriteLine("*** ID : {0}", data.ID);
    Console.WriteLine("*** Url : {0}", data.Url);
    Console.WriteLine();
}

service.Dispose();

Las membresías son otro aspecto interesante que nos ofrece nuestro SharePoint social. Es decir, a qué grupos pertenece tal usuario. Similar a esto, también nos interesa saber a qué organizaciones pertenece el usuario. Para hacerlo, usaremos GetUserMemberships y GetUserOrganization, los cuales regresan (como cabría esperar) una colección de objetos de tipo MembershipData y OrganizationProfileData, respectivamente (¿comienzas a ver el patrón?).

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

MembershipData[] memberships = service.GetUserMemberships("LABWF\\thor");
foreach (MembershipData data in memberships)
{
    Console.WriteLine("Membrecía: {0}", data.DisplayName);
    Console.WriteLine("*** Group: {0}", data.Group);
    Console.WriteLine("*** ID : {0}", data.ID);
    Console.WriteLine("*** MailNickname: {0}", data.MailNickname);
    Console.WriteLine("*** MemberGroup: {0}", data.MemberGroup.SourceReference);
    Console.WriteLine("*** MemberGroupID: {0}", data.MemberGroupID);
    Console.WriteLine("*** Privacy : {0}", data.Privacy);
    Console.WriteLine("*** Source: {0}", data.Source);
    Console.WriteLine("*** Url : {0}", data.Url);
    Console.WriteLine();
}

OrganizationProfileData[] organizations = service.GetUserOrganizations("LABWF\\thor");
foreach (OrganizationProfileDatadata in organizations)
{
    Console.WriteLine("Organización: {0}", data.DisplayName);
    Console.WriteLine("*** ID : {0}", data.RecordID);
    Console.WriteLine();
}

service.Dispose();

El perfil de un usuario, más su información relacionada (en este caso: colegas, organizaciones, membrecías, enlaces y favoritos) cubre gran parte de la información que uno cabría esperar de una red social (corporativa). Pero SharePoint va un paso más allá: no nada más nos provee la posibilidad de consultar esta información, sino también permite compararla con la de otros usuarios. Por ejemplo: ¿qué usuarios tenemos en común? ¿Qué membrecías compartimos? ¿Cuál es nuestro jefe común?

El servicio web expone el método GetInCommon, el cual toma la cuenta de un usuario como parámetro. Este método compara dicho usuario contra el usuario actual (en este caso, el usuario almacenado en las credenciales del servicio web, o dicho de otra forma, el usuario que manda llamar al servicio web), y rergesa una colección (sorpresa sorpresa) de objetos tipo InCommonData. Esta clase proxy tiene tres miembros: una colección de ContactData, Colleagues, que representa los colegas en común; una colección de MembershipData, Memberships, que representa las membrecías e común; y una propiedad de tipo ContactData, Manager, que representa el jefe en común.

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

InCommonData data = service.GetInCommon("LABWF\\thor");
service.Dispose();

Console.WriteLine("Jefe común: {0}", data.Manager.Name);

Console.WriteLine("Colegas en común:");
foreach (ContactData colleague in data.Colleagues)
    Console.WriteLine("*** {0}", colleague.Name);

Console.WriteLine("Membrecías en común:");
foreach (MembershipData membership in data.Memberships)
    Console.WriteLine("*** {0}", membership.DisplayName);

Console.WriteLine();

Alternativamente, puedes llamar a los métodos GetCommonManager, GetCommonColleagues y GetCommonMemberships, por separado, si no quieres traerte toda la información.

Pues bien, como puedes ver UserProfileService es el primer paso para crear aplicaciones sociales en SharePoint. Por supuesto, no es la única forma. Ahora hemos usado servicios web, pero también podríamos usar el modelo de objetos. Eso será asunto de otra entrada.