Mostrando entradas con la etiqueta SharePoint 2010. Mostrar todas las entradas
Mostrando entradas con la etiqueta SharePoint 2010. Mostrar todas las entradas

jueves, 8 de agosto de 2013

Error configurando SQL Server Reporting Services 2012 en SharePoint 2010

Hola amigos, el día de hoy con un escenario que encontré hace unos días que intentaba configurar SSRS 2012 en SharePoint 2010.

El escenario es el siguiente: tenía una granja pequeña de SharePoint 2010 SP 2 con dos servidores y un servidor de base de datos con SQL Server 2012 SP 1.

Como parte de los requerimientos era necesario habilitar el servicio de Reporting Services en SharePoint 2010, por lo que procedí a descargar el Add-in de Reporting Services para SharePoint:

Microsoft® SQL Server® 2012 SP1 Reporting Services Add-in for Microsoft® SharePoint®

Sin embargo después de instalar el Add-in en los servidores de SharePoint y seguir el procedimiento standard de instalación de Microsoft, que consiste en la ejecución de los comandos de PowerShell:


Install-SPRSService
Install-SPRSServiceProxy
get-spserviceinstance -all |where {$_.TypeName -like "SQL Server Reporting*"} | Start-SPServiceInstance


Y posteriormente la creación del service application de Reporting Services en el sitio central de administración, noto que no me es posible crearlo, al revisar los servicios iniciados en el servidor me doy cuenta que el servicio SQL Server Reporting Services Service tuvo un error al iniciarse. Como buen consultor intento iniciarlo una vez más (porque todos pensamos que los problemas se resuelven haciendo las mismas cosas muchas veces?) y veo el siguiente error en pantalla:

Pero como siempre, este tipo de mensajes no son de mucha ayuda por lo que revise el visor de eventos de Windows para ver si existe alguna información extra que me ayude a corregir el error, pero solo encuentro el mismo mensaje:

Installation Error: Could not find SOFTWARE\Microsoft\Microsoft SQL Server\110 registry key Error with installing SQL Server Reporting Service Application

Después de un rato de investigación y de no encontrar nada, vuelvo a leer y revisar los procedimientos de instalación (oficiales y no oficiales) y me doy cuenta que algunos blogs hacen referencia a este Add-in, sin embargo otros hacen referencia, implícitamente, al uso de la media de SQL Server 2012, es decir del archivo ISO con que se instaló SQL Server 2012 y lo utilizan para instalar el Add-in de SSRS en los servidores de SharePoint.

Dado que en este punto no sabía que otra cosa intentar más que utilizar los bits de SQL Server 2012 SP 1 para configurar Reporting Services procedo a desinstalar el Add-in:


Y posteriormente utilizando los bits de SQL Server 2012 SP 1 con que se instaló el servidor de base de datos, ejecuto el instalador  en los servidores de SharePoint 2013 y selecciono solo los componentes necesarios de Reporting Services.


Acto seguido vuelvo a ejecutar los comandos de PowerShell y reviso si el servicio fue iniciado adecuadamente, y efectivamente todo funciona como debería.



Ya solo por curiosidad reviso la versión del Add-in instalado y note que si es diferente a la versión que había estado instalando previamente: 


Espero que este post les sirva y ahorre tiempo en el futuro.

Happy configuring!

martes, 23 de julio de 2013

SharePoint 2010 Service Pack 2

El dia de hoy se han hecho públicos los bits del service pack 2 de SharePoint 2010, por lo que a continuación les comparto un enlace de donde pueden descargarlos:

SharePoint Foundation 2010 SP2
SharePoint Server 2010 SP2

En este link pueden encontrar el listado de cambios que se incluyen en el service pack 2

Por su puesto tambien estan disponibles los service pack 2 para los respectivos language packs

Service Pack 2 for Microsoft SharePoint Foundation 2010 Language Pack
Service Pack 2 for Microsoft 2010 Server Language Pack

No olviden primero instalar y probar estos nuevos bits en ambientes de desarrollo y/o pre producción.



Happy updating!

viernes, 18 de enero de 2013

Pruebas unitarias con SharePoint Emulator Package

No sé si ustedes hayan notado el mes pasado (con todas las fiestas que había) que el equipo de Visual Studio anuncio el paquete de SharePoint Emulators de Visual Studio 2012 para SharePoint 2010.

De entrada este paquete solo aplica para SharePoint 2010 y utilizándolo en Visual Studio 2012. Es comprensible que este paquete solo aplique para SharePoint 2010, dado el poco tiempo que lleva SharePoint 2013 liberado, además de que la finalidad del emulador es poder probar código de servidor, mismo que Microsoft desalienta escribir para SharePoint 2013.

Este paquete se puede instalar a través de NuGet. Solo tienen que buscarlo y aparece dentro de los resultados.

  
En el pasado había sido bastante difícil aislar los componentes SharePoint de una solución para realizar pruebas unitarias, debido a que estos componentes dependen enormemente de SharePoint, es decir, para poder realizar la ejecución de pruebas de esos componentes era necesario tener instalado SharePoint.
 
SharePoint Emulators Package nos permite crear pruebas unitarias y ejecutarlas sin tener instalado un ambiente de SharePoint, este paquete se basa en el concepto de Shims de Microsoft Fakes para crear un contexto (un conjunto de Shims) en memoria de los objetos más comunes dentro de la API de SharePoint. Con lo que, para crear una prueba unitaria que utiliza objetos de la API de SharePoint lo único que debe hacer es encerrar el código que estamos probando dentro de un SharePoint Emulator Scope.

[TestMethod]
public void CreateList()
{
	using (SharePointEmulationScope scope = new SharePointEmulationScope(Microsoft.QualityTools.Testing.Emulators.EmulationMode.Enabled))
	{
		using (SPSite site = new SPSite("http://UrldePrueba/"))
		{
			using (SPWeb web = site.OpenWeb())
			{
				Assert.IsNotNull(web);

				//Definiendo la lista
				Guid listId = site.RootWeb.Lists.Add(listName, listName, SPListTemplateType.GenericList);
				SPList list = site.RootWeb.Lists[listId];
				Assert.IsNotNull(list);
				Assert.AreEqual(list.Title, listName, "Bad list name!");

				//agregando los campos a la lista
				list.Fields.Add("Code", SPFieldType.Number, true);
				list.Fields.Add("Name", SPFieldType.Text, true);
				list.Fields.Add("Description", SPFieldType.Text, true);

				//agregando dos nuevos elemento a la lista
				SPListItem item1 = list.Items.Add();
				item1["Code"] = 1;
				item1["Name"] = "West";
				item1["Description"] = "West Region";
				item1.Update();

				SPListItem item2 = list.Items.Add();
				item2["Code"] = 1;
				item2["Name"] = "East";
				item2["Description"] = "East Region";
				item2.Update();

				Assert.AreEqual(list.ItemCount, 2, "Bad number of items");
			}
		}
	}
}


Como mencionaba antes, este paquete solo contiene los Shims para los objetos de uso más común dentro de la API de SharePoint, por lo que habrá casos es los que al hacer uso de ciertos métodos o propiedades, que devuelven un tipo de objeto que no tiene su correspondiente Shim dentro del emulador,  obtendremos excepciones de tipo NotSupportedException, como por ejemplo al intentar utilizar el método GetItems de un objeto SPList, que devuelve un objeto de tipo SPListItemCollection.


El siguiente código muestra el método que se está probando, en la línea 17 está la llamada al método GetItems que lanzará la excepción.
public string GetNameByCode(string listName, SPWeb web, int code)
{
	SPList list = web.Lists[listName];

	var listQuery = new SPQuery
	{
		Query = String.Format(
			"" +
			"" +
			"" +
			"{0}" +
			"" +
			"", code)
	};

	string name = string.Empty;
	foreach (SPListItem item in list.GetItems(listQuery))
	{
		name = item["Name"].ToString();
	}

	return name;
}


Lo genial de esto, es que es posible inyectar Shims al Emulador para los tipos no soportados, como por ejemplo para SPListItemCollection, de la forma tradicional, es decir usando Shims, como se aprecia de las líneas 41 a la 50
[TestMethod]
public void SearchItemById()
{
	int searchById = 2;

	using (SharePointEmulationScope scope = new SharePointEmulationScope(Microsoft.QualityTools.Testing.Emulators.EmulationMode.Enabled))
	{
		using (SPSite site = new SPSite("http://UrldePrueba/"))
		{
			using (SPWeb web = site.OpenWeb())
			{
				Assert.IsNotNull(web);

				//Definiendo la lista
				Guid listId = site.RootWeb.Lists.Add(listName, listName, SPListTemplateType.GenericList);
				SPList list = site.RootWeb.Lists[listId];
				Assert.IsNotNull(list);
				Assert.AreEqual(list.Title, listName, "Bad list name!");

				//agregando los campos a la lista
				list.Fields.Add("Code", SPFieldType.Number, true);
				list.Fields.Add("Name", SPFieldType.Text, true);
				list.Fields.Add("Description", SPFieldType.Text, true);

				//agregando dos nuevos elemento a la lista
				SPListItem item1 = list.Items.Add();
				item1["Code"] = 1;
				item1["Name"] = "West";
				item1["Description"] = "West Region";
				item1.Update();

				SPListItem item2 = list.Items.Add();
				item2["Code"] = 2;
				item2["Name"] = "East";
				item2["Description"] = "East Region";
				item2.Update();

				Assert.AreEqual(list.ItemCount, 2, "Bad number of items");

				//Agregamos el Shimp para el objeto SPListItemCollection
				if (scope.EmulationMode == EmulationMode.Enabled)
				{
					var fakeList = new ShimSPList(list);
					fakeList.GetItemsSPQuery = (query) =>
					{
						var shim = new ShimSPListItemCollection();
						shim.Bind(new[] { list.Items[0], list.Items[1] });
						return shim.Instance;
					};
				}

				RegionGateway gateway = new RegionGateway();

				//Ahora al realizar la llamada al metodo GetNameByCode, no se lanzara la excepcion
				string result = gateway.GetNameByCode(listName, site.RootWeb, searchById);
				Assert.AreNotEqual(string.Empty, result, true, "Search Failed!");


				Region searchItem = gateway.GetRegionById(listName, web, 1);
				Assert.IsNotNull(searchItem);
				Assert.AreEqual("West", searchItem.Name, true);
			}
		}
	}
}

Sin duda alguna un importante paquete que debiéramos usar desde ya para las pruebas unitarias de nuestro código. Ahora lo importante sería preguntar para cuando este paquete estará disponible para SharePoint 2013.

Por último les comparto el proyecto de Visual Studio para que vean el código y puedan analizarlo mejor.


Referencias

Happy Code!

domingo, 4 de noviembre de 2012

SharePoint 2010 - New instances of this workflow template are currently disallowed

¿Les ha pasado que alguna vez quieren iniciar una instancia de un flujo de trabajo y aparece este mensaje de error?

Bueno púes a mí me había pasado muchas veces, solo que nunca al utilizar código para iniciar automáticamente un flujo de trabajo, es decir, siempre que había obtenido este mensaje de error había sido utilizando la interfaz de usuario, para lo cual existen muchas referencias en la red para solucionarlo.

SharePoint 2007 - New instances of this workflow template are currently disallowed
Error: New instances of this workflow template are currently disallowed.

¿Pero y cómo podemos resolverlo cuando hablamos de código?

Al igual que cuando lidiamos con este error al intentar inicializar in flujo de trabajo utilizando la interfaz de usuario, este error se debe a que existe más de una versión del mismo flujo relacionada con la lista o cualquier otro elemento al que se pueda asociar un workflow como librerías de documentos, sitios, etc., en mi caso específico con un tipo de contenido.

Para ejemplificar este escenario, muestro a continuación mi código de powershell que tenía este error, y el cual simplemente intentaba iniciar programáticamente todas las instancias de una lista de SharePoint con el estatus de “Cancelled”:

cls
#Add-PSSnapin microsoft.sharepoint.powershell
$siteUrl="http://sp2010/mysite"
$site = Get-SPSite -Identity $siteUrl

foreach($web in $site.AllWebs)
{
    $list=$web.Lists["List"]
    $manager=$site.WorkFlowManager
   
    foreach($ct in $list.ContentTypes)
    {
        if($ct.Name -eq "MyContentType")
        {
            $swapWorkflowAssociation = $Ct.WorkflowAssociations[0]
        }
    }
    
    #Si la referencia al objeto WorkflowAssociation es null, no intentamos iniciar el WF
    if($swapWorkflowAssociation -eq $null) { continue } 
   
    foreach($listItem in $list.Items)
    {
        foreach($workflowInstance in $listItem.Workflows)
        {
        
            if($workflowInstance.InternalState -eq [Microsoft.SharePoint.Workflow.SPWorkflowState]::Cancelled)
            {
               $data=$workflowAssociation.AssociationData
               try
               {
                    $result = $manager.StartWorkflow($listItem, $workflowAssociation, $data, $True)
               }
               catch
               {
                    #Write-Host $_.Exception.ToString()
               }
               
               $cadena =   "Se inicia el WF|" + $web.Url + "|" + $listItem.ID.ToString() + "|" + $listItem.Title + "|" + $workflowInstance.InternalState
               
               Write-Host $cadena 
            }  
        }
    } 
}


Como comente antes, en la línea marcada obtenía este mensaje de error. El error estaba en que debido al proceso de prueba y error desarrollo iterativo, el tipo de contenido tenía más de una versión del flujo de trabajo asociado, por lo que al intentar recuperar una referencia al objeto WorkflowAssociation, no se recuperaba precisamente la más “reciente” o actual. El truco está en que las versiones viejas del flujo de trabajo, además de ser establecidas con el atributo “No New Instances”, siempre son renombradas de la forma: “NombreDeMiWorkflow (fecha)”, excepto para la Asociación más reciente (la correcta).

Por lo que, para evitar este error, se debe validar que la propiedad Name del objeto WorkflowAssociation, sea exactamente como la establecimos en un principio, sin fechas (o algún otro texto adicional).
$swapWorkflowAssociation = $Ct.WorkflowAssociations[0]

Para concluir, el código corregido

cls
Start-SPAssignment –Global
#Add-PSSnapin microsoft.sharepoint.powershell
$siteUrl="http://sp2010/mysite"

$site = Get-SPSite -Identity $siteUrl

foreach($web in $site.AllWebs)
{
    $list=$web.Lists["List"]
    $manager=$site.WorkFlowManager
   
    foreach($ct in $list.ContentTypes)
    {
        if($ct.Name -eq "MyContentType")
        {
            foreach($swapWorkflowAssociation in $Ct.WorkflowAssociations)
            {
                if($swapWorkflowAssociation.Name -eq "MyWorkflowName") 
                {
                    $workflowAssociation = $swapWorkflowAssociation
                }
                
            }
        }
    }
   
   #Si la referencia al objeto WorkflowAssociation es null, no intentamos iniciar el WF
   if($swapWorkflowAssociation -eq $null) { continue } 
   
    foreach($listItem in $list.Items)
    {
        foreach($workflowInstance in $listItem.Workflows)
        {
        
            if($workflowInstance.InternalState -eq [Microsoft.SharePoint.Workflow.SPWorkflowState]::Cancelled)
            {
               $data=$workflowAssociation.AssociationData
               try
               {
                    $result = $manager.StartWorkflow($listItem, $workflowAssociation, $data, $True)
               }
               catch
               {
                    #Write-Host $_.Exception.ToString()
               }
               
               $cadena =   "Se inicia el WF|" + $web.Url + "|" + $listItem.ID.ToString() + "|" + $listItem.Title + "|" + $workflowInstance.InternalState
               
               Write-Host $cadena 
            }  
        }
    } 
}

Happy Coding!

viernes, 12 de octubre de 2012

Extraer archivos WSP instalados en una granja SharePoint

Hace unos días mientras me encontraba en medio de una migracion de SharePoint 2007 a SharePoint 2010, me vi en la necesidad de descargar un par de soluciones personalizadas (archivos WSP) de la granja de SharePoint 2007 ya que no había código fuente de esas soluciones y se tenían que hacer ajustes para que funcionaran de manera adecuada.

Después de buscar un par de minutos me encontré con esta utilidad que pueden descargar desde MSDN, me refiero al SharePoint Farm Solution Extractor, funciona bastante bien y es muy fácil de usar.

Ademas también encontré en este foro de Microsoft, que otra forma practica de hacerlo es utilizando PowerShell, sin embargo para mi no era muy practico ya que el servidor no tenia instalado PowerShell :(

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
$farm = [Microsoft.SharePoint.Administration.SPFarm]::Local
$farm.Solutions | % { 
  $filename = ($pwd.ToString() + "\" + $_.SolutionFile.Name); 
  write-host ("Saving" + $filename); 
  $_.SolutionFile.SaveAs($filename) 
}


Por ultimo, también les comparto un ejemplo de como guardar todos los archivos WSP's de una granja SharePoint 2010 en un directorio que indiquen utilizando PowerShell

cls
#path en donde se guardaran los archivos WSP's, este path debe existir antes de ejecutar el script
$path="c:\wsps\"
$farm = Get-SPFarm
foreach($item in $farm.Solutions)
{
 $file = $item.SolutionFile
 Write-Host "Guardando el archivo" $file.Name
 $file.SaveAs($path+$file.Name)
}


Happy Coding!

domingo, 12 de agosto de 2012

Creando custom workflow activities para SharePoint 2010

Una de las mejores prácticas a la hora de crear flujos de trabajo (no solo para SharePoint) es encapsular funcionalidad común o reutilizable en actividades personalizadas, esto nos permitirá hacer flujos de trabajo mucho más mantenibles, disminuyendo así la cantidad de código que escribimos dentro del “code behind”, y es que al menos en mi muy humilde opinión, el código que escribimos debería ser mínimo y para casos muy específicos.

Además de todas las ventajas que implica el uso de actividades personalizadas, es bastante sencillo crearlas, a continuación explicaré el paso a paso de cómo crear una actividad que nos permita la creación de sub-sitios.

Pensemos en el siguiente escenario: El administrador de un sitio recibe muchas peticiones para generar sub sitios dentro del sitio que él administra, esta aburrido de hacer está tarea tan repetitiva, él lo que desea es salir temprano de la oficina y darle duro al PS3 Xbox 360, por lo que se le ocurre automatizar el proceso de solicitud de creación de sitios mediante una lista en la cual los solicitantes puedan ingresar la información fundamental para la creación del sitio y una justificación de porque lo necesitan, de esta manera el administrador solo recibe un mail de que tiene una tarea pendiente, entra a revisar la tarea y la aprueba (si es el caso) y automágicamente el sitio se provisiona, cool no!?. Pues entonces comencemos a  darle forma a los sueños de este administrador ficticio.

Primero creamos una solución de tipo de Sequential Workflow que nos servirá para probar la actividad personalizada que crearemos, después creamos un proyecto de tipo Workflow Activity Library que será en donde crearemos las actividades de workflow personalizadas.


Acto seguido agregamos una clase con el nombre que tendrá la actividad personalizada, en este caso CreateNewSubSite, y heredamos de la clase base Activity.


Creamos las propiedades que tendrán la información necesaria para crear el sub sitio: SiteName, SiteUrl, SiteLCID, TemplateName, SiteDescription, UseUniquePermissions y una propiedad para almacenar el contexto de ejecución del workflow.  Lo primero que debemos notar es que las propiedades que creamos son dependency properties (propiedades de dependencia), esto para permitir a cualquiera que use nuestra actividad poder enlazar (Binding) estas propiedades para que en tiempo de ejecución los valores se calculen. Si aún no sabes que es o para que se utilizan las property dependencies, lee esto.

public static readonly DependencyProperty SiteNameProperty = DependencyProperty.Register("SiteName", typeof(string), typeof(CreateNewSubSite));
public static readonly DependencyProperty SiteUrlProperty = DependencyProperty.Register("SiteUrl", typeof(string), typeof(CreateNewSubSite));
public static readonly DependencyProperty SiteLCIDProperty = DependencyProperty.Register("SiteLCID", typeof(uint), typeof(CreateNewSubSite), new PropertyMetadata((uint)1033));
public static readonly DependencyProperty TemplateNameProperty = DependencyProperty.Register("TemplateName", typeof(string), typeof(CreateNewSubSite), new PropertyMetadata("STS#1"));
public static readonly DependencyProperty SiteDescriptionProperty = DependencyProperty.Register("SiteDescription", typeof(string), typeof(CreateNewSubSite));
public static readonly DependencyProperty UseUniquePermissionsProperty = DependencyProperty.Register("UseUniquePermissions", typeof(bool), typeof(CreateNewSubSite));

public static DependencyProperty __ContextProperty = System.Workflow.ComponentModel.DependencyProperty.Register("__Context", typeof(Microsoft.SharePoint.WorkflowActions.WorkflowContext), typeof(CreateNewSubSite));

[Description("The name of the new subsite")]
public string SiteName
{
 get { return (string)GetValue(SiteNameProperty); }
 set { SetValue(SiteNameProperty, value); }
}

[Description("Relative Url of the new subsite. You should consider that any space will be codified, for example MySite=MySite, Financial Area=Financial%20Area")]
public string SiteUrl
{
 get { return (string)GetValue(SiteUrlProperty); }
 set { SetValue(SiteUrlProperty, value); }
}

[Description("Specify the LCID of the language that will be used to create the subsite. Example: English=1033, Spanish=3082. For more reference you can see http://technet.microsoft.com/en-us/library/ff463597.aspx")]
public uint SiteLCID
{
 get { return (uint)GetValue(SiteLCIDProperty); }
 set { SetValue(SiteLCIDProperty, value); }
}

[Description("Specify the template name that will be used to create the subsite. Example: GLOBAL#0 = Global template, STS#0 = Team Site, STS#1 = Blank Site.")]
public string TemplateName
{
 get { return (string)GetValue(TemplateNameProperty); }
 set { SetValue(TemplateNameProperty, value); }
}

[Description("Specify the description of the new subsite")]
public string SiteDescription
{
 get { return (string)GetValue(SiteDescriptionProperty); }
 set { SetValue(SiteDescriptionProperty, value); }
}

[Description("Specify if the site will have unique permissions")]
public bool UseUniquePermissions
{
 get { return (bool)GetValue(UseUniquePermissionsProperty); }
 set { SetValue(UseUniquePermissionsProperty, value); }
}

public WorkflowContext __Context
{
 get { return (WorkflowContext)GetValue(__ContextProperty); }
 set { SetValue(__ContextProperty, value); }
}

La parte medular para la creación de nuestra actividad personalizada es sobreescribir el método Execute, el cual contiene el código principal a ejecutar por la actividad personalizada. En este ejemplo tomamos los valores de las propiedades para crear el sub sitio, no sin antes ejecutar algunas validaciones.

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
 SPSecurity.RunWithElevatedPrivileges(() =>
  {
   using (SPSite site= new SPSite (__Context.Site.ID))
   {
    using (SPWeb web = site.AllWebs[__Context.Web.ID])
    {
     //before to add the new subsite, we need to validate some settings, remember! never trust in another dev
     //first at all, validate the LCID
     SPLanguage language = SPRegionalSettings.GlobalInstalledLanguages.Cast().FirstOrDefault(x => x.LCID == SiteLCID);
     if (language == null)
     {
      throw new ArgumentException(string.Format("The value '{0}' for SiteLCID is not supported, are you sure corresponding language pack is already installed?", this.SiteLCID));
     }
     
     //then validate the name of site template
     SPWebTemplate webTemplate = site.GetWebTemplates(this.SiteLCID).Cast().FirstOrDefault(x => x.Name == this.TemplateName);
     if (webTemplate == null)
     {
      throw new ArgumentException(string.Format("The specified TemplateName='{0}' is not supported for specified language='{1}'", this.TemplateName, language.DisplayName));
     }

     //then validate the SiteUrl is unique
     if (web.Webs.Names.Contains(SiteUrl))
     {
      throw new ArgumentException("The SiteUrl is not unique, there is another subsite with the same SiteUrl");
     }

     web.Webs.Add(SiteUrl, SiteName, SiteDescription, SiteLCID, webTemplate, UseUniquePermissions, false);
    }
   }
  }
 );

 return ActivityExecutionStatus.Closed;
}

Así de simple ya tenemos una actividad personalizada lista para ser utilizada. Para este ejemplo agregamos el proyecto del workflow (WorkfllowDemo) y el proyecto de las actividades personalizadas (CustomActivities) en la misma solución, por lo que al compilar la solución automáticamente la actividad que creamos aparece en el toolbox.

Aunque también se puede solo agregar la referencia a la dll que contiene las actividades utilizando la ventana de diálogos para agregar elementos al Toolbox, para poder utilizar las actividades sin necesidad del código fuente.
El paso siguiente es arrastrar la actividad CreateNewSubSite al diseñador de workflows y configurar sus propiedades con la información para crear el subsitio.

Como se aprecia en la imagen anterior, agregamos solo una actividad más para escribir en la lista de Workflow History que el sitio fue creado correctamente. Ahora hablemos un poco a cerca de la lista que utilizaremos para las solicitudes de nuevos sitios, la cual solo contiene dos columnas: SiteName y SiteUrl. Es a esta lista a la cual asociaremos el flujo de trabajo.

Configuramos el flujo para que se ejecute al crear un nuevo elemento en la lista y podremos ver como se crea el sitio automáticamente, además del mensaje en el historial del flujo de trabajo





Les comparto el código y si tienen algún comentario o pregunta no duden en hacerla

Happy coding!

viernes, 3 de agosto de 2012

Accediendo al contexto de SharePoint desde un flujo de trabajo

Escribo este post ya que he notado que muchos desarrolladores al crear flujos de trabajo utilizando Visual Studio 2010 para SharePoint 2010, incluso para Project Server 2010, se quejan que durante la ejecución del flujo de trabajo no tienen acceso al "contexto", es decir no pueden utilizar la instancia de la clase SPContext accediéndola de la manera común Microsoft.SharePoint.SPContext.Current.

Y si bien es cierto que durante la ejecución del flujo de trabajo si intentamos acceder al contexto de SharePoint de esta manera siempre obtendremos null, la solución es crear durante la inicialización del flujo de trabajo una instancia del objeto Microsoft.SharePoint.WorkflowActions.WorkflowContext,
la cual nos permitirá acceder a gran parte de la información del contexto de ejecución de SharePoint desde el flujo de trabajo.

Para inicializar una instancia de la clase WorkflowContext, seguimos los siguientes dos sencillos pasos:

Declaramos una propiedad del tipo WorkflowContext y posteriormente la instanciamos utilizando el constructor sin parámetros que existe.

public Microsoft.SharePoint.WorkflowActions.WorkflowContext WFContext { get; set; }

public Workflow1()
{
    WFContext = new Microsoft.SharePoint.WorkflowActions.WorkflowContext();
    InitializeComponent();
}
Después inicializamos la propiedad en el evento Invoked de la actividad OnWorkflowActivated que se agrega automáticamente cuando creamos el flujo de trabajo

private void onWorkflowActivated1_Invoked(object sender, ExternalDataEventArgs e)
{
    WFContext.Initialize(workflowProperties);
}


El código completo se vería asi:

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Linq;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.WorkflowActions;

namespace WorkflowDemo.Workflow1
{
    public sealed partial class Workflow1 : SequentialWorkflowActivity
    {
        public Guid workflowId = default(System.Guid);
        public SPWorkflowActivationProperties workflowProperties = new SPWorkflowActivationProperties();

        public Microsoft.SharePoint.WorkflowActions.WorkflowContext WFContext { get; set; }

        public Workflow1()
        {
            WFContext = new Microsoft.SharePoint.WorkflowActions.WorkflowContext();
            InitializeComponent();
        }

        private void onWorkflowActivated1_Invoked(object sender, ExternalDataEventArgs e)
        {
            WFContext.Initialize(workflowProperties);
        }
    }
}

Y eso es todo, ahora podemos acceder al contexto de SharePoint utilizando esta propiedad.
Happy Coding!

martes, 10 de julio de 2012

Finaliza el soporte para SharePoint 2010

El día de hoy finaliza el soporte para SharePoint 2010 en cualquier versión que este entre RTM y el service pack 1, que quiere decir esto?

Que deben actualizar sus granjas de SharePoint 2010 al SP 1. Si su granja de SharePoint 2010 no tiene al menos el SP 1, no tendrán soporte o será solo soporte limitado

Así que no esperen mas y actualicen las su granjas de SharePoint 2010 que aun no cuenten con al menos el Service Pack 1

Lista de todos los paquetes SP1 de SharePoint 2010 y Office Server 2010
Service Pack 1 for SharePoint 2010 Products is Now Available for Download


sábado, 2 de junio de 2012

Sexto Simposio LatinoAmericano de SharePoint

Como cada año se llevó a cabo el sexto simposio latinoamericano de SharePoint, en el cual tuve el privilegio de participar en la presentación "El poder del Business Intelligence con Visio 2010 y SharePoint 2010" en conjunto con Elizabeth Contreras (Project Manager de Project y Visio en Microsoft México) y Antonio Razo (Consultor Sr de BI), a continuación les comparto la ppt que presente.

Cualquier comentario es bienvenido



Happy Coding!

martes, 1 de mayo de 2012

Sexto Simposio Latinoamericano de SharePoint en México

Como ya se está volviendo tradición, este año se llevara a cabo de nueva cuenta el Simposio Latinoamericano de SharePoint en México, siendo este el primer año en que se estará transmitiendo a través de Live Meeting.

El evento presencial será de nueva cuenta en las oficinas de Microsoft México

Aquí la invitación oficial:


No olviden realizar su registro y allá nos vemos :)

domingo, 22 de abril de 2012

[SharePoint Troubleshooting] - Access Denied PRTH_E_ACCESS_DENIED en servcio de búsqueda

En los pasados días me encontré con el siguiente escenario en la granja de SharePoint 2010 de uno de mis clientes. Se tiene el servicio de búsqueda creado y configurado adecuadamente, de hecho ya tiene unos meses funcionando con normalidad y la búsqueda funciona en todos los sitios de la compañía. De pronto un día notan que en un Web Application en particular la búsqueda no está mostrando resultados.

De manera sistemática, algunas de los primeros puntos a revisar en este escenario fueron:
  • Privilegios de la cuenta de acceso al contenido
  • Que el host header del web application que no devuelve resultados este agregado dentro de las direcciones a rastrear dentro de alguno de los orígenes de contenido
  • Que el servicio de búsqueda esté funcionando y/o verificar cuando fue la última vez que se realizo un rastreo

Después de revisar los puntos anteriores, nos damos cuenta que todo parece ir bien con el servicio de búsqueda, solo vemos un par de errores en el log de rastreo de uno de los orígenes de contenido, y que precisamente corresponde al sitio en que no se muestran resultados de las búsquedas, pero no hay mayor información de cuál es el origen de estos errores.
Después de mucho buscar en los logs de SharePoint, encontramos los siguientes mensajes.
CHttpAccessorHelper::InitRequestInternal - Access Denied PRTH_E_ACCESS_DENIED on request for "http://extranet.xxxxx.xxx" hr = 0x80041205 [httpacchelper.cxx:268] d:\office\source\search\native\gather\protocols\http\httpacchelper.cxx
CHttpProbeHelper::ProbeServer:InitiRequest failed for "http://extranet.xxxxxx.xxx" Return error to caller, hr=80041205 [stscommon.cxx:439] d:\office\source\search\native\gather\protocols\common\stscommon.cxx

Lo que nos da una pequeña pista, se trata de permisos de la cuenta de acceso. Intentamos acceder al sitio utilizando esta cuenta desde el servidor que tiene el servicio de búsqueda,  después de ingresar las credenciales, nos aparece la pantalla de login de manera repetida (tres veces) hasta que nos presenta un error de autentificación. Estonces llegamos a la raíz del error, no es un problema de permisos de la cuenta sino un tema de configuración, ya que es un comportamiento que he visto bastante y que se refiere a la configuración de la característica de seguridad DisableLoopbackCheck, de la cual ya he hablado anteriormente.

Todo se soluciona, siguiendo el KB 896861 y aplicando las configuraciones que correspondan en los servidores SharePoint, Si no sabes si deshabilitar la caracteristica DisableLoopbackCheck o agregar una entrada en BackConnectionHostNames, te recomiendo leer mi post en donde hablo de cuando aplicar una u otra solución
Happy configuring!

lunes, 12 de diciembre de 2011

[SharePoint 2010] - Resolver usuarios y grupos a partir de su Login Name o Email - SPUtility.ResolvePrincipal

Cuantas veces no hemos requerido buscar a un usuario a partir de su Email o de su LoginName, para obtener uno de estos datos, por ejemplo para enviarle un correo o asignarle una tarea dentro de un flujo de trabajo.

En el pasado utilizábamos el método GetLoginNameFromEmail de la clase SPUtility si conocíamos el email y necesitábamos el Login Name del usuario, sin embargo en la versión 2010 de la API de SharePoint este método ha sido marcado como obsoleto, lo que significa que si lo usamos producirá una alerta la cual nos recomienda, en su lugar, el uso del método ResolvePrincipal.

La ventaja de utilizar ResolvePrincipal es, en primera, que obtenemos de regreso un objeto del tipo SPPrincipalInfo el cual tiene varias propiedades útiles para trabajar con usuarios y grupos. Segunda, que como parámetro para realizar la búsqueda recibe el Display Name, Login Name o Email (solo uno de ellos), es decir que si conocemos cualquiera de estos tres valores de un usuario o grupo es posible resolverlo. Otro aspecto importante es que la búsqueda que realiza no está restringida a un sitio de SharePoint en particular.

A continuación un par de ejemplos de cómo utilizar este método para resolver usuarios.

SPWeb web = SPContext.Current.Site.RootWeb; //or SPControl.GetContextWeb(Context) or workflowProperties.Site.RootWeb, etc

//obtener el usuario a partir del display name
SPPrincipalInfo info = SPUtility.ResolvePrincipal(web, "Homero J. Simpson", SPPrincipalType.User, SPPrincipalSource.MembershipProvider | SPPrincipalSource.Windows, null, false);

//obtener el usuario a partir del Email
info = SPUtility.ResolvePrincipal(web, "hsimpson@sp2010.com", SPPrincipalType.All, SPPrincipalSource.All, web.AllUsers, true);

Por último, aunque no menos importante, mencionar que este método no esta disponible en soluciones SandBox.

Happy Coding!

lunes, 21 de noviembre de 2011

QI for IEnumVARIANT failed on the unmanaged server

Bueno, por si alguna vez les ha ocurrido este error al tratar de ingresar a la página de administración del servicio de búsqueda.


Inclusive al intentar recuperar algún valor mediante PowerShell este mismo error aparece, una posible solución que a mí me funciono fue reiniciar el IIS utilizando el siguiente comando

iisreset /noforce

Esto en cada uno de los servidores de crawling

Happy Configuring!

Crear custom workflow tasks forms [No Infopath]

Como recordaran en el post anterior, mostré un ejemplo de cómo crear columnas de sitio y content types programáticamente, en este post voy a dar un ejemplo de cómo crear un formulario personalizado para el tipo de contenido que creamos en ese ejemplo anterior.

Es común que creemos tipos de contenido personalizados (custom content types) para asociarlos a la lista de tareas de workflows (Workflow Tasks), esto con el fin de poder recolectar más información del usuario cuando se le asigna una tarea dentro de un flujo de trabajo, y también es muy común que necesitemos personalizar el formulario de captura que el usuario utiliza para ingresar esa información adicional que necesitamos recolectar dentro del flujo de trabajo. Por lo anterior y utilizando el tipo de contenido que creamos en el post anterior, y que de manera premeditada hice que heredara del tipo de contenido WorkflowTask (recuerdan?).
También es importante mencionar que el custom approval workflow task form que crearemos será utilizando un formulario ASP.NET, creo que la mayoría sabe que no simpatizo mucho con InfoPath, además que ejemplos de esos hay bastantes en la red.
El escenario de ejemplo es el siguiente, cree una lista llamada “Solicitud Vacaciones”, en la que los usuarios podrán capturar las peticiones para tomar días de vacaciones, una vez que registren sus solicitud se enviará un correo de aviso al usuario responsable y se le asignará una tarea para que apruebe o rechace dicha solicitud, en caso de aprobación o rechazo se le notificará al usuario solicitante.
Como primer paso, creamos un flujo de trabajo sencillo, en este caso construiré un flujo de aprobación de solicitud de vacaciones, como se aprecia en la siguiente imagen:
   
No voy a explicar el cómo construir el flujo de trabajo, dado que la finalidad de este post no es mostrar cómo hacer un flujo de trabajo,  adicional a que hay muchos ejemplos en la red. Sin embargo voy a compartir el código por si a alguien le interesa ver cómo fue construido el Workflow.

Como se aprecia en la siguiente imagen, se agregó una actividad de tipo CreateTaskWithContentType la cual recibe el Id del Content Type a partir del cual se creara la tarea que será asignada al usuario responsable de autorizar la solicitud de vacaciones.








 

Como se aprecia en la imagen, el ID del tipo de contenido se asocia a una propiedad declarada en código, la que manipularemos para buscar y asignar el ID de tipo de contenido correcto cuando el Workflow se inicialice, en el evento WorkflowActivated.


private void onWorkflowActivated(object sender, ExternalDataEventArgs e)
{
 WorkflowContext.Initialize(workflowProperties);
 workflowID = workflowProperties.WorkflowId;

 //si el tipo de contenido no ha sido asociado a la lista de tareas
 try
 {
  if (workflowProperties.TaskList.ContentTypes[contentTypeName] == null)
  {
   SPContentType ctype = WorkflowContext.Web.AvailableContentTypes[contentTypeName];
   if (ctype != null)
   {
    workflowProperties.TaskList.ContentTypes.Add(ctype);
    workflowProperties.TaskList.Update();
   }
  }

  SPContentType customContentType = WorkflowContext.Web.AvailableContentTypes[contentTypeName];
  this.CustomContentTypeId = customContentType.Id.ToString();

 }
 catch (Exception ex)
 {
  //registramos la excepcion
 }
}

Estableciendo el formulario de edición al tipo de Contenido

Regresando a la definición del tipo de contenido que creamos en el post anterior, que cómo podrán recordar fue provisionado programáticamente. El siguiente paso es establecer por lo menos el formulario de edición del tipo de contenido, es decir, el formulario que el usuario visualizará cuando vaya a aprobar o rechazar la tarea asignada. Para que el Content Type sepa que formulario debe invocar, utilizaremos la propiedad EditFormUrl la que estableceremos con el valor del formulario ASP.NET que vamos a crear en el siguiente paso.

Noten la línea 131:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;

namespace WF1.Features.Feature1
{
    /// 
    /// This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
    /// 
    /// 
    /// The GUID attached to this class may be used during packaging and should not be modified.
    /// 

    [Guid("dd6b6ec4-3391-46de-a659-55244237f346")]
    public class Feature1EventReceiver : SPFeatureReceiver
    {
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPWeb web = null;
                if (properties.Feature.Parent is SPSite)
                {
                    SPSite sites = (SPSite)properties.Feature.Parent;
                    web = sites.RootWeb;
                }
                else
                {
                    web = (SPWeb)properties.Feature.Parent;
                }
                if (web == null)
                    return;

                //Nombre del grupo de Content Types que crearemos
                string contentTypeGroupName = "Blog - Content Types";
                //Nombre del tipo de contenido que crearemos
                string contentTypeName = "Blog - Response Workflow Content Type";
                //Nombre del grupo de columnas de sitio que crearemos
                string columnGroupName = "Blog - Site Columns";
                //nombre de la columna de sitio que permitira al usuario seleccionar su respuesta (Approve | Reject)
                string choiceFieldName = "ChoiceWFResponse";
                //nombre de la columna de sitio que permitira al usuario capturar comentarios a cerca de su respuesta
                string commentsFieldName = "CommentsWFResponse";


                SPFieldMultiLineText approveRejectCommentsField = null;
                SPFieldChoice approveRejectChoiceField = null;

                /*Columna de sitio para seleccionar Aprobar o Rechazar*/
                //mucho ojo! Validamos si la columa de sitio ya existe
                if (!web.Fields.ContainsField(choiceFieldName))
                {
                    string approveReject = web.Fields.Add(choiceFieldName, SPFieldType.Choice, true);
                    approveRejectChoiceField = (SPFieldChoice)web.Fields.GetFieldByInternalName(approveReject);
                    approveRejectChoiceField.Group = columnGroupName;
                    approveRejectChoiceField.EditFormat = SPChoiceFormatType.RadioButtons;
                    approveRejectChoiceField.Choices.Add("Approve");
                    approveRejectChoiceField.Choices.Add("Reject");
                    approveRejectChoiceField.DefaultValue = "Approve";
                    approveRejectChoiceField.Update();
                }
                //Si ya existe, solo obtenemos una referencia a la columna
                else
                {
                    approveRejectChoiceField = web.Fields[choiceFieldName] as SPFieldChoice;
                }

                /*Columna de sitio para permitir al usuario ingresar un comentario*/
                if (!web.Fields.ContainsField(commentsFieldName))
                {
                    string comments = web.Fields.Add(commentsFieldName, SPFieldType.Note, true);
                    approveRejectCommentsField = (SPFieldMultiLineText)web.Fields.GetFieldByInternalName(comments);
                    approveRejectCommentsField.Group = columnGroupName;
                    approveRejectCommentsField.Update();
                }
                else
                {
                    approveRejectCommentsField = web.Fields[commentsFieldName] as SPFieldMultiLineText;
                }

                //
                //Ahora creamos el tipo de contenido
                //

                //heredamos del WorkflowTask Content Type
                SPContentType workflowTaskCType = web.AvailableContentTypes[SPBuiltInContentTypeId.WorkflowTask];
                SPContentType approvalRejectCType = null;
                bool isUpdating = false;

                //validamos si el tipo de contenido ya existe
                if (web.AvailableContentTypes[contentTypeName] == null)
                {
                    approvalRejectCType = new SPContentType(workflowTaskCType, web.ContentTypes, contentTypeName);
                    approvalRejectCType = web.ContentTypes.Add(approvalRejectCType);
                    approvalRejectCType.Group = contentTypeGroupName;
                }
                else
                {
                    approvalRejectCType = web.ContentTypes[contentTypeName];
                    isUpdating = true;
                }

                if (!isUpdating)
                {
                    //agregamo la columna de Seleccion
                    SPFieldLink approveRejectFieldRef = new SPFieldLink(approveRejectChoiceField);
                    approveRejectFieldRef.Required = true;
                    approveRejectFieldRef.DisplayName = "Approve or Reject the task";
                    approvalRejectCType.FieldLinks.Add(approveRejectFieldRef);

                    //agregamo la columna de Comentarios
                    SPFieldLink commentsFieldRef = new SPFieldLink(approveRejectCommentsField);
                    commentsFieldRef.DisplayName = "Comments";
                    approvalRejectCType.FieldLinks.Add(commentsFieldRef);
                }
                else
                {
                    //Actualizamos la columna de Seleccion
                    SPFieldLink approveRejectFieldRef = approvalRejectCType.FieldLinks[approveRejectChoiceField.Id];
                    approveRejectFieldRef.Required = true;
                    approveRejectFieldRef.DisplayName = "Approve or Reject the task";

                    //Actualizamos la columna de Comentarios
                    SPFieldLink commentsFieldRef = approvalRejectCType.FieldLinks[approveRejectCommentsField.Id];
                    commentsFieldRef.DisplayName = "Comments";
                }

                approvalRejectCType.EditFormUrl = "_layouts/WF1/CustomApprovalTask.aspx";

                //actualizamos el tipo de contenido
                approvalRejectCType.Update(true);
            }
            catch (Exception ex)
            {
                //log Exception
            }
        }

    }
}

Existen algunas otras propiedades con las cuales podemos indicar que formulario debe mostrarse dependiendo de la vista que sea solicitada, tales como: DisplayFormUrl, MobileDisplayFormUrl, etc, etc

Creación del formulario

El siguiente paso es la creación del formulario que será invocado cuando el usuario haga clic en editar tarea. Para esto agregamos un elemento Application Page y la nombramos como CustomApprovalTask.aspx, el cual usaremos para crear la forma de edición para el tipo de contenido.



A continuación el código del formulario, en realidad es bastante sencillo solo presenta un par de controles, una caja de texto para capturar los comentarios y un dropdownlist para elegir una opción (Aprobar, Rechazar).



using System;
using System.Collections;
using System.Web;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Workflow;

namespace WF1.Layouts
{
    public partial class CustomApprovalTask : LayoutsPageBase
    {
        protected SPList targetTasksList;
        protected SPListItem targetTask;
        protected SPWorkflow workflowInstance;
        protected SPWorkflowModification workflowModification;

        string choiceFieldName = "ChoiceWFResponse";
        //nombre de la columna de sitio que permitira al usuario capturar comentarios a cerca de su respuesta
        string commentsFieldName = "CommentsWFResponse";


        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            /**/
            // Retrieve the current task list and task item
            targetTasksList = Web.Lists[new Guid(Request.Params["List"])];
            targetTask = targetTasksList.GetItemById(int.Parse(Request.Params["ID"]));

            //Obtiene el autorizador de la tarea
            string Aprobador = Convert.ToString(targetTask[SPBuiltInFieldId.AssignedTo]);
            int IdAporbador = Convert.ToInt32(Aprobador.Remove(Aprobador.IndexOf(';'), Convert.ToInt32(Aprobador.Length - Aprobador.IndexOf(";"))));

            if ((Web.CurrentUser.ID != IdAporbador) )
            {
                ClosePopup();
            }

            /**/
            if (!this.Page.IsPostBack)
            {
                // Populate fields on the form
                lblTitle.Text = targetTask["Title"] != null ? targetTask["Title"].ToString() : string.Empty;
                txtCommnents.Text = targetTask[commentsFieldName] != null ? targetTask[commentsFieldName].ToString() : string.Empty;

                ListItem selectedItem = ddlResponse.Items.FindByValue(targetTask[choiceFieldName].ToString());

                if (selectedItem != null)
                {
                    this.ddlResponse.ClearSelection();
                    selectedItem.Selected = true;
                }
            }

        }

        protected void UpdateTaskFromControls(SPListItem targetTask, System.Collections.Hashtable taskProperties)
        {
            // Update task item fields
            taskProperties[SPBuiltInFieldId.TaskStatus] = SPWorkflowStatus.Completed;
            taskProperties[commentsFieldName] = txtCommnents.Text;
            taskProperties[choiceFieldName] = ddlResponse.SelectedValue;
        }

        public void btnSaveTask_Click(object sender, EventArgs e)
        {
            try
            {
                Hashtable taskProperties = new Hashtable();
                UpdateTaskFromControls(targetTask, taskProperties);
                SPWorkflowTask.AlterTask(targetTask, taskProperties, false);

            }
            catch (Exception exception)
            {
                SPUtility.Redirect("Error.aspx", SPRedirectFlags.RelativeToLayoutsPage,
                HttpContext.Current,
                "ErrorText=" + SPHttpUtility.UrlKeyValueEncode(exception.Message));
            }
            ClosePopup();
        }

        public void Cancel_Click(object sender, EventArgs e)
        {
            ClosePopup();
        }

        protected void ClosePopup()
        {
            this.Response.Clear();
            this.Response.Write("Closing ...");
            this.Response.Flush();
            this.Response.End();
        }
    }
}





Pueden descargar el código de aquí

Happy Coding!

miércoles, 12 de octubre de 2011

Creando Site Columns y Content Types programáticamente

El día de hoy quiero compartir con ustedes un ejemplo de cómo crear columnas de sitio y tipos de contenido programáticamente. El objetivo final es crear un tipo de contenido que herede del tipo de contenido WorkflowTask (el que se utiliza para las tareas que se asignan en los flujos de trabajo), utilizando dos columnas de sitio, la primera será una columna que permitirá al usuario capturar su respuesta (Aprobar | Rechazar), la segunda permitirá capturar comentarios acerca de su respuesta.

Sin mucho preámbulo comencemos creando un proyecto en blanco con un elemento Feature con scope Site, al cual agregaremos un Event Receiver
Inmediatamente después comenzamos a tirar un poco de código en el método FeatureActivated para crear en primera instancia las dos columnas de sitio que utilizaremos:
SPWeb web = null;
if (properties.Feature.Parent is SPSite)
{
    SPSite sites = (SPSite)properties.Feature.Parent;
    web = sites.RootWeb;
}
else
{
    web = (SPWeb)properties.Feature.Parent;
}
if (web == null)
    return;
                
//Nombre del grupo de Content Types que crearemos
string contentTypeGroupName = "Blog - Content Types";
//Nombre del tipo de contenido que crearemos
string contentTypeName = "Blog - Response Workflow Content Type";
//Nombre del grupo de columnas de sitio que crearemos
string columnGroupName = "Blog - Site Columns";
//nombre de la columna de sitio que permitira al usuario seleccionar su respuesta (Approve | Reject)
string choiceFieldName = "ChoiceWFResponse";
//nombre de la columna de sitio que permitira al usuario capturar comentarios a cerca de su respuesta
string commentsFieldName = "CommentsWFResponse";


SPFieldMultiLineText approveRejectCommentsField = null;
SPFieldChoice approveRejectChoiceField = null;
                
/*Columna de sitio para seleccionar Aprobar o Rechazar*/
//mucho ojo! Validamos si la columa de sitio ya existe
if (!web.Fields.ContainsField(choiceFieldName))
{
    string approveReject = web.Fields.Add(choiceFieldName, SPFieldType.Choice, true);
    approveRejectChoiceField = (SPFieldChoice)web.Fields.GetFieldByInternalName(approveReject);
    approveRejectChoiceField.Group = columnGroupName;
    approveRejectChoiceField.EditFormat = SPChoiceFormatType.RadioButtons;
    approveRejectChoiceField.Choices.Add("Approve");
    approveRejectChoiceField.Choices.Add("Reject");
    approveRejectChoiceField.DefaultValue = "Approve";
    approveRejectChoiceField.Update();
}
//Si ya existe, solo obtenemos una referencia a la columna
else
{
    approveRejectChoiceField = web.Fields[choiceFieldName] as SPFieldChoice;
}

/*Columna de sitio para permitir al usuario ingresar un comentario*/
if (!web.Fields.ContainsField(commentsFieldName))
{
    string comments = web.Fields.Add(commentsFieldName, SPFieldType.Note, true);
    approveRejectCommentsField = (SPFieldMultiLineText)web.Fields.GetFieldByInternalName(comments);
    approveRejectCommentsField.Group = columnGroupName;
    approveRejectCommentsField.Update();
}
else
{
    approveRejectCommentsField = web.Fields[commentsFieldName] as SPFieldMultiLineText;
}

Una vez que las hemos creado, procedemos a crear el tipo de contenido utilizando las columnas de sitio.
//
//Ahora creamos el tipo de contenido
//

//heredamos del WorkflowTask Content Type
SPContentType workflowTaskCType = web.AvailableContentTypes[SPBuiltInContentTypeId.WorkflowTask];
SPContentType approvalRejectCType = null;
bool isUpdating = false;

//validamos si el tipo de contenido ya existe
if (web.AvailableContentTypes[contentTypeName] == null)
{
    approvalRejectCType = new SPContentType(workflowTaskCType, web.ContentTypes, contentTypeName);
    approvalRejectCType = web.ContentTypes.Add(approvalRejectCType);
    approvalRejectCType.Group = contentTypeGroupName;
}
else
{
    approvalRejectCType = web.ContentTypes[contentTypeName];
    isUpdating = true;
}

if (!isUpdating)
{
    //agregamo la columna de Seleccion
    SPFieldLink approveRejectFieldRef = new SPFieldLink(approveRejectChoiceField);
    approveRejectFieldRef.Required = true;
    approveRejectFieldRef.DisplayName = "Approve or Reject the task";
    approvalRejectCType.FieldLinks.Add(approveRejectFieldRef);

    //agregamo la columna de Comentarios
    SPFieldLink commentsFieldRef = new SPFieldLink(approveRejectCommentsField);
    commentsFieldRef.DisplayName = "Comments";
    approvalRejectCType.FieldLinks.Add(commentsFieldRef);
}
else
{
    //Actualizamos la columna de Seleccion
    SPFieldLink approveRejectFieldRef = approvalRejectCType.FieldLinks[approveRejectChoiceField.Id];
    approveRejectFieldRef.Required = true;
    approveRejectFieldRef.DisplayName = "Approve or Reject the task";

    //Actualizamos la columna de Comentarios
    SPFieldLink commentsFieldRef = approvalRejectCType.FieldLinks[approveRejectCommentsField.Id];
    commentsFieldRef.DisplayName = "Comments";
}

//actualizamos el tipo de contenido
approvalRejectCType.Update(true);

Hacemos Deploy de nuestro proyecto y validamos que se hayan creado las columnas de sitio y el tipo de contenido


Listo!

Pueden descargar el código de aquí

Happy Coding!

martes, 20 de septiembre de 2011

Reemplazando el diagrama de Workflow en SharePoint 2010 usando un Delegate Control

Como sabemos, los controles delegados (Delegate Controls) son básicamente contenedores que encapsulan contenido y que SharePoint permite substituir este contenido con contenido personalizado diferente a la implementación predeterminada. Una de sus funciones primordiales es proveer de una manera fácil de hacer Branding de los sitios SharePoint. SharePoint define varios controles delegados dentro de la master page v4.


En este post, les propongo usar esta característica para sustituir (y mejorar) la manera predeterminada en que se muestran los diagramas de los flujos de trabajo en la página de detalle del flujo de trabajo en SharePoint 2010.


Y poder adicionarle un poco de vida!


Primero se debe crear un proyecto nuevo y vacío (obviamente de SharePoint 2010)

Agregamos un usercontrol, en el cual vamos a escribir el código aspx que sustituirá al diagrama de flujo. De hecho este user control tendra el mismo contenido que el WorkflowStatus.ascx, y solo agregaremos una referencia a un archivo js que será el encargado de manipular el diagrama de Visio utilizando la API de Visio Services.

Agregamos una nueva Feature con Scope Site a la solución, la cual se encargará de desplegar el elemento que crearemos en el siguiente paso

La magia viene aquí; agregamos un nuevo elemento a la solución de tipo Empty Element y agregamos la siguiente definición:

El valor del atributo Id y el atributo Sequence son importantes ya que el Id debe coincidir con la definición del control que queremos reemplazar, y el valor de la propiedad Sequence debe ser menor a la especificada en la definición original del control para que tenga precedencia sobre este. A continuación como fue definido originalmente el control en la feature VisioWebAccess

Si quieren saber como poder agregar este tipo de mejoras a los diagramas de Visio, pueden checar dos de mis anteriores posts:

Anticipando algunas de sus posibles preguntas:
Porque utilizar un delegate control solo para adicionar la referencia a un archivo js?
Se podría haber agregado la referencia a este archivo .js desde la Master Page, incluso se puede sustituir el archivo workflowstatus.js que es el que se encarga de manera predeterminada del dibujado de todos los diagramas de Visio para los flujos de trabajo, sin embargo esto es muy invasivo ya que sería un cambio global y de la manera que este post propone el cambio sería a nivel de Sitio, es decir, lo menos invasivo posible.

Happy Coding!

Etiquetas

SharePoint 2010 (38) Microsoft (32) Desarrollo SharePoint (31) Gerardo Reyes Ortiz (27) SharePoint (20) SharePoint 2013 (18) Errores SharePoint (12) México (10) PowerShell (9) Silverlight (8) Visio Services (7) Features (6) MVP (6) Silverlight 3 (6) WebCast (6) Workflows (6) Configuracion SharePoint 2010 (5) D.F. (5) API REST (4) Configuracion SharePoint 2010; (4) Troubleshooting (4) Visual Studio 2010 (4) Visual studio (4) WSS (4) Web parts (4) Apps (3) Comunidad SharePoint (3) Configuración SharePoint 2013 (3) ODATA (3) SharePoint Server (3) SharePoint; Instalación SharePoint; Troubleshooting; Search Service (3) Silverlight 3.0 (3) Silverlight Toolkit (3) WebParts (3) javascript (3) jquery (3) Eventos SharePoint (2) Office 2010 (2) PeoplePicker (2) REST (2) SQL Server (2) Scripting (2) Search Service Application (2) SharePoint Designer (2) UPA (2) UPS (2) Workflows SharePoint (2) host header (2) Apps Development (1) Big Bang (1) CAS (1) CSOM (1) Codeplex (1) CompartiMOSS (1) Configuracion SharePoint 2010; Errores SharePoint (1) Configuracion SharePoint 2010; SharePoint 2010 (1) Custom Actions (1) Custom Editor Parts (1) Delegate Controls (1) Deployment (1) DisableLoopbackCheck (1) Document Library (1) Entrevista (1) Examenes de Certificación (1) Extract WSP (1) FBA (1) FS4SP (1) Fakes (1) Fast Search Server 2010 For SharePoint (1) Fiddler (1) HTTP.SYS (1) HTTPS (1) JSON (1) Language Pack's (1) Latam (1) MAXDOP (1) MCSM (1) MSExpertos (1) MVC (1) Microsoft México (1) Microsoft; Codeplex; Screencast; (1) My Sites (1) SQL Server 2012 (1) SQL Server Reporting Services (1) Screencast (1) Screencast; (1) Service Applications (1) Service Pack (1) SharePoint 2007 (1) SharePoint 2010 SP 1 (1) SharePoint API (1) SharePoint Conference (1) SharePoint Emulators (1) SharePoint Farm (1) SharePoint Health Analyzer (1) SharePoint Magazine (1) SharePoint Online (1) SharePoint Search (1) SharePoint Test (1) SharePoint; Desarrollo SharePoint (1) Shims (1) Simposio (1) Simposio Latinoamericano (1) SkyDrive Pro (1) Soporte Microsoft (1) Templates (1) Tip (1) VSeWSS (1) Virtual Machine (1) Visual Studio 2012 (1) WCF (1) WSS; IIS 7 (1) Web API (1) Web Content Management (1) Web Services (1) Windows 8 (1) Windows Live ID (1) Xml (1) appcmd (1) iOS (1) jqGrid (1) onload function (1)