viernes, 6 de marzo de 2015

Search.FindOne() arroja el error: "An operations error occurred". in ASP.NET

Utilicé para un proyecto el DirectorySearcher para traer información a través de LDAP del Active Directory, tenia una función para traerme los grupos a los que pertenece un usuario con el siguiente código:

DirectorySearcher search = new DirectorySearcher(entry);
search.SearchScope = SearchScope.Subtree;               
search.Filter = "(&(objectClass=user)(SamAccountName=" + usuario + "))";
search.PropertiesToLoad.Add("memberOf");
SearchResult result = search.FindOne();

Sin embargo, al publicar el codigo en un servidor, la ultima linea me mandaba el siguiente error: An operations error occurred.

Después de revisar un rato en Internet, las dos cosas que me funcionaron para solucionar el problema fueron:

1. Es necesario deshabilitar la autenticación anónima y la personalización (ASP.NET Impersonation), en mi caso, como reviso el usuario de AD que entra en la aplicación solo tengo activado la autenticación por Windows.

2. El AppPool de la aplicación debe correr con la cuenta de NetworkService. Para cambiar la cuenta hay que dar click derecho sobre el AppPool, Seleccionar "Opciones avanzadas", en Identidad (Identity) y en la lista desplegable, seleccionar "NetworkService".




martes, 12 de marzo de 2013

Error: 'The user does not exist or is not unique' - SharePoint 2007

Recientemente tuve un problema con SharePoint 2007, donde hubo una actualización del usuario en Active Directory lo que ocasionó que, SharePoint ya no actualizara el usuario, por lo que me vi en la necesidad de modificarlo directamente en la base de datos.

En primera instancia, sé que los usuarios son almacenados en la tabla dbo.UserInfo, por lo que procedí a buscar al anterior usuario:

SELECT * FROM dbo.UserInfo WHERE tp_Login = 'DOMAIN\olduser'

Una vez revisado el usuario, hice un "Update":

UPDATE dbo.UserInfo SET tp_Login = 'DOMAIN\newuser' WHERE tp_Login = 'DOMAIN\olduser' AND tp_ID = '999'

Una vez haciendo esto, parecia estar todo correcto, el usuario al momento de agregar documentos, o iniciar sesión siempre veía a DOMAIN\newuser.

Sin embargo, tuve al agregarlo a un grupo, fue donde me apareció el siguiente error:

'The user does not exist or is not unique'.

Además noté que al buscar el usuario en "Select People and Groups" por nombre me aparecía algo como:

Display Name Title Deparment E-mail Account Name
Full Name Engineer TI newuser@domain DOMAIN\newuser
Full Name Engineer TI newuser@domain DOMAIN\olduser

Incluso si hacía un 'SELECT' sobre la tabla de UserInfo buscando al antiguo usuario no lograba verlo. Buscando en Internet, hubo muchas soluciones desde actualizar/migrar la información del usuario utilizando stsadm, o borrar la caché de SharePoint pero no me funcionó. Siendo un poco obvio, supuse que en algún lado debería guardar al anterior usuario, por lo que buscando un rato en las tablas de SharePoint y con la siguiente consulta encontré después de algunas horas el registro del usuario anterior en otra tabla:

SELECT * FROM AllUserData WHERE nvarchar3 = 'NHELIOS\olduser'

Por lo que nuevamente realice un update:

UPDATE AllUserData SET nvarchar3 = 'DOMAIN\newuser' WHERE nvarchar4 = 'DOMAIN\olduser'

Nuevamente hice la búsqueda del usuario y ahora si me apareció una única vez:

Display Name Title Deparment E-mail Account Name
Full Name Engineer TI newuser@domain DOMAIN\newuser

Finalmente, agregué al usuario al grupo que necesitaba y pude agregarlo satisfactoriamente. Debo mencionar que esta solución implica modificar directamente la base de datos, solución que debería ser la última opción ya que modificar la base de datos sin tener el cuidado correspondiente puede afectar la funcionalidad de cualquier aplicación.

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:

viernes, 16 de diciembre de 2011

Registros duplicados

Recientemente estuve trabajando en localizar y eliminar duplicados de registros en uno de los inventarios que manejamos con SQL Server 2005.

La premisa es muy sencilla: "Localiza y elimina los registros duplicados"

La tabla es sencilla sin muchas columnas, simplemente es un inventario de impresoras donde almacenamos el tipo de impresora, la marca y modelo, su dirección IP y dirección MAC, número de serie y el puerto por donde sale a la red, bien hasta aquí.

El problema es que los usuarios nos avisaron de que había registros donde se veían direcciones MAC duplicadas y también encontraron Numeros de Serie duplicados, me puedo imaginar al pobre usuario intentando actualizar su inventario para darse cuenta de que hay discrepancias y que ahora no esta seguro "cual registro es el bueno".

Sin duda eliminar registros duplicados puede ser un trabajo muy sencillo o sumamente complicado y digo esto por la razón de que si sencillamente quieres quitar duplicados sin revisar o calificar los registros para saber "cual es el bueno" entonces la solución esta a un 'tris' de tus dedos, pero si vas a revisar cuales son esos duplicados, analizar los valores de las columnas para decidir cual es registro con el que te quedas y finalmente borrar lo que decides que son registros basura entonces todo toma un sentido diferente y la tarea se vuelve complicada por no decir que consume mucho tiempo  (al menos hablando desde mi experiencia y asegurando que hice todo lo posible por rápidamente entender los mejores pasos a tomar para resolver este divertido reto).

Continuando, describo las opciones que tuve y cual tome así como los pasos que seguí para mejorar el inventario.

Debido a que yo trabajo con tecnología de Microsoft mis opciones se convirtieron en usar cualquiera de las siguiente herramientas:

  • Excel 2007
  • Access 2007
  • Lenguaje Transact SQL
  • Integration Services (SSIS)
Lo primero que pense fué "Voy a automatizar esto con SSIS y en un santiamén lo resuelvo" (ja! iluso) y encontre este excelente artículo de Todd McDermid titulado "Eliminating Duplicate Primary Keys in SSIS". 

No quise utilizar ni Excel ni Access porque sabía que habría mas trabajo manual del que pudiera soportar, sin embargo no descarto que se puedan utilizar y que puedan ayudar mucho en la tarea de encontrar duplicados y eliminarlos de la tabla. Tal vez si el trabajo es hecho por mas de una persona se pueda facilitar el proceso, en mi caso solo estaba yo para resolver el problema y por eso es que necesite ayuda de algo mas sofisticado como Integration Services.

Solo un comentario mas sobre Excel 2007, sus tablas dinámicas si me sivieron para filtrar los registros y con esto me pude dar una mejor idea de los pasos que tomaría para automatizar el proceso de elimado de duplicados, ya eso es todo ahora si a continuar...

Aviso importante: Me voy a permitir describir a continuación y en resumen lo que Todd ahí comparte con respecto al tema de "deduplicación de registros", cualquier buena idea aquí mostrada la tome a partir de su publicación.

Usando SSIS hay 3 formas para eliminar duplicados de una tabla: La super fácil, La moderadamente dificil y La muy dificil.

La super fácil consta de sencillamente poner en un "Data Flow" un componente de conexión a la tabla donde están los registros duplicados, luego conectarla a un componente "Sort" para ordenar y en este es donde debes utilizar la columna donde se sabe que estan los datos duplicados. En el editor del componente Sort se activa la casilla de elimnar duplicados y finalmente se conecta a un componente de destino.




La moderadamente difícil (que fue la finalmente yo tome como base para comenzar mi trabajo) utiliza una columna donde se califican los registros. El concepto a primera vista es sencillo de entender, digamos que se tiene un registro que esta repetido tres veces, a partir del valor o los valores de los campos de esos registros se crea una columna con la calificación y esta es la que servirá para tomar el mejor calificado y conservarlo. Este modo es solo bueno sí hay la forma de determinar esta "calificación", yo en realidad no hice mucho aqui y mi punto de partida fué tomando el campo de clave primaria (priamry key) viendo cual era el registro mas nuevo para borrar los mas viejos, asi de sencillo.

Bien, Todd describe por pasos como crear ese paquete así:

  1. Después de crear el componente de origen de datos, se envían esos datos aun componente de "Derived Column" en donde se crea esta columna de calificación la cual se utilizará mas adelante.
  2. Luego se conecta a un componente "Multicast" para hacer una copia de estos datos. Se utilizarán dos salidas que para efectos de simplicidad se describen como Salida A y Salida B.
  3. La Salida A se conecta con un componente "Aggregate" para realizar la operación de agregación "Group By"  UNICAMENTE a la(s) columna(s) donde estan los datos repetidos. En mi caso fué la dirección MAC de la impresora. Ojo!, al conectar la Salida A al componente Aggregate las demás columnas y sus datos se perderán, no hay problema y por eso reitero unicamente agrupar la(s) columnna(s) que tiene los datos repetidos.
  4. A continuación se envía la Salida A a un componente "Sort" y se ordena primero por la columna de clave principal (primary key) y después por la columna de calificación. Es necesario hacer esto pues el componente de Merge Join requiere que haya al menos una columna ordenada.
  5. Conectar la Salida A a un componenete Merge Joing. Ponerlo en LEFT Side.
  6. Enviar la Salida B a un componente "Sort" y ordenar por la columna donde estan los datos repetidos. NO seleccionar la casilla para remover duplicados.
  7. Conectar la Salida B al componente Merge Join, configurarlo en RIGHT Side.
  8. Editar el componente "Merge Join", hay que dejar la configuración predeterminada de "Inner Join" esto hace que el resultado sean solo aquellos registros donde haya coincidencia en la columna que esta ordenada. Finalmente, se activa la casilla en los campos de la Salida B o lo que entro por RIGHT Side del componente.
La teoría y los pasos ahi estan, la siguiente imagen muestra lo que yo hice, puse tres secciones enmarcadas en rojo para describir los pasos principales del proceso, primero se obtienen los datos duplicados, se califican en el componente "Derived Colunn" y se pasan al componente "Multicast". Despues se trabaja con cada una de las salidas donde la Salida A se agrupa por las columnas donde haya datos repetidos y se ordenan. La Salida B unicamente se ordena por la columna donde haya datos duplicados.

Ahora bien, en mi caso decidi primero atacar aquellos registros donde hubiera unicamente dos registros duplicados. Lo decidi asi porque al analizar los datos en la tabla dinámica de Excel me di cuenta de que en esas dos repeticiones podía tomar el registro donde la clave principal fuera mayor y eliminar el otro registro. Por esta razon agregue el componente "Conditional Splilt" para tomar unicamente los registros donde hubiera dos repeticiones.
Despues de hacer la selección de los registros con solo dos duplicados, se pasan los datos por el componente Merge Join para efectuar la operación Inner Join, el resultado es entonces enviado al tercer paso del proceso. En este paso vuelo a pasar los datos por un componente "Aggregate" porque es ahi donde seleccione con la función MIN el registro que tenía que eliminar. 
El componente "Multicast" que viene despues tiene como objetivo crear una copia de esos registros que iba a borrar. Una copia fué enviado a un archivo plano para tenerlo como referencia en caso de que me hubiera equivocado y necesitara restaurarlos y la otra copia se fué a un componente "OLE DB Command" donde escribi un "query" bastante sencillo:

DELETE FROM Tabla_Impresoras WHERE ID = ?

Aqui la imagen de como se ve mi paquete:




Algo que no comente fué que en el componente "Aggregate" en el cuadro 2, hay que agrupar por la columna donde estan los datos duplicados con la función de agregación "Group By" y luego hay que incluir la columna de la clave principarl con la función de conteo, de otra forma el componente de agregación del cuadro 3 no va a funcionar.

Aquí la imagen de como quedo el mío:




La muy difil no la comento mucho pues no use este metodo sin embargo la propuesta de Todd es interesante. Para esta opearción se utiliza un componente "Script" que funciona de manera asíncrona.
Se agregan las columnas que se desea pasar al componente y luego configura las columnas de salida. Finalmente hay que tirar codigo en el "Script" que contenga la logica necesaria para la seleccionar los registros que estan duplicados.

Espero esta entrada haya sido provechosa, termino agradeciendote por haber leido hasta aqui y comentando que además de estas opciones, hay personas que estan haciendo deduplicación con las fucniones RANK() y ROW_NUMBER() de SQL.

Yo hice algunas pruebas, aqui mi script


SELECT ROW_NUMBER() OVER (PARTITION BY MACAddress ORDER BY SerialNumber ASC) AS RowNumber, MACAddress, SerialNumber, IPAddress
FROM Printer_Manual_Data
WHERE MACAddress IS NOT NULL


Saludos y suerte!

viernes, 24 de junio de 2011

Vincular una lista de SharePoint con una tabla de SQL

Vincular una lista de SharePoint con una tabla de SQL fué un reto la primera vez que me lo sugieron. Normalmente no me gusta mezclar las cosas, sí las tablas estan en SQL pues que se queden todas ahi y las sacamos a través de reportes, sí son listas de SharePoint entonces que se queden ahi y las sacamos con vistas o exportamos a Excel.

Antes de comenzar tengo que aclarar que en este post no estoy poniendo instrucciones paso a paso para que hagas este ejercicio, estoy simplemente resumiendo el trabajo que hice. Si en tu caso te urge mas info y quieres ponerte en contacto conmigo mandame correo-e a fabian.munozag@gmail.com

El caso fúe el siguiente: Tengo una base de datos en SQL Server 2005, quiero relacionar un registro de una de esas tablas a un registro en una lista de SharePoint.

Porque asi?, porque nuestros usuarios les gustan los formularios de SharePoint además de que es muy comodo crear tus tipos de contenido, ponerlos en una lista y de ahi crear tus registros.

Estos son los pasos que segui:

1.- Crear el tipo de contenido: crear tipos de contenido es muy util porque lo puedes reutilizar, además es heredable a sub-sitios, sí haces cambios o actualizaciones todas las listas que lo usen serán actualizadas, es una forma efectiva de hacer el trabajo de administración de registros. Cuando hagas esto asegurate de que en tu tipo de contenido hayas incluido el campo de ID que va a guardar el ID del registro en tu tabla de SQL.






2.- Agrega tu tipo de contenido a tu lista. Otra de las ventajas de trabajar con tipos de contenido es que puedes vincular varios y de diferentes tipos a tu lista ;-) Mira la imagen abajo y veras que hay el tipo de contenido Hardware y el tipo PC Checklist Base.





3.- Abre su SharePoint Designer y conectate al sitio en cuestión.
4.- Crear una biblioteca para guardar Web Part Pages, esto es importante porque este tipo de contendio son paginas .ASPX ya con las zonas para meter Web Parts. Las vistas de datos que vas a agregar son Web Parts.
5.- Crea una conexión a tu tabla en SQL Server desde SharePoint Designer. En mi post anterior comente como hacer una y las cosas que tienes que cuidar sí no te quieres enfrascar en horas de tedio buscando porque no funciona, aqui esta la liga Crear conexión a SQL desde SPD

6.- Bien aqui llega la parte que mas me gusta. En la paginas, pones a la izquierda el Data View con el detalle de tu registro de la tabla en SQL y a la derecha el detalle de los registros relacionados que estan en la lista de SharePoint. Aqui va la imagen y abajo la explicación.

En el data view que ves a la derecha estoy mostrando unicamente un solo registro, esto con la idea de que los usuarios vean el registro xxxxxx y a su derecha los elementos relacionados. Para que se vea asi, tienes que configurar el data view y cambiar su layout a Repeating form with border, si no me equioco es el cuarto de arriba hacia abajo en la pestaña Layout del cuadro de dialogo Data View Properties. Luego te vas a la pestaña Paging y en Display items in sets of this size: y pones un 1. Ojo hay que configurar las opciones de filtrado para muestre el registro que el usuario seleccionó desde la lista donde aparecen todos los regirstros de la tabla de SQL, el ID de ese registro va a venir en el QueryString ya que ahi lo configuré.

Talvez ya para este punto te tengo todo confindido(a), por favor sigueme leyendo y mas adelante te ire aclarando el punto ;-)

Lo que sigue es crear el otro Data View, solo que este va a conectado a tu lista de registros en SharePoint, el procedimiento es casi igual solo que mucho mas sencillo porque solo tienes que arrastrar la lista de hacia el Web Part Zone y SharePoint Designer hace el resto:



Por ultimo hay que configurar el filtro de este Data View, la idea es que muestre solo aquellos registros relacionados con el Data View de la izquierda (aquel que trae el registro de tu tabla en SQL). Primero selecciona el Data View de tu lista de SharePoint (el que acabas de insertar), luego abre el menu contextual de Common Data View Tasks y da clic en la primera opción: Filters, esto abre otro cuadro de dialogo con las opciones para que configures tu filtro. Te muesto la imagen para que lo veas mas claramente:



En la imagen puedes ver que estoy seleccionando el campo (Field Name) Workstation ID, ese viene del tipo de contenido que creaste en los primeros pasos y por eso es tan importante. Cabe mencionar que ya que el ID en mi tabla de SQL es un campo numerico el tipo de dato de mi campo en la lista de SharePoint también es numerico y con cero decimales, eso es importante.

Regresando al tema del filtro, esta pagina que estamos creando la mando llamdar desde otra pagina. Asi es, perdona si te lo estoy haciedo mas confuso pero la razón es esta. Cuando mis usuarios entran a SharePoint, quieren ver primero que nada una lista con los registros de la tabla de SQL y luego si quieren ver el detalle de un registro cualquiera dan clic y los mando a esta pagina. Cuando los mando a esta página, envio en la URL o QueryString el ID del registro y esto me sirve para filtrar los webparts. El post de donde saque la idea y que viene muy bien redactado esta aqui

Aqui la imagen de los filtros en mis web parts:


Esta imagen muestra el filtro configurado en el Data View de la izquierda, el que muestra un solo registro de la tabla de SQL.


 Este otro es la configuración del parametro que servirá para filtrar el del web part de la derecha, que tiene los registros de la lista de SharePoint.

Basicamente estos son los pasos, nuevamente me disculpo por lo posiblemente confuso de mi explicación, mandame correo electronico y con gusto te ayudo.