WF: Recopilación de algunas cosas interesantes!

Siguiendo con el rescate de algunos de los posts publicados en nuestro blog, en este post voy a hacer una recopilación de algunas cosas interesantes que hemos contado sobre WF. Algunas ya las conoceréis, pero seguro que con otras os sorprendo.


 


Utilidades y Otros elementos para sacarle partido a la tecnología


Como sucede cada vez que aparece una nueva tecnología tan potente como WF, en seguida en la comunidad aparecen un montón de utilidades para sacarle partido a la tecnología. Aquí os dejo algunos links interesantes.


 


 Herramientas / Utilidades


 


·        Aplicación de escritorio, WFPad








 


Permite diseñar workflows, probarlos, subir vuestras propias actividades customizadas y que genera XML (de hecho, se trata de un editor de workflows que utiliza xoml para su creación). Esta aplicación es una prueba de cómo se puede sacar el diseño y prueba de un workflow fuera de Visual Studio 2005. Os podéis descargar la aplicación aquí. Carlos Segura ya nos había contado  en su blog sobre la disponibilidad de esta utilidad.


·        Igual que tenemos una aplicación de escritorio para diseñar y probar nuestros workflow, existe una versión online de otro diseñador embebido en una aplicación web construida con JavaScript (y que por cierto, han actualizado desde la última vez que lo visité). Como se comenta en este link, netfxlive se concibió como repositorio centralizado de actividades WF, de manera que cualquiera pueda construir y probar online los workflows construidos con las actividades de dicho repositorio (me han sorprendido algunas de las actividades disponibles, como sendSMSActivity o doGoogleSearchActivity).



·        Esquema XAML para WF, que habilita soporte de intellicense en la definición de workflows mediante XML. Os lo podéis descargar de aquí.


·        Nuevas plantillas para la creación de workflows puramente en XAML


 


Actividades interesantes para WF


·        Actividades específicas para invocación de servicios de WCF en los workflows.









 


 


 


Como véis, estas actividades ya están disponibles en la toolbox de un proyecto de WF en Visual Studio Orcas (sólo para .NET Framework 3.5). En el blog del CIIN ya puse un post de algunas de las novedades del Visual Studio Orcas, que espero poner ampliado en Geeks.


 


 


·        Nuevas actividades para acciones y funcionalidades más concretas. Las últimas que se han subido al sitio oficial de WF permiten hacer la restauración de una BD a partir de un back.


 


Nuevo material de formación para WF


 


Ya lo han anunciado algunos bloggers de Geeks (como Jorge Serrano), os lo recuerdo de todos modos:


 


·        Blog de Paul Andrew.


·        Enlace de descarga en Microsoft.


 


Artículos Interesantes:


·        Sobre persistencia de los datos de un workflow.








 


 


Introducción al servicio de tracking, en el que se introducen los principios de este servicio en WF, los elementos claves del mismo, su funcionamiento y cómo podemos consumirlo en nuestras aplicaciones. Como se destaca en el artículo, se pueden capturar tres tipos de eventos: de workflow, de ejecución de actividad y de usuario. También resulta interesante el diagrama de la arquitectura del servicio de tracking, y la descripción detallada de los distintos elementos.


 


 


·         Tracking Services en profundidad, en el que se explica en profundidad el funcionamiento de este servicio. Como muestra, tenemos el siguiente diagrama de funcionamiento.



En el artículo se explican en detalle conceptos como el de Tracking Profile que permite especificar la información (eventos y datos) de la que se desea hacer el tracking, y los componentes de un Tracking Profile: workflow track points, activity track points y user track points.  


·        Creación de Tracking Services personalizados, en el que se explica cómo crear un servicio de tracking que cumpla unas especificaciones.


·        Lanzando eventos de workflow desde WCF, interesante artículo en el que se combinan WCF y WF para modelar un workflow que responda a eventos lanzados por un servicio de WCF.


 


Implementaciones & Ejemplos


·        Servicio de persistencia de WF en una BD Oracle, en este blog os podéis bajar una implementación del servicio de persistencia de WF para una BD Oracle.


·        Realizando transacciones en WF, ejemplo interesante de wei-Meng Lee en el que nos detalla cómo podemos realizar transacciones dentro de un workflow modelado con WF.


 


Otros


·        ¿En qué escenarios es útil WF? Sin duda, cuando hablamos de las capacidades de Windows Workflow Foundation (WF), una de las preguntas que nos surge es cuáles son sus escenarios de aplicación más interesantes. Pues bien, Paul Andrew (product manager de Microsoft y uno de los padres de WF) nos comenta en su blog algunos de estos escenarios:


o   Construir un servidor para Business Process Management (BPM), siendo el motor de ejecución el núcleo de tal servidor como mecanismo de ejecución, control y seguimiento de los workflows que implementemos.


o   Implementar lógica de negocio que requiere características long running, las ventajas que aporta WF en este caso vienen dadas por el hecho de que por defecto está preparado para persistir información y estados de ejecución (desde una perspectiva High Level), lo que reduce mucho tiempo de desarrollo al tener gran parte del trabajo realizado.


o   Situaciones que impliquen cambios dinámicos de reglas de negocio, esto facilita la creación de workflows estándar que podemos utilizar en múltiples entornos, y en el que los cambios necesarios los incluimos en las reglas de negocio asociadas a los workflows para soportar así cada caso específico.


o   Hacer visualizables los procesos de negocio de una organización, a través de los servicios de runtime de WF que nos permite hacer, entre otros, un seguimiento del estado de ejecución de nuestro workflow (tracking service), generación automática de logs, y por supuesto, el hecho de que podamos modelar el workflow de manera visual en base a arrastrar una serie de piezas o actividades que interconectamos de acuerdo a las directrices de los procesos de negocio a modelar.


 


En este mismo blog podéis encontrar otra entrada interesante en las que se explica que WF no es un producto, sino la tecnología estándar de Microsoft para poder crear workflows.


 


·        ¿Cuántos patrones de ejecución podemos tener en WF? Una primera respuesta es que dos: workflows secuenciales o workflows de máquina de estados. Ahora bien, como se comenta en este interesante post de  Matt Winkle,  hay otros patrones que pueden ser utilizados aunque WF no los ofrezca out-of-the-box. Como dice Matt, hay que pensar en WF como motor de procesamiento de propósito general, que no está limitado a procesar workflows de naturaleza puramente secuencial o máquina de estados, sino que está preparado para procesar los patrones de ejecución personalizados que definamos. La creación de nuevos patrones de ejecución se basan en construir nuestras propias actividades de tipo composite.


 


Finalmente, y como curiosidad, aquí os dejo el enlace al proyecto Mono que incluirá también la capacidad para construir workflows. Desde este enlace podéis acceder a otras iniciativas en plataforma .NET y en plataforma Java para disponer de capacidad de creación de workflows. Espero que este recopilatorio os sea de utilidad. Tengo pensado hacer otro de WSS 3.0 / MOSS.

Optimización de correos HTML para dispositivos móviles

En un proyecto reciente, se nos ha planteado la necesidad de mejorar la presentación de unos boletines de correo HTML, para dispositivos PocketPC y Blackberry. El contenido inicial del mensaje era HTML estándar, con referencias externas a imágenes, para reducir su tamaño.


El resultado es el de la imagen siguiente, el navegador móvil elimina tablas, parte del formato… pero inserta las referencias a imágenes que dan formato al correo como referencias externas, complicando mucho la legibilidad del mensaje.


Formato correo no optimizado


Este comportamiento puede modificarse incluyendo en el mensaje MIME una sección “alternate text”, es decir, una equivalencia en texto plano del contenido del mensaje. Los PocketPC mostrarán este contenido. Pero…los Blackberry no, siempre muestran la versión HTML con los enlaces a imágenes. Además, cuando se lee el correo con Outlook y éste carga las imágenes, sustituye la versión texto del mensaje con una versión propia, generada a partir del HTML, que vuelve a tener las referencias a imágenes. Es decir, más trabajo para pocos resultados.


La otra solución es incluir las imágenes que necesita el mensaje para mostrarse como añadidos inline, de forma que las referencias para insertar las imágenes sean de la forma <img src=”cid:nombre_imagen.jpg”>.


En este caso, ni PocketPC ni Blackberry mostrarán las referencias a las imágenes insertadas, mejorando mucho la legibilidad (en este caso, el mensaje era sencillo, pero en otros más complejos se aprecia mucho más), eso si, a costa de aumentar un poco el tamaño de los mensajes por los añadidos.


Formato mensaje optimizado

SharePoint 2007 optimizado para dispositivos móviles

Hola muy buenas, me llamo Mario Rivero y soy becario del CIIN. LLevo ya unos meses trabajando sobre todo con SharePoint y su personalización. Este es mi primer post, espero que os guste.


El objetivo de este post es mostrar la manera de realizar dentro de un mismo sitio Web de SharePoint dos formas de visualización: una para PC’s y otra optimizada para dispositivos móviles. Para ello nos vamos a valer de las Variations, una funcionalidad que ofrece Microsoft Office SharePoint Server 2007 para construir sitios multilenguaje.


Los pasos que vamos a seguir van a ser los siguientes:


·         Configurar las variations en MOSS 2007


·         Creación de una master page optimizada para dispositivos móviles


·         Redireccionamiento automático del sitio


·         Reducir el tiempo de descarga de las páginas


Configuración de las Variations en MOSS 2007


·        Activar las Variations


Para poder construir un sitio multilenguaje desde cero hay que activar en MOSS 2007 las Variations, justo después de haber creado un nuevo site. Para ello nos vamos a Site Actions->Site Settings->Modify All Site Settings y en la columna de Site Collection Administration pinchamos en Variations. Nos aparecerá una venta como la siguiente:


 


Figura 1: Página de configuración de las Variations


En el campo Location tenemos que indicar cuál va a ser la raíz del sitio. Esta página raíz va a estar oculta al usuario ya que sólo se van a cargar en el navegador los sitios creados como Variations Labels. Por defecto viene que la raíz sea /. Lo dejamos tal cual.


Marcamos la casilla “Automaticaly create site and page variations”  para que el proceso de creación de las Variations sea automático y le damos a OK.


·        Definir las Variations Labels


A continuación vamos a crear los dos sitios (Uno para plataforma PC y otro para plataforma móvil) con las Variations Labels. Cada Variation Label se corresponde con un sitio. Podemos crear cuantos sitios queramos, pero siempre va haber uno principal del cual se van a replicar todos los demás sitios. Para acceder a las Variations Labels nos vamos a la columna Site Collection Administration y pinchamos en Variation Labels. Para crear una nueva Variation Label hacemos clic sobre New Label apareciéndonos la siguiente ventana:



Figura 2: Página para la creación de Variation Label


 


Ahora vamos a crear el sitio principal del cual se repliquen todos los demás. En esta ventana rellanamos los campos Label Name Y Display Name con el nombre que le queramos dar. Como este va a ser el sitio principal le damos el valor Home .El campo Locale sirve para elegir el idioma del nuevo sitio que se va a crear. En nuestro caso da exactamente igual debido a las modificaciones que haremos posteriormente. Además hay que marcar la casilla “Set this variation to be the source variation” para que este sitio sea la fuente de la jerarquía, o sea, que cada sitio nuevo creado con las variations se replicará de este sitio. En el combo de debajo tenemos que elegir la plantilla de publicación que queremos para el sitio. Cuando están activas las Variations, SharePoint solo nos deja elegir entre “Publishing Site with WorkFlows” y “Publishing Site”. Elegimos ésta última ya que no vamos a hacer uso de los WorkFlows. Le damos a OK.


Con esto ya estamos listos para crear cuantos sitios queramos y con el idioma que queramos, que se replicaran del sitio creado en el paso anterior. En nuestro caso solo necesitamos crear uno, que será el sitio para plataformas de dispositivos móviles. Seguimos los pasos anteriores para crear una nueva Variation Label y nos aparecerá una ventana como la anterior. En esta ventana rellenamos los campos Label Name Y Display Name con el valor Movil y elegimos otro idioma cualesquiera. Damos a OK.


Ya tenemos creados los dos sitios, uno para vista normal y otra para vista móvil. Ahora, cada vez que creemos en nuestro sitio “Home” (que hemos hecho que sea el principal) una página o un sub-sitio, estos se replicarán en el sitio “Movil” (El tiempo de espera para que se replique una página o un sub-sitio es de 1 minuto desde su publicación).


El último paso es crear la estructura necesaria para que funcionen los dos sitios. Para ello, en la ventana de las Variations Labels pinchamos sobre Create Hierachies. Esto hará que se cree una jerarquía entre el sitio raíz definido al principio y los dos nuevos sitios creados.



Figura 3: Página de configuración de las Variation Labels


 


Ya estamos listos para crear contenido en el sitio Home y ver como se replica automáticamente en el sitio Movil.


Nota: Para que todo funcione correctamente hay que añadir la IP de la maquina donde se está ejecutando SharePoint como un acceso alternativo. Esto se hace en la administración central de SharePoint. Hay que ir a Operations y en la sección Global Configuration pinchar sobre Alternate Access Mappings. Dentro de la ventana que nos sale pinchamos sobre Edit Public URLs y en la siguiente ventana rellenamos el campo Internet con la dirección IP de nuestra máquina. Le damos a Save y listo, ya podemos navegar correctamente desde cualquier sitio externo a nuestra máquina.


Creación de una Master Page optimizada para  dispositivos móviles.


Cuando activamos las Variations, SharePoint nos da la opción de cambiar la Master Page de cada sitio, por lo que podemos tener una Master Page diferente para cada sitio creado. En nuestro caso sólo vamos a cambiar la Master Page del sitio “Movil”.


Ahora vamos a crearnos una Master Page adecuada para su visualización en dispositivos móviles. Nosotros empezamos a crearla a partir de una minimal master page que se puede encontrar en http://msdn2.microsoft.com/en-us/library/aa660698.aspx. Si queremos que nuestra Master Page cumpla las normas de accesibilidad de la W3C hay que echarle un vistazo a dos guías publicadas por ellos: CSS Mobile Profile 2.0 y Mobile Web Best Practices 1.0.


 Una vez hecho esto, hay que abrir el sitio con SharePoint Designer y guardar la Master Page creada en la carpeta  _catalogsmasterpage. Con esto estamos guardando la Master Page en la base de datos de la Web Application con lo que una vez publicada estará disponible para los dos sitios creados.


Para cambiar de Master Page en el sitio “Movil” nos dirigimos a Site Actions->Site Settings->Modify All Site Settings y en la columna Look and Feel pinchamos sobre Master page.



 


Figura 4: Página para cambiar la Master Page


 


Elegimos la Master Page creada en al paso anterior y damos a OK.


Llegados a este punto, ya tenemos creado nuestros dos sitios con el mismo contenido y sus respectivas  Master Page.


 


Redireccionamiento automático del Sitio


 


La principal característica de las Variations es que dependiendo del idioma seleccionado en el navegador que usemos se cargará un sitio u otro. Por ejemplo, si el navegador está en español se cargara el sitio “Home” puesto que le habíamos definido el idioma “Spanish”, y si el navegador está en el idioma que seleccionamos para el sitio “Movil” pues se cargará este último.


 


Pero este no es el comportamiento que queremos para nuestro sitio. Lo que queremos es que dependiendo de la plataforma con la que se acceda se cargue un sitio u otro, y además que lo haga de forma automática, sin ningún tipo de intervención por parte del usuario. Si accedemos desde una plataforma Windows, Mac, Linux, etc. se cargará el sitio “Home” y si accedemos desde una plataforma WinCE, Palm OS, BlackBerry, etc. se cargará el sitio “Movil”.


Para conseguir esto tenemos que modificar el comportamiento de las Variations. La primera página que se carga cuando están activas las Variations es VariationRoot.aspx que tiene como plantilla a la página VariationRootPageLayout.aspx que se encuentra en la carpeta _catalogasmasterpages en la base de datos de  la Web Application. El código de esta página es el siguiente:







<%@ Register TagPrefix=”Publishing” TagName=”VariationsRootLanding” src=”~/_controltemplates/VariationsRootLanding.ascx”%>


<Publishing:VariationsRootLanding runat=”server” id=”VariationsRootLanding1″/>


<html xmlns:mso=”urn:schemas-microsoft-com:office:office” xmlns:msdt=”uuid:C2F41010-65B3-11d1-A29F-00AA00C14882″><head>


<META name=”WebPartPageExpansion” content=”full”>


<!–[if gte mso 9]><xml>


<mso:CustomDocumentProperties>


<mso:ContentType msdt:dt=”string”>Page Layout</mso:ContentType>


<mso:PublishingAssociatedContentType msdt:dt=”string”>;#Redirect Page;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900F


D0E870BA06948879DBD5F9813CD8799;#</mso:PublishingAssociatedContentType>


<mso:PublishingHidden msdt:dt=”string”>true</mso:PublishingHidden>


</mso:CustomDocumentProperties>


</xml><![endif]–>


<title>Variations Root Page</title></head>



Como podemos observar en las líneas de color rojo, se está haciendo referencia a un control ASP.Net llamado VariationsRootLanding.ascx. Este control se encuentra en una carpeta del servidor cuya ruta es:







C:Program FilesCommon FilesMicrosoft Sharedweb server extensions12TEMPLATECONTROLTEMPLATES


 


En este control se encuentra todo la lógica necesaria para que el sitio se redireccione automáticamente dependiendo del idioma del navegador. El método que realiza esta tarea es:







private string GetRedirectTargetUrl()


Este método devuelve el valor de una variable llamada matchedUrl que contiene la URL, en formato string, de la página que se tiene que cargar. Por la tanto es aquí donde tenemos que meter el código para cambiar el comportamiento del componente y conseguir que al cargar la URL de nuestro sitio en el navegador, se redireccione automáticamente dependiendo de la plataforma en la que estemos trabajando. Para ello nos vamos a valer de una ServerVarible llamada HTTP_USER_AGENT que contiene información sobre el tipo de navegador, el tipo de plataforma, etc. El código a introducir en el método GetRedirectTargetUrl (justo al final del método, sustituyendo a la línea return (string.IsNullOrEmpty(matchedUrl) ? sourceLabelUrl : matchedUrl);) para detectar la plataforma es el siguiente:







string UserAgent;


UserAgent = Request.ServerVariables[“HTTP_USER_AGENT”];


if (UserAgent.Contains(“Palm”) || UserAgent.Contains(“CE”) || UserAgent.Contains(“BlackBerry”))


                {


                               matchedUrl=”<URL  del sitio Movil>”;


                }


                else       


                {


                               matchedUrl=”<URL  del sitio Home>”;


                }


                return matchedUrl;


Si queremos añadir más plataformas de dispositivos móviles, tan solo tenemos que añadir el nombre de la plataforma como un “or” en la sentencia if.


Reducir el tiempo de descarga de las páginas


Siguiendo los pasos anteriores, tenemos un sitio realizado en SharePoint que  se redirecciona automáticamente a un sitio optimizado para móviles  cuando se accede desde un dispositivo móvil. Pero ahora nos enfrentamos a un problema, las páginas realizadas con SharePoint suelen ser bastante pesadas debido a la descarga de un script llamado Core.js, lo que hace que la navegación por las páginas en dispositivos móviles, con un ancho de banda reducido, sea muy lenta. 


Este script se encarga entre otras cosas de la autentificación de los usuarios para que aparezca el menú Site Actions, por lo que no lo podemos quitar de cualquier manera. Hay un método para optimizar las descargas de las páginas que consiste en descargar al cliente el script Core.js después de que se halla visualizado la página, mientras  se está navegando por ella. Esto solo ocurre para los usuarios anónimos. Para los usuarios autentificados el script Core.js se descarga de manera normal.


Antes de empezar a explicar este método hay tres requerimientos que se deben cumplir para poder aplicarle correctamente:


·         Hay que asegurarse de que la Site Master Page es diferente a la System Master Page. Esto se puede comprobar en Site Actions->Site Settings->Modify All Site Settings y en la columna Look and Feel pinchamos sobre Master Page.


·         Hay que asegurarse de que la Site Master Page elegida no contenga ningún control que requiera el script Core.js que sea visible por un usuario anónimo.


·         Hay que asegurarse de que la Site Master Page elegida no contenga ningún control ScrptLink referenciando al Core.js.


 Los pasos a seguir son los siguientes:


1.      Hay que crear una DLL, a la cual llamaremos PerfTools.dll con el siguiente código:







using System;


using System.Collections.Generic;


using System.ComponentModel;


using System.Text;


using System.Web;


using System.Web.UI;


using System.Web.UI.WebControls;


using Microsoft.SharePoint;


 


namespace WebControls


{


    [DefaultProperty(“Text”)]


    [ToolboxData(“<{0}:RegisterCoreWhenAuthenticatedControl runat=server></{0}:RegisterCoreWhenAuthenticatedControl>”)]


    public class RegisterCoreWhenAuthenticatedControl : WebControl


    {


        protected override void OnInit(EventArgs e)


        {


            if (HttpContext.Current.Request.IsAuthenticated)


            {


                Microsoft.SharePoint.WebControls.ScriptLink.RegisterCore(this.Page, true);


            }


            base.OnInit(e);


        }


    }


}


Esta DLL hay que añadirla en la GAC del servidor. Es importante no olvidarse de firmarla, ya que si no lo hacemos no podremos añadirla a la GAC.


2.      Modificar el archivo web.config de la Web Application con el siguiente código:







<SafeControls>


<SafeControl Assembly=”PerfTools, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3ec1cbf2475be08c” Namespace=”WebControls” TypeName=”*” Safe=”True” />


</SafeControls>


Asegurarse de poner el PublicKeyToken correcto.


3.      Crear una nueva página aspx llamada CorePreLoad.aspx con el siguiente código:







<%@ Register Tagprefix=”SharePoint” Namespace=”Microsoft.SharePoint.WebControls” Assembly=”Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” %>


<html>


<head>


<title>Pre-Load Core.js</title>


</head>


<body>


<SharePoint:ScriptLink name=”core.js” runat=”server” />


 


<script language=”javascript”>


 DisableRefreshOnFocus();


</script>


 


</body>


</html>


Guardar la página en el directorio _layouts del servidor.


 


 


4.      Ahora hay que modificar todas las Page Layouts del directorio _catalogsmasterpage, incluida la PageLayoutTemplate.aspx. De este modo todas las Page Layouts que se creen nuevas vendrán ya con el código que vamos a insertar dentro del tag con ID PlaceHolderAdittionalPageHead (Si no existe habrá que crearlo):







<SharePointWebControls:ScriptLink runat=”server”/>


<PerfTools:RegisterCoreWhenAuthenticatedControl runat=”server”/>


Además habrá que registrar la DLL creada en el paso 1:







<%@ Register TagPrefix=”PerfTools” Namespace=”WebControls” Assembly=”PerfTools, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3ec1cbf2475be08c ” %>


Y por último, al final del código hay que insertar el siguiente control:







<asp:Content ContentPlaceholderID=”PlaceHolderBottomIFrame” runat=”server”>


<iframe src=”http://<Nombre de la máquina>/_layouts/CorePreLoad.aspx” style=”display:none”/>


</asp:Content>


5.      En todas las Master Page de nuestro sitio hay que poner el siguiente PlaceHolder entre los tags </form> y </body>:







</form>


    <asp:ContentPlaceHolder id=”PlaceHolderBottomIFrame” runat=”server” />


  </body>


6.      El último paso es publicar todas las páginas que hayan sido modificadas.


 


 


Con este método se consigue reducir aproximadamente un 50 % el tamaño de la página a descargar, lo cual es una buena optimización. De esta forma la navegación por las páginas del sitio será mucho más fluida. 


 


Bibliografía


·         Más sobre el archivo VariationRootLanding.ascx:


http://msdn2.microsoft.com/en-us/library/ms562040.aspx


 


·         El método del Core.js explicado en inglés:


http://blogs.msdn.com/ecm/archive/2007/02/21/building-a-new-page-layout-which-does-not-reference-core-js-but-downloads-it-while-the-page-is-being-viewed-thereby-optimizing-response-time.aspx


 

Personalizar estilos de la Content Query Web Part

En el post anterior hemos hablado de cómo podemos utilizar y personalizar la Content Query Web Part (CQWP) de MOSS para mostrar información de nuestro sitio de SharePoint, como podemos capturar nuevos campos de las listas que tenemos creadas para que aparezcan en nuestras consultas y como podemos utilizar y personalizar los estilos propios que vienen asociados a ella y que se encuentran definidos en el fichero /Style Library/XSL Style Sheets/ItemStyle.xsl


Ahora vamos a ver cómo podemos definir gracias a algunas de las propiedades que se pueden utilizar de la CQWP que utilice un fichero XSL personalizado con sus propias plantillas de estilos, como definir que uno de estos que sea el estilo por defecto y veremos algunos ejemplos de plantillas a utilizar. Para ello vamos a realizar los siguientes pasos:



  • Vamos a la galería de web Part de nuestro sitio, editamos la ContentQuery.webpart y lo exportamos.


  • Una vez exportado el fichero .webpart lo abrimos con cualquier editor de textos y añadimos las siguientes propiedades:


    • ItemXslLink: Para utilizar un nuevo fichero de estilos XSL (ItemStyle_CIIN.xsl)








<property name=”ItemXslLink” type=”string”>/Style Library/XSL Style Sheets/ItemStyle_CIIN.xsl</property>



  • ItemStyle: Para establecer una plantilla predeterminada de este nuevo fichero








<property name=”ItemStyle” type=”string”>Estilo1</property>



  • CommonViewFields: Como vimos en el post anterior, con esta propiedad especificamos los campos que podemos utilizar en las plantillas:








<property name=”CommonViewFields” type=”string”> PublishingPageContent,Note;ArticleByLine,String;ArticleStartDate,DateTime;</property>



  • Una vez que ya tenemos el fichero configurado, le damos un nuevo nombre y lo volvemos a subir a la galería de web part.

  • Antes de empezar a utilizar esta nueva web part, tenemos que crear el nuevo fichero de estilos XSL que le hemos definido en la propiedad ItemXslLink. Para ello creamos un nuevo fichero con SharePoint Designer, le damos el nombre ItemStyles_CIIN.xsl y lo guardamos en la carpeta de /Style Library/XSL Style Sheets. Dentro de este fichero vamos a definir nuestras nuevas plantillas:

Cabecera del fichero









<xsl:stylesheet


version=”1.0″


exclude-result-prefixes=”x d xsl msxsl cmswrt”


xmlns:x=”http://www.w3.org/2001/XMLSchema”


xmlns:d=”http://schemas.microsoft.com/sharepoint/dsp”


xmlns:cmswrt=”http://schemas.microsoft.com/WebParts/v3/Publishing/runtime”


xmlns:ddwrt=”http://schemas.microsoft.com/WebParts/v2/DataView/runtime”


xmlns:xsl=”http://www.w3.org/1999/XSL/Transform” xmlns:msxsl=”urn:schemas-microsoft-com:xslt”>


 


Plantilla 1











<xsl:template name=”Estilo1″ match=”Row[@Style=’Estilo1′]” mode=”itemstyle”>


    <xsl:variable name=”Fecha_Noticia”>


     <xsl:value-of select=”ddwrt:FormatDateTime(string(@ArticleStartDate) ,1033 ,’dd-MM-yyyy’)” />


    </xsl:variable>


<xsl:variable name=”SafeLinkUrl”>


<xsl:call-template name=”OuterTemplate.GetSafeLink”>


<xsl:with-param name=”UrlColumnName” select=”‘LinkUrl'”/>


</xsl:call-template>


</xsl:variable>


<xsl:variable name=”SafeImageUrl”>


<xsl:call-template name=”OuterTemplate.GetSafeStaticUrl”>


<xsl:with-param name=”UrlColumnName” select=”‘ImageUrl'”/>


</xsl:call-template>


</xsl:variable>


<xsl:variable name=”DisplayTitle”>


<xsl:call-template name=”OuterTemplate.GetTitle”>


<xsl:with-param name=”Title” select=”@Title”/>


<xsl:with-param name=”UrlColumnName” select=”‘LinkUrl'”/>


</xsl:call-template>


</xsl:variable>


<xsl:variable name=”LinkTarget”>


<xsl:if test=”@OpenInNewWindow = ‘True'” >_blank</xsl:if>


</xsl:variable>


<table cellpadding=”0″ cellspacing=”0″ width=”100%”>


    <tr>


        <td class=”Estilo1Title” colspan=”2″>


         <xsl:call-template name=”OuterTemplate.CallPresenceStatusIconTemplate”/>


        <a href=”{$SafeLinkUrl}” target=”{$LinkTarget}” title=”{@LinkToolTip}”>


    <xsl:value-of select=”$DisplayTitle”/>


        </a>


</td>


</tr>


<tr class=”Estilo1Border”>


    <xsl:if test=”string-length($SafeImageUrl) != 0″>


    <td class=”Estilo1Image”>


    <a href=”{$SafeLinkUrl}” target=”{$LinkTarget}”>


    <img class=”image” src=”{$SafeImageUrl}” alt=”{@ImageUrlAltText}” /></a>


    </td>


    </xsl:if>


    <td class=”Estilo1Description” valign=”top”>


        <span style=”font-size: smaller;”>


            <xsl:value-of select=”$Fecha_Noticia”/> – Publicado por: <xsl:value-of select=”@ArticleByLine” />


        </span><br/>     


    <xsl:value-of select=”substring(@PublishingPageContent, 0, 200)” disable-output-escaping=”yes”/>…


    </td>


</tr>


</table>


</xsl:template>


 


Plantilla 2











<xsl:template name=”Estilo2″ match=”Row[@Style=’Estilo2′]” mode=”itemstyle”>


        <xsl:variable name=”Created”>


         <xsl:value-of select=”ddwrt:FormatDateTime(string(@ArticleStartDate) ,1033 ,’dd-MM-yyyy’)” />


        </xsl:variable>


<xsl:variable name=”SafeLinkUrl”>


<xsl:call-template name=”OuterTemplate.GetSafeLink”>


<xsl:with-param name=”UrlColumnName” select=”‘LinkUrl'”/>


</xsl:call-template>


</xsl:variable>


<xsl:variable name=”DisplayTitle”>


<xsl:call-template name=”OuterTemplate.GetTitle”>


<xsl:with-param name=”Title” select=”@Title”/>


<xsl:with-param name=”UrlColumnName” select=”‘LinkUrl'”/>


</xsl:call-template>


</xsl:variable>


<xsl:variable name=”LinkTarget”>


<xsl:if test=”@OpenInNewWindow = ‘True'” >_blank</xsl:if>


</xsl:variable>


<div id=”linkitem” class=”item link-item”>


<xsl:call-template name=”OuterTemplate.CallPresenceStatusIconTemplate”/>


    <a href=”{$SafeLinkUrl}” target=”{$LinkTarget}” title=”{@LinkToolTip}”>


        <xsl:value-of select=”$Created”/> – <xsl:value-of select=”$DisplayTitle”/>


    </a>


</div>


</xsl:template>


 


Plantilla 3











<xsl:template name=”Estilo3″ match=”Row[@Style=’Estilo3′]” mode=”itemstyle”>


        <xsl:variable name=”Fecha_Noticia”>


         <xsl:value-of select=”ddwrt:FormatDateTime(string(@ArticleStartDate) ,1033 ,’dd-MM-yyyy’)” />


        </xsl:variable>


<xsl:variable name=”SafeLinkUrl”>


<xsl:call-template name=”OuterTemplate.GetSafeLink”>


<xsl:with-param name=”UrlColumnName” select=”‘LinkUrl'”/>


</xsl:call-template>


</xsl:variable>


<xsl:variable name=”SafeImageUrl”>


<xsl:call-template name=”OuterTemplate.GetSafeStaticUrl”>


<xsl:with-param name=”UrlColumnName” select=”‘ImageUrl'”/>


</xsl:call-template>


</xsl:variable>


<xsl:variable name=”DisplayTitle”>


<xsl:call-template name=”OuterTemplate.GetTitle”>


<xsl:with-param name=”Title” select=”@Title”/>


<xsl:with-param name=”UrlColumnName” select=”‘LinkUrl'”/>


</xsl:call-template>


</xsl:variable>


<xsl:variable name=”LinkTarget”>


<xsl:if test=”@OpenInNewWindow = ‘True'” >_blank</xsl:if>


</xsl:variable>


<div id=”linkitem” class=”item”>


<xsl:if test=”string-length($SafeImageUrl) != 0″>


<div class=”image-area-left”>


<a href=”{$SafeLinkUrl}” target=”{$LinkTarget}”>


<img class=”image” src=”{$SafeImageUrl}” alt=”{@ImageUrlAltText}” />


</a>


</div>


</xsl:if>


<div class=”link-item”>


     <a href=”{$SafeLinkUrl}” target=”{$LinkTarget}” title=”{@LinkToolTip}”>     


<xsl:value-of select=”$Fecha_Noticia” /> – <xsl:value-of select=”@ArticleByLine” /><br/><br/>


     <xsl:call-template name=”OuterTemplate.CallPresenceStatusIconTemplate”/>


<xsl:value-of select=”$DisplayTitle”/>


</a>


<div class=”description”>


<xsl:value-of select=”@Description” />


</div>


</div>


</div>


</xsl:template>


 


Anexo:


Existen muchas otras propiedades que se pueden personalizar de la Content Query Web Part, para ver un listado completo visita este link de MSDN:


http://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.publishing.webcontrols.contentbyquerywebpart_properties.aspx

Personalización de la Content Query Web Part

En este post vamos a explicar cómo funciona la Content Query Web Part y cómo
podemos personalizarla para que muestre elementos de una lista en concreto, con
campos propios que definamos en esa lista y creando un estilo que mejore la
forma de presentarlo en el navegador.

Antes de empezar a explicar cómo se personaliza es necesario comentar que la
Content Query Web Part pertenece solamente a versiones de MOSS, no de WSS, y nos
aparece siempre que tengamos activada la feature de publicación a nivel de Site
Collection

Al insertar esta Web Part en una página, podemos observar dentro de sus
propiedades todos los elementos con los que podemos trabajar, que son el lugar
de donde queremos obtener información, que filtros aplicar, tipos de contenidos,
estilos de presentación, etc…

Por defecto, la Content Query Web Part muestra únicamente un listado de los
elementos encontrados en:

  • Todos los elementos de todos los sitios de la
    Site Collection.
  • Todos los elementos del sitio y subsitios que
    lo integran.
  • Los elementos de una lista o librería de
    documentos concreta.

Figura 1:
Orígenes para definir la consulta que se va a realizar en la Content Query Web
Part.

Nosotros vamos a seleccionar la última opción puesto que nos permitirá
mostrar los elementos de una lista o librería concretas que ya tengamos definida
en nuestra colección de sitio. Para poder mostrar más información en la consulta
devuelta por la Content Query Web Part (sólo devuelve el nombre de los elementos
encontrados), es necesario personalizarla a dos niveles:

  • Campos que se van a visualizar en la consulta
    devuelta.
  • Como se va a mostrar la información que se
    visualiza, es decir, que estilo se va a utilizar para mostrar la información.

Campos a visualizar en la consulta

Lo primero que debemos hacer a la hora de personalizar la content query web
part es exportar la existente y realizar todos los cambios, para ello vamos a la
galería de Web Parts de MOSS: Acciones el Sitio -> Configuración
del sitio
y luego en la sección Galerías
escogemos la opción Elementos Web. La pantalla que se
abre muestra el listado con las Web Parts listas para ser usadas. Buscamos en la
lista la Content Quey Web Part para editarla:

Figura 2:
Galería de Web Parts en MOSS.

Editamos la web part, y a través de la opción de exportación, la exportamos
para poder personalizarla:

Figura 3:
Edición de la Content Query Web Part.

Una vez exportada la Content Query Web Part (en formato .webpart), vamos a
modificar su comportamiento para que devuelva en la consulta los campos que
consideremos oportunos. Lo podemos hacer con cualquier editor de texto, y la
sección a modificar del archivo .webpart es la referente a la propiedad (dentro
de la sección properties) con nombre
CommonViewFields en la que especificaremos los campos
que nos interesa que nos muestre el resultado de la consulta que realiza la
Content Query Web Part:

Figura 4:
Modificación del archivo .webpart.

En este caso hemos especificado que la consulta nos devuelva los siguientes
campos de la lista (de tipo Announcements): Title,
Body, y DateTime. Además,
especificamos para cada campo su tipo. Pero, ¿qué son estos campos en la
lista de MOSS sobre la que vamos a definir la consulta de contenido?
Estos
campos son los nombres que MOSS utiliza internamente para denominar las columnas
de una lista o una librería de documentos (Nota: Una
forma de averiguar el nombre interno de cada campo es desde a través de la url
que aparece en el navegador en el momento en que entramos en la visualización de
detalle de un campo de la lista).

Una vez configurado el archivo .WebPart, la subimos a la galería de Web Parts
con otro nombre distinto al de la Content Query Web Part y definiendo otras
propiedades a nivel de elemento de la galería que consideremos convenientes
(grupo de la web part, en que agrupaciones de las existentes queremos agregarla,
una descripción,…).

Personalización de la consulta devuelta

Una vez que hemos especificado los campos que va a devolver la Content Query
Web Part personalizada, el siguiente paso es especificar cómo se va a visualizar
dicha información (recordemos que la Content Query Web Part de MOSS solo
visualiza el nombre de los elementos devueltos en la consulta). Para ello,
tenemos que personalizar el fichero xsl en el que se definen las distintas
formas en que la Content Query Web Part va a mostrar la consulta devuelta.

Figura 5:
Configuración de la presentación de la información con la Content Query Web
Part.

Para que el estilo que queremos (en este caso, junto con el nombre del
elemento aparezca la fecha en que se creó y una parte de la descripción) es
necesario que modifiquemos el archivo ItemStyle.xls. Este archivo se encuentra
almacenado en la base de datos de contenidos MOSS y para personalizarlo tenemos
que utilizar Sharepoint Designer. Se encuentra localizado en la biblioteca de
estilos del sitio raíz Style Library y luego en la
biblioteca XSL Style Sheets:

Figura 6:
Localización del archivo ItemStyle.xls con Sharepoint Designer.

Esta hoja xsl también la podemos localizar a través de la interfaz de usuario
de MOSS: Bibliotecas de Documentos -> Biblioteca de estilos
y luego la carpeta XSL Styles Sheets:

Figura 7:
Localización del archivo ItemStyle en MOSS.

Una vez que tenemos localizado el archivo, lo abrimos y tenemos que
configurar los siguientes elementos:

  • Añadir en la sección
    xsl:stylesheet del archivo el esquema XML que nos
    permita presentar la información devuelta en la consulta con los campos que
    queremos:

<xsl:stylesheet

version=”1.0″

exclude-result-prefixes=”x d xsl msxsl cmswrt”

xmlns:x=”http://www.w3.org/2001/XMLSchema”

xmlns:d=”http://schemas.microsoft.com/sharepoint/dsp”

xmlns:cmswrt=”http://schemas.microsoft.com/WebParts/v3/Publishing/runtime”

xmlns:xsl=”http://www.w3.org/1999/XSL/Transform”
xmlns:msxsl=”urn:schemas-microsoft-com:xslt”

xmlns:ddwrt=”http://schemas.microsoft.com/WebParts/v2/DataView/runtime”>

 

El esquema a añadir es el que se ha remarcado en negrita en el listado
anterior.

  • El estilo de presentación con el que vamos a
    mostrar los distintos elementos que devuelve la Content Query Web Part:

<xsl:template name=”Avisos”
match=”Row[@Style=’Avisos’]” mode=”itemstyle”>

        <xsl:variable
name=”Created”>

        
<xsl:value-of select=”ddwrt:FormatDateTime(string(@Expires)
,1033 ,’dd-MM-yyyy’)” />

        </xsl:variable>

<xsl:variable
name=”SafeLinkUrl”>

<xsl:call-template
name=”OuterTemplate.GetSafeLink”>

<xsl:with-param
name=”UrlColumnName” select=”‘LinkUrl'”/>

</xsl:call-template>

</xsl:variable>        

<xsl:variable
name=”SafeImageUrl”>

<xsl:call-template
name=”OuterTemplate.GetSafeStaticUrl”>

<xsl:with-param
name=”UrlColumnName” select=”‘ImageUrl'”/>

</xsl:call-template>

</xsl:variable>

<xsl:variable
name=”DisplayTitle”
>

<xsl:call-template name=”OuterTemplate.GetTitle”>

<xsl:with-param
name=”Title” select=”@Title“/>

<xsl:with-param
name=”UrlColumnName” select=”‘LinkUrl'”/>

</xsl:call-template>

</xsl:variable>

<xsl:variable
name=”LinkTarget”>

<xsl:if
test=”@OpenInNewWindow = ‘True'” >_blank</xsl:if>

</xsl:variable>

<div
id=”linkitem” class=”item”>

<xsl:if
test=”string-length($SafeImageUrl) != 0″>

<div
class=”image-area-left”>

<a href=”{$SafeLinkUrl}” target=”{$LinkTarget}”>

<img
class=”image” src=”{$SafeImageUrl}” alt=”{@ImageUrlAltText}” />

</a>

</div>

</xsl:if>

<div
class=”link-item2″>

    
<xsl:call-template name=”OuterTemplate.CallPresenceStatusIconTemplate”/>

                <a href=”{$SafeLinkUrl}”
target=”{$LinkTarget}” title=”{@LinkToolTip}”>

                
<xsl:value-of select=”$Created”/> – <xsl:value-of
select=”$DisplayTitle”/>

                </a>                

                <div class=”description”>

                
<xsl:value-of select=”substring(@Body, 0, 200)”
disable-output-escaping=”yes”/>…

                <br/>

                
<a class=”link-item” href=”{$SafeLinkUrl}” target=”{$LinkTarget}” title=”Leer
mas”>Leer más…</a><br/>

                </div>

</div>

</div>

</xsl:template>

 

Como se ve en el listado anterior, al especificar
el estilo que queremos aplicar para la visualización de los datos devueltos por
la consulta, es necesario indicar los siguientes elementos:

  • El nombre de la plantilla que aparecerá en la
    configuración de la presentación de la Content Query WebPart que hemos
    personalizado. En este caso el nombre de la plantilla es
    Avisos.
  • El estilo que van a seguir los elementos de la
    plantilla, que de nuevo hemos denominado Avisos.
  • Los distintos elementos e información que se
    van a visualizar por resultado devuelto:
    • La variable Created
      que es de tipo fecha, y cuyo valor se obtiene a partir de la columna Expires
      devuelta por la consulta (Nota:
      Además, en este caso se ha formateado la fecha al formato utilizado en España
      utilizando la función FormatDateTime). Esta variable visualizará la fecha de
      creación de cada ítem de la lista sobre el que se ha definido la consulta.
    • La variable
      DisplayTitle de tipo string, y cuyo valor se obtiene a
      partir de la columna Title devuelta por la consulta. Esta variable visualizará
      el título de cada ítem de la lista sobre la que se ha definido la consulta.
    • Una porción de la columna
      Body devuelta por la consulta para visualizar un
      resumen de cada elemento devuelto por la consulta.

<xsl:value-of select=”substring(@Body, 0,
200)” disable-output-escaping=”yes”/>…

  • Finalmente, se ha añadido un vínculo a la
    noticia utilizando la forma estándar de definir enlaces en HTML y apuntando al
    detalle del elemento:

     

    <a class=”link-item” href=”{$SafeLinkUrl}” target=”{$LinkTarget}” title=”Leer mas”>Leer
    más…</a><br/>

     

Sin más, una vez que se han personalizado tanto la web part como el archive
ItemStyle.xls, ya podemos utilizarla en nuestro sitio MOSS y obtener el
resultado esperado.

Figura 8: Aspecto final que ofrece la Content
Query Web Part personalizada.

WSS 3.0 / MOSS: Copiando datos entre listas utilizando un workflow!

Hacía bastante tiempo que la gente del CIIN no publicábamos nada (casi 1 mes desde el último post sobre LINQ), y la razón de esto es que entre WSS 3.0 y su hermano mayor MOSS apenas nos queda tiempo para contaros cosas interesantes que estamos probando y descubriendo. Asique he decidido darme un pequeño respiro y contaros como hemos solucionado un problema que nos ha surgido en una de las especificaciones del proyecto de MOSS en el que estamos involucrados.


El problema


El problema que se planteo es el siguiente: en un entorno de Extranet (implementado con plantillas de tipo Team Site) se pensó en utilizar la lista Issue Tracking para que los clientes puedan dar de alta las incidencias que tienen con los productos o servicios de la empresa dueña de la Extranet. La cuestión es que las incidencias que dan de alta los clientes tienen que tener un formato diferente al que ven los administradores o responsables de Help Desk de la Extranet (estos necesitan más información), por lo que con crear dos Content Types para cada formato de incidencia es suficiente. Hasta aquí no hay problema, con las funcionalidades que nos da WSS 3.0 / MOSS podemos conseguir lo anterior para una lista.


Ahora bien, sería deseable que el cliente sólo pudiera dar de alta incidencias con el formato específicamente creado para ellos, y que los administradores pudieran añadir a la incidencia creada más información que el cliente no puede modificar…y es aquí dónde está el problema, puesto que una lista de WSS 3.0 / MOSS no puede ser configurada (con la funcionalidad out-of-the-box) para que jugando con los permisos se muestre un content type u otro dependiendo del usuario que está loggeado en la Extranet, es decir, si la lista tiene dos content types, se mostrarán los dos cuando se va a crear un nuevo elemento en la misma.



¿Qué podemos hacer para lograr la funcionalidad requerida? La solución que ofrecimos fue crear dos listas, cada uno con su Content Type diferenciado, de manera que cuando un usuario cliente diese de alta una incidencia en la lista especifica, esta apareciera reflejada automáticamente en la lista de incidencias del servicio de atención al cliente reflejando la información introducida por el cliente, y con la posibilidad de poder añadir mucha más información para resolver la incidencia de manera adecuada. Además, y por simplicidad, estos content types se han creado de manera que hereden el uno del otro dado que comparten campos comunes.


¿Cómo movemos los datos entre las dos listas?


A priori pensamos dos formas (espero vuestras sugerencias para incluir formas adicionales a las que hemos pensado) que permitieran que el elemento creado en una lista se creara automáticamente en la otra:


·         Mediante un workflow, asociado a la lista y con la lógica necesaria para que al ser creado un elemento en la lista origen, creara una copia del mismo en la lista origen.


·         Utilizando un manejador de eventos (esta idea fue sugerencia de mi compañero Ángel), de manera que al lanzarse un evento de tipo ItemAdding o ItemAdded, se copiase el elemento creado en la lista origen a la lista destino.


Como la idea era dar una solución que aprovechase funcionalidades out-of-the-box de WSS 3.0 / MOSS y Sharepoint Designer 2007 (SD 2007), nos decantamos por la primera opción, es decir, para copiar un dato entre las dos listas nos basta con crear un workflow mediante SD 2007 y utilizando las acciones estándar que ofrece.


De la euforia a la desilusión


Dicho y hecho, con SD 2007 creamos un workflow sencillito vinculado a la lista origen que realizase lo anterior aprovechando que existe una acción denominada  (en la versión en español) Copiar elemento de lista.



Tras probar que el workflow, no contenía errores, pasamos a desplegarlo  y a probar que funcionaba correctamente. Efectivamente, como usuario administrador todo va como la seda, los ítems que creo en la lista origen aparecen automáticamente en la lista destino…pero, ¿qué sucede si el usuario que crea el elemento no tiene permisos para ver y menos aún para modificar elementos en la lista destino? Nos loggeamos como un usuario de tipo cliente, que no tiene permisos sobre la lista destino (ni siquiera la ve), y creamos una incidencia, tras guardar la incidencia este es el resultado:



La copia ha fallado, se ha producido un error en la ejecución del workflow. Tras quedarme un poco perplejo, pues como administrador todo iba bien, paso a investigar que está pasando. Como me suele decir Ángel, aunque sea un poco tedioso, lo mejor es ver que nos dicen los logs de WSS 3.0. Tras bucear un poco en el último log generado (en la carpeta 12LOGS del servidor de WSS 3.0 / MOSS) veo que efectivamente se han producido varios errores muy feos de tipo Workflow Infrestructure:







Microsoft.SharePoint.SPException: Esta lista no existe  La página seleccionada contiene una lista que no existe. Puede que otro usuario la haya eliminado. —> System.Runtime.InteropServices.COMException (0x81020026): Esta lista no existe  La página seleccionada contiene una lista que no existe. Puede que otro usuario la haya eliminado.     at Microsoft.SharePoint.Library.SPRequestInternalClass.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, ISP2DSafeArrayWriter p2DWriter, Int32& plRecycleBinCount)     at Microsoft.SharePoint.Library.SPRequest.GetListsWithCallback(S…  


…tring bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, ISP2DSafeArrayWriter p2DWriter, Int32& plRecycleBinCount)     — End of inner exception stack trace —     at Microsoft.SharePoint.Library.SPRequest.GetListsWithCallback(String bstrUrl, Guid foreignWebId, String bstrListInternalName, Int32 dwBaseType, Int32 dwBaseTypeAlt, Int32 dwServerTemplate, UInt32 dwGetListFlags, UInt32 dwListFilterFlags, Boolean bPrefetchMetaData, Boolean bSecurityTrimmed, Boolean bGetSecurityData, ISP2DSafeArrayWriter p2DWriter, Int32& plRecycleBinCount)     at Microsoft.SharePoint.SPListCollection.EnsureListsData(… 


 at Microsoft.SharePoint.SPListCollection.EnsureListsData(String strListName)     at Microsoft.SharePoint.SPListCollection.ItemByInternalName(String strInternalName, Boolean bThrowException)     at Microsoft.SharePoint.SPListCollection.GetListById(Guid uniqueID, Boolean bThrowException)     at Microsoft.SharePoint.SPListCollection.get_Item(Guid uniqueID)     at Microsoft.SharePoint.Workflow.SPWinOEWSSService.CopyListItem(Guid id, Guid listId, Int32 itemId, Guid toListId, Boolean overwrite)               



Es decir, el workflow iniciado bajo la cuenta user1 no ha podido copiar el elemento de la lista origen a la lista destino porque no encuentra esta última (lógico, pues no tiene permisos de acceso). Luego, aunque todo parecía muy bonito, como el workflow credo por SD 2007 al ejecutarse toma la identidad del cliente que creó el elemento en la lista origen, WSS  3.0 no va a permitir crearlo en la lista de destino puesto que el usuario no tiene permisos para ello…con lo bien que pintaban las cosas!. Tras intentar varias cosas con SD 2007 (como ver si se podía cambiar el valor del campo Created By), decidimos pasar a la alternativa dos para crear el workflow que copiase el elemento creado en la lista destino: crearlo con Visual Studio 2005, utilizando para ello las plantillas para la creación de workflows para WSS 3.0 que vienen con el SDK, y resolviendo el problema de permisos con el que nos hemos encontrado.


La solución


Por tanto, la idea era modelar un workflow para WSS 3.0 utilizando las plantillas para creación de workflows que vienen con el SDK de WSS 3.0 y que solventara el problema de permisos anterior a través de la correspondiente elevación de privilegios. El workflow creado es realmente sencillo, y está formado por dos actividades:


·         Una actividad de tipo OnWorkflowActivated que es obligatoria en todo workflow de WSS 3.0 pues es la actividad de inicio del mismo.


·         Una actividad de tipo code, en la que introduciremos la lógica necesaria para copiar el elemento de la lista origen a la lista destino en el momento en el que este se crea.



Una vez creado el workflow con el diseñador, pasamos a codificar los elementos necesarios, es decir, los manejadores de las dos actividades que integran el workflow.


Manejador para la actividad OnWorkflowActivated


Este manejador es realmente sencillo, y lo único que tiene que hacer es inicializar la propiedad pública workflowId que se crea al diseñar el workflow con la análoga de las propiedades de activación del workflow:







        private void ActivarWorkflow(object sender, ExternalDataEventArgs e)


        {


            workflowId = workflowProperties.WorkflowId;


        }


Las propiedades de activación del workflow representadas por el objeto workflowProperties que es una instancia de la clase SPWorkflowActivationProperties, y que representa las propiedades iníciales de la instancia del workflow en el momento en el que se lanza:


·         El usuario que añadió el workflow (propiedad OriginatorUser, de tipo SPUser).


·         La lista e ítem a las que se ha añadido el workflow: propiedades Item (de tipo SPListItem) y List (de tipo SPList).


·        


La idea es que cuando se lanza una nueva instancia del workflow, WSS para un objeto de tipo SPWorkflowActivatioProperties a la actividad OnWorkflowActivated de nuestro workflow.


Manejador para la actividad Code del workflow


Y es aquí donde llega lo bueno, porque en este punto tendremos que crear toda la lógica necesaria para implementar los dos requisitos funcionales:


·         Realizar una elevación de privilegios para que el elemento creado en la lista origen se pueda copiar en la lista destino.


·         Realizar la copia efectiva del elemento creado en la lista destino.


Para realizar la elevación de privilegios, tendremos que utilizar un método especial del objeto SPSecurity de WSS 3.0:







SPSecurity.RunWithElevatedPrivileges(delegate()


            {


                // Obtener un contexto de seguridad               


                using (SPSite site=new SPSite(workflowProperties.SiteId))


                {


                    using (SPWeb web = site.OpenWeb(workflowProperties.WebId))


                    {


                        //Lógica para copiar el ítem en la lista


                    }


                }


            });                              


Lo que hacemos con el método RunWithElevatedPrivileges de la clase SPSecurity es ejecutar toda la lógica que viene a continuación con permisos de control total aunque el usuario que inicia el workflow no los tenga. Además, especificamos que el código se ejecute en el site collection (objeto site de tipo SPSite, obtenido mediante la propiedad SiteId de workflowProperties) dónde se ha iniciado el workflow, y dentro de esta site collection en la web dónde este se ha iniciado (objeto web de tipo SPWeb, obtenido a través de la propiedad WebId de workflowProperties). Este método define un parámetro, que puede ser:


·         Un objeto de tipo SPSecurity.CodeToRunElevated, que representa un método que va a correr con elevación de privilegios.


·         Utilizando un método anónimo mediante delegate(), que es justo la opción que hemos utilizado.


Una vez que tenemos garantizado que podemos hacer operaciones contra un ítem (en este caso será una lista) de WSS 3.0 sobre el que se han establecido restricciones, pasamos a la parte “fácil” que es copiar el elemento de la lista origen a la lista destino. Este es el código que he utilizado (ya me diréis que os parece):







//Elementos de WSS 3.0 necesarios


SPList splListaOrigen = web.Lists[workflowProperties.ListId];


SPList splListaDestino = web.Lists[LISTA_DESTINO];


SPField spfCampo;


SPFile spfAttachment;


SPAttachmentCollection spacAttachments;


 


//Contadores para los bucles.


int i, j, k;


 


SPListItem spliItemACrear = splListaDestino.Items.Add();


SPListItem spliItemActual = workflowProperties.Item;


 


i = 0;


for (i = 0; i < spliItemActual.Fields.Count; i++)


{


    spfCampo = spliItemActual.Fields[i];


    try


    {


        j = 0;


        for (j = 0; j < spliItemACrear.Fields.Count; j++)


        {


            if (spfCampo.InternalName == spliItemACrear.Fields[j].InternalName)


            {


                if (spliItemActual[spfCampo.InternalName] != null &&


                    spfCampo.ReadOnlyField == false)


                {


                    if (spfCampo.InternalName != ATTACHMENT_FIELD)


                    {


                        spliItemACrear[spfCampo.InternalName] = spliItemActual[spfCampo.InternalName];


                    }


                    else


                    {


                        spacAttachments = spliItemActual.Attachments;


                        k = 0;


                        for (k = 0; k <= spacAttachments.Count – 1; k++)


                        {


                            spfAttachment = web.GetFile(spliItemActual.Attachments.UrlPrefix + spacAttachments[k]);


                            spliItemACrear.Attachments.Add(spfAttachment.Name, spfAttachment.OpenBinary());


                        }


                    }


                }


            }


        }


    }


    catch (Exception ex)


    {


        //Tratamiento de las excepciones que se puedan producir.


    }


}


//Creamos el elemento en la lista destino


try


{


    //Actualizamos los campos ID Internet y Fecha de alta en el item origen


    spliItemActual[ID_INTERNET_FIELD] = spliItemActual[ID_LISTA_FIELD];


    spliItemActual[FECHA_ALTA_FIELD] = DateTime.Now;


    spliItemActual.Update();


    //Creamos el item en destino


    spliItemACrear[ID_INTERNET_FIELD] = spliItemActual[ID_INTERNET_FIELD];


    spliItemACrear.Update();


}


catch (Exception ex)


{


    //Tratamiento de las excepciones que se puedan producir.


}


Analizando un poco el código anterior, destacaría los siguientes puntos:


·         La lista origen la vamos a tener en el objeto splListaOrigen que es de tipo SPList, y la obtenemos a través del método Lists (que obtiene la colección de listas de una web, y que admite del objeto web. A este método le especificamos que lista queremos abrir a través del Id de la lista que de nuevo obtenemos a partir de una de las propiedades de workflowProperties: ListId. La lista de destino la tendremos en el objeto splListaDestino utilizando el mismo método, pero esta vez a través de su nombre.







SPList splListaOrigen = web.Lists[workflowProperties.ListId];


SPList splListaDestino = web.Lists[LISTA_DESTINO];


 


·         El siguiente paso consiste en obtener de algún modo el ítem creado en la lista para poder copiarlo en el origen y preparar la lista destino. De nuevo lo obtenemos a partir de workflowProperties y la propiedad Item que lógicamente es de tipo ListItem. Para empezar a añadir elementos a la lista destino, utilizamos el método Add():







SPListItem spliItemACrear = splListaDestino.Items.Add();


SPListItem spliItemActual = workflowProperties.Item;


 


·         Para ir añadiendo los campos del ítem de la  lista origen en la lista destino he utilizado dos bucles for anidados. En el primero voy leyendo campo a campo del elemento a crear, y en el segundo me recorro los campos que forman un elemento en la lista destino (recordemos que las listas utilizan Content Types diferentes para la creación de elementos, por lo que un elemento de la lista origen no tendrá el mismo número de campos que un elemento de la lista destino). Es en este segundo nivel dónde, sabiendo que los Content Types de las listas están relacionados (el content type de la lista origen hereda de la lista destino) dónde se realiza la comparación campo a campo para añadir en la lista destino aquellos campos que realmente puedan ser añadidos:







            if (spfCampo.InternalName == spliItemACrear.Fields[j].InternalName)


            {


                   //Lógica de asignación


            }


               


Pero las comprobaciones no acaban aquí, además tendremos que comprobar que el campo a añadir tiene un contenido no nulo, y sobre todo que no tenga la propiedad ReadOnly a true, puesto que de ser así, el mismo campo en el destino no se podrá actualizar.







                if (spliItemActual[spfCampo.InternalName] != null &&


                    spfCampo.ReadOnlyField == false)


                {


                     //Lógica de asignación


                }


¿Se acabaron las comprobaciones? Pues no, hay un campo que merece un tratamiento especial: los attachments asociados al ítem creado. Para el resto de campos, la asignación es directa:







                    if (spfCampo.InternalName != ATTACHMENT_FIELD)


                    {


                        spliItemACrear[spfCampo.InternalName] = spliItemActual[spfCampo.InternalName];


                    }


                    else


                    {


                        spacAttachments = spliItemActual.Attachments;


                        k = 0;


                        for (k = 0; k <= spacAttachments.Count – 1; k++)


                        {


                            spfAttachment = web.GetFile(spliItemActual.Attachments.UrlPrefix + spacAttachments[k]);


                            spliItemACrear.Attachments.Add(spfAttachment.Name, spfAttachment.OpenBinary());


                        }


                    }


Por lo tanto, cuando el campo del ítem origen sea Attachments, lo que tenemos que hacer es utilizar un objeto de tipo SPAttachmentCollection, en el código spacAttacments, en el que recogeremos los attachments asociados al ítem creado en la lista origen (propiedad Attachments del objeto spliItemActual).  Una vez que tenemos los attachments, nos recorremos los ítems individuales de la colección de attachments y los vamos cogiendo mediante un objeto de tipo SPFile utilizando para ello el método GetFile del objeto web (recordemos que es de tipo SPWeb) al que le pasamos la ubicación del archivo (que obtenemos a través de la propiedad UrlPrefix de la colección de attachments del elemento de la lista origen y el nombre físico del attachment).


Sin más, añadimos cada attachment a los attachments del elemento a crear mediante el método Add() de la propiedad Attachments, al que le tenemos que indicar el nombre del attachment (propiedad Name del objeto SPFile) y un array de bystes (byte[]) que contiene el attachment y que se obtiene mediante el método OpenBinary() del objeto SPFile utilizado.


·         Sin más, una vez que tenemos preparado el ítem a crear, se lo añadimos de forma efectiva a la lista destino utilizando el método Update().







spliItemACrear.Update();


Y ya está, con este código podemos copiar el ítem creado en la lista de destino y solventando el problema de permisos con el que nos encontramos en SD 2007.


Desplegando y probando el workflow


Para probar el workflow en nuestra Extranet, lo primero es desplegarlo en el sitio en el que vayamos a utilizarlo (lo podemos desplegar a nivel de Site Collection o en un cierto sitio web). Para desplegarlo vamos a utilizar la opción que por defecto nos ofrece un proyecto de workflow para WSS 3.0 / MOSS: a través de una feature. Los pasos necesarios son:


·         Configurar el archivo feature.xml, en el que especificaremos cuál es el archivo de manifiesto en el que están descritos los elementos de nuestro workflow.







<Feature  Id=”4AA1EA9A-0A4F-41f9-A7CA-C1309C42A534″


          Title=”Copia de una lista origen a otra”


          Description=”Copia datos de la lista cliente a la lista Semicrol!”


          Version=”12.0.0.0″


          Scope=”Site”


          xmlns=”http://schemas.microsoft.com/sharepoint/”>


                <ElementManifests>


                               <ElementManifest Location=”workflow.xml” />


                </ElementManifests>


                <Properties>


                               <Property Key=”GloballyAvailable” Value=”true” />


                </Properties>


</Feature>


Lo más importante en este archivo es que nos aseguremos de utilizar un Id único, el cuál obtenemos a partir de la herramienta Create GUID de Visual Studio 2005, y que especifiquemos correctamente el nombre del archivo de manifiesto del workflow.



·         Configurar el archivo de manifiesto de nuestro workflow:







<Elements xmlns=”http://schemas.microsoft.com/sharepoint/”>


                <Workflow


                                Name=”Copiar item a lista”


                                Description=”Ejemplo de workflow que crea una tarea de revisión.”


                                Id=”ACD332EC-60EE-4dd6-8F17-222A8D88A3F3″


                                CodeBesideClass=”CopiarIncidencia.Workflow1″


                                CodeBesideAssembly=”CopiarIncidencia, Version=1.0.0.0,


                             Culture=neutral,PublicKeyToken=ac35d273278cd512″>


                               <Categories/>


                               <MetaData>


                                               <StatusPageUrl>_layouts/WrkStat.aspx</StatusPageUrl>


                               </MetaData>


                </Workflow>


</Elements>


 De nuevo tenemos que tener cuidado con la configuración de este archivo, prestando atención a los siguientes atributos:


o   Id, que tiene que ser único y se genera de nuevo con la utilidad Create GUID.


o   CodeBesideClass, en el que especificamos el nombre del proyecto y de la clase de nuestro workflow.


o   CodeBesideAssembly, en el que tenemos que especificar el nombre del assembly, su versión y su KeyToken exactos para no tener problemas. Para obtener esta información podemos utilizar la herramienta Reflector de Lutz Roeder.


·         Configurar el archivo de instalación de la feature, Install.Bat para que despliegue y active la feature dónde nosotros especifiquemos:







echo Copying the feature…


 


rd /s /q “%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESCopiarIncidencia”


mkdir “%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESCopiarIncidencia”


 


copy /Y feature.xml  “%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESCopiarIncidencia”


copy /Y workflow.xml “%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEFEATURESCopiarIncidencia”


xcopy /s /Y *.aspx “%programfiles%Common FilesMicrosoft Sharedweb server extensions12TEMPLATELAYOUTS”


 


echo Adding assemblies to the GAC…


 


“%programfiles%Microsoft Visual Studio 8SDKv2.0Bingacutil.exe” -uf CopiarIncidencia


“%programfiles%Microsoft Visual Studio 8SDKv2.0Bingacutil.exe” -if binDebugCopiarIncidencia.dll


 


:: Note: 64-bit alternative to lines above; uncomment these to install on a 64-bit machine


::”%programfiles% (x86)Microsoft Visual Studio 8SDKv2.0Bingacutil.exe” -uf MyFeature


::”%programfiles% (x86)Microsoft Visual Studio 8SDKv2.0Bingacutil.exe” -if binDebugMyFeature.dll


 


 


echo Activating the feature…


 


pushd %programfiles%common filesmicrosoft sharedweb server extensions12bin


 


::Note: Uncomment these lines if you’ve modified your deployment xml files or IP forms


stsadm -o deactivatefeature -filename CopiarIncidenciafeature.xml -url http://MiSitio


stsadm -o uninstallfeature -filename CopiarIncidenciafeature.xml


 


stsadm -o installfeature -filename CopiarIncidenciafeature.xml -force


stsadm -o activatefeature -filename CopiarIncidenciafeature.xml -url http://MiSitio


 


 


echo Doing an iisreset…


popd


iisreset


 Analizando el archive de instalación, vemos que los pasos necesarios son:


i)      Crear una carpeta dentro de  12TEMPLATEFEATURES que es la ubicación dónde WSS 3.0 / MOSS almacena todas las features que utiliza.


ii)    Copiar los archivos feature.xml y workflow.cml a esta carpeta.


iii)   Copiar otros archivos que puedan ser necesarios (este no es el caso), cómo páginas aspx o formularios infopath para configurar /interactuar el / con el workflow.


iv)  Copiar el assembly en la GAC del servidor mediante el comando gacutil del SDK de Visual Studio.


v)    Instalar la feature utilizando el comando stsadm con las opciones filename indicando el asembly e installfeature  indicando el archivo xml que describe la feature.


vi)  Activar la feature en el sitio que deseemos utilizando de nuevo stsadm con las mismas opciones especificadas en la instalación.


vii) Por último, es necesario hacer un iisreset (se puede especificar que se haga sólo de un sitio de WSS 3.0 / MOSS y no todo el IISS).


Sin más, ya estamos listos para probar que todo funciona como esperábamos. Para ello, y tras añadir una instancia del workflow creado a la lista origen, creamos un ítem en la lista origen mediante el usuario user1 que no tenía permisos sobre la lista destino:



Ahora al crear la incidencia se ha desencadenado el workflow  (también podéis observar que user1 ni siquiera puede visualizar la lista de destino) y no ha producido ningún tipo de error como nos ocurría con el workflow creado con SD 2007:



Comprobemos que la incidencia se ha copiado en la lista destino, para ello nos logeamos como administradores en la Extranet (o como un usuario con el nivel de permisos adecuado para ver y modificar elementos de esa lista):



Cómo veis, la incidencia se ha creado en la lista destino que ahora si es visible puesto que estamos logeados como administrador y además ha sido creada por la Cuenta del sistema en lugar de por user1 debido al mecanismo de elevación de privilegios que hemos utilizado en el workflow.


Y hasta aquí llega lo que os quería contar sobre como copiar elementos entre dos listas de WSS 3.0 / MOSS desencadenando un workflow y solventando escenarios típicos de distintos niveles de permisos en las listas. Espero que el post os haya parecido interesante. Os dejo también el proyecto para que lo podáis descargar y utilizar.