Parallel Series: La clase estática Parallel

parallel_class

Acabo de publicar otro post relacionado con las ‘Parallel Series’:

 

3 métodos para los reyes elfos bajo el cielo

Hoy quiero hablaros de la clase estática Parallel. Esta clase provee soporte para paralelizar bucles y regiones, y al igual que PLINQ su uso es muy sencillo. Cabe destacar que está especialmente optimizada para iteraciones, y que en este contexto se desenvuelve un poco mejor que PLINQ. No hay una diferencia significativa en tiempos absolutos, pero puede verse perfectamente si utilizamos el magnífico profiler de Visual Studio 2010. No obstante, pueden existir situaciones en las que si se necesita afinar mucho el rendimiento en iteraciones, y aquí es dónde tiene más sentido utilizar dos de los tres métodos de esta clase: For y ForEach. Al tercero lo llamaremos Cirdan y apenas aparecerá en esta historia (en realidad me refiero a Invoke pero tampoco aparecerá por aquí)…

Podéis acceder al artículo completo aquí:

http://lluisfranco.com/2011/06/26/parallel-series-la-clase-esttica-parallel/

Un saludo,

Parallel Series: Video – 02 PLINQ

claqueta

Acabo de publicar otro vídeo de las las ‘Parallel Series’. Os dejo aquí también el enlace:

http://lluisfranco.wordpress.com/2011/07/01/parallel-series-video-02-plinq/

En este segundo vídeo de las Parallel Series haremos un breve recorrido por las principales características de Parallel LINQ.

Un saludo,

Nacen las Parallel Series

Como ya os prometí a algunos de vosotros, he empezado a publicar una nueva serie que promete ser la más larga de las que he publicado hasta ahora. El tema a tratar va a ser la programación paralela mediante la Task Parallel Library incorporada en .NET 4.0 y Visual Studio 2010.

parallelism

Cuento con la ventaja de haber estado creando material durante el último año y medio, en el que he publicado algunos documentos, varios vídeos y hasta un webcast con los chicos de SecondNug. Todo este material y alguno más que tengo en mente formará parte de las Parallel Series, que serán publicadas en mi otro blog y no aquí, porque la publicación cruzada entre WordPress y Community Server es un autentico drama.

Sin embargo, prometo avisar aquí cada vez que publique un nuevo post se la serie. De momento tengo unos cinco o seis artículos publicados (la serie posiblemente llegará a los 20) y mi intención es ir publicando al menos uno por semana, y si puedo dos mucho mejor :-)

Os dejo con el índice de contenidos de la serie:

Y algunos de los artículos ya publicados (y los que faltan):

Índice de contenidos de las Parellel Series

Artículos relacionados

A medida que vaya publicando en el otro blog iré actualizando también este de aquí.

Un saludo!

PD – Me encantaría recibir feedback vuestro, así que se agradecerán comentarios de todo tipo (en el otro blog).

Editar documentos almacenados como array de bits en SQL Server [FileStream] (1/n)

Bajo este título -a priori tan chorra-, voy a empezar una serie de posts que muestren cómo almacenar documentos (ficheros NTFS) en una base de datos SQL Server 2008 mediante el uso de FILESTREAM storage, y cómo visualizarlos y editarlos con su aplicación asociada. En realidad, ésta última parte –la edición- es la única compleja, pero me ha parecido un buen tema para empezar una serie, y así de paso retomar las viejas costumbres.

FILESTREAM Storage:

Una de las características más esperadas de SQL Server 2008 –al menos por mi parte- es el almacenamiento en FILESTREAM de grandes ficheros como array de bytes, en campos de tipo varbinary(MAX) dentro de nuestra base de datos. El punto fuerte de este tipo de almacenamiento es que en lugar de almacenar el array de bytes dentro de la tabla lo almacena de forma transparente en el sistema de ficheros NTFS, pero con todas las ventajas de SQL Server (Atomicidad, facilidad para realizar backups, rendimiento, seguridad, etc.). Esto permite además que el tamaño de la BD no crezca despesuradamente, ya que la edición express *sólo* permite almacenar hasta 10GB.

Configuración de FILESTREAM Storage:

  1. Activar FILESTREAM en nuestro servidor SQL Server.
  2. Crear una base de datos habilitada para FILESTREAM
  3. Crear una tabla con un campo de tipo varbinary(MAX) habilitado para FILESTREAM.

Bien, supongamos que ya disponemos de nuestra base de datos y hemos creado una sencilla tabla para almacenar documentos. Algo parecido a esto:

filestream

Dónde los campos más relevantes son:

  • FileId: Un campo Guid que identifica cada documento guardado de forma única.
  • Document: El campo varbinary(MAX) en el que vamos a volcar el array de bytes de los ficheros a almacenar.
  • OriginalPath: En realidad no es importante, pero uno de los metadatos que vamos a necesitar más adelante es la extensión del fichero, para que pueda ser abierta con su aplicación asociada (Word, Excel, Acrobar Reader, etc.)

 

Vamos a ver cómo insertar documentos en dicha tabla. Nada mejor que un poco de C# y LINQ2SQL (o EF, eso es lo de menos):

using (OpenFileDialog opendialog = new OpenFileDialog())

{

    opendialog.Title = "Import document to database";

    opendialog.Filter = "All Documents|*.doc;*.docx;*.xls;*xlsx;*.ppt;*.pptx;*.pdf;*.txt";

    if (opendialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)

    {

        var newdoc = new FileStreamDocument();

        newdoc.FileId = Guid.NewGuid();

        newdoc.OriginalPath = opendialog.FileName;

        newdoc.Document = File.ReadAllBytes(opendialog.FileName);

        newdoc.ComputerName = Environment.MachineName;

        context.FileStreamDocuments.InsertOnSubmit(newdoc);

        context.SubmitChanges();

    }

}

En el código anterior mostramos un OpenFileDialog, el usuario selecciona un fichero, leemos el contenido y creamos un FileStreamDocument para insertarlo en la BD. La belleza subyacente es que SQL Server se encargará de almacenar dichos ficheros fuera de la tabla, en el sistema de ficheros NTFS del servidor. Y además, de forma totalmente transparente para nosotros :-)

En los próximos posts veremos dónde se almacenan REALMENTE estos ficheros, cómo visualizarlos, y lo más importante de todo, cómo editarlos y guardarlos otra vez en la base de datos de forma transparente para el usuario.

Nos leemos pronto! 😀

Usando ASP.NET membrership en Winforms (3 / n)

El modelo de objetos de membresía (Membership object model)

Membershipom

O lo que es lo mismo, el pan nuestro de cada día en lo relativo a administrar la seguridad basada en membresía. Para acceder a este cojunto de clases es necesario agregar los namespaces System.Web y System.Web.Extensions, que contienen las clases necesarias para interactuar con la base de datos de membresía descrita en el artículo anterior.

References

Conocer este conjunto de clases es vital para poder aprovechar todas las características de este sistema, de modo que aquí va una tabla con las principales elementos y funcionalidades:

Clase/interfaz

Funciones

Membership

Proporciona los servicios de suscripción generales.

Crea un nuevo usuario.

Elimina un usuario.

Actualiza un usuario con nueva información.

Devuelve una lista de usuarios.

Encuentra un usuario por el nombre o el correo electrónico.

Valida (autentica) un usuario.

Obtiene el número de usuarios conectados.

Busca los usuarios por el nombre de usuario o la dirección de correo electrónico.

MembershipUser

Proporciona información sobre un usuario concreto.

Obtiene la contraseña y la pregunta de la contraseña.

Cambia la contraseña.

Determina si el usuario está conectado.

Determina si el usuario está validado.

Devuelve la fecha de la última actividad, del último inicio de sesión y del último cambio de contraseña.

Desbloquea un usuario.

MembershipProvider

Define la funcionalidad de los proveedores de datos que el sistema de suscripción puede utilizar.

Define los métodos y las propiedades que necesita implementar un proveedor utilizado en la suscripción.

MembershipProviderCollection

Devuelve una colección de todos los proveedores disponibles.

MembershipUserCollection

Almacena las referencias a los objetos MembershipUser.

MembershipCreateStatus

Proporciona los valores descriptivos de éxito o error al crear un nuevo usuario suscrito.

MembershipCreateUserException

Define la excepción que se produce si no se puede crear un usuario. Hay un valor de enumeración MembershipCreateStatus disponible a través de la propiedad StatusCode que describe el motivo de la excepción.

MembershipPasswordFormat

Especifica los posibles formatos de almacenamiento de contraseñas utilizados por los proveedores de suscripciones incluidos con ASP.NET (Clear, Hashed, Encrypted).

Configurando nuestra aplicación

Nota: Esto pensaba dejarlo para más adelante, pero lo posteo aquí también por si alguien quiere animarse a hacer pruebas 😉

Para poder acceder al modelo de objetos, además de agregar las dos referencias que hemos comentado antes, también es necesario modificar el fichero de configuración de la aplicación (Web.config en un proyecto Web y App.config en un proyecto WinForms). Existen multitud de artículos que describen cómo hacerlo en el primero de los casos, así que nos vamos a centrar en el segundo.

Empezaremos con la configuración mínima para que funcione nuestra aplicación, y posteriormente iremos viendo distintas opciones para permitir que el sistema de seguridad se comporte de un modo distinto, por ejemplo, pidiendo una contraseña no tan restrictiva, o que no sea necesario indicar el email del usuario, o la pregunta y respuesta de seguridad.

El primer paso para configurar nuestra aplicación es agregar un fichero de configuración (App.config) a nuestro proyeco, o si ya tenemos uno, abrirlo para su edición. Toda la configuración se va a centrar en dos secciones: <connectionStrings> y <system.web>. En la primera es especificará una entrada que apunte a la base de datos de membresía, mientras que en la segunda se especificarán los valores para los proveedores de usuarios y roles:

<configuration>
  <connectionStrings>
    <add name="MemberShipConnectionString"
         connectionString="data source=.SQLEXPRESS;Integrated Security=True;DataBase=Test"
         providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <system.web>
    <roleManager enabled="true">
      <providers>
        <clear/>
        <add name="AspNetSqlRoleProvider"
             type="System.Web.Security.SqlRoleProvider"
             connectionStringName="MemberShipConnectionString"
             applicationName="Test"/>
      </providers>
    </roleManager>
    <membership defaultProvider="SqlProvider">
      <providers>
        <clear/>
        <add name="SqlProvider" 
             type="System.Web.Security.SqlMembershipProvider"
             connectionStringName="MemberShipConnectionString"
             applicationName="Test"/>
      </providers>
    </membership>
  </system.web>
</configuration>

Oservemos que la cadena de configuración apunta a la BD que creamos en el post anterior, y que esta cadena de conexión se usa en la configuración de los proveedores de usuarios y roles. Otro detalle muy importante es que la clave applicationName contiene el nombre de nuestra aplicación para el sistema de membresía, de modo que todos los usuarios y roles que se creen estarán agrupados bajo este nombre de aplicación.

Por defecto, esta configuración hace que al momento de crear un usuario sea necesario proporcionar: Nombre, contraseña de un mínimo de 7 carácteres y uno de ellos ‘especial’ (por ejemplo: @#$%), email, pregunta de seguridad, respuesta de seguridad, y si va a estar aprobado o no. No está nada mal, de hecho podríamos decir que la configuración predeterminada es a su vez, la más segura.

Si deseamos cambiar esta configuración, par permitir que un usuario no pueda preguntarle al sistema cuál es su contraseña (sólo pueda resetearla), o por ejemplo para ‘relajar’ un poco la seguridad (que quede claro que esto no lo recomiendo en absoluto, y que es simplemente un ejercicio didáctico) podemos agregar atributos al fichero de configuración:

<membership defaultProvider="SqlProvider">
  <providers>
    <clear/>
    <add name="SqlProvider" 
         type="System.Web.Security.SqlMembershipProvider"
         connectionStringName="MemberShipConnectionString"
         applicationName="Test"
         enablePasswordRetrieval="false" 
         enablePasswordReset="true" 
         requiresQuestionAndAnswer="false" 
         requiresUniqueEmail="false" 
         passwordFormat="Hashed"/>
  </providers>
</membership>

Tenéis una lista completa de los atributos en las propiedades de la clase SqlMemberShipProvider.

Y ahora, un poco de ‘chicha’ en forma de código:

Ejemplos a cholón:

Crear un usuario:

MembershipUser user = Membership.CreateUser("admin", "$admin123");

Más sencillo imposible, verdad? Ahora bien, hay que tener presente que este código fallará con la configuración predeterminada, ya que debe suministrarse email, y la pregunta y respuesta de seguridad. En su lugar debería usarse este código:

MembershipCreateStatus status;
MembershipUser user = Membership.CreateUser(
    "admin", "$admin123", "admin@test.com",
    "pregunta de seguridad?", "respuesta de seguridad", 
    true, out status);
if (status == MembershipCreateStatus.Success)
{
    MessageBox.Show("Usuario creado correctamente!");
}

Bueno, tampoco no es demasiado complicado. La única diferencia es que se proporcionan más argumentos y que devuelve un objeto que define el estado de la creación del usuario, para saber si se ha creado con éxito.

Eliminar un usuario:

Membership.DeleteUser("admin");

Casi demasiado facil 😛

Obtener un usuario:

MembershipUser user = Membership.GetUser("admin");

A partir de este usuario recuperado podemos efectuar cualquier operación soportada.

Obtener todos los usuarios:

MembershipUserCollection users = Membership.GetAllUsers();
foreach (MembershipUser user in users)
{
    Console.WriteLine(user.UserName);
}

Obtenemos una colecció e iteramos por ella para mostrar los nombres.

Validar un usuario:

if (Membership.ValidateUser("admin", "$admin123"))
{
    MessageBox.Show("usuario correcto!");
}
else
{
    MessageBox.Show("usuario o contraseña incorrecto!");
}

Ideal para formularios de inicio de sesión. Tened cidado, que si se introduce mal 3 veces se bloquea el usuario.

Obtiene el número de usuarios conectados:

int num = Membership.GetNumberOfUsersOnline();

Sin comentarios.

Busca usuarios por el nombre de usuario o la dirección de correo electrónico:

MembershipUserCollection users = Membership.FindUsersByName("pepe");
MembershipUserCollection users = Membership.FindUsersByEmail("pepe@test.com");

Del mismo modo que GetAllUsers devuelve una colección de usuarios.

Obtiene la contraseña de un usuario:

MembershipUser user = Membership.GetUser("admin");
string pwd = user.GetPassword();
string pwd = user.GetPassword("respuesta de seguridad");

Tiene dos sobrecargas, en la segunda debe especificarse la respuesta de seguridad.

Sólo es válido si se ha especificado el atributo EnablePasswordRetrieval.

Cambia la contraseña de un usuario:

MembershipUser user = Membership.GetUser("admin");
user.ChangePassword("$admin123", "@nuevapassword2010");

Resetea la contraseña de un usuario:

MembershipUser user = Membership.GetUser("admin");
string pwd = user.ResetPassword();
string pwd = user.ResetPassword("respuesta de seguridad");

Tiene dos sobrecargas, en la segunda debe especificarse la respuesta de seguridad.

Sólo es válido si se ha especificado el atributo EnablePasswordReset.

Desbloquea un usuario:

MembershipUser user = Membership.GetUser("admin");
bool success = user.UnlockUser();

Particularmente útil cuando tenemos un usuario bloqueado, ya sea por un intento de intrusión, como por la típica amnesia matutina.

Y esto es todo por hoy, la semana que viene más :-)

Espero que esto anime a más de uno a usar este sistema de gestión de usuarios, ya veis que no es nada complicado.

Saludos a 0º (hoy ni frío ni calor),

Nota: Es la primera vez que escribo en el blog todos los días de una semana, así que esto hay que celebrarlo con un cafelito. Hasta luego! :-)


Artículos anteriores de la serie:

Usando ASP.NET membrership en Winforms (1 / n)

Usando ASP.NET membrership en Winforms (2 / n)

Usando ASP.NET membrership en Winforms (2 / n)

Creando la base de datos de membresía

La base de datos de membrsía es el lugar en el que se van a almacenar los datos de los usuarios de nuestras aplicaciones. Si, de nuestras aplicaciones en plural, porque este sistema está preparado para gestionar datos de tantas aplicaciones como sea necesario. De modo que el primer paso va a ser crear esta base de datos en un servidor SQL Server. Por cierto, cualquier edición vale, desde la Express hasta la Professional.

Lo primero que vamos a hacer es ejecutar el comando aspnet_regsql desde la línea de comandos de Visual Studio, para iniciar el asistente que nos guiará en la creación de nuestra base de datos de membresía:

aspnet_regsql

Una vez iniciado, el aisitente pregunta si vamos a crear la base de datos o a eliminar la información de membresía de una base de datos ya existente. Una vez elegimos la primera opción nos pedirá el nombre del servidor de SQL y de la base de datos. En caso que no exista la base de datos se creará una. En nuestro caso elegiremos la instancia de SQL Server express local y (en un derroche de imaginación) llamaremos a nuestra base de datos ‘Test’:

createdbwizard1

Examinando la base de datos creada

testdb Una vez creada, nos conectaremos a la base de datos para examinar que objetos se ha creado. Lo primero que llama la atención es que hay una serie de tablas, vistas y procedimientos almacenados cuyo nombre empieza por el prefijo ‘aspnet_’, y yo me pregunto ¿no hubiese sido mucho más elegante utilizar un Schema en lugar de esta nomenclatura? Bueno, supongo que si se ha usado esta nomenclatura será por compatibilidad con SQL Server 2000, ya que los esquemas no aparecieron hasta la versión 2005.

La estructura de esta base de datos no es importante para nosotros, ya que en ningún caso vamos a acceder directamente a la base de datos. Todos nuestro trabajo se realizará a través del modelo de objetos de membresía desde nuestra aplicación .NET. De todos modos demos un vistazo a las tablas más interesantes:

aspnet_Applications: Es la tabla en la que se almacenan los datos de las distintas aplicaciones que van a usar esta base de datos. Sólo guarda el nombre y un identificador GUID para usarlo como clave en las tablas de usuarios, roles, etc.

aspnet_MemberShip: Guarda los datos de los usuarios del sistema de Membresía de forma conjunta con otra tabla llamada aspnet_Users. Así, los datos de cada usuario del sistema quedan divididos entre estas dos tablas, guardando la primera los datos relativos a la seguridad (contraseña, tipo de almacenamiento de la contraseña, pregunta y respuesta de seguridad) y en la segunda los datos propios del usuario (nombre, fecha de la última actividad).

aspnet_Roles: Almacena los datos relativos a los grupos de usuarios. Los grupos son usados para establecer los permisos de uno o varios usuarios. Posteriormente desde nuestra aplicación preguntaremos si un usuario pertenece a uno u otro rol para permitir o revocar permiso sobre ciertas acciones.

aspnet_UsersInRoles: Como su nombre indica mantiene una relación de los usuarios que pertenecen a cada uno de los roles. De este modo podremos saber qué usuarios pertenecen a qué roles.

Existen otras características que pasaremos por alto porque dan para otros artículos, como por ejemplo la posibilidad de almacenar datos para distintos usuarios  sin tener que almacenarlos en otra tabla de otra base de datos.

Nota: Hay un detalle curioso que refuerza la posibilidad de la compatibilidad con versiones anteriores, y es la auséncia de funciones de usuario en la base de datos. Resulta curioso como muchas de las características de los procedimientos almacenados se podrían haber implementado como funciones.

Accediendo a la base de datos desde .NET

En el siguiente post veremos el modelo de objetos de membresía, este modelo está disponible para acceder y manipular este repositorio desde nuestras aplicaciones .NET, y que se encuentra en los namespaces System.Web y System.Web.Extensions (aunque como decíamos podemos usarlo desde cualquier tipo de aplicación).

Saludos,


Artículos anteriores de la serie:

Usando ASP.NET membrership en Winforms (1 / n)

 


 

Actualización (artículos siguientes):

Usando ASP.NET membrership en Winforms (3 / n)

Usando ASP.NET membrership en Winforms (1 / n)

membership

Sistemas de autenticación de usuarios

Una de las preguntas más habituales en los foros de desarrollo tiene relación con los sistemas de autenticación de usuarios. Normal ¿qué aplicación de hoy en día no necesita de un sistema que gestione los usuarios, roles y permisos de la aplicación?

Para ello existen varias alternativas pensadas para diferentes tipos de aplicaciones: Por ejemplo, si sabemos que nuestra aplicación va a ejecutarse en un entorno corporativo, ¿qué mejor que usar el directorio activo de la empresa para gestionar lo relativo a la seguridad? Por otro lado, si no estamos seguros si va a existir un directorio activo, lo mejor va a ser usar nuestra propia base de datos de gestión de usuarios.

En este punto es dónde veo más lagunas y en el que se va a centrar esta serie de posts.

Antes de nada, que es mejor? Usar un sistema existente o crear nuestro propio sistema de gestión de usuarios? Hablaremos de esto más adelante :-)

ASP.NET Membership

Existe un sistema de seguridad basado en proveedores de autenticación llamado ASP.NET Membership, que apareció con ASP.NET 2.0, y que originariamente estaba pensado para su uso en proyectos Web. Este sistema permite gestionar los usuario y roles de nuestras aplicaciones, y expone un completo modelo de objetos para usar en nuestras aplicaciones y simplificar la creación de usuarios, roles, validaciones, y todas aquellas tareas típicas de una gestión de usuarios.

Lo mejor de este sistema es que está basado en proveedores y que es extensible. Dicho de otro modo, podemos usar una base de datos de SQL para validar los usuarios, pero también admite otros proveedores de validación, como un directorio activo o de otros fabricantes como Oracle. Además, el hecho de que sea extensible hace que incluso nosotros podamos escribir nuestro propio proveedor de autenticación.

Escenarios

Lo habitual es usar este sistema en aplicaciones Web, ya que no suele ser habitual que los usuarios de un sitio Web utilicen autenticación contra un directorio activo. Por otro lado, desde hace unos años, cada vez oigo lo útil que sería disponer de este modelo para aplicaciones cliente, ya sean WinForms o WCF. Bien, en esta serie nos centraremos en esto: Usar ASP.NET membership en aplicaciones cliente para disponer de un sistema de gestión de usuarios ‘prefabricado’.

Existen dos posibles escenarios: En el primero estamos desarrollando una única aplicación cliente y queremos usar este sistema. En la segunda, estamos creando varias aplicaciones (WinForms, WPF, e incluso Web) y nos gustaría montar un único sistema centralizado basado en servicios, que serán usados por las distintas aplicaciones. Esto es posible desde el Framework 3.5 SP1 y se llama ‘Client applicaction Services’.

Al lío

En los próximos posts vamos a ver cómo:

  • Crear la base de datos de membresía.
  • Escenario 1: Crear una aplicación WinForms que utilice el sistema de membresía.
  • Escenario 2: Crear una solución de ejemplo para ver el uso de ‘Client applicaction Services’.
  • Desmitificando: Discutir acerca de si es mejor un sistema estándar o crear nuestro propio sistema.
  • Y alguna cosilla más que se nos ocurrirá… :-)

En los próximos días (si hay suerte uno por día, y si no uno por mes :-P) iré publicando los siguientes posts de la serie.

Nos leemos pronto!


Actualización (artículos siguientes):

Usando ASP.NET membrership en Winforms (2 / n)

Usando ASP.NET membrership en Winforms (3 / n)

SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (VII)

Entradas anteriores de la serie:

SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (I)

SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (II)

SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (III)

SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (IV)

SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (V)

SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (VI)


bender3

Han pasado unos días desde que publiqué la última entrada, y es que voy absolutamente desbordado de curro.

Pero como lo prometido es deuda, vamos a proseguir con el tema, ya quedan un par de temas por ver antes de concluir la serie:

  • Cómo ejecutar distintos threads y mostrar el progreso (Callbacks).

  • Cómo distribuir nuestro complemento, creando un archivo MSI (Windows Installer packages).

  • Además, os dije que en el último post publicaría el código fuente del proyecto… y varios de vosotros ya lo habéis reclamado con insistencia 😛

    De modo que vamos a hacerlo al revés: Hoy publico el código fuente del proyecto, y mañana (espero) continuaré el resto de la serie. De este modo al menos ya tendreis el código para jugar con él, y poder machacarme a preguntas…

    ProjectExplorer

    El proyecto incluye el código de ejemplo del Add-In y el proyecto de instalación, y lo podéis descargar desde este enlace de Skydrive:

    http://cid-f3a970280830b5fe.skydrive.live.com/self.aspx/MSDN%20Samples/OutlookToMOSS/OutlookToMOSS.zip

    Saludos desde Andorra,

    SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (VI)

    Entradas anteriores de la serie:

    SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (I)

    SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (II)

    SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (III)

    SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (IV)

    SharePoint 2007 + Outlook 2007: Guardar correos de forma masiva (V)


    bender3

    Ahora que parece que Bender está un poco más sobrio que de costumbre, prosigamos con la serie… que sabéis como se pone cuando no bebe.

    Menú del día:

    De primero: Cómo guardar un fichero en una biblioteca de documentos de SharePoint manteniendo el control de versiones.

    De segundo: Cómo modificar los metadatos del fichero una vez subido a la biblioteca (From, To, Subject).

    Postre: Pastel especial de la casa.

    Pan, Vino y Café incluidos

    El primero:

    Es curioso, pero SharePoint en la versión actual no implementa “de serie” ningún mecanismo para subir documentos a una librería de documentos. Ahora que nos hemos quedado con la boca abierta, vamos a explicar algunos mecanismos para hacerlo por nuestra cuenta.

    El bueno de mi tío Rodrigo hace un tiempo que publicó un post al respecto, en el cual se mostraba cómo subir ficheros a un SharePoint. Pero como él mismo decía, la pega estaba en que usando esta técnica no se disponía de historial de versiones. La alternativa era crearse un WebService propio, tal y como se explica en este artículo, aunque es una solución que no siempre se puede usar. Hoy veremos una tercera alternativa que permite hacerlo sin tener que recurrir a la creación de un WebService. La he probado en un servidor MOSS y en un par de WSS, uno local y el otro hospedado en un hosting en USA. Y en ambos funciona perfectamente, aunque en el segundo los tiempos se demoran un poco…

    Describamos los elementos que necesitamos a continuación:

    Para empezar vamos a crear una clase FileInfo para encapsular lo relativo al fichero que vamos a subir (nombre, propiedades, URI, y por supuesto los bytes):

    public class FileInfo
    {            
        public string m_URL;            
        public byte[] m_bytes;            
        public Dictionary<string, object> m_properties;            
        public ListInfo m_listInfo;            
        public bool m_ensureFolders = true;            
        private Uri m_uri;    
        
        public bool HasProperties            
        {                
            get 
            { 
                return m_properties != null && m_properties.Count > 0; 
            }            
        }            
     
        public string RelativeFilePath            
        {                
            get 
            { 
                return m_URL.Substring(m_URL.IndexOf(m_listInfo.m_rootFolder) + 1); 
            }            
        }          
        
        public Uri URI      
        {               
            get                
            {                    
                if (m_uri == null) m_uri = new Uri(m_URL);                    
                return m_uri;                
            }            
        }            
        
        public string LookupName            
        {                
            get  
            {                    
                if (m_listInfo != null && !string.IsNullOrEmpty(m_listInfo.m_listName))   
                    return m_listInfo.m_listName;      
                return URI.LocalPath;           
            }            
        }            
        
        public FileInfo(string url, byte[] bytes, Dictionary<string, object> properties)    
        {                
            m_URL = url.Replace("%20", " ");     
            m_bytes = bytes;               
            m_properties = properties;     
        }                    
    }

    A continuación un método Upload con dos sobrecargas, que será el que invocaremos desde la ventana encargada de mostrar el progreso de la operación. Éste será el encargado de recibir la URL de destino, los bytes del fichero a subir (previamente debemos guardar el elemento de correo en un fichero o stream), las propiedades, y el elemento de correo para poder actualizar las columnas de metadatos. Este método Upload será el encargado de a su vez llamar al método TryToUpload, que realmente será el encargado de subir el fichero:

    public bool Upload(string destinationUrl, byte[] bytes, Dictionary<string, object> properties, Outlook.MailItem item) 
    {
        return Upload(new FileInfo(destinationUrl, bytes, properties), item);
    }
     
    public bool Upload(FileInfo fileInfo, Outlook.MailItem item)
    {
        if (fileInfo.HasProperties)
            fileInfo.m_listInfo = m_lists.Find(fileInfo);
        bool result = TryToUpload(fileInfo, item);
        if (!result && fileInfo.m_ensureFolders)
        {
            string root = fileInfo.URI.AbsoluteUri.Replace(fileInfo.URI.AbsolutePath, "");  
            for (int i = 0; i < fileInfo.URI.Segments.Length - 1; i++)     
            {
                root += fileInfo.URI.Segments[i];
                if (i > 1) CreateFolder(root);
            }
            result = TryToUpload(fileInfo, item);
        }            
        return result;
    }

    Y para terminar, el método TryToUpload que se encarga de crear el objeto WebRequest necesario, crear el stream y actualizar los valores de las propiedades de los campos, para invocar finalmente el método UpdateListItems del servicio Web:

    private bool TryToUpload(FileInfo fileInfo, Outlook.MailItem item)
    {
        try
        {
            WebRequest request = WebRequest.Create(fileInfo.m_URL);
            request.Credentials = sharePointLists.Credentials;  
            request.Method = "PUT";
            byte[] buffer = new byte[1024];
            using (Stream stream = request.GetRequestStream())
            using (MemoryStream ms = new MemoryStream(fileInfo.m_bytes))
                for (int i = ms.Read(buffer, 0, buffer.Length); i > 0; i = ms.Read(buffer, 0, buffer.Length))
                {
                    stream.Write(buffer, 0, i);
                }
            WebResponse response = request.GetResponse();
            response.Close();
            if (fileInfo.HasProperties)
            {
                StringBuilder sb = new StringBuilder();
                sb.Append("<Method ID='1' Cmd='Update'><Field Name='ID'/>");
                sb.AppendFormat("<Field Name='FileRef'>{0}</Field>", fileInfo.m_URL);
                foreach (KeyValuePair<string, object> property in fileInfo.m_properties)
                {
                    sb.AppendFormat("<Field Name='{0}'>{1}</Field>", property.Key, property.Value);
                }
                updateMetadataColumns(fileInfo, item, sb);
                sb.Append("</Method>");
                System.Xml.XmlElement updates = (new System.Xml.XmlDocument()).CreateElement("Batch");
                updates.SetAttribute("OnError", "Continue");
                updates.SetAttribute("ListVersion", fileInfo.m_listInfo.m_version);
                updates.SetAttribute("PreCalc", "TRUE");
                updates.InnerXml = sb.ToString();
                sharePointLists.Url = fileInfo.m_listInfo.m_webUrl + "/_vti_bin/Lists.asmx";
                XmlNode updatesResponse = sharePointLists.UpdateListItems(fileInfo.m_listInfo.m_listName, updates);    
                if (updatesResponse.FirstChild.FirstChild.InnerText != "0x00000000")
                    throw new Exception("Could not update properties."); 
            }
            return true;
        }
        catch (WebException ex)
        {
            throw ex;
        }
    }

     

    Lo juntamos todo, compilamos, probamos a subir algunos mensajes varias veces y voilá! Si observamos el control de versiones veremos lo siguiente:

    ControlVersiones

    El segundo:

    Si hemos observado con atención el código del método TryToUpload seguramente hemos visto una llamada a un método updateMetaDataColumns, al cual se le pasa el StringBuilder que estamos construyendo, para que nos agregue los valores de los metadatos (cuya sintaxis que expresarse en XML según la ayuda del método UpdateListItems). El código es muy sencillo y básicamente lo que hace es comprobar si en la lista destino existen las columnas de metadatos, y en caso afirmativo modificar su valor:

    private void updateMetadataColumns(FileInfo fileInfo, Outlook.MailItem item, StringBuilder sb)
    {
        var cols = from c in SharePointExtensions.getListColumns(
                   sharePointLists, fileInfo.m_listInfo.m_listName)
                   where c.Hidden == false && c.Sealed == false && c.ReadOnly == false
                   select c;
        Dictionary<string, SPColumnInfo> columns = cols.ToDictionary(c => c.Name);
        if (columns.ContainsKey(Properties.Settings.Default.COL_SUBJECT) && item != null)
        {
            sb.AppendFormat("<Field Name='{0}'>{1}</Field>",
                Properties.Settings.Default.COL_SUBJECT, item.Subject);
        }
        if (columns.ContainsKey(Properties.Settings.Default.COL_FROM) && item != null)
        {
            sb.AppendFormat("<Field Name='{0}'>{1}</Field>",
                Properties.Settings.Default.COL_FROM, item.SenderName);
        }
        if (columns.ContainsKey(Properties.Settings.Default.COL_TO) && item != null)
        {
            sb.AppendFormat("<Field Name='{0}'>{1}</Field>",
                Properties.Settings.Default.COL_TO, item.To);
        }
        if (columns.ContainsKey(Properties.Settings.Default.COL_CC) && item != null)
        {
            sb.AppendFormat("<Field Name='{0}'>{1}</Field>",
                Properties.Settings.Default.COL_CC, item.CC);
        }
        if (columns.ContainsKey(Properties.Settings.Default.COL_BCC) && item != null)
        {
            sb.AppendFormat("<Field Name='{0}'>{1}</Field>",
                Properties.Settings.Default.COL_BCC, item.BCC);
        }
        if (columns.ContainsKey(Properties.Settings.Default.COL_SIZE) && item != null)
        {
            sb.AppendFormat("<Field Name='{0}'>{1}</Field>",
                Properties.Settings.Default.COL_SIZE, item.Size);
        }
        if (columns.ContainsKey(Properties.Settings.Default.COL_SENT) && item != null)
        {
            string dt = item.SentOn.ToString("yyyy-MM-ddTHH:mm:ssZ");
            sb.AppendFormat("<Field Name='{0}'>{1}</Field>",
                Properties.Settings.Default.COL_SENT, dt);
        }
        if (columns.ContainsKey(Properties.Settings.Default.COL_IMPORTANCE) && item != null)
        {
            string im = string.Empty;
            switch (item.Importance)
            {
                case Microsoft.Office.Interop.Outlook.OlImportance.olImportanceHigh:
                    im = Properties.Settings.Default.COL_IMPORTANCE_HIGH;
                    break;
                case Microsoft.Office.Interop.Outlook.OlImportance.olImportanceLow:
                    im = Properties.Settings.Default.COL_IMPORTANCE_LOW;
                    break;
                case Microsoft.Office.Interop.Outlook.OlImportance.olImportanceNormal:
                default:
                    im = Properties.Settings.Default.COL_IMPORTANCE_MEDIUM;
                    break;
            }
            sb.AppendFormat("<Field Name='{0}'>{1}</Field>",
                Properties.Settings.Default.COL_IMPORTANCE, im);
        }
    }

    El postre:

    Ingredientes :
    125 gr. de chocolate
    60 gr. de mantequilla
    125 gr. de azúcar
    3 huevos
    60 gr. de maizena

    Receta :
    Fundir el chocolate con la mantequilla a fuego lento.
    Separar las yemas y las claras.
    Mezclar el azúcar con las yemas de los huevos.
    Añadir la maicena y después el chocolate fundido.
    Montar las claras de huevo a punto de nieve y añadirlas lentamente a la mezcla.
    Verter la mezcla en un molde donde se ha extendido la mantequilla previamente.
    Hornearlo entre 20 ó 25 minutos a 180º.

    Para terminar:

    En los próximos artículos veremos cómo realizar este proceso de subida de ficheros de forma asíncrona usando callbacks, para poder continuar trabajando con Outlook y mostrar el progreso en una barra. También para cerrar la serie, veremos los pasos que hay que hacer para construir el proyecto de instalación de nuestro Add-In, y así poder distribuirlo.

    Nos vemos!