Tuesday, March 27, 2012

Información Interesante y de Valor para el día a día

Rápidamente quiero compartir mis últimas lecturas en español sobre temas de SharePoint. Una que ya conocía, los amigos de CompartiMOSS, revista en Español, únicamente relacionada con SharePoint http://www.gavd.net/servers/compartimoss/compartimoss_main.aspx

Seguidamente de SolidQ, un artículo muy bueno, bastante detallado sobre la aplicación de servicios de Web Analytics http://www.solidq.com/sqj/es/Pages/2011_September_Issue/Practicas-recomendadas-de-SharePoint-Creacion-y-configuracion-de-aplicaciones-de-servicio-con-y-sin-PowerShell-parte-2.aspx

Feliz lectura.

Tuesday, March 20, 2012

No se puede representar el formulario. Esto puede deberse a un problema en la configuración del Servicio de estado de Microsoft SharePoint Server

Hola de nuevo. Intentando configurar un WorkFlow de Aprobación de SharePoint 2010, puede recibirse el siguiente mensaje:
No se puede representar el formulario. Esto puede deberse a un problema en la configuración del Servicio de estado de Microsoft SharePoint Server. Para obtener más información, póngase en contacto con el administrador del servidor.
Desafortunadamente el mensaje no dice mucho, pero su solución es realmente rápida.
1. Se necesita acceso a la Administración Central
2. Ejecutar el Asistente de Configuración que provee SP 2010:
  • Navegue a la Administración Central
  • Haga clic en la opción Asistente de Configuración o Configuration Wizards
  • Inicie el asistente
  • Deberá solicitarsele que active el State Service, podría ya estar seleccionada, así que clic en Siguiente.
  • Salte el paso de configurar un nuevo Sitio. Puede aparecer un Error. Si vuelve a correr el Wizard y vuelve a saltarse la creación del Sitio verá que el Wizard finaliza correctamente.
  • Haga clic en el botón Finish

Ahora es momento de intentar nuevamente crear el WorkFlow de Aprobación de SP 2010.

Felíz WF!!

Friday, March 16, 2012

La validación del esquema encontró errores que no son de tipo de datos

Hola de nuevo. Trabajando en la solución de un problema en un formulario de InfoPath 2007, encontré una buena solución en un blog. No es mí costumbre pero se debe re-postear, porque realmente funciona lo que ahi se utiliza. Hice uso de la solución y el problema se solucionó.

El error original es el siguiente:
Microsoft.Office.InfoPath.Server.SolutionLifetime.SchemaValidationException: La validación del esquema encontró errores que no son de tipo de datos. en Microsoft.Office.InfoPath.Server.SolutionLifetime.SchemaValidation.Validate(Document document, XPathNavigator subtreeToValidate, XmlOperation operation) en Microsoft.Office.InfoPath.Server.DocumentLifetime.XmlNotificationManager.NodeChangedHandler(Object sender, DomEventArgs e) en Microsoft.Office.InfoPath.Server.Xml.DomEvents.FireQueuedEvents() en Microsoft.Office.InfoPath.Server.Xml.DomEvents.AtomicEvent.System.IDisposable.Dispose() en Microsoft.Office.InfoPath.Server.Xml.XPath.InfoPathXPathNavigator.SetValue(String value) en Microsoft.Office.InfoPath.Server.Xml.XPath.XPathNavigatorHost.<>c__DisplayClass1.b__0() en Microsoft.Office.InfoPath.Server.DocumentLifetime.OMExceptionManager.ExecuteOMCallWithExceptions(OMCall d, ExceptionFilter exceptionFilter) en Microsoft.Office.InfoPath.Server.DocumentLifetime.OMSecurityContext.ExecuteOMCall(Solution solution, SecurityLevel methodSecurityLevel, ExceptionFilter exceptionFilter, OMCall d) en Requerimiento_De_Compra.FormCode.Partida_Changed(Object sender, XmlEventArgs e)

La propuesta está en esta dirección, gracias a Share Notes por compartirla:

Solución

Felíz solución.

Friday, March 02, 2012

Implementando una WebPart XSLT en SharePoint Server 2010


Una de las características importantes con las que cuenta SharePoint, que puede considerarse como uno de sus pilares, son las WebParts, o conocidas también como Elementos Web.

Estos elementos Web permiten esencialmente desplegar información, la cual típicamente se encuentra almacenada en listas o bibliotecas de SharePoint. Pueden haber otras fuentes de datos, pero no son interés de esta entrada por ahora. Vamos a mostrar cómo usar una WebPart que consume información de una lista o biblioteca, y por medio de XSLT, manipular dicha información para mejorar el aspecto gráfico de la WebPart que por defecto, despliega la información de manera tabular, y con una presentación poco familiar para usuarios finales. Suponiendo que tenemos una biblioteca de activos multimedia en SharePoint Server 2010, vamos agregarla a una página de contenido que llamaremos XSLT.aspx, como un Elemento Web para desplegar los videos que ahí se han dado de alta.

  • Lo primero que hacemos es crear la página de contenido como una página de publicación o de elementos web. Luego editarla en modo avanzado en SharePoint Designer 2010.
  • A continuación agregamos en la zona correspondiente, a través de las siguientes opciones una vista de datos vacía: Insertar – Vista de Datos – Vista de Datos Vacía.
  • En este momento se debe poder visualizar una opción que permite seleccionar la fuente de datos para la vista de datos, la cual en este caso, será la biblioteca de activos multimedia. SharePoint Designer despliega una ventana que permite seleccionar de entre las listas y bibliotecas disponibles. En este caso tenemos una biblioteca llamada Videos.

Paso1

En la imagen se pueden observar los siguientes elementos:

  • La vista de datos vacía que se acaba de insertar en código se interpreta como

    <WebPartPages:DataFormWebPart><DataFields>
    </DataFields>
    </WebPartPages:DataFormWebPart>

  • En la ventana de Selector de Orígenes de Datos se puede observar la biblioteca que se quiere usar como fuente, en este caso Videos.

  • En este momento se puede seleccionar la biblioteca Videos y hacer clic en el botón Aceptar. En ese instante se debe activar la ventana Detalles del origen de datos, la cual despliega todos los campos disponibles en la biblioteca de Videos. En este caso vamos a desplegar el Título y el Thumbnail (Vista previa de la dirección URL de la imagen) asociados a cada ítem dentro de la biblioteca. Seleccionando los 2 campos al tiempo se puede proceder a arrastrar y soltar dichos campos en la WebPart agregada previamente.

Fields

  • Hasta este momento contamos ya con una DataFormWebPart conectada a una biblioteca llamada Videos, como fuente de datos. Ahora si se explora el código generado en SharePoint Designer, se aprecia que se tiene el tag <XSL>, lo cual indica que esta WebPart viene ya lista para ser modificada a través de dichos elementos.

DataFormWP

Se puede observar que ya se han agregado los campos Título y el Thumbnail como un elemento IMAGE. Si se hace clic sobre los campos se puede apreciar que son totalmente modificables como elementos HTML.

<td class="ms-vb">
<xsl:value-of select="@Title"/>
</td>
<td class="ms-vb">
<img border="0" src="{@AlternateThumbnailUrl}"

alt="{@AlternateThumbnailUrl.desc}"/>
</td>

Es momento entonces de realizar los cambios necesarios para que los elementos queden mejor organizados. Para esto vamos a requerir de una hoja de estilos personalizada con las siguientes clases:

.comun { font-size: 12px; line-height: 16px; margin: 0 0 13px; }
.comun h3,
.comun h4 { font-family: "Myriad Pro", sans-serif; text-align: left; }
.comun h3 a,
.comun h4 a { color: #00a2ec; font-size: 11px; font-weight: normal; }
.comun h3 { color: #003b71; font-size: 21px; line-height: 23px; padding: 10px 13px 7px; }
.comun h4 { color: #003b71; font-size: 15px;}
.comun h3 small,
.comun h4 small { margin: 0 0 0 6px; }
.comun small { color: #232323; font-size: 11px; font-style: italic; }
.comun a { color: #00a2ec; text-decoration: none; }
.comun a:hover { text-decoration: underline; }
.comun a small { color: #00a2ec; }
.comun p { margin: 0 0 7px; }
.comun ul { color: #0053a1; list-style-type: disc; margin: 0 0 10px 15px; }
.comun li { margin: 0 0 7px; }
.lateral { background: #fff url("lateral.gif") repeat-x left bottom; border: 1px solid #cbe3ab; width: 255px; }
.lateral h4 { background: #cbe3ab; border: 1px solid #e6e6e6; padding: 7px 13px; }

.contenido { padding: 10px 13px; }

.fix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
* html .fix { zoom: 1; } /* IE6 */
*:first-child+html .fix { zoom: 1; } /* IE7 */

.ver_mas { border-top: 1px dotted #b3b4b4; margin: 5px 0 0; padding: 7px 0 0; text-align: right; }


#video {}
#video ul { list-style-type: none; margin: 0 0 10px; }
#video li { color: #4f4e4e; }
#video img { border: 1px #2664a9 solid; float: left; margin: 0 7px 0 0; width: 76px; }

En la ruta de Activos del sitio se puede crear una carpeta llamada CSS y ahí crear un archivo, por ejemplo, estilos.css, donde se debe copiar el código anterior.

Luego de eso en la página maestra del sitio, de puede hacer referencia a la hoja de estilos así:

<SharePoint:CssRegistration name="/SiteAssets/CSS/estilos.css" After="corev4.css" runat="server"/>

  • Teniendo una hoja de estilos lista, es momento de aplicar los estilos a la WebPart que ya habíamos agregado para desplegar los videos. Lo primero que se debe ubicar es la siguiente sección de código:

<xsl:template name="dvt_1.rowview">
<tr>
<xsl:if test="position() mod 2 = 1">
<xsl:attribute name="class">ms-alternating</xsl:attribute>
</xsl:if>
<xsl:if test="$dvt_1_automode = '1'" ddwrt:cf_ignore="1">
<td class="ms-vb" width="1%" nowrap="nowrap">
<span ddwrt:amkeyfield="ID" ddwrt:amkeyvalue="ddwrt:EscapeDelims(string(@ID))" ddwrt:ammode="view"></span>
</td>
</xsl:if>
<td class="ms-vb">
<xsl:value-of select="@Title"/>
</td>
<td class="ms-vb">
<img border="0" src="{@AlternateThumbnailUrl}" alt="{@AlternateThumbnailUrl.desc}"/>
</td>
</tr>
</xsl:template>

Este código debe entonces quedar así:

<xsl:template name="dvt_1.rowview">
<xsl:if test="$dvt_1_automode = '1'" ddwrt:cf_ignore="1">
<span ddwrt:amkeyfield="ID" ddwrt:amkeyvalue="ddwrt:EscapeDelims(string(@ID))" ddwrt:ammode="view"></span>
</xsl:if>

<li class="clearfix">
<a href="#">
<img border="0" src="{@AlternateThumbnailUrl}" alt="{@AlternateThumbnailUrl.desc}"/>
</a>
<p><xsl:value-of select="@Title"/></p>
</li>
</xsl:template>

  • Seguido entonces se debe ubicar la siguiente sección en el código:

<xsl:otherwise>
<table border="0" width="100%" cellpadding="2" cellspacing="0">
<tr valign="top">
<xsl:if test="$dvt_1_automode = '1'" ddwrt:cf_ignore="1">
<th class="ms-vh" width="1%" nowrap="nowrap"></th>
</xsl:if>
<th class="ms-vh" nowrap="nowrap">Título</th>
<th class="ms-vh" nowrap="nowrap">Vista previa de la dirección URL de la imagen</th>
</tr>
<xsl:call-template name="dvt_1.body">
<xsl:with-param name="Rows" select="$Rows"/>
<xsl:with-param name="FirstRow" select="1" />
<xsl:with-param name="LastRow" select="$LastRow - $FirstRow + 1" />
</xsl:call-template>
</table>
</xsl:otherwise>

Se debe reemplazar por lo siguiente:

<xsl:otherwise>
<div class="comun lateral" id="video">
<h4>Videos</h4>
<xsl:if test="$dvt_1_automode = '1'" ddwrt:cf_ignore="1">
<th class="ms-vh" width="1%" nowrap="nowrap"></th>
</xsl:if>

<xsl:call-template name="dvt_1.body">
<xsl:with-param name="Rows" select="$Rows"/>
<xsl:with-param name="FirstRow" select="1" />
<xsl:with-param name="LastRow" select="$LastRow - $FirstRow + 1" />
</xsl:call-template>



</div>
</xsl:otherwise>

  • Luego de los cambios anteriores, se debe tener lo siguiente en modo de diseño en SharePoint Designer.

estilos

Como se puede observar no se tiene una imagen asociada al video. Pero suponiendo que no se tiene una imagen cargada, se puede hacer lo siguiente para usar la imagen por defecto de los videos que SharePoint ofrece.

  • Seleccione en la página en modo de diseño en SharePoint Designer la imagen que debe verse así en código.

<a href="#">
<img border="0" src="{@AlternateThumbnailUrl}" alt="{@AlternateThumbnailUrl.desc}"/>
</a>

Se debe cambiar por lo siguiente:

<xsl:if test="@AlternateThumbnailUrl != ''">
<a href="#">
<img border="0" src="{@AlternateThumbnailUrl}" alt="{@AlternateThumbnailUrl.desc}"/>
</a>
</xsl:if>
<xsl:if test="@AlternateThumbnailUrl = ''">
<a href="#">
<img border="0" src="/_layouts/images/VideoPreview.png" alt="Video"/>
</a>
</xsl:if>

Se aprecia que cargaría una imagen por defecto en caso de que no se haya cargado un thumbnail asociado al video. En ese caso una imagen perteneciente al conjunto de imágenes de SharePoint 2010, llamada VideoPreview.png. Debe estarse viendo así la WebPart en el diseñador:

videos

  • Finalmente se guardan los cambios, y se puede visualizar el preview de la página XSLT.aspx a través del navegador Web, con lo que se tiene lo siguiente:

Preview

Por ahora esta entrada ha cumplido su objetivo, la tarea siguiente sería que podamos hacer clic en la imagen del video, pero que tenga asociada la URL al video y este se despliegue, por ejemplo en la misma página o en una nueva página. Para tener una idea pueden remitirse a una entrada donde se habla de algo similar, para tener más ideas.

Feliz WebPart XSLT rediseñada.

Friday, November 25, 2011

Aprobar una Tarea Programáticamente

Hola de nuevo. Este es un tema que varios han comentado bastante en Foros, Blogs, y bueno cada medio que permita dar a entender la problemática.
En resumen lo que necesitábamos en nuestro caso era poder Aprobar una Tarea asociada a un WorkFlow diseñado con SharePoint Designer, pero que al tiempo el WorkFlow vaya avanzando.
Uno de los inconvenientes que los usuarios finales encuentran con los WorkFlows de SharePoint Designer es tener que completar las tareas asignadas, pero de paso también tener que aprobar el ítem de la lista que tiene asociado el WorkFlow.
Para evitar esto, podemos crear una WebPart que nos permita tanto aprobar (realizar la tarea y cambiar el estado del ítem), como puede ser rechazar o finalmente cancelar en caso de no querer ejecutar ninguna acción en el momento.
Supongamos entonces que tenemos un WorkFlow bastante simple, con 3 pasos, durante los cuales un primer paso es asignar una tarea a un Primer Aprobador. Luego en el siguiente paso el primer aprobador requiere Revisar y Aprobar un ítem de una lista o quizá un documento en una biblioteca, y quiere poder hacer todo desde un único punto, y no tener que vérselas con la opción de Realizar Tarea. En caso que el primer aprobador apruebe, entonces viene un tercer paso donde el segundo aprobador da por terminado el proceso Aprobando o Rechazando el ítem.
A continuación se puede apreciar el WorkFlow diseñado:
Task1
Cuando se crea un WorkFlow en SharePoint Designer, este a su vez crea una serie de formularios ASPX personalizados que se pueden encontrar en la siguiente localización, junto a los archivos de configuración y reglas del WorkFlow. Esas páginas ASPX se corresponden o sirven con mecanismo que permite la Realización de la Tarea, que se ha configurado como una Acción en algún paso del WorkFlow, por ejemplo en los pasos Revisar y Aprobar 1 del WorkFlow de este ejemplo.
Task2
En rojo se enmarcan las páginas ASPX respectivas de cada Acción de Tarea en los pasos del WorkFlow.
Pero llega el momento de crear la WebPart que permita entonces reemplazar la WebPart por defecto de cada página ASPX asociada a una Tarea. La WebPart por defecto lo único que permite realizar es la acción de Completar la Tarea, que es lo que queremos evitar, y que mejor eso se haga y también se modifique automáticamente el estado del WorkFlow.
La lista que se asociará con el WorkFlow deberá contener un campo llamado Estado de tipo elección y campo llamado Aprobador de tipo Texto. Los dos campos podrán estar ocultos en la lista porque son únicamente un medio de ejecución y control para la WebPart personalizada.
No voy a explicar cómo construir la WebPart ya se aen WSPBuilder para SharePoint 2007 o una WebPart visual en SharePoint 2010. Dejo el código a continuación. Este ejemplo asume que la WebPart utiliza un control ASCX. En el cual estarán las tres acciones de Aprobar, Rechazar y Cancelar, que son básicamente botones.
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.Utilities;
using System.Collections;
using System.Globalization;
namespace WSPBuilderCOA
{
    public partial class EditTask : System.Web.UI.UserControl
    {
        private string url;
        public string Url
        {
            set
            {
                url = value;
            }
            get
            {
                return url;
            }
        }
        protected void Page_Load(object sender, EventArgs e)
        {
            //this.lblMensaje = "";
        }
        protected void btnAprobar_Click(object sender, EventArgs e)
        {
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite(url))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        web.AllowUnsafeUpdates = true;
                        try
                        {
                            SPList task = web.Lists["Tareas"];
                            SPListItem item = task.Items.GetItemById(Convert.ToInt32(Request.Params["ID"]));
                            if (item["WorkflowListId"] != null)
                            {
                                Guid sourceListID = new Guid(item["WorkflowListId"].ToString());
                                SPList sourceList = web.Lists.GetList(sourceListID, true);
                                int sourceListItemID = Convert.ToInt32(item["WorkflowItemId"]);
                                SPListItem sourceListItem = sourceList.GetItemById(sourceListItemID);
                                //El primer aprobador realiza la tarea y cambia el estado del ítem a Aprobado en Primera Instancia
                                if (sourceListItem["Aprobador"].ToString() == "Aprobador 1" && Convert.ToString(sourceListItem["Estado"]) == "En Revisión")
                                {
                                    sourceListItem["Estado"] = "Aprobado en Primera Instancia";
                                    sourceListItem["Aprobador"] = "Aprobador 1";
                                    sourceListItem.Update();
                                    //Get the workflow instance id from Task item
                                    Guid taskWorkflowInstanceID = new Guid(item["WorkflowInstanceID"].ToString());
                                    SPWorkflow workflow = item.Workflows[taskWorkflowInstanceID];
                                    SPWorkflowTask wfTask = workflow.Tasks[item.UniqueId];
                                    Hashtable ht = new Hashtable();
                                    ht[SPBuiltInFieldId.Completed] = "TRUE";
                                    ht["Completed"] = "TRUE";
                                    ht[SPBuiltInFieldId.PercentComplete] = 1.0f;
                                    ht["PercentComplete"] = 1.0f;
                                    ht["Status"] = "Completed";
                                    ht[SPBuiltInFieldId.TaskStatus] = SPResource.GetString(new CultureInfo((int)wfTask.Web.Language, false), Strings.WorkflowStatusInProgress, new object[0]);
                                    ht[SPBuiltInFieldId.WorkflowOutcome] = "Approved";
                                    ht["TaskStatus"] = "Approved";
                                    ht["FormData"] = SPWorkflowStatus.InProgress;

                                    SPWorkflowTask.AlterTask((wfTask as SPListItem), ht, true);
                                    Response.Redirect(Request.Params["Source"]);
                                }
                                else
                                {
                                    if (sourceListItem["Aprobador"].ToString() == "Aprobador 1" && Convert.ToString(sourceListItem["Estado"]) == "Aprobado en Primera Instancia")
                                    {
                                        sourceListItem["Estado"] = "Aprobado en Segunda Instancia";
                                        sourceListItem["Aprobador"] = "Aprobador 2";
                                        sourceListItem.Update();
                                        //Get the workflow instance id from Task item
                                        Guid taskWorkflowInstanceID = new Guid(item["WorkflowInstanceID"].ToString());
                                        SPWorkflow workflow = item.Workflows[taskWorkflowInstanceID];
                                        SPWorkflowTask wfTask = workflow.Tasks[item.UniqueId];
                                        Hashtable ht = new Hashtable();
                                        ht[SPBuiltInFieldId.Completed] = "TRUE";
                                        ht["Completed"] = "TRUE";
                                        ht[SPBuiltInFieldId.PercentComplete] = 1.0f;
                                        ht["PercentComplete"] = 1.0f;
                                        ht["Status"] = "Completed";
                                        ht[SPBuiltInFieldId.TaskStatus] = SPResource.GetString(new CultureInfo((int)wfTask.Web.Language, false), Strings.WorkflowStatusInProgress, new object[0]);
                                        ht[SPBuiltInFieldId.WorkflowOutcome] = "Approved";
                                        ht["TaskStatus"] = "Approved";
                                        ht["FormData"] = SPWorkflowStatus.InProgress;
                                        SPWorkflowTask.AlterTask((wfTask as SPListItem), ht, true);
                                        Response.Redirect(Request.Params["Source"]);
                                    }
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            //lblMensaje.Text = "Ha ocurrido un error aprobando: " + ex.Message + " - " + ex.Source;
                        }
                        finally
                        {
                            web.AllowUnsafeUpdates = false;
                        }
                    }
                }
            });
        }

        protected void btnRechazar_Click(object sender, EventArgs e)
        {
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite(url))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        web.AllowUnsafeUpdates = true;
                        try
                        {
                            SPList task = web.Lists["Tareas"];
                            SPListItem item = task.Items.GetItemById(Convert.ToInt32(Request.Params["ID"]));
                            if (item["WorkflowListId"] != null)
                            {
                                Guid sourceListID = new Guid(item["WorkflowListId"].ToString());
                                SPList sourceList = web.Lists.GetList(sourceListID, true);
                                int sourceListItemID = Convert.ToInt32(item["WorkflowItemId"]);
                                SPListItem sourceListItem = sourceList.GetItemById(sourceListItemID);
                                //El primer aprobador realiza la tarea y cambia el estado del ítem a Aprobado en Primera Instancia
                                if (sourceListItem["Aprobador"].ToString() == "Aprobador 1" && Convert.ToString(sourceListItem["Estado"]) == "En Revisión")
                                {
                                    sourceListItem["Estado"] = "Rechazado";
                                    sourceListItem["Aprobador"] = "Aprobador 1";
                                    sourceListItem.Update();
                                    //Get the workflow instance id from Task item
                                    Guid taskWorkflowInstanceID = new Guid(item["WorkflowInstanceID"].ToString());
                                    SPWorkflow workflow = item.Workflows[taskWorkflowInstanceID];
                                    SPWorkflowTask wfTask = workflow.Tasks[item.UniqueId];
                                    Hashtable ht = new Hashtable();
                                    ht[SPBuiltInFieldId.Completed] = "TRUE";
                                    ht["Completed"] = "TRUE";
                                    ht[SPBuiltInFieldId.PercentComplete] = 1.0f;
                                    ht["PercentComplete"] = 1.0f;
                                    ht["Status"] = "Completed";
                                    ht[SPBuiltInFieldId.TaskStatus] = SPResource.GetString(new CultureInfo((int)wfTask.Web.Language, false), Strings.WorkflowStatusInProgress, new object[0]);
                                    ht[SPBuiltInFieldId.WorkflowOutcome] = "Approved";
                                    ht["TaskStatus"] = "Approved";
                                    ht["FormData"] = SPWorkflowStatus.InProgress;
                                    SPWorkflowTask.AlterTask((wfTask as SPListItem), ht, true);
                                    Response.Redirect(Request.Params["Source"]);
                                   
                                }
                                else
                                {
                                    if (sourceListItem["Aprobador"].ToString() == "Aprobador 1" && Convert.ToString(sourceListItem["Estado"]) == "Aprobado en Primera Instancia")
                                    {
                                        sourceListItem["Estado"] = "Rechazado";
                                        sourceListItem["Aprobador"] = "Aprobador 2";
                                        sourceListItem.Update();
                                        //Get the workflow instance id from Task item
                                        Guid taskWorkflowInstanceID = new Guid(item["WorkflowInstanceID"].ToString());
                                        SPWorkflow workflow = item.Workflows[taskWorkflowInstanceID];
                                        SPWorkflowTask wfTask = workflow.Tasks[item.UniqueId];
                                        Hashtable ht = new Hashtable();
                                        ht[SPBuiltInFieldId.Completed] = "TRUE";
                                        ht["Completed"] = "TRUE";
                                        ht[SPBuiltInFieldId.PercentComplete] = 1.0f;
                                        ht["PercentComplete"] = 1.0f;
                                        ht["Status"] = "Completed";
                                        ht[SPBuiltInFieldId.TaskStatus] = SPResource.GetString(new CultureInfo((int)wfTask.Web.Language, false), Strings.WorkflowStatusInProgress, new object[0]);
                                        ht[SPBuiltInFieldId.WorkflowOutcome] = "Approved";
                                        ht["TaskStatus"] = "Approved";
                                        ht["FormData"] = SPWorkflowStatus.InProgress;
                                        SPWorkflowTask.AlterTask((wfTask as SPListItem), ht, true);
                                        Response.Redirect(Request.Params["Source"]);
                                    }
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            //lblMensaje.Text = "Ha ocurrido un error aprobando: " + ex.Message + " - " + ex.Source;
                        }
                        finally
                        {
                            web.AllowUnsafeUpdates = false;
                        }
                    }
                }
            });
        }
    }
}
El código es únicamente como demostración, no hemos revisado ninguna buena práctica o que quede mejor ordenado y estructurado porque no es el objetivo de esta entrada. Lo realmente interesante de todo eso, es la parte donde se ejecutan las acciones que permiten Aprobar y Terminar cada tarea asociada, pero que adicionalmente cambian el campo Estado de la lista y que hace que el WorkFlow pueda ir avanzando acorde a la lógica diseñada en SharePoint Designer.
En la siguiente imagen se aprecian la WebPart personalizada y la que ofrece SharePoint por defecto para cada Tarea. El ejemplo muestra lo que vería el usuario Aprobador 2 cuando va a Editar su Tarea asignada.
Task3
Feliz Tarea Programática.

Tuesday, November 01, 2011

Video con SharePoint 2010


Hola de nuevo. Lo primero comentar sobre una gran falencia de nuestra querida plataforma, y es el no poder embeber con facilidad vídeo dentro de campos de tipo Rich HTML. Una gran falencia porque debería ser algo natural de hacer, y no se puede. No voy a entrar en detalles, pero por ejemplo les dejo una muestra, de todo lo que toca hacer para embeber video en una entrada de Blog de SharePoint 2010.

http://blogs.msdn.com/b/sharepointdesigner/archive/2009/12/11/video-blogging-with-javascript-and-the-media-web-part.aspx

Así sucesivamente si se navega por internet buscando soluciones, ninguna es simple, y siempre tocará hacer unos cuantos pasos.

Ahora bien, más allá de eso esta entrada quiere mostrarles cómo usar la WebPart de Media que SharePoint Server 2010 nos ofrece como mecanismo para presentar vídeo y audio. Esta WebPart aparentemente tiene una limitación, y es que para el usuario final, solo le permitirá establecer "rígidamente" el vínculo a un vídeo o archivo de audio, sin permitir ningún tipo de interacción dinámica. Recuerden siempre el cliente quiere una presentación de vídeo tipo YouTube, no un único vídeo preestablecido.

Lo comentado al inicio de esta entrada va directamente relacionado con esto, porque lo primero que los clientes quieren es, por ejemplo, embeber vídeo en cuerpos de Noticias, en entradas de Blog, y en general tener canales de vídeo, tipo YouTube.

Entonces, luego de tanta palabra, pongamos manos a la obra:

  • Desarrollando la lógica de negocio: El método a continuación que se explica, puede meterse en un proyecto de biblioteca de clases.

El siguiente método tiene como objetivo conectarse a una biblioteca de Media y desplegar el listado de Vídeos, que se filtran a través de una vista de dicha biblioteca.

La vista es una excelente técnica cuando por ejemplo se quiere limitar a una cantidad de ítems, ordenarlos, agruparlos, y en general poder tener los ítems listos para ser consumidos a través de código .NET.

Lo primero que tenemos a continuación es la rutina en C#, que a través del modelo de objetos administrado retorna la lista genérica con los ítems consultados de la vista de esa biblioteca de media de SharePoint.

public IList<TGVideo> GetVideos(string listName, string viewName)
{
IList<TGVideo> videos = new List<TGVideo>();
TGVideo video = null;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (SPSite site = new SPSite(SPContext.Current.Site.Url))
{
using (SPWeb web = site.OpenWeb())
{
SPList list = web.Lists[listName];
if (list != null)
{
try
{
SPView view = list.Views[viewName];
SPListItemCollection items = list.GetItems(view);
foreach (SPListItem item in items)
{
video = new TGVideo();
video.Title = item.Name.Split('.')[0];
video.UrlVideo = "/paginas/videos.aspx?video=" + item.Name;
if (item["AlternateThumbnailUrl"] != null)
video.Thumbnail = ((SPFieldUrlValue)item["AlternateThumbnailUrl"]).Url;
else
video.Thumbnail = "/_layouts/images/VideoPreview.png";
videos.Add(video);
}
}
catch (Exception ex)
{
EventLog log = new EventLog();
if (!(log.Source == "Errores WebPart"))
log.Source = "Errores WebPart";
log.WriteEntry("Error en el Método GetVideos: " + ex.Message + "- InnerException: " + ex.InnerException + "- Source: " + ex.Source + "- StackTrace: " + ex.StackTrace,
EventLogEntryType.Error, 1);
throw;
}
}
}
}
});
return videos;
}

Lo primero que se aprecia es que el método retorna una lista genérica de elementos de tipo TGVideo. Dicha clase luce así:

public class TGVideo
{
private string title;
public string Title
{
get
{
return title;
}
set
{
title = value;
}
}
private string thumbnail;
public string Thumbnail
{
get
{
return thumbnail;
}
set
{
thumbnail = value;
}
}
private string urlVideo;
public string UrlVideo
{
get
{
return urlVideo;
}
set
{
urlVideo = value;
}
}
}

El método recibe como parámetros el nombre de la lista e incluso el nombre de la vista que finalmente servirá para consultar los ítems.

Recordemos que una sitio de SharePoint hay usuarios que se loguean y tendrán privilegios de administración, pero otros de solo lectura. Esos usuarios de solo lectura son muy limitados, y si eso no se tiene en cuenta, las WebParts que construimos pueden fallar. Por tal razón es que toda la parte esencial del método se envuelve dentro de la rutina SPSecurity.RunWithElevatedPrivileges, que en términos generales permite ejecutar todo el código que encierra con permisos elevados.

La manera de instanciar objetos SPSite y SPWeb, es la mejor práctica usar USING. En este caso no se aprovecha obtener los objetos del contexto de SharePoint con la clase SPContext, porque SPSecurity.RunWithElevatedPrivileges no funciona así, por tal motivo es que esas instancias se obtienen de ese modo, y por lo tanto se hace DISPOSE formal con la clausula USING.

Luego se puede apreciar como se obtiene la lista a través de su nombre. Muy importante recordar que no es una buena práctica cuando se tiene muchas listas. Lo mejor será instanciar la lista a través de su ID para aumentar el rendimiento. Luego se obtienen los ítems desde la vista y a partir de ahí se itera para recuperar la información deseada a través de la clase de entidad TGVideo.

La página Video.aspx es la que servirá como contenedor o visor de los videos, más adelante se explica su funcionalidad.

Luego si quiere obtener el Thumbnail asociado al video se usa el campo con su nombre interno AlternateThumbnailUrl. En caso que no se haya establecido la ruta una imagen que sirva como thumbnail se usa la imagen por defecto de SharePoint, para tal caso se encuentra en la ruta: /_layouts/images/VideoPreview.png

Finalmente como manejo de errores se escribe una rutina que permita registrar los sucesos en el visor de eventos de Windows, nombrando la entrada como Errores WebPart.

  • Desarrollando la WebPart Visual: Las WebParts visuales son un concepto y técnica esperado desde hace ya bastante tiempo, al menos desde que desarrollamos para SharePoint 2007. En esencia es una separación entre lógica de WebPart (eventos) y presentación (HTML y Codebehind), a través de una clase que hereda de la clase WebPart y otra que es básicamente un control de usuario ASCX. Muy importante recordar que cuando vayan a crear el proyecto de tipo SharePoint 2010 en VS.NET 2010, deben configurarlo como un deploy de tipo FARM. Las WebParts visuales no pueden ser desplegadas como tipo SandBox. En este caso se asume que se va a agregar al proyecto de tipo SharePoint 2010 un nuevo ítem de tipo WebPart visual llamado VideosPart.

En el HTML del control ASCX tendremos lo siguiente:

<div>
<asp:GridView ID="grvVideos" CellSpacing="10" CellPadding="5" runat="server" GridLines="None" ShowHeader="false" AutoGenerateColumns="false">
    <Columns>
        <asp:TemplateField>
            <ItemTemplate>
                <a href='<%#Eval("UrlVideo")%>'>
                    <asp:Image ID="imgThumbnail" ImageUrl='<%#Eval("Thumbnail")%>' runat="server" Width="120" Height="90" BorderWidth="0" />
                </a>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField>
            <ItemTemplate>
                <asp:Label ID="lblTitle" runat="server" Text='<%#Eval("Title")%>' />
                <br />
                <a href='<%#Eval("UrlVideo")%>'>Ver Video</a>
               
            </ItemTemplate>
        </asp:TemplateField>
    </Columns>
</asp:GridView>
</div>
<div>
    <asp:Label ID="lblMessage" runat="server" />
</div>

En esencia cada Eval() hace llamado a las propiedades definidas en la clase de entidad TGVideo.

Ahora bien, el codebehind del control ASCX deberá tener lo siguiente:

private TGServices services = null;

        private string listName;
        public string ListName
        {
            get { return listName; }
            set { listName = value; }
        }

        private string viewName;
        public string ViewName
        {
            get { return viewName; }
            set { viewName = value; }
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            this.lblMessage.Text = "";
            if (!Page.IsPostBack)
            {
                services = new TGServices();
                IList<TGVideo> videos = null;
                try
                {
                    videos = services.GetVideos(listName, viewName);
                    grvVideos.DataSource = videos;
                    grvVideos.DataBind();
                }
                catch (Exception ex)
                {
                    this.lblMessage.Text = "Se ha presentado un error cargando la información: " + ex.Message;
                }
                if (grvVideos.Rows.Count == 0 || videos == null)
                    this.lblMessage.Text = "Actualmente no hay información para desplegar en este elemento.";
            }
        }

Si se fijan es muy poco código, porque toda la complejidad se delegó en la capa de negocio. Lo primero es una instancia de la clase que contendrá el método que se explicó de la lógica de negocio. En este ejemplos e asume que dicha clase se llama TGServices. Luego se definen 2 propiedades que servirán como puente de comunicación entre la clase WebPart y el con trol ASCX. Para este caso la WebPart proporcionará como propiedades configurables el nombre de la lista y de la vista. Y Finalmente se invoca el método que retorna los videos en forma de lista genérica que se pasa como fuente de datos al GridView.

Finalmente así lucirá el código de la clase WebPart:

[Personalizable(), WebBrowsable, Category("Configuración"), WebDisplayName("Nombre de la Lista")]
        public String ListName { get; set; }

        [Personalizable(), WebBrowsable, Category("Configuración"), WebDisplayName("Nombre de la Vista")]
        public String ViewName { get; set; }

        // Visual Studio might automatically update this path when you change the Visual Web Part project item.
        private const string _ascxPath = @"~/_CONTROLTEMPLATES/Familia.WebParts/VideosPart/VideosPartUserControl.ascx";

        protected override void CreateChildControls()
        {
            Control control = Page.LoadControl(_ascxPath);
            ((VideosUserControl)control).ListName = ListName;
            ((VideosUserControl)control).ViewName = ViewName;
            Controls.Add(control);
        }

Se aprecia que las propiedades definidas en el code behind del control de usuario son usadas para pasar dichos valores desde la clase de tipo WebPart.

Hasta este punto tenemos ya lista la WebPart visual que consume y despliega un listado de Videos. Pero como ya se explicó, la URL configurada al hacer clic sobre el thumbnail o la opción Ver Video de la WebPart, lleva a una página llamada Videos.aspx. Como se pudo observar es una página que se ha creado en una biblioteca llamada Páginas de SharePoint. La página Videos.aspx se ha creado como una Página con WebParts. En esa página se deberá agregar un DIV de esta forma, por ejemplo en el Place Holder Main:

<div id="divMediaWebpart">
</div>

El único objetivo del DIV anterior es ser el contenedor de lo que será la WebPart de Media de SharePoint 2010, agregada dinámicamente.

Luego si se quiere agregando una WebPart de Editor de contenido, o directamente en el HTML de la página a través de SharePoint Designer 2010, agregar el siguiente JavaScript:

<script type="text/javascript">
   
        $(document).ready(function() {
            var videoHolder = document.getElementById('divMediaWebpart');
            mediaPlayer.createMediaPlayer(
            videoHolder, videoHolder.id, '640px', '390px',
            {
                 displayMode: 'Inline',
                 mediaTitle: '',
                 mediaSource: '/Videos/' + getQuerystring('video',''),
                 previewImageSource:'/_layouts/images/VideoPreview.png',
                 autoPlay: true,
                 loop: false,
                 mediaFileExtensions:'wmv;wma;avi;mpg;mp3;',
                 silverlightMediaExtensions:'wmv;wma;mp3;'
             }
         );

           
        });
 
    </script>

Muy importante a tener en cuenta los siguiente:

  1. El JavaScript anterior solo funciona agregando la siguiente referencia, bien sea directamente en la página o en la página maestra del sitio: <script type="text/javascript" src="/_layouts/mediaplayer.js"></script>
  2. El JavaScript usa una función que permite leer valores pasados por QueryString cuya implementación es similar a esto:

    function getQuerystring(key, default_)
    {
       if (default_==null) default_="";
          key = key.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
       var regex = new RegExp("[\\?&]"+key+"=([^&#]*)");
       var qs = regex.exec(window.location.href);
       if(qs == null)
         return default_;
       else
           return qs[1];
    }

    La función anterior en el JavScript de la página Videos.aspx estaría esperando un nombre de parámetro llamado video, que en esencia trae el nombre del video junto a su extensión. Dichos Videos deberán alojarse en una biblioteca de Media llamada Videos.

  3. Debido a que se hace uso de algunas opciones de JQuery, es también obligatorio adicionar una referencia a la librería respectiva, por ejemplo en la página maestra lo siguiente:

    <script type="text/javascript" src="/SiteAssets/JS/jquery-1.4.3.min.js"></script>

Con lo anterior se puede disfrutar de una página que despliega Video dinámicamente así:

WebPart desplegando el listado de Videos:

VideoPart

Luego al hacer clic en Ver Video o en el thumbnail se hace un redirect a la página ya explicada Videos.aspx:

Video

Feliz video player!!

Wednesday, September 21, 2011

SharePoint 2010 Web Content Management

Este video merece la pena ser divulgado, excelente presentación del tema.



Feliz wcm!!