martes, 24 de julio de 2012

Item Event Receivers en SharePoint Services 3.0

Acabo de pasar una de esas situaciones en las que un componente desarrollado por mi pasa bien por las pruebas en el ambiete de desarrollo y al momento de llevarlo a producción aparecen errores que uno no esperaba y peor aún no entiende. En mi caso fué con un Item Event Handler de SharePoint Services 3.0

Desarrolle un manejador de eventos para el evento ItemDeleted y para llevarlo a producción copie el archivo .dll de mi código lo puse en el Assemblies folder, luego hice los archivos Feature.xml y Elements.xml y hasta aquí todo bien. Puse esos archivos en el panal (SharePoint Hive) y utilizando la herramienta stsadm en la consola de comandos instalé mi Feature.

Pude ver mi Feature en el sitio en donde lo instalé pero al momento de dar clic en el botón para activar boom! error.

Aquí el "descriptivo" mensaje de error:


Input string was not in a correct format.   at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
   at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
   at System.Int32.Parse(String s, NumberStyles style, IFormatProvider provider)
   at Microsoft.SharePoint.SPEventReceiverDefinitionCollection.GetOneEventReceiverFromXml(Object[,] receivers, Int32 i, XmlNode receiverXml)
   at Microsoft.SharePoint.SPEventReceiverDefinitionCollection.GetEventReceiversFromXml(XmlNodeList receiversXml, SPEventHostType hostType, Int32& count, Object& receivers)
   at Microsoft.SharePoint.SPEventReceiverDefinitionCollection.GetFeatureEventReceivers(Int32& count, Object& receivers)
   at Microsoft.SharePoint.SPEventReceiverDefinitionCollection.EnsureEventReceivers()
   at Microsoft.SharePoint.SPEventReceiverDefinitionCollection.get_Count()
   at Microsoft.SharePoint.SPEventReceiverDefinitionCollection.GetSqlToAddEventReceiversToList(SPList list, Byte[] sourceId, SPEventReceiverSource sourceType)
   at Microsoft.SharePoint.SPEventElement.UpdateEventReceiversForList(SPWeb web, SPSqlCommand sqlcmdAppendOnly, GetSqlToUpdateEventReceivers getSqlToUpdateEventReceivers)
   at Microsoft.SharePoint.SPEventElement.ElementActivated(SPFeaturePropertyCollection props, SPSqlCommand sqlcmdAppendOnly, SPWebApplication webApp, SPSite site, SPWeb web, Boolean fForce)
   at Microsoft.SharePoint.Administration.SPElementDefinitionCollection.ProvisionContentTypeAndEventReceiverBindings(SPFeaturePropertyCollection props, SPSite site, SPWeb web, Boolean fForce)
   at Microsoft.SharePoint.Administration.SPElementDefinitionCollection.ProvisionElements(SPFeaturePropertyCollection props, SPWebApplication webapp, SPSite site, SPWeb web, Boolean fForce)
   at Microsoft.SharePoint.SPFeature.ProvisionElements(SPFeaturePropertyCollection props, SPWebApplication webapp, SPSite site, SPWeb web, Boolean fForce)
   at Microsoft.SharePoint.SPFeature.Activate(SPSite siteParent, SPWeb webParent, SPFeaturePropertyCollection props, Boolean fForce)
   at Microsoft.SharePoint.SPFeatureCollection.AddInternal(Guid featureId, SPFeaturePropertyCollection properties, Boolean force, Boolean fMarkOnly)
   at Microsoft.SharePoint.SPFeatureCollection.Add(Guid featureId)
   at Microsoft.SharePoint.WebControls.FeatureActivator.BtnActivateFeature_Click(Object objSender, EventArgs evtargs)
   at System.Web.UI.WebControls.Button.OnClick(EventArgs e)
   at System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument)
   at System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
   at System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument)
   at System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

No tengo la menor remota idea de que se trata este error, busqué en google como todo buen desarrollador responsable de su código pero no enconté nada que me diera una luz. Luego me decidí por llamarles por teléfono a dos buenos compañeros, excelentes desarrolladores para Dynamics CRM y uno de ellos (JLAGG) me hizo ver algo que no había contemplado al inicio porque me irrité y me fuí inmediatamente a buscar solución: sí el error sucede al intentar activar, entonces el problema no esta en el código del assembly (el archivo .dll) sino que debe de estar en alguno de los archivos xml (Feature, Elements)... claro! (vean en el error la parte que puse en negrita, es una función que al parecer sirve para tomar elementos para el Feature y que lee los datos extraídos de algún archivo xml el cual puede ser el Elements.xml). 

Aquí el código de mis archivos xml.

xml version="1.0" encoding="utf-8" ?>
<Feature Scope="Web"
            Title="Server ICF Deleting Event Handler"
            Description="Deletes all related Server CIs from Server ICF at its deletion"
            Id="ECA9EDD7-B30B-4eee-8E46-3D19AFE7928F"
            Hidden="FALSE"
            xmlns="http://schemas.microsoft.com/sharepoint/">
      <ElementManifests>
            <ElementManifest Location="Elements.xml"/>
      ElementManifests>
Feature>


/////////////////////////////////////////////////////////////////////////////

      <Receivers ListTemplateId="100">
            <Receiver>
                  <Name>ICF Event ReceiversName>
                  <Type>ItemDeletedType>
                  <SequenceNumber>SequenceNumber>
                  <Assembly>ICF_EventReceivers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxxxx Assembly>
                  <Class>ICF_EventReceivers.ServerICFHandlerClass>
                  <Data>Data>
                  <Filter>Filter>
            Receiver>                  
      Receivers>
Elements>

Arriba pueden ver el código de ambos, el primero corresponde al Feature.xml y el siguiente a Elements.xml en caso de que no lo hubieran notado.

Sin dar tanto rodeo el error esta en el tag <SequenceNumber><SequenceNumber> el cual no tiene valor alguno, porque lo hice así? bueno pues porque obedientemente seguí un código que encontré donde no pusieron ese valor.

Solo se que este número determina la secuencia de ejecución del Receiver, esto es porque en un sitio pueden haber varios Event Recievers y es por esto que es necesario poner un valor.

Después de poner 1000 como valor y haberlo intentado activar todo funcionó bien.
Morajela: no sigan obedientemente todos los pasos en los blogs que ven, hay que analizar un poco el código ;-)


miércoles, 11 de abril de 2012

jqGrid en SharePoint

Durante las pasadas semanas he estado trabajando en como poder usar un Grid de jQuery para mostrar registros de una tabla de SQL Server dentro de una pagina de SharePoint Services 3.0 y he tenido buenos resultados, seré sincero y diré que no me fue fácil pero sin embargo ya quedo listo.

Decidí utilizar jqGrid, considero que su funcionalidad es muy buena, rápida y ofrece lo que necesitaba.

Descargue los archivos necesarios para el jqGrid, escogí mi tema con jQueryUI y todo lo puse en una biblioteca en mi sitio de SharePoint que llamé "Site Assets".



La pagina donde puse el jqGrid es una pagina con un Layout sencillo, únicamente tiene una zona para WebParts. En esa zona puse dos Content Editor Web Parts (CEWP): en el de arriba puse la tabla y el div que se convierten en el jqGrid y en el de abajo el que tengo vinculado a mi archivo .js donde desarrolle los javascripts.


Script en el CEWP - Grid:



Estoy usando la clase ms-toolbar para el formato de las celdas de la tabla, de esta manera hago que se parezca al formato de SharePoint sin tener que crear mis propios estilos con CSS.

En el botón SaveItem1 y SaveItem2 a pesar de que tengo un alert cuando se carga la pagina y entra jQuery, sobreescribo  el evento onclick con la función que deseo.

//Selector de todos los controles input con id que contenga "SaveItem" y ponerlos en la variable btns
/*
var btns = $('input[id^="SaveItem"]'); 
       //Para cada input encontrado, remover el atributo onclick y luego ligarlo con mi función llamada handler
$.each(btns,function(index){
$(this).removeAttr("onclick", null);
$(this).bind('click',handler);
});
*/

En el boton gobackbutton1 tengo un script que regresa a la pagina anterior para el evento onclick, no necesite sobreescribirlo con jQuery.

Script en el CEWP - jqGrid scripting file:
(Comparto el codigo que me funciono para el jqGrid )

/*
$("#list").jqGrid({
datatype: "json",
mtype: 'GET',
url:'/_layouts/servergriddata.ashx',
jsonReader: {root: 'rows',
page: 'page',
total: 'total',
records: 'records',
repeatitems: false,
id: 'ItemID'},
colNames:['Id','Server Name','Serial #','Asset Tag','Tier 3','Status','Category','Supported'],
colModel :[
{name:'ItemID', width:30},
    {name:'Atrium_Name', width:90},
    {name:'Atrium_SerialNumber', width:60},
    {name:'Atrium_TagNumber', width:60},
    {name:'Atrium_Item_Tier3', width:50},
    {name:'Atrium_AssetLifecycleStatus', width:50},
    {name:'Atrium_NetworkSystemType',width:50},
    {name:'Atrium_Supported',width:55}],
pager: '#pager',
rowNum:25,
rowList:[25,50,75,100],
sortname: 'ItemID',
viewrecords:true,
gridview: true,
height:"555",
width:"700",
multiselect: true,
caption: 'Select from existing server',
loadtext:'Loading, please wait',
caption:'Select the server record you wish to edit'
});
*/

Para sacar los datos del servidor de SQL hice un "handler", pude haber creado un Feature pero la verdad es que me pareció mas engorroso. El handler es un archivo con extensión .ashx y utilize C# para crea toda la funcionalidad de obtención de datos y conversión de objetos a JSON. También ahí puse el código necesario para ordenar, filtrar y paginar el jqGrid.

El archivo servergriddata.ashx después lo copié y pegué en:
C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS

A esta ruta se le llaman "SharePoint hive" o Panal de SharePoint. Ya que lo que necesitaba era recibir una cadena de texto en formato JSON el manejador .ashx fué mi mejor opción. Todo lo "pesado" se realiza en el servidor y no en el cliente, hay otras ventajas tambien de usarlos para SharePoint como el hecho de que son sencillos de hacer funcionar, se crea y una vez que este funcionando como se espera es copiado en el panal de SharePoint y se puede mandar llamar asi: http://servidosSharePoint/sitio/_latyouts/TuHandler.ashx Listo, una belleza!, como lo comentaba antes preferí no usar Features (Caracteristicas) por el engorro de crear los archivos xml y todas las pruebas que iba a estar haciendo no quería estar actualizando la cache a cada rato.

Para hacer mis pruebas con el handler y el jqGrid utilice IE 7 (la mayoría de mis usuarios utilizan este navegador) y Chrome con sus excelentes herramientas para el desarrollador. Con estas podía ver exactamente lo que el jqGrid hacía y como mandaba pedir los datos a mi handler, asi me di cuenta de los parametros y pude decidir como obtenerlos y usarlos.






Cuando obtengo los datos de la tabla de SQL creo objetos y los meto en un arreglo luego utilice el JavascriptSerializer para convertirlos a una cadena JSON, lo que no me daba cuenta es que jqGrid necesita información sobre los datos que esta recibiendo y esta se debe poner en la cadena serializada http://www.trirand.com/jqgridwiki/doku.php?id=wiki:retrieving_data

Finalmente pude mostrar en una pagina de SharePoint (WSS3) un jqGrid con registros de una base de datos de SQL Server. Al final asi es como se vé:


Gracias por leerme. Regalame un comentario.

lunes, 9 de enero de 2012

Vincular lista de SharePoint con Google Maps

Estuve trabajando en un proyecto de corto tiempo para lograr vincular una lista de SharePoint (la versión que tuve para hacerlo fué WSS 3.0) a un mapa de Google, gran parte de mi tiempo la use para conseguir información de otros pera ver como le habían hecho y una  pequeña parte fué para implementar el JavaScript que lograra el objetivo.

Antes de comenzar, si no estas interesado en construir y personalizar tu propio código entonces ve los vínculos siguientes a desarrollos ya hechos y publicados en CodePlex

1.- Display Google Maps in a Sharepoint Web Part using a Sharepoint List
2.-  Google Map WebPart from SharePoint List
3.-  Basic SharePoint-Google-Maps-WebPart for SharePoint-Lists

Yo use el tercero, me gusto y revisando/analizando el codigo aprendí algunas cosas que no sabía, mi problema fué que no hacía lo que yo requería. Sin embargo si vale la pena conocer estos desarrollos.

Personalmente tengo que decir que este reto me encantó, desesperó, emocionó y angustió. Pase mucho tiempo haciendo pruebas con la API 2.0 de Google maps, luego me percaté de que pronto estará fuera y que Google no dará mas servicio a esta por lo que decidí intentarlo con la API 3 (al final lo logre) sin embargo me estaba demorando mucho y en mi desesperación intente inclusive hacerlo con el servicio de mapas de Microsoft llamados Bing Maps a lo cual diré que me dejo mucho de que desear, tan solo en el tema de la velocidad de carga de los mapas fué motivo suficiente para que reintentara con Google o talvez no supe usarlo como se debe para sacarle todo el jugo, este punto después lo voy a intentar solo por curiosidad.

Nota 1: solo en algunos de los pasos que voy a describir voy a explicar a detalle, en la mayoría no pues estaré asumiendo que los lectores tienen suficiente experiencia en SharePoint como para crear una lista personalizada e importar datos de Excel. También aviso que la información aquí contendida es el resultado de haber tomado ideas de otros blogs (los cuales nombraré mas adelante), quiero agradecer en especial a mis buenos amigos Manuel Iván García Sandoval y Luis Angel Gutierrez Guevara por su enorme ayuda con JavaScript, ellos dos son dos de los mejores desarrolladores web que yo conozco.

Nota 2: estoy usando el API 3 de Google, en este artéculo no describo a detalle ninguna de las funciones que uso en mi JavaScript por la razón de que Google ha documentado bastante bien y no tendría caso que yo las repita, sin embargo si gustas enviarme correo electrónico para preguntarme sobre alguna en particular puedes hacerlo. Debido a que no estoy pagando por el servicio de mapas Google me limita a únicamente 5 (cinco) Geolocalizaciones (para decirlo de otra forma poner 5 pines) por búsqueda por lo que si tu deseas mejorar la velocidad de la busqueda de Geolocalización y no tener limite entonces hay que pagarle a Google.

Objetivo y Características


Objetivo: Conectar una lista de SharePoint con información geográfica (dirección, calle, estado, codigo postal, latitud, longitud) a un mapa de Google Maps de manera que cuando la lista se cargue muestre en el mapa "pins" de su posición.

El listado que me dieron lo tuve que "normalizar" y crear una tabla con una relación de uno a varios, entonces lo que me quedó fué una lista con el encabezado o registro padre y otra lista con los registros relacionados o hijos. Además preferí mucho mas hacerlo así pues me dió la posibilidad de crear un combo box para de ahí seleccionar el conjunto de registros que quería ver, algo como un filtrado (vean la imagen 1)

Funcionalidad deseada:
  1. "Pins" para cada lugar o locación
  2. Poder utilizar ya sea la dirección del lugar o su latitud & longitud
  3. Al dar clic sobre el pin del lugar mostrar una ventana (o globo si asi gustan) de información
  4. Al dar clic sobre la dirección en la lista de SharePoint  mostrar una ventana (o globo si asi gustan) de información
  5. En la ventana de información, proporcionar un vínculo para poder editar el registro correspondiente al "pin" del mapa.
Imagenes del resultado final:

1. Así se ve la página principal, en el combo box tengo la lista de registros padre, abajo el webpart (Content Editor WebPart) con el mapa de Google y al final la lista de registros que son filtrados dependiendo del valor del combo.











La siguiente imagen muestra cuando un elemento del combo es seleccionado, primero se carga la lista de SharePoint y esta manda llamar funciones del mapa encargadas de poner los "pins" de cada registro, se crean también los vínculos en el campo de dirección que ayudarán al usuario a localizar el pin deseado.

3. Cuando el usuario da clic en alguno de los "pin" se muestra una ventana de información y en esta hay un vínculo para poder editar el registro de la lista de SharePoint que corresponde al "pin". Los rectangulos rojos muestran donde estan los vínculos: uno en la ventana de info y el otro en el campo de direccíón de la lista


Pasos para la creación de todos los elementos

1. Crear una lista personalizada en SharePoint para los registros padre y cargarla con los datos
1.1 Columnas para lista de registros padre:
  • Titulo/Nombre: single line of text
2. Crear una lista personalizada en SharePoint para los registros hijo y cargarla con los datos
2.1 Columna para los registos, (aqui si es importante tomar en cuenta el tipo de campo pues te va a ahorrar mucho tiempo a la hora de meter el JavaScript y el XSLT)
  • (Padre) Nombre: Lookup (relacionado a la lista padre)
  • Dirección - Multiple lines of text
  • Ciudad - Single line of text
  • Estado - Single line of text
  • Latitud - Single line of text
  • Longitud - Single line of text
MUCHO OJO!, en el campo de Dirección que es único Multiple lines of text, si no quieres problemas en el codigo JavaScript de estar quitando simbolos basura asegurate de que el tipo de texto permitdo sea "Plain Text" ... Ahhh!!! no puedo creer el tiempo que perdí buscando la forma de limpiar el valor de este campo hasta que me puse a ver investigar y encontré que cuando el texto es enriquecido y tiene formato, los valores traen codigo que el explorador convierte en negritas, tipos de letra y demás.


3. Abre la pagina principal con SharePoint Designer (SPD), quita todo web part que no te sirva para este proyecto y agrega un control DropDown List de ASP.NET. Yo lo puse en una celda entre la zona de web parts header y la zona left, preferí hacerlo asi en caso de que necesitara poner algun banner de instrucciones en la zona header.

4. Configura el orgien de datos del control para que este contectado a la lista con los registros padre, ordenalos por el nombre o título del registro y activa la casilla de "Enable AutoPostBack" del control

5. Agrega en la zona left un web part de tipo Web Content Editor este será el que muestre el mapa, mas adelante describiré como se configura.

6. Inserta debajo del web part del punto anterior un Data View, este contendrá los registros hijo relacionados y que serán filtrados cuando el usuario seleccione algun elemento del DropDown List. Configura el Data View para que muestre los datos que necesites, yo puse los siguientes:
  • Nombre del registro padre
  • Dirección
  • Ciudad
  • Region
  • Estado
  • Codigo Postal
También configuré para tener funcionalidad de filtrado y agrupación, hay muchos usuarios que les gusta poder hacer esto cuando hay muchos registros en la vista de datos.
Si no sabes como configurar una vista de datos, por favor ve estos vínculos de mi blog: 

Sigue la parte mas dificil y te recomiendo que la hagas unicamente hasta que termines de configrar el Data View. Tenemos que agregar codigo XSL que se encargará de mandar llamar la función que pondrá el pin en el mapa.

Da clic en el Data View y cambia a la vista de codigo en SPD, localiza la línea de código:

Debajo de esta agrega (y de ser necesrio modifica) este código:


Lo que esta plantilla o  "Template" hace es que para cada registro de la vista de datos, revisa sí el registro contiene información de latitud y longitud y dependiendo del resultado manda llamar a la función showLatLonLocation o la funcion showAddress. Los valores para los parametros que mando a las fucniones se obtienen utiliznado XSL.

En la función showAddress el primer parametro es el que uso para poner el título al pin, el título aparece desplegado como un "Tool Tip Text" cuando el usuario pone el puntero del ratón sobre el pin y lo necesito tambien para la función que localiza el pin cuando el usuario da clic sobre la dirección. El titulo del pin esta compuesto de-- Dirección + Ciudad + Estado  Cada valor separado por una coma. 

Otro "Mucho Ojo" aqui, debido a que el campo de dirección es de múltiples lineas de texto necesitamos normalizarlo para remover los espacios no deseados, no hacer esto genera codigo que no es comprensible. Para lograrlo se utiliza la función normalize-space()

7. Modifica el campo de dirección para que trabaje como vínculo a la función de localización de "pin" que mas adelante describo. En el cuadro de selección formato del campo cambia el valor de "Format as:" a "Hyperlink", te aparecerá un mensaje de aviso de cambio de formato solo da clic en Aceptar y luego selecciona "Hyperlink options...", en el cuadro de dialogo verás que se puede configurar el texto que va a mostrarse en la vista de datos y el texto del hipervínculo el cual hay que cambiar para que mande llamar a la fucnión: 
javascript:markerClicked('{@Address},{@City},{@State}','{@Facility}','{@Address}','{@IPRanges}','{@ID}')

Con este codigo se manda llamar a la función markerClicked y se le envian los parametros para la búsqueda del "pin" que corresponde a la dirección seleccionada asi como los textos que aparecerán en la ventana de información del "pin".

Ahora hay que conectarlo con el DropDown List para que los registros del Data View sean filtrados. Selecciona el Data View y crea un nuevo parametro que reciba el valor del control DropDown, tienes que fijaste en el nombre del control, en mi caso el control se llama DropDownList1.

El nombre del parametro lo necesitarás para la creación del filtro. Una vez que hayas creado el parametro, vuelve a seleccionar el Data View y ahora configura el filtro agregando uno nuevo y seleccionando el campo del nombre del padre, el que creaste como tipo Lookup.

Con el parametro y el filtro cada vez que el usuario cambie de selección del DropDown el Data View será actualizado y el mapa también.

8. A continuación crea una biblioteca de documentos si tu sitio aun no tiene una. En esta se almacena el documento de texto que contiene las funciones para que trabaje el mapa de Google, esta es la parte divertida pues ahi podrás modificar e incluir la funcionalidad que desees, yo me demore algun tiempo en entender las funciones porque estuve analizando los codigos de las paginas de ejemplo de los mapas que pone Google, hay cosas muy interesantes, de ahi se me ocurrió la idea de poder seleccionar el "pin" dependiendo de la dirección y enriquecer la experiencia del usuario.

A continuación pega todo el codigo que sigue, guardalo en un archivo .txt y subelo a tu biblioteca de documentos. Luego que lo tengas ahí abre tu explorador y edita la pagina, localiza el Web Content Editor y modificalo. Tienes que poner en el campo "Content Link" la dirección de tu archivo de texto ya en la biblioteca.

El código de el archivo de texto es el que pongo a continuación:
Finalizo este articulo agradeciendote por haber leído hasta aquí, regalame un comentario. Dejo las direcciones web de los blogs que me sirvieron para armar mi proyecto: