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!!

AddContentDB Access Denied Error

Hola de nuevo. Se me han ido creo que aproximadamente entre probando, leyendo blogs, tomando café, y bueno, unas cuantas cosas más, unas 12 a 14 hrs de mí tiempo, tratando de hacer lo siguiente:

1. Mi cliente quiere renombrar la base de datos de contenido de la administración central: Por ejemplo de esto, SharePoint_AdminContent_66877b93-3d3a-4dd1-b90f-399670de5e7b a esto SharePoint_AdminContent, y tiene razón, ese nombre con ese GUID da escalofrios.

2. Es de un cuidado extremo esta tarea, porque entre los pasos, se debe desasociar la BDs de Contenido de nada menos y nada más que la Administración Central. Por eso hagan Backup!!

3. Pero entre tanto blog, entre tanto Foro, puedo ver de regreso a gente descuidada, que finalmente ayuda a medias.

4. Lo que ocurre es que por algún motivo al ejecutar el comando del STSADM, por ejemplo:

C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\BIN>stsa
dm -o addcontentdb -url http://centraladminsite:puerto -databasename SharePoint_AdminContent

De donde lo que se evidencia es la intención de volver a asociar la BDs de contenido nueva con el sitio de Administración Central, produce un error de Acceso negado.

El blog con los pasos pero incompleto es este: http://guru-web.blogspot.com/2007/03/renaming-central-admin-content-database.html

El foro que me motivó a la solución es este: http://www.office-outlook.com/outlook-forum/index.php/m/672431/

Pero aun se quedan incompletos, porque seguirá dando error de Acceso Negado hasta que no se detenga el servicio de Windows llamado Windows SharePoint Services Timer y se vuelva a iniciar.

Al final la administración central deberá funcionar y quedar asociada a una BDs de contenido re-bautizada, digamos con un nombre más elegante.

Felíz re-bautizo

Wednesday, September 14, 2011

SharePoint answers

Hola de nuevo. De casualidad entré a un enlace de Twitter y llegué a un sitio bastante interesante de SharePoint. Es una especie de Foro on steroids, y navegando dí con una pregunta, la cual en unos 10 minutos, lei, probé en SharePoint 2010 y pude entregarle una respuesta al autor. De paso revisen el JavaScriopt de ese genio porque está bastante bueno, es algo así como para detectar si tenes Silverligth instalado.

Question

Felíz answer!!

Monday, September 12, 2011

SharePoint Microsoft Community Specialist

Hace varios años que vengo colaborando y ayudando en muchos temas a la comunidad Microsoft. Ahora me llegan a la mente INETA, Celulas Microsoft, Influencers, y bueno, charlas y particpación en eventos, donde llama la atención el interés de Microsoft por promover sus tecnologías, pero tratando de hacerlo bien, y llegando de la mejor manera a su amplia comunidad de desarrolladores.

Ahora me llega un nuevo reto, que Microsoft lo ha llamado "Especialista de comunidad", en mí caso, por un interés grande en SharePoint, y que espero a través de estos canales de Web 2.0 seguir aportando con algo para que exista información y soluciones puntuales a problemáticas, nuevas ideas, y en general tela de donde cortar sobre la plataforma SharePoint.

Gracias a Microsoft por este nuevo reconocimiento y al buen amigo Walter Novoa.

Friday, September 09, 2011

WebPart ASCX en SharePoint 2007 con WSP Builder

Hola de nuevo. A continuación les comparto una de mis demostraciones de los pasos básicos a seguir cuando queremos crear una WebPart con un control de usuario ASCX para SharePoint 2007.

Esto tiene ventajas obvias sobre la creación de WebParts que requieren la implementación manual y agregación de cada control a renderizar y por supuesto programar a nivel de eventos en la WebPart.

El ejemplo hace uso de Visul Studio .NET 2008, WSPBuilder y MOSS 2007. Esto también podría hacerse para WSS 3.0 sin ningún problema. WSPBuilder es la herramienta recomendada para las tareas de desarrollo sobre SP 2007.


Thursday, September 08, 2011

SharePoint en el lado del cliente - JQuery

Suelo hablar poco de las tecnologías y menos tratar de filosofar, prefiero remitirlos a los "gurus" y por supuesto a algún buen libro.

Lo real es que SharePoint 2010 cuenta con capacidad directa para soportar cosas bien interesantes, entre las que podemos citar el uso de JQuery, REST, JSON, modelo de objetos del lado del cliente, entre otros aspectos interesantes que nos estan beneficiando directamente a los desarrolladores.

A continuación les dejo un sencillo ejemplo que tiene por objetivo leer información de una lista de tipo calendario y presentar las fechas de cumpleaños y festivos qe ahi se han definido, haciendo uso de un poco de JQuery.

< script type="text/javascript" >
$(document).ready(function()
{
$.getJSON("http://urlsitosharepoint/_vti_bin/ListData.svc/Calendario",function(data) {
var count = 0;
var fechaActual=new Date();
var diaActual = fechaActual.getUTCDate()-1;
var mesActual = fechaActual.getMonth()+1;
var anioActual = fechaActual.getFullYear();
var eventos = "false";
$('#resultarea').text("");

$.each(data.d.results, function(i,result) {

var tmpInicio = eval(result.HoraDeInicio.replace(/\/Date\((\d+)\)\//gi, "new Date($1)"));

var dia = tmpInicio.getUTCDate()-1;
var mes = tmpInicio.getMonth()+1;
var anio = tmpInicio.getFullYear();

if(diaActual==dia && mesActual == mes && anioActual==anio)
{

html = "
Hoy " + result.Título +"
";
$('#resultarea').append($(html));
eventos="true";
}

});

if(eventos == "false")
{
$('#resultarea').text("Sin eventos el día de hoy");
}


});

});< /script >< div id="resultarea" >< / div >
< div >< a href="/Lists/Calendario/calendar.aspx">Ver todos < / a >

Lo primero que se debe notar es la llamada a ListData.svc, ahi prácticamente está lo interesante que ofrece SP 2010. Para no decir más revisen aquí.

Luego JQuery llega al rescate, como ha venido haciéndolo desde hace un tiempo acá, y bueno nos provee el mecanismo getJSON para comunicarnos con el servidor así.

El resto amigos creo que es muy conocido por todos, uso de JavaScript común y corriente. Interesante de todo eso la linea var tmpInicio = eval(result.HoraDeInicio.replace(/\/Date\((\d+)\)\//gi, "new Date($1)"));
donde logramos tomar la columna HoraDeInicio de la lista Calendario, y bueno la muy obvia Título con el objeto pertinente del recorrido del ciclo each: result.Título.

Es evidente que esto tiene grandes ventajas, cero deploy en el servidor, cero interacción con los cara dura de IT, rapida manipulación de data sin tanto problema: solo un poco de JQUERY, JavaScript y típico HTML.

Feliz client programming.

Friday, August 05, 2011

SharePoint y OBAs Part II

De regreso y avanzando con esta serie de entradas relacionadas a soluciones de SharePoint que involucran una WebPart e interacción con un cliente Office como Excel, voy a seguir dejándoles algunos ejemplos de código que he usado para esto.

Ver Parte I

Qué se debe hacer cuando necesitamos obtener las vistas que pertenecen a una lista específica?

public IList < TGLista > GetViews(Guid idList)
{
SPWeb webList = this.GetWebByIdList(idList);
if (webList != null)
{
TGLista vista = null;
IList vistas = new List();
SPList listaTemp = null;
try
{
listaTemp = webList.Lists[idList];
foreach (SPView temp in listaTemp.Views)
{
vista = new TGLista();
vista.Id = temp.ID;
vista.Titulo = temp.Title;
vistas.Add(vista);
}
}
catch (SPException ex)
{
throw;
}
catch (Exception ex)
{
throw;
}
finally
{
if (webList != null)
webList.Dispose();
}

if (vistas != null)
{
//Hace un ordenamiento ASCENDENTE de la lista
var list = from temp in vistas
orderby temp.Titulo
select temp;
return list.ToList();
}
}
return null;
}

Nuevamente se aprecia el uso de una clase entidad para almacenar la información a pasar entre capas usando una colección genérica de dichos objetos.

Quizá lo que más rescato como buena práctica del método anterior es que si se fijan, el método recibe por parámetro el ID de la lista para la cual queremos obtener sus vistas. No el nombre de la lista (string) o su índice dentro de alguna colección, que puede ser un entero. No se recibe su GUID que es la técnica mejor considerada cuando uno quiere referirse a una lista de SharePoint. La razón que más me gusta y que resguarda las soluciones de problemas "tontos", es que aunque los usuarios finales cambien el nombre de la lista (título o caption), el ID interno se conserva, y eso ya evita que el código quede "débil". Si el usuario borra su lista, apague y vámonos, ahí no hay anda que hacer, pero bueno eso ya son situaciones que se corrigen con un buen entrenamiento y conocimiento de los usuarios de las soluciones.


Un poco de CAML, como buena práctica no le cae mal a una solución SharePoint 2007.

private SPListItem GetItemByTitle(string titulo)
{
SPWeb web = SPContext.Current.Site.RootWeb;
SPQuery query = new SPQuery();
query.Query = "" + titulo + "";
try
{
SPList list = web.Lists["Objetivos KPI"];
foreach (SPListItem item in list.GetItems(query))
{
return item;
}
}
catch (Exception ex)
{
throw;
}
return null;
}

Lo único más interesante a rescatar es el uso de la consulta CAML que lo único que hace es filtrar los ítems de la lista por un título dado. De aquí solamente quiero decirles que usen herramientas como CAML Builder

Y qué pasa cuando quiero guardar un archivo generado por la solución en una biblioteca de SharePoint?

public void SaveLibrary(string file,Stream stream,Guid idList)
{
SPFolder spFolder = null;
SPFile spFileTemp = null;
TGServices services = new TGServices();
SPListItem item = null;
try
{
SPSecurity.RunWithElevatedPrivileges(delegate()
{
using (SPWeb web = this.GetWebByIdList(idList))
{
web.AllowUnsafeUpdates = true;
spFolder = web.Folders["Plantillas Excel"];
spFileTemp = spFolder.Files.Add(file, stream, true);
spFolder.Update();
item = spFileTemp.Item;
item.UpdateOverwriteVersion();
web.AllowUnsafeUpdates = false;
}
});
}
catch (SPException ex)
{
throw;
}
catch (Exception ex)
{
throw;
}
finally
{
if (stream != null)
stream.Close();
}
}

Y para usar el método anterior podría tenerse algo similar:

FileInfo fileToLibrary = null;
try
{
fileToLibrary = new FileInfo(file);
link = services.SaveLibrary(file, fileToLibrary.OpenRead(), new Guid(IdLista));
}


Lo primero que el método recibe es el nombre de un archivo por ejemplo miarchivo.xlsx. Luego lo más importante recibe un Stream, en el ejemplo anterior se muestra cómo crear dicho stream y pasárselo al método en modo openRead(). Y finalmente pasar el ID de la lista, que quizá nos sirva como fuente para obtener el sitio de ejecución del proceso.

Muy importante la línea SPSecurity.RunWithElevatedPrivileges(delegate()
porque ejecuta el proceso de guardar a la biblioteca de documentos Plantillas Excel en un modo de privilegios elevados, así no tener problemas de acceso negado si el usuario ejecutando la solución no tiene suficientes permisos sobre la biblioteca.

Y siendo el resto de código algo entendible, lo realmente importante es dejar la seguridad de la ejecución como estaba web.AllowUnsafeUpdates = false;
Eso indica que solo ese tramo del código fue elevado, el resto de la ejecución continuará con los permisos del usuario que está ejecutando la solución.


Feliz coding!!

Thursday, August 04, 2011

Arquitectura SharePoint

Creo que para todos los que nos movemos en proyectos que involucran a la plataforma SharePoint, teniendo en mente proyectos de un tamaño mediano a grande. Es un hecho la necesidad de tener una arquitectura y una planificación en general de lo que va a ser la solución en términos de los requerimientos del negocio. Con SharePoint 2010, los elementos de arquitectura a considerar siguen siendo los mismos básicos de la plataforma empresarial 2007. Pero cuando uno revisa en detalle la nueva versión, comienza a encontrar elementos que han sido totalmente rediseñados, y es muy importante tener elementos de donde poder arrancar para poder establecer nuestras arquitecturas iniciales.

En el repositorio de Microsoft hay un gran número de documentos, artículos, gráficos, en fin, mucha información, que personalmente pienso debería haber sido organizada de otro modo, y ser un poco más descriptiva y práctica. Pero siendo eso lo que tenemos de primera mano para planear las primeras etapas de la arquitectura, no está demás arrancar por ahí. Lo que uno si nota es que al menos eso da una idea de los elementos primarios que uno debe considerar en una arquitectura de una solución SharePoint, por ejemplo: Planeación de sitios, Planeación de Seguridad, Planeación de Aplicaciones de Servicio (nuevo en SP 2010), y en general planeación de los elementos que van a impactar sobre las necesidades del negocio: gestión documental, gestión de contenido web, inteligencia de negocio, interacción social, entre otros.

Planning and Architecture

Específicamente cuando se va a realizar una planeación y arquitectura para un tema como Gestión Documental, en SP 2010 aparecen elementos muy importantes, que deberán tenerse muy en cuenta para implementar una verdadera solución de gestión documental con SharePoint. Aparecen aplicaciones de servicio como es la de Metadata Administrada. Esto habilita posibilidades que no existían en SP 2007, como por ejemplo poder tener verdaderas taxonomías de información. Almacenes de términos, sinónimos, centros de registros, tag sociales, conjuntos de documentos, IDs de documentos, HUBs de tipos de contenido, y muchos otros elementos, van atados a este nuevo servicio empresarial. Si realmente se requiere una solución de gestión documental lo suficientemente avanzada, un arquitecto mínimamente deberá entender todos los conceptos subyacentes y por supuesto tener idea cómo se implementan y usan en SP 2010.

Metadata Administrada

Otro elemento esencial en la planeación y arquitectura de una solución SharePoint es un Plan de Gobierno. Esto para muchas empresas llega a ser un tema nuevo, pero si la empresa es grande, tiene la plataforma SP y no tiene políticas y estándares para la misma implementados, antes de hacer nada debería iniciar a crear un Plan de Gobierno. Esto termina siendo un documento donde se consignan las "leyes" que absolutamente todo el mundo en la organización deberá respetar y cumplir.

Plan de Gobierno

Quiero hacer claridad en que no se debe desmeritar todo esto en soluciones más pequeñas, pero bueno, un arquitecto debe poner en la balanza, soluciones, presupuestos, tiempos, y en general los recursos de cada proyecto, para ver si ameritan contemplar todos estos elementos. Es lo ideal tener en cuenta todo esto, pero requiere tiempo y eso significa dinero.

Todo lo anterior es la globalidad de las cosas o elementos que las necesidades de negocio van a requerir de una solución SharePoint. Pero llegan a terrenos más personales, qué hay de la arquitectura de desarrollos .NET para SharePoint?

Lo primero que una Arquitecto SharePoint que tenga dentro de sus soluciones implementar elementos con la plataforma .NET para SharePoint debe conocer es lo que Microsoft ya ha preparado como una vista rápida de los elementos arquitectónicos que se deberán tener en cuenta para los desarrollos o personalizaciones sobre SharePoint 2010 específicamente, igual lo hay para las versiones anteriores.

Software development kit

SharePoint Guidance

Algo insólito, y pienso que no se hace en muchos casos es el uso de UML en proyectos .NET para SharePoint, al menos ningún libro, artículo, en general ningún "gurú" habla del tema. Más allá de los "monachos" UML, lo preocupante es la falta de propaganda de los patrones de diseño que una buena solución .NET para SharePoint mínimamente deberá implementar. Hablo de UML porque es nuestro lenguaje formal para dar a entender los patrones. De los cientos de libros de desarrollo que hay para SP 2010 y 2007, es ya aburridor y tentador para la hoguera, su insufrible repetición de lo mismo con distintas palabras de lo que todos ya sabemos: WebParts, receptores de eventos, listas, bibliotecas, en fin temas de desarrollo que realmente deberían subirse de categoría y mostrar soluciones más serias de ejemplo en estos relucientes libros de 500 a 1000 páginas.

Feliz Arquitectura

Wednesday, August 03, 2011

SharePoint 2007 y OBAs

Hola de nuevo. En dias pasados había estado desarrollando una solución en SharePoint Server 2007, exactamente una WebPart, que tenía como finalidad tomar datos de SharePoint (KPIs) y los exportaba a un template de Excel donde esos datos terminaban siendo tabulados, aplicados sobre formulas, y en otras páginas del archivo Excel gráficados. Quisiera compartir algunas rutinas de código comunes que se usaron, que quiza puedan ser de utilidad a la comunidad.

Anoto que los ejemplos son fragmentos de toda la solución, así que recibirán parámetros, tendrán nombres específicos, llamados quizá a otros métodos, que ya ustedes deberán modificar según su necesidad.

1. Lo primero que vale la pena aclarar es que para esta solución SharePoint se hace uso de WSPBuilder, se los recomiendo, y aquí pueden ver su uso.

2. Debido a que se utilizó OPEN XML SDK para todo lo que tenía que ver con las operaciones sobre Excel del lado del servidor, para finalmente producir un archivo en formato XLSX, les recomiendo mucho usar el SDK provisto por Microsoft, pero ante todo denle una mirada a esta herramienta (Open XML SDK Tool).

Es una herramienta bastante buena que les acepta un archivo Excel en formato XLSX, y finalmente les genera el código C# que permitiría producirlo. Es una ventaja porque uno puede hacer un archivo de prueba, tal como quiere generarlo, y ahorrarse mucho tiempo de desarrollo desde ceros. Toca ajustarlo a lo dinámico de la solución, pero eso ya es algo que evidentemente debe hacerse, pero la estructura general del desarrollo, estaría lista.

3. Ya en código, tú primer requerimiento puede ser obtener todas las listas genéricas (evitar las listas especiales de SharePoint) del site collection y de todos sus sitios hijos, para que con ese listado llenar un control en la capa de presentación que podría ser un ComboBox, un ListBox, etc

public IList < TGlista > GetListas()
{
SPWeb web = SPContext.Current.Site.RootWeb;
TGLista lista = null;
SPListCollection listasTemp = web.Lists;
IList listas = new List();
try
{
if (listasTemp != null)
{
foreach (SPList listaTemp in listasTemp)
{
if (listaTemp.BaseTemplate == SPListTemplateType.GenericList)
{
lista = new TGLista();
lista.Id = listaTemp.ID;
lista.Titulo = listaTemp.Title;
listas.Add(lista);
}
}
}

SPWebCollection webs = web.Webs;
if (webs != null)
{
foreach (SPWeb webTemp in webs)
{
try
{
listasTemp = webTemp.Lists;
foreach (SPList listaTemp in listasTemp)
{
if (listaTemp.BaseTemplate == SPListTemplateType.GenericList)
{
lista = new TGLista();
lista.Id = listaTemp.ID;
lista.Titulo = listaTemp.Title;
listas.Add(lista);
}
}
}
finally
{
if (webTemp != null)
webTemp.Dispose();
}
}
}

if (listas != null)
{
//Hace un ordenamiento ASCENDENTE de la lista
var list = from temp in listas
orderby temp.Titulo
select temp;
return list.ToList();
}
}
catch (Exception ex)
{
throw;
}
return null;
}
Lo primero que se observa es lo que retorna el método:IList < TGlista > . TGLista no es más que una clase entidad, que sirve como contenedor para generar un listado genérico de tipos TGLista, lo cual es ya una técnica de programación muy sencilla y fácil de pasar datos entre las diferentes capas de la solución .NET. El control de la capa de presentación aceptará el tipo IList sin ningún problema.

Luego en la linea SPWeb web = SPContext.Current.Site.RootWeb; se obtiene un objeto de tipo SPWeb, pero usando la clase SPContext, lo cual es la técnica recomendada cuando se programa una WebPart que estará dentro del contexto del sitio de SharePoint. Super importante, no se hace DISPOSE de la instancia web.

Esta linea if (listaTemp.BaseTemplate == SPListTemplateType.GenericList) es la que evita que en el listado se obtengan las listas especiales de SharePoint.

Ahora bien, el requerimiento incluia las listas de los sitios hijo del SiteCollection. Esta linea obtiene todos los sitios hijo SPWebCollection webs = web.Webs; Ahora con esa colección por cada sitio hijo se obtienen su listas del mismo modo. Importante el DISPOSE que si se debe hacer de cada subsite.

finally
{
if (webTemp != null)
webTemp.Dispose();
}

La razón de lo anterior es que cada subsite obtenido es un nuevo SPWeb que en este caso si debe hacerse DISPOSE formal.

Finalmente un poco de LINQ no hace daño para ordenar la lista generica de elementos.
if (listas != null)
{
//Hace un ordenamiento ASCENDENTE de la lista
var list = from temp in listas
orderby temp.Titulo
select temp;
return list.ToList();
}

Por ahora esto es todo, luego seguiremos compartiendo más TIPs de código .NET para SharePoint.

Felíz Coding!!