Thursday, May 24, 2012

SharePoint 2010 LookUp Error

Hola de nuevo. Llega el momento de compartir la solución a un problema que viene desde la versión 2007 de SharePoint, y ahora la veo heredada en SP 2010. Es un problema que afecta principalmente en términos de estética, y algo que Microsoft debería solucionar cuanto antes.
Afortunadamente para la Plataforma, muchos desarrolladores han implementado soluciones al problema, así que los clientes no se asusten de ver esto:
Error DropDownList
Como se aprecia en la figura anterior, es evidente el problema, al desplegar los ítems del Combo, la información se despliega más abajo. En el caso de SharePoint 2010, donde muchos formularios se despliegan como Modal Popups, eso es aun, un problema mayor.
Hay explicaciones del por qué sucede lo anterior, pero la verdad es que debería corregirse pronto, y esencialmente tiene que ver con la cantidad de ítems presentados. De la imagen anterior lo que se tiene es una Lista de Noticias y una de Comentarios, relacionadas entre sí. De la imagen se aprecia que se está creando un comentario para una noticia seleccionada, pero el número de Noticias que han sido publicadas superan las 20. Es correcto, 20 es el valor a partir del cual este problema comienza a ocurrir, y en general es a causa de que internamente, SharePoint renderiza el control de manera diferente a como lo hace cuando tiene menos de 20 ítems en el campo de tipo LookUp de la lista comentarios, que se comporta en este caso como la lista hija. Para mayor información del problema visitar http://social.msdn.microsoft.com/Forums/en-US/sharepoint2010general/thread/64796605-bcbb-4a87-9d8d-9d609579577f/
Es bueno leer tales discusiones para tener una mejor aproximación del problema, y tener un mejor punto de partida para elegir la solución que más nos convenga.
Así que a continuación dejo una de esas soluciones, donde se propone hacer uso de JavaScript para corregir el problema:
  • Habiendo detectado el problema, se deberá desproteger y editar en modo avanzado el formulario ASPX donde se presenta el error utilizando SP Designer 2010. Para este caso es el formulario NewForm y EditForm.aspx. El primero permite crear un nuevo comentario, y el segundo Editar uno existente.
  • Suponiendo que vamos a usar NewForm.aspx, luego de la etiqueta de cierre </WebPartPages:ListFormWebPart> de la WebPart ListFormWebPart, se debe adicionar una WebPart de Editor de Contenido Web.
Combo2
  • Guarde la página NewForm.aspx y ábrala en su navegador Web, y ponga en modo de Edición la página.
  • En este momento puede agregar contenido a la WebPart de Editor de Contenido. En ese momento agregue el siguiente código JavaScript.
<script type="text/javascript">
$(document).ready(function () {
var columnName = "Noticia";
OverrideDropDownList(columnName);

function OverrideDropDownList(columnName) {       
  var lookupDDL = new DropDownList(columnName);
   if (lookupDDL.Type == "C") {                          
    lookupDDL.Obj.css('display', 'none');                
    lookupDDL.Obj.next("img").css('display', 'none');                
    // Construct the simple drop down field with change trigger                
    var tempDDLName = "tempDDLName_" + columnName;                
    if (lookupDDL.Obj.parent().find("select[ID='" + tempDDLName + "']").length == 0) {                        
     lookupDDL.Obj.parent().append("<select name='" + tempDDLName + "' id='" + tempDDLName + "' title='" + tempDDLName + "'></select>");
     lookupDDL.Obj.parent().find("select[ID='" + tempDDLName + "']").bind("change", function () {                                
     updateOriginalField(columnName, tempDDLName);                       
     });                
     }                
     var splittedChoices = lookupDDL.Obj.attr('choices').split("|");              
     var hiddenVal = $('input[name=' + lookupDDL.Obj.attr("optHid") + ']').val()                
     if (hiddenVal == "0") {                        
      hiddenVal = lookupDDL.Obj.attr("value")                
     }                 
     lookupDDL = new DropDownList(tempDDLName);               
     for (var i = 0; i < splittedChoices.length; i++) {                        
      var optionVal = splittedChoices[i];                        
      i++;                        
      var optionId = splittedChoices[i];                        
      var selected = (optionId == hiddenVal) ? " selected='selected'" : "";                        
      lookupDDL.Obj.append("<option" + selected + " value='" + optionId + "'>" + optionVal + "</option>");               
      }        
     }
    }
    function updateOriginalField(child, temp) {        
     var childSelect = new DropDownList(child);        
     var tempSelect = new DropDownList(temp);        
     childSelect.Obj.attr("value", tempSelect.Obj.find("option:selected").val());         
     var hiddenId = childSelect.Obj.attr("optHid");        
     $('input[name=' + hiddenId + ']').val(tempSelect.Obj.find("option:selected").val());
    }
    function DropDownList(colName) {       
     if ((this.Obj = $("select[Title='" + colName + "']")).html() != null) {        
      this.Type = "S";        
     } else if ((this.Obj = $("input[Title='" + colName + "']")).html() != null) {        
      this.Type = "C";       
     } else if ((this.Obj = $("select[ID$='SelectCandidate'][Title^='" + colName + " ']")).html() != null) {        
      this.Type = "M";        
     } else if ((this.Obj = $("select[ID$='SelectCandidate'][Title$=': " + colName + "']")).html() != null) {        
      this.Type = "M"; } else         this.Type = null; } }); </script>
La línea var columnName = "Noticia"; debe adecuarla según el nombre de su campo. En este caso se llama Noticia el campo LookUp que relaciona la lista de comentarios con las noticias.
  • Es momento de terminar la edición de la página y probar el formulario NewForm.aspx para verificar que los ítems se despliegan de manera correcta.
Combo3
Espero sea de utilidad la anterior solución, la cual ha sido probada y validada para solucionar el problema en mención.



Wednesday, May 02, 2012

Control de Campo Personalizado en SharePoint 2010

Hola de nuevo. Cuantas veces hemos pensado en que una de las dolencias de SharePoint siempre ha sido no activar el control picker, que permite desplegar una ventana que fácilmente permita seleccionar elementos dados de alta en el sitio, que se encuentran por ejemplo en diferentes bibliotecas de documentos, como pueden ser: imágenes, videos, archivos en general, y que se obtenga la URL a dichos elementos, y esta quede guardada en una columna de una lista o una biblioteca?

La respuesta a este interrogante se conoce como los controles personalizados en SharePoint. Que es un mecanismo que permite que se puedan agregar tipos de campo especializados, que pueden ser reutilizados como cualquier tipo de columna, en listas o bibliotecas.

Esta entrada está dedicada a esa opción que SharePoint ofrece a través de programación .NET. Pongamos manos a la obra, y veamos cómo se puede implementar algo como  una columna de tipo picker que nos ayude a mejorar la experiencia de usuario. La siguiente imagen muestra dicho campo en funcionamiento:
Custom Control 1

En la imagen anterior se apreciaran claramente 2 campos que utilizan el control que desarrollaremos en esta entrada: Thumbnail y Url Video. La idea es poder seleccionar una imagen o un video y que en el campo respectivo quede asociada la URL a dicho recurso.
En general las listas en SharePoint hacen uso de conceptos generales que son requeridos para su funcionamiento, y que a su vez componen lo que se conoce como controles de campo personalizados. Entre estos conceptos se encuentran los que se conocen como campos (Fields), columnas de sitio y tipos de campos. Cada uno de esos campos o columnas de sitio, tienen o se constituyen por los tipos de campos: Texto, Enteros, Decimal, Fecha, LookUp, entre otros. Un tipo de campo personalizado, es lo que permite extender la plataforma SharePoint más allá de lo que ofrece, permitiendo así, generar soluciones ajustadas a los requerimientos de negocio. Un ejemplo podría ser un tipo de campo personalizado que dispare validaciones especiales acorde a la entrada de datos del usuario, que difícilmente podrá ser resuelto con facilidad con las funcionalidades por defecto de los tipos ya comentados.
Una de las cosas faltantes en las herramientas de desarrollo para SharePoint ofrecidas en VS. NET 2010 es una plantilla para crear tipos de campo personalizados, así que básicamente los pasos para iniciar una solución de este tipo es la siguiente:
  1. Crea un proyecto de SharePoint nuevo basado en la plantilla de Proyecto de SharePoint vacío.
  2. Cuando se haya creado tal proyecto adicionar una clase por cada tipo de campo personalizado que se piense crear.
A continuación se presenta la definición de la clase AssetUrlSelectorField que hereda de un tipo de campo base SPFieldText, que hace parte del desarrollo del control que se ha mostrado anteriormente:
class AssetUrlSelectorField: SPFieldText
    {
        // Methods
        public AssetUrlSelectorField(SPFieldCollection fields,string fieldName) : base(fields, fieldName)
        {}
        public AssetUrlSelectorField(SPFieldCollection fields,
        string typeName, string displayName) : base(fields, typeName, displayName)
        {}
        // Properties
        public override BaseFieldControl FieldRenderingControl
        {
            get
            {
                BaseFieldControl control = new AssetUrlSelectorFieldControl();
                control.FieldName = base.InternalName;
                return control;
            }
        }
    }

La clase AssetUrlSelectorField define el tipo de campo personalizado que vamos a utilizar en el desarrollo del control. Es muy importante y obligatorio implementar los dos constructores públicos, que son requeridos por SharePoint Foundation. Finalmente para este caso específico se ha sobrescrito una propiedad que esencialmente se encarga de retornar el control que definiremos en otra clase.
Ahora bien, hasta el momento hemos podido ver la definición base de nuestro campo personalizado. Pero adicionalmente podemos mejorar la experiencia de usuario con nuestro control, implementando lo que se conoce como un control de campo personalizado, que provee al usuario con una experiencia de edición personalizada. La definición e implementación de esta nueva clase se muestra a continuación:

class AssetUrlSelectorFieldControl : BaseFieldControl
    {
        protected AssetUrlSelector urlSelector;
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            // Set the value if this is a postback.
            if (this.Page.IsPostBack)
            {
                try
                {
                    this.ListItemFieldValue = urlSelector.AssetUrl;
                }
                catch (Exception ex)
                {
                    EventLog log = new EventLog();
                    if (!(log.Source == "Mi Log"))
                        log.Source = "Mi Log";
                    log.WriteEntry("Error en el Método OnLoad: " + ex.Message + "- InnerException: " + ex.InnerException + "- Source: " + ex.Source + "- StackTrace: " + ex.StackTrace,
                      EventLogEntryType.Error, 1);
                    throw;
                }
            }
        }
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            // Add the asset picker when in edit mode.
            try
            {
                urlSelector = new AssetUrlSelector();
                this.Controls.Add(urlSelector);
            }
            catch (Exception ex)
            {
                EventLog log = new EventLog();
                if (!(log.Source == "Mi Log"))
                    log.Source = "Mi Log";
                log.WriteEntry("Error en el Método CreateChildControls: " + ex.Message + "- InnerException: " + ex.InnerException + "- Source: " + ex.Source + "- StackTrace: " + ex.StackTrace,
                  EventLogEntryType.Error, 1);
                throw;
            }
        }
        public override object Value
        {
            get
            {
                this.EnsureChildControls();
                return urlSelector.AssetUrl;
            }
            set
            {
                this.EnsureChildControls();
                urlSelector.AssetUrl = (string)this.ItemFieldValue;
            }
        }
    }

La clase anterior es la que se ha invocado inicialmente en la clase AssetUrlSelectorField, en la propiedad sobrescrita FieldRenderingControl. Esta clase aprovecha la clase AssetUrlSelector  la cual es la que provee la funcionalidad de tipo picker usada en SharePoint por defecto. Para mayor información de dicha clase se puede consultar su documentación en MSDN http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.publishing.webcontrols.asseturlselector.aspx

Para evitar confusiones, los namespaces utilizados en las clases mostradas son:

using Microsoft.SharePoint;
using Microsoft.SharePoint.Publishing.WebControls;
using Microsoft.SharePoint.WebControls;
using System.Web.UI;
using System.Diagnostics;

Con las clases definidas, muy fáciles de entender por cualquier desarrollador a continuación se requiere de la creación y definición de un archivo XML que al momento de hacer el despliegue de la solución en el servidor de SharePoint permite identificar el control que podrá ser utilizado dentro del sitio que se requiera o esté disponible la característica. SharePoint 2010 mantiene estos archivos XML en la ruta TEMPLATE/XML, en la cual muchos de sus tipos de campo están definidos. El nombrado de dicho archivo debe comenzar con el nombre fldtypes_, y de ahí se completa acorde a cada necesidad, en este caso por ejemplo: fldtypes_AssetUrlSelectorCustomField.xml. El contenido de este archivo XML que debemos agregar manualmente al folder mapeado XML, se presenta a continuación:

<?xml version="1.0" encoding="utf-8"?>
<FieldTypes>
  <FieldType>
    <Field Name="TypeName">AssetUrlSelectorField</Field>
    <Field Name="ParentType">Text</Field>
    <Field Name="TypeDisplayName">Picker</Field>
    <Field Name="TypeShortDescription">Picker de Activos</Field>
    <Field Name="UserCreatable">TRUE</Field>
    <Field Name="ShowOnListCreate">TRUE</Field>
    <Field Name="ShowOnSurveyCreate">TRUE</Field>
    <Field Name="ShowOnDocumentLibraryCreate">TRUE</Field>
    <Field Name="ShowOnColumnTemplateCreate">TRUE</Field>
    <Field Name="FieldTypeClass">
      WebParts.AssetUrlSelectorCustomField.AssetUrlSelectorField,$SharePoint.Project.AssemblyFullName$
    </Field>
  </FieldType>
</FieldTypes>

La definición <Field Name="FieldTypeClass">
WebParts.AssetUrlSelectorCustomField.AssetUrlSelectorField,$SharePoint.Project.AssemblyFullName$
</Field> puede variar acorde a su implementación, por ejemplo el nombre del namespace, en este caso WebParts, que usted haya usado en su proyecto en Visual Studio .NET.

En general la solución debería verse así:
Custom Control 2
Debido a que se ha utilizado una carpeta mapeada (XML) a la ruta de SharePoint 2010 que se ha explicado mantiene los archivos XML, automáticamente VS.NET 2010 sabe donde desplegar el XML definido para esta solución.
Luego de hacer el despliegue con VS.NET 2010, se puede proceder navegar hasta una lista, crear una nueva columna y ahí deberá estar disponible el tipo de control personalizado, por ejemplo:
Custom Control 3

El título Picker de Activos es el que se definió en el valor de la propiedad TypeShortDescription.

En conclusión, aunque no se cuenta con una experiencia de desarrollo 100% definida en VS.NET 2010, los pasos para desarrollar un control de campo personalizado y un campo personalizado base, no son complejos, y realmente son la puerta a implementaciones mucho más avanzadas que permitan con mayor facilidad cumplir requerimientos de nuestros clientes, que suelen salirse de los límites de la funcionalidad por defecto de la Plataforma.