Definir Lookup Site Columns desde una Feature

Otra de las sorpresas que nos guarda Sharepoint son los campos lookup. Los campos lookup nos permiten tener desde las listas referencia a valores de otra listas. El problema viene cuando queremos crear una feature que contenga un tipo de campo Lookup para poder exportarlo a otro entorno. Esto se debe a que la definición de este tipo de campos lleva asociada el GUID de la lista y el GUID del web site donde se aloja, por lo que al exportarlo a una colección distinta no podremos hacerlo ya que la instancia de la lista de referencia tendrá un GUID distinto.

Googleando un poco he encontrado varias soluciones a esta carencia de sharepoint:

  • Definición del campo a nivel de lista en lugar de un site column
  • Creando un Feature Receiver que recalcule el id de la lista de referencia

 

Definición del campo a nivel de lista en lugar de un site column

Según algunas referencias de internet (http://blogs.msdn.com/joshuag/archive/2008/03/14/add-sharepoint-lookup-column-declaratively-through-caml-xml.aspx) en lugar de indicar el GUID de la lista en el campo «List», podemos indicar la URL de la lista. Lo malo es que lo que no dicen es que esto no funciona con los site column’s, ya que solo es válido para columnas que definamos asociados a la propia lista.

 

Creando un Feature Receiver que recalcule el id de la lista de referencia

Consiste en recalcular el GUID de la lista de referencia al activar la característica.

Crearemos nuestro site column de forma normal y en la propiedad «List» indicaremos el nombre de la lista (no url). Esto hará que se cree el site column pero con una referencia errónea, por lo que al intentar utilizarlo nos dará un error al no encontrar la lista. Por lo que asociaremos a nuestra feature un FeatureReceiver que busque el guid de la lista en el sitio donde se está creando y lo reasigne al campo.

<?xml version=»1.0″ encoding=»utf-8″?>
<Elements xmlns=»http://schemas.microsoft.com/sharepoint/»>
      <Field Type=»Lookup»
      DisplayName=»Locations»
      Required=»FALSE»
     List=»LocationsList»
      ShowField=»LinkTitleNoMenu»
      UnlimitedLengthInDocumentLibrary=»FALSE»
      Group=»COBDemo»
      ID=»{6F26090A-C2AE-44d7-8F70-EE1663FE29F1}»
      SourceID=»{8c066b26-5a3e-4e1b-85ec-7e584cf178d7}»
      StaticName=»Locations»
      Name=»Locations»
    />
    </Elements>

 

<Feature xmlns=»http://schemas.microsoft.com/sharepoint/» Id=»B188E645-F989-4baa-A109-D2313648432E»
         Title=»COB.Demo.ListBasedSiteColumns»
         Description=»Creates one of more site columns which get their data from lists (lookup fields).
          The main definition of the columns is in CAML schema, but a feature receiver is used to fix up
          the reference to the list. Both the feature and assembly deployment (to the GAC) are handled by COB.Demo.ListBasedSiteColumns.wsp.»
         Scope=»Site» Hidden=»FALSE»
         Version=»1.0.0.0″
         ReceiverAssembly=»COB.Demo.ListBasedSiteColumns, Version=1.0.0.0, Culture=neutral, PublicKeyToken=417a990752680f01″
         ReceiverClass=»COB.Demo.ListBasedSiteColumns.FeatureReceiver»
>
  <ElementManifests>

…………….
  </ElementManifests>
  <Properties>
  </Properties>
</Feature>

 

Existe un proyecto en codeplex SP2007LookupFields que implementa el FeatureReceiver que lo hace por nosotros. Solo tenéis que descargarlo, instalar el assembly y asociarlo en nuestra Feature.

Aunque existe otra solución que mejora  SP2007LookupFields que hace que no tengas que indicar la ruta del fichero xml con la definición: http://mexicanratdog.wordpress.com/2007/10/01/create-lookup-site-columns-target-lists-through-a-feature/ ,en este ejemplo utilizan la propia definición de la Feature y buscan todos los campos del Tipo Lookup y les intenta cambiar el valor de la propiedad «List». Muy recomendable !!!

 

Solo una pequeña cuestión en este proyecto, y es que tiene un pequeño error que hace que se pueda realizar un Dispose sobre el RootWeb, el trozo de código que debéis corregir es el siguiente:

En lugar del código siguiente:

using (currentWeb)

{

……..

}

 

lo sustituiremos por :

 

try{

………

}finally{

if (!currentWeb.IsRootWeb)

currentWeb.Dispose();

}

 

A la hora de modificar el campo lookup desde el FeatureReceiver en ocasiones puede que tengamos problemas al borrar y volver a crear el site column ya que puede que tengamos una instancia y no podamos eliminarla. Para solucionarlo podemos reemplazar el código de la función createLookupColumn por el siguiente basado en el artículo: Field Definition Schema

 

private void createLookupColumn(SPWeb web, string sColumnDefinitionXml, string sColumnName)

{

SPFieldLookup lookupColumn = (SPFieldLookup)web.Fields[sColumnName];

            lookupColumn.SchemaXml = sColumnDefinitionXml;

            lookupColumn = (SPFieldLookup)web.Fields[sColumnName];

            lookupColumn.LookupWebId = web.ID;

            lookupColumn.Update();

}

Cómo pasar parámetros a Upload Form en Sharepoint

Para subir documentos a las librerías de documentos de Sharepoint disponemos de la página Upload.aspx, esta página consiste en una Application page que se ocupa de cargar el documento en la librería indicada por parámetro para a continuación redireccionar a la página de edición (EditForm) de la librearía.

En ocasiones nos encontramos con librerías de documentos enlazadas a una lista mediante un campo Lookup, en la página DispForm querremos entonces mostrar los detalles de la lista y los documentos cuyo campo Lookup tenga el mismo valor que el elemento de la lista visualizando. Hasta ahí fácil, solo tenemos que conectar las listas. El problema viene cuando queremos que desde esa página Dispform se puedan subir documentos, pero que directamente en la página EditForm seleccione automáticamente el campo Lookup en lugar de tenerlo que seleccionar.

image

Para hacerlo modificaremos el menú de la librería de documentos en la página DispForm para que pase por parámetro a la página Upload el identificador del elemento seleccionado.

Para personalizar nuestra barra de herramientas podremos o bien customizar el botón de nuevo o cargar de la librería o personalizar el toolbar de la librearía en la página DispForm. En micaso he preferido personalizar el Toolbar de la página.

 

Desde el designer indicamos en las propiedades de la vista de datos la opción «Barra de harremiantas de Sharepoint», para a continuación editar por código nuestra toolbar.

image

A continuación eliminamos lo botones estándar que nos añade y añadimos el nuestro que redirija directamente a la página Upload

<SharePoint:FormToolBar ID=»FormToolBar1″ runat=»server» ControlMode=»Display» >
    <Template>
         <a href=»/_layouts/Upload.aspx?List={$ListID}&amp;IdElemento={string($IdElemento)}»>Subir</a>
    </Template>
</SharePoint:FormToolBar>

Fijaros en varias cosas:

Por un lado he indicado en la url la página «/_layouts/Upload.aspx» en lugar de «NombreDeLibrearíaDocumentos/Upload.aspx», esto es porque la página de nuestra librearía lo que hace es reenviar directamente a la página layput con los parámetros que necesite, pero si le pasamos un parámetro a la página upload de la librearía reenviará los parámetros directamente sin tener en cuenta los valores. Es decir que enviaría una cosa parecida a la siguiente imagen, por lo que tendríamos que leer el parámetros con el carácter «?» delante.

clip_image001[4]

 

Por otro lado, he utilizado he definido un parámetro IdElemento con el valor de la variable $IdElemento. Esta variable la he definido mediante un parámetro en la definición de la conexión entre los webparts.

clip_image001[6]

Esta opción la disponemos al crear la conexión desde la opción «Crear nuevo parámetro».

image 

 

Al seleccionar nuesta acción del ToolBar nos reenviará a la página Upload con nuestro id seleccionado. Además cuando subamos nuestro documento, la propia página Upload nos redireccionará a la página EditForm con los valores de nuestro parámetros de forma automática.

image

 

Ahora solo nos queda leer el parámetro en la página EditForm y completar el campo lookup. Para hacerlo he utilizado javascript editando la página EditForm:

 

<script type=»text/javascript»>

    // This javascript sets the default value of a lookup field identified

    // by <<FIELD DISPLAY NAME>> to the value stored in the querysting variable

    // identified by <<QUERYSTRING VARIABLE NAME>>

    // Customize this javascript by replacing <<FIELD DISPLAY NAME>> and

    // <<QUERYSTRING VARIABLE NAME>> with appropriate values.

    // Then just paste it into NewForm.aspx inside PlaceHolderMain

    _spBodyOnLoadFunctionNames.push(«fillDefaultValues»);

    function fillDefaultValues() {

      var qs = location.search.substring(1, location.search.length);

      var args = qs.split(«&»);

      var vals = new Object();

      for (var i=0; i < args.length; i++) {

        var nameVal = args[i].split(«=»);

        var temp = unescape(nameVal[1]).split(‘+’);

        nameVal[1] = temp.join(‘ ‘);

        vals[nameVal[0]] = nameVal[1];

      }

      setLookupFromFieldName(«lista1», vals[«IdElemento»]);

    }

    function setLookupFromFieldName(fieldName, value) {

      if (value == undefined) return;

      var theSelect = getTagFromIdentifierAndTitle(«select»,»Lookup»,fieldName);

    // if theSelect is null, it means that the target list has more than

    // 20 items, and the Lookup is being rendered with an input element

      if (theSelect == null) {
alert(«null»);
        var theInput = getTagFromIdentifierAndTitle(«input»,»»,fieldName);

        ShowDropdown(theInput.id); //this function is provided by SharePoint

        var opt=document.getElementById(theInput.opt);

        setSelectedOption(opt, value);

        OptLoseFocus(opt); //this function is provided by SharePoint

      } else {

        setSelectedOption(theSelect, value);

      }

    }

    function setSelectedOption(select, value) {
    alert(«Paso1»);

      var opts = select.options;

      var l = opts.length;

      if (select == null) return;

      for (var i=0; i < l; i++) {

        if (opts[i].value == value) {

          select.selectedIndex = i;

          return true;

        }

      }

      return false;

    }

    function getTagFromIdentifierAndTitle(tagName, identifier, title) {

      var len = identifier.length;

      var tags = document.getElementsByTagName(tagName);

      for (var i=0; i < tags.length; i++) {

        var tempString = tags[i].id;

        if (tags[i].title == title && (identifier == «» || tempString.indexOf(identifier) == tempString.length – len)) {

          return tags[i];

        }

      }

      return null;

    }

    </script>

 

Mediante el script _spBodyOnLoadFunctionNames.push(«fillDefaultValues»); indico que se ejecute la función  fillDefaultValues al cargarse la página. A continuación se leen los parámetros proporcionados por la querystring, se selecciona el valor del parámetro IdElemento en el control «list1» (que corresponde con mi campo lookup) mediante el script setLookupFromFieldName(«lista1», vals[«IdElemento»]);

 

image

 

Espero que os haya servido. Ale a currar que ya es hora!!

Ghosted o unghosted, ¿Quién corre más?

Siguiendo con el anterior artículo «Page Templates con ayuda del Sharepoint Designer«, he caído en la cuenta de la cantidad de personsalizaciones que se hacen sobre sharepoint sobre entornos de producción sin que seamos conscientes de cómo afecta esto a nuestros sistema.

Esta claro que una personalización para un sitio con una relativa carga de usuarios es incluso hasta aconsejable ya que el coste en desarrollo podría penalizarnos demasiado. Pero si lo que nos estamos planteando es el desarrollo de una página corporativa o extranet, deberíamos tener en cuenta que una gran volumen de usuarios podría provocar una denegación del servicio.

Para ver como afecta al rendimiento la forma de diseñar la solución he estado realizando una serie de pruebas sobre un wss3. Para ello he creado un aplicación web con una colección y dos listas con unos mil elementos y varios campos con valores aleatorios. Una vez creadas las listas, realicé varias pruebas:

  • Una página ghosted, sin editar ni customizar
  • Una página ghosted con edición de contenido
  • Y una página unghosted

 

La prueba de carga consistía en realizar una consulta a todos los elementos de la lista llamando a AllItems, y después consultar un elemento de la lista distinto en cada test, con un incremento de carga progresivo de 50 usuario cada 30 segundos por un tiempo máximo de 10 minutos. Estas pruebas se han repetido varias veces cada una y reiniciando el servidor en cada paso.

No os fijéis en los resultados numéricos absolutos, sino en las diferencias entre cada escenario. Esto se debe a que las pruebas están realizadas sobre un wss3 contra una bbdd alojada en la misma máquina, por lo que he tenido que bajarle los hilos del workerprocess para que deje tiempo de proceso al SqlServer.

 

Una página ghosted, sin editar ni customizar

Podemos observar como el proceso se ha comportado relativamente bien hasta el minuto 6’45, donde ha empezado a aumentar el tiempo de respuesta producirse errores de denegación de servicio.También se aprecia como ha conseguido mantener los tiempos de respuesta aun con una carga elevada. Aunque los errores han ido creciendo poco a poco no han adquirido un número elevado teniendo en cuenta que tenía una carga de unos 800 usuarios, por lo que la respuesta a los usuarios sería relativamente aceptable.

 

image

 

Una página ghosted con edición de contenido

Vemos como en este caso los errores de solicitud se empiezan a producir mucho antes, justo a partir del minuto 5’50» con una carga total de unos 560 usuarios. Además el tiempo de respuesta y los errores han seguido creciendo.

image

Ya podemos observar como este tipo páginas tiene un rendimiento 30% inferior y una media de solicitudes por segundoinferior al anterior.

 

Y una página unghosted

En este caso se ha customizado con Sharepoint Designer la página AllItems de la lista y su masterpage.

Este escenario, ha resultado ser muy parecido al anterior pero fijaros que con tiempos de respuesta más elevados y con una carga de cpu muy superior. Si observáis la línea azul (tiempo de respuesta) existe un salto que coincide con la falta de respsuesta del servidor al VisualStudio debido a un consumo elevado..

En este caso ha conseguido un rendimiento aceptable con una carga de unos 600 usuarios, un 25% inferior al primer caso.

image

 

 

Conclusiones

Aunque los ejemplos han sido casos muy sencillos, en un entorno real con muchas personalizaciones habríamos visto como la diferencia habría sido mucho mayor. Tener en cuenta además que esto se ha realizado en una granja con un solo servidor, si hubiéramos tenido varios frontales el rendimiento de las páginas ghosted se incrementaría de forma proporcional al incremento de servidores, pero en el caso de las unghosted crecería en una menor medida debido a que la mayor parte del procesamiento se invertiría en consultar la bbdd de contenidos.