Hazte Extensible con MEF

[UPDATE] Os podéis descargar el código de ejemplo

En la nueva versión del Framework (4.0) tendremos una librería que nos permitirá crear aplicaciones extensibles con cierta facilidad. Managed Extensibility Framework (MEF) permite que nuestras aplicaciones sean capaces de ofrecer nuevas funcionalidades sin necesidad de recompilar el código, tan solo añadiendo la DLL extensible como si de un Plug-in se tratase.

mef-blocks

¿Cómo funciona MEF?

MEF se basa en un Catálogo y en un CompositeContainer. El catálogo es el encargado de realizar el descubrimiento de las extensiones y el contenedor se encarga de descubrir y cargar las extensiones y satisfacer las dependencias.

Las partes extensibles deben de cumplir un contrato o interfaz y son cargadas en nuestra aplicación a través del catálogo. Veamos un ejemplo:

Definamos un contrato para el envío de una lista de ficheros a diferentes destinos (ftp, http, etc.)

   1: /// <summary>

   2: /// Contrato para el envío de una lista de ficheros.

   3: /// </summary>

   4: public interface ISend

   5: {

   6:     /// <summary>

   7:     /// Nombre del modo de envío

   8:     /// </summary>

   9:     string Name { get; }

  10:  

  11:     /// <summary>

  12:     /// Método que realiza el envío de la lista de ficheros

  13:     /// </summary>

  14:     /// <param name="files">Lista de ficheros a enviar</param>

  15:     /// <returns>Devuelve True si se ha realizado el envío correctamente.</returns>

  16:     bool AllFiles(List<string> files);

  17: }

Este contrato es el que tendrán que cumplir las extensiones que podemos añadir a nuestra aplicación.

El siguiente paso es realizar la carga de las extensiones para tenerlas disponibles para su uso.

   1: [Import]

   2: public ISend Send { get; set; }

   3:  

   4: private void Compose()

   5: {

   6:     var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

   7:     var container = new CompositionContainer(catalog);

   8:     container.ComposeParts(this);

   9: }

Instanciamos el Catálogo de nuestro ensamblado y realizamos la carga de las extensiones con el CompositionContainer. Cuanto se carga la extensión, quedará disponible para el uso en la propiedad definida por el atributo Import.

Otra posibilidad, a parte de la de crear nuestros propios catálogos, es utilizar el System.ComponentModel.Composition.Hosting.DirectoryCatalog. Esta catálogo nos permite realizar la carga de las extensiones desde un directorio de la siguiente forma:

   1: [ImportMany("send", typeof(ISend))]

   2: public IEnumerable<ISend> Sends { get; set; }

   3:  

   4: private void Compose()

   5: {

   6:     string path = Assembly.GetExecutingAssembly().CodeBase.Replace("file:///", string.Empty).Substring(0, path.LastIndexOf(@"/"));

   7:  

   8:     var catalog = new DirectoryCatalog(path + @"/Extensions/");

   9:     var contanier = new CompositionContainer(catalog);

  10:     contanier.ComposeParts(this);

  11: }

Como vemos en el código, hemos cambiado el Import por ImportMany para que sea capaz de cargar todos los extensores que se encuentren en el directorio que le vamos a especificar. Luego, en vez de utilizar el AssemblyCatalog, cargamos los extensores del directorio Extensions con el DirectoryCatalog. Esto realizará la carga del Enumerable de ISend y podremos seleccionar uno de ellos o todos para utilizar sus métodos.

Para ejecutar los métodos de un extensor sólo tendremos que obtenerlo de la propiedad definida con el Import (o del enumerado del ImportMany) y realizar la llamada al método que se ha definido en la interfaz.

   1: Send.AllFiles(ficheros);

   1: foreach (ISend item in Sends)

   2: {

   3:     item.AllFiles(ficheros);

   4: }

El contenedor es el encargado de instancias la extensión y permitir la ejecución del método requerido.

¿Cómo creamos extensiones?

Las extensiones no son más que ensamblados que implementan la interfaz del contrato, cumpliendo así con este y aplicando su propia lógica de programación.

   1: [Export("send", typeof(ISend))]

   2: public class Ftp : ISend

   3: {

   4:     string ftpAddress = "ftp.test.es";

   5:  

   6:     public string Name

   7:     {

   8:         get { return "FTP"; }

   9:     }

  10:  

  11:     public bool AllFiles(List<string> files)

  12:     {

  13:         FtpWebRequest ftpRequest = FtpWebRequest.Create(ftpAddress) as FtpWebRequest;

  14:         ftpRequest.Method = WebRequestMethods.Ftp.UploadFile;

  15:         ftpRequest.Credentials = System.Net.CredentialCache.DefaultCredentials;

  16:  

  17:         foreach (string file in files)

  18:         {

  19:             FileStream stream = File.OpenRead(file); 

  20:             byte[] buffer = new byte[stream.Length];

  21:             stream.Read(buffer, 0, buffer.Length); 

  22:             stream.Close();

  23:  

  24:             Stream reqStream = ftpRequest.GetRequestStream(); 

  25:             reqStream.Write(buffer, 0, buffer.Length); 

  26:             reqStream.Close();

  27:         }

  28:  

  29:         return false;

  30:     }

  31: }

Básicamente declaramos nuestra clase con el atributo Export y, opcionalmente, le especificamos de que tipo es. Implementamos los métodos que sean requeridos por la interfaz y listo. Compilamos este ensamblado, lo ponemos en el directorio que hemos indicado para la carga de los extensores y listo. Ya tenemos accesible esta extensión para el envío de ficheros por ftp. El resto de extensiones se desarrollan de la misma manera y se colocan en ensamblados por separado en la carpeta de las extensiones.

Fíjense que nuestra interfaz tiene un método llamado Name que podemos utilizar para realizar consultas LINQ y obtener el extensor que queremos, consultando por nombre.

   1: ISend extensor = Sends.Where(s => s.Name == "FTP").FirstOrDefault();

   2: extensor.AllFiles(ficheros);

Con MEF tenemos un Framework sencillo que nos permite añadirle mucha potencia a nuestras aplicaciones. Con pocas líneas de código hemos sido capaces de añadir diversas funcionalidades a nuestra aplicación sin tener que recompilarla y instalarla en los clientes. Por supuesto, esto es un pequeño ejemplo de la potencia de MEF, ahora nos quedaría poder personalizar el catálogo para que un usuario pueda elegir que extensores quiere, poder configurar estos extensores, etc.

 

Saludos a todos…

Más rápido con Visual Studio 2010. Depuración colaborativa

Cuantas veces le hemos enviado un bug a otro desarrollador y no poder volver reproducirlo. El equipo de Visual Studio nos quiere ayudar con estos problemillas. Una de las nuevas funcionalidades que tenemos es la posibilidad de crear depuraciones en colaboración. Os pongo un ejemplo y lo explico.

Cuando el Desarrollador 1 le envía el error al Desarrollador 2, se pierde mucha información que el Desarrollador 1 ya ha investigado y que el Desarrollador 2 tiene que volver a encontrar e investigar.

Veamos cómo podemos mejorar este proceso utilizando Visual Studio 2010. El Desarrollador 1 depurando su código encuentra que puede existir un error en la llamada a un método de un componente del Desarrollador 2.

En este punto, el Desarrollador 1 debería de enviar el error al Desarrollador 2, pero en vez de enviárselo y que el Desarrollador 2 tenga que volver a realizar la depuración, generará una serie de información de depuración que se le envía al Desarrollador 2 con el error. El Desarrollador 1 comparte su entorno de depuración con el Desarrollador 2. Lo primero de todo es exportar el breakpoint actual para que el Desarrollador 2 sepa dónde tiene que empezar la depuración del error.

Lo siguiente es compartir la información que el Desarrollador 2 debe de inspeccionar al depurar. Para hacer esto, primero debemos anclar la variable que causa el error y exportar esos datos de depuración. Incluso podemos añadirle comentarios a la variable para que lo lea el Desarrollador 2.

Ahora, el Desarrollador 2 puede continuar con la depuración del Desarrollador 1 importando la información de depuración que se le ha enviado.

Cuando el Desarrollador 2 importa el entorno de depuración, puede continuar con el mismo, incluso con los valores de las variables y los comentarios que se han exportado.

Este proceso de Depuración Colaborativa nos permite mejorar la integración del equipo de Testers y que estos sean capaces de enviar errores e información de calidad para poder reproducirlos y corregirlos. Aunque no hayamos hablado de él, tenemos que tener en cuenta el nuevo proceso de depuración de Visual Studio 2010 llamado Intellitrace.

Intellitrace nos permite mantener un histórico de nuestra depuración para poder navegar hacia atrás en nuestro código y visualizar el estado de este. Espero poder escribir un artículo en breve sobre él explicando su uso y sus ventajas (por ejemplo, si estamos probando nuestra aplicación y se produce una excepción, no tenemos que poner un breakpoint y volver a empezar hasta llegar a él para depurar el error, si tenemos habilitado Intellitrace automáticamente podremos depurar la excepción cuando esta se produzca).

 

Saludos a todos…

SharePoint 2010. Client API

En el artículo Consultando listas con WCF Data Services veíamos como podíamos utilizar el nuevo servicio ODATA para consultar y actualizar los datos de las listas de SharePoint. Hoy nos toca explicar la nueva API manejada de cliente Microsoft.SharePoint.Client.

Esta nueva API, además de ofrecernos la capacidad de realizar consultas y actualizaciones sobre las listas, nos permite interactuar con los objetos más comunes de SharePoint. Con los WCF Data Services sólo podemos consultar y actualizar los elementos de las listas, mientras que con esta API podremos crear y eliminar listas, crear, actualizar y eliminar elementos de las listas, modificar documentos de las librerías, crear sitios, administrar permisos, administrar web parts, etc.

Estas funcionalidades las tenemos presentes en los servicios web de SharePoint, pero no es un secreto que estos servicios web son algo engorrosos y complicados de desarrollar. Normalmente desarrollamos nuestros servicios web que, utilizando el API de servidor, expone las funcionalidades que necesitábamos, pero con una interfaz más amigable y programable.

Tenemos tres posibles clientes para este API:

  • Código Manejado .NET
  • Silverlight
  • ECMAScript

Escribir código con la API de cliente es muy similar a escribir con el API de servidor, salvo por algunas especificaciones que iremos viendo.

Hablemos del modelo

Cuando nuestra aplicación utiliza el API para realizar consultas sobre SharePoint, el API genera un XML que es enviado al servidor, el servidor recibe esa petición, procesa las peticiones utilizando el modelo de objetos del servidor y devuelve al API la respuesta en formato JSON. El API de cliente analiza el JSON recibido y le entrega al código los objetos con los datos solicitados (objetos .NET u objetos JavaScript).

Para mejorar el rendimiento de las peticiones al servidor, el API necesita de la llamada a un método para que se realizan todos los procesos especificados. Esto es, podemos realizar múltiples procedimientos (actualizar elementos de lista, crear lista, etc) y estos no se ejecutarán en el servidor hasta que se lo especifiquemos con la llamada al método ExecuteQuery.

Vamos a un ejemplo y así comprenderemos esta funcionalidad del API.

Utilizando el API

Lo primero que tenemos que hacer es agregar en nuestro proyecto las referencias a dos ensamblados de SharePoint, Microsoft.SharePoint.Client.dll y Microsoft.SharePoint.Client.Runtime.dll (de momento los podemos encontrar  en nuestro servidor en la carpeta %ProgramFiles%Common FilesMicrosoft SharedWeb Server Extensions14ISAPI, en un futuro me imagino que habrá algún instalador para instalar estas API en un entorno de desarrollo que no tenga SharePoint).

Para consultar un sitio, ejecutamos el siguiente código:

   1: ClientContext context = new ClientContext("http://intranet.contoso.com");

   2: Web site = context.Web;

   3: context.Load(site);

   4: context.ExecuteQuery();

   5: Console.WriteLine("Título: {0}", site.Title);

Lo primero que tenemos que hacer es instanciar un ClientContext con la url de nuestro sitio, este es el encargado de realizar las llamadas XML y recibir el JSON de nuestro servidor. Luego nos creamos una variable para recibir los valores de nuestro sitio, le especificamos al contexto que realice la carga de éste y en la llamada a ExecuteQuery es cuando se envía y se recibe la información del servidor.

Es importante tener en cuanta la llamada Load con la variable de nuestro site. Esta llamada especifica al contexto que realice la carga de esta tipo de variable, sino lo especificáramos, la variable site estaría vacía y no tendría la información que estábamos esperando. Resumiendo:

  • Especificamos al contexto las operaciones que tiene que enviar al servidor. Esto incluye acceso a datos de listas, sitios, web parts, consultas CAML, objetos a crear, modificar, eliminar, etc.
  • Luego realizamos la llamada a ExecuteQuery, que envía la petición XML al servidor y recibe los objetos JSON con los datos solicitados.

Siguiendo con este modelo de desarrollo, veamos como podemos crear una lista e insertar elementos en ella, para esto necesitamos hacer uso de dos clases ListCreationInformation y ListItemCreationInformation que nos permite especificar los valores específicos de cada elemento. Por ejemplo, para la creación de la lista le especificamos el Title, TemplateType, etc, mientras que para cada elemento lo único que necesitamos es crear un nuevo Item en la lista y ejecutar el Update cuando hayamos terminado de crearlo.

   1: ClientContext context = new ClientContext("http://intranet.contoso.com");

   2: Web site = context.Web;

   3:  

   4: //Creamos una lista de Tasks

   5: ListCreationInformation listCreationInformation = new ListCreationInformation();

   6: listCreationInformation.Title = "Tareas";

   7: listCreationInformation.TemplateType = (int)ListTemplateType.Tasks;

   8: List list = site.Lists.Add(listCreationInformation);

   9:  

  10: //Creamos elemento en la lista de Tasks

  11: ListItemCreationInformation listItemCreationInformation = new ListItemCreationInformation();

  12:  

  13: ListItem item = list.AddItem(listItemCreationInformation);

  14:  

  15: item["Title"] = "Titulo tarea 1 desde API";

  16: item["Body"] = "Descirpcion de la tarea 1";

  17: item.Update();

  18:  

  19: item = list.AddItem(listItemCreationInformation);

  20: item["Title"] = "Titulo tarea 1 desde API";

  21: item["Body"] = "Descirpcion de la tarea 1";

  22: item.Update();

  23:  

  24: context.ExecuteQuery();

y como se realizan consultas sobre las listas:

   1: ClientContext context = new ClientContext("http://intranet.contoso.com");

   2:  

   3: List list = context.Web.Lists.GetByTitle("Tasks");

   4:  

   5: CamlQuery camlQuery = new CamlQuery();

   6: camlQuery.ViewXml = "<View/>";

   7: ListItemCollection listItems = list.GetItems(camlQuery);

   8:  

   9: context.Load(list);

  10: context.Load(listItems);

  11: context.ExecuteQuery();

  12:  

  13: foreach (ListItem listItem in listItems)

  14:     Console.WriteLine("Id: {0} Titulo: {1}", listItem.Id, listItem["Title"]);

Primero obtenemos la lista por nombre (Lists.GetByTitle), creamos un CalmQuery y obtenemos los elementos de la lista con list.GetItems(CalmQuery). Le especificamos al contexto que realice la carga de la lista y de los elementos de la lista y ejecutamos la consulta.

Si pensamos en las listas que los usuarios crean en SharePoint, cabe la posibilidad de tener listas con gran cantidad de campos y nuestra aplicación sólo necesita 2 o 3 para realizar un proceso. Vemos como realizar una consulta de una lista especificando que nos devuelve sólo aquellos campos que necesitamos.

   1: ClientContext context = new ClientContext("http://intranet.contoso.com");

   2:  

   3: List list = context.Web.Lists.GetByTitle("Tasks");

   4: CamlQuery camlQuery = new CamlQuery();

   5: camlQuery.ViewXml =

   6:     @"<View>

   7:         <Query>

   8:           <Where>

   9:             <Eq>

  10:               <FieldRef Name='Status'/>

  11:               <Value Type='Text'>Completed</Value>

  12:             </Eq>

  13:           </Where>

  14:         </Query>

  15:         <RowLimit>100</RowLimit>

  16:      </View>";

  17:  

  18: ListItemCollection listItems = list.GetItems(camlQuery);

  19: context.Load(

  20:      listItems,

  21:      items => items

  22:          .Include(

  23:              item => item["Title"],

  24:              item => item["Assigned To"],

  25:              item => item["Completed"]));

  26:  

  27: context.ExecuteQuery();

  28:  

  29: foreach (ListItem listItem in listItems)

  30: {

  31:     Console.WriteLine("Titulo: {0}", listItem["Title"]);

  32:     Console.WriteLine("Categoria: {0}", listItem["Assigned To"]);

  33:     Console.WriteLine("Completado: {0}", listItem["Completed"]);

  34:     Console.WriteLine();

  35: }

 

En el ejemplo anterior, obtenemos una lista por nombre, creamos un CamlQuery para realizar una consulta a esa lista y cargamos los elementos de la lista especificándole, con la expresión Lambda Include, los campos que queremos que nos devuelvan.

Ahora vamos a ver como se pueden realizar actualizaciones de los datos de la lista que hemos obtenido.

   1: foreach (ListItem listItem in listItems)

   2: {

   3:     listItem["Title"] = "Nuevo titulo";

   4:     listItem.Update();

   5: }

   6:  

   7: context.ExecuteQuery();

Para cada elemento de la lista, realizamos los cambios necesarios y ejecutamos el método Update para que el contexto sepa que tiene que realizar la actualización de ese elemento y especifique el proceso en la respuesta XML que envía al servidor.

Todo esto y mucho más es lo que podemos hacer con esta API, incluso podemos recorrer los campos de una lista y generar el xml  con los campos que la componen. También es interesante conocer la posibilidad de realizar consultas limitando el número de elementos que recibimos, o incluso paginando las consultas recibiendo los elementos paginados.

   1: ListItemCollectionPosition itemPosition = null;

   2: while (true)

   3: {

   4:     CamlQuery camlQuery = new CamlQuery();

   5:  

   6:     camlQuery.ListItemCollectionPosition = itemPosition;

   7:     camlQuery.ViewXml =

   8:         @"<View>

   9:         <ViewFields>

  10:           <FieldRef Name='Title'/>

  11:           <FieldRef Name='AssignedTo'/>

  12:           <FieldRef Name='Status'/>

  13:         </ViewFields>

  14:         <RowLimit>10</RowLimit>

  15:       </View>";

  16:  

  17:     ListItemCollection listItems = list.GetItems(camlQuery);

  18:     context.Load(listItems);

  19:     context.ExecuteQuery();

  20:     itemPosition = listItems.ListItemCollectionPosition;

  21:  

  22:     foreach (ListItem listItem in listItems)

  23:         Console.WriteLine("  Titulo: {0}", listItem["Title"]);

  24:  

  25:     if (itemPosition == null)

  26:         break;

  27:  

  28:     Console.WriteLine(itemPosition.PagingInfo);

  29:     Console.WriteLine();

  30: }

 

Vemos como se especifica un límite de 10 elementos con RowLimit en el CamlQuery y se utiliza la clase ListItemCollectionPosition para mantener el cursor en la página de elementos que tenemos actualmente en memoria.

Esta API es un gran avance que nos permite crear código de cliente para interactuar con nuestro queridísimo SharePoint, por ejemplo, para crear documentos Words en memoria con Open XML y almacenarlos en una Biblioteca de Documentos. Si, lo sé, antes también podíamos hacerlo, pero los afortunados como yo que hemos utilizado los servicios web de SharePoint, siempre nos acordamos del equipo de desarrollo que lo pensó.

 

Saludos a todos…