Valores únicos en una lista usando Callbacks en Sharepoint 2007 – UniqueColumn FieldType

Estábamos trabajando en un proyecto sobre Sharepoint 2007 para nuestra intranet y nos encontramos con el problema que necesitábamos en una lista hacer un campo lookup hacia otra lista que se encontraba en otro subSite del portal. Si bien Sharepoint 2007 nos permite realizar campos lookup en nuestra listas, los mismos son solo contra lista que se encuentran en el mismo sitio donde estamos creando dicho campo. Para solucionar el problema encontramos una solución publicada  por “Tony Bierman” llamada Cross-Site.
Entonces me acorde de un problema que habíamos tenido en un proyecto en el cual necesitábamos que una columna tuviera valores únicos y que los mismos no se pudieran repetir. Si bien las listas de Sharepoint tiene una semejanza con una tabla de base de datos, no lo son y hay muchas restricciones que  podemos establecer en una tabla que en una lista no, pero después el comportamiento es similar.  Haciendo un poco de investigación Santiago encontró una característica para Sharepoint 2007 basada en policitas que nos permite crear una  restricción sobre un campo de una lista y que chequea que los valores en los mismos sean únicos, acá les dejo el link a CodePlex donde encontraran esta solución y otras más.
Quise adentrarme un poco más en el tema y crear un tipo de columna para WSS 3.0 que me permitirá tener valores únicos en la misma, pero además quise que la validación fuera utilizándose la característica de Callbacks de Asp.Net 2.0 y que la validación se llevara en el servidor, pero sin tener que someter la página completa, como suele pasar en Sharepoint 2007 y WSS 3.0 cuando se validad cualquier restricción en campo de un elemento de la lista.
Lo que se debía hacer es fácil, tomar el valor que ingresa el usuario en el campo correspondiente y controlar que el mismo ya no estuviera almacenado en la lista, la idea estaba y la solución que debía desarrollar también y no era difícil de llevar a cabo, dato que para saber  si un valor para una columna determinada ya fue guardado en la lista o bien podemos recorrer todos sus elementos o bien podemos realizar una consulta con CAML, adivinen que, yo prefiero usar una consulta con CAML, es mas eficiente que recorrer todos los elementos ya que nunca vamos a saber cuántos elementos tendremos en la lista.
Comencé a crear mi tipo de columna (FieldType) personalizada, así que lo primero que hice fue abrir mi Visual Studio y generarme un proyecto nuevo utilizando las Plantillas de proyectos para Sharepoint. En este caso me genere un proyecto vacio y agregue una por una todas las referencias que necesitaba para llevar a cabo el desarrollo.
En la imagen 1 podrán apreciar como quedo el proyecto en Visual Studio una vez terminado el desarrollo.

[Imagen 1]
1_Proyecto_Visual_Studio_FieldType 

Bien, lo primero que vamos hacer es crearnos un archivo *.snk para firmar nuestro Assembly dado que el mismo deberá ser colocado en la GAC del servidor, el mismo lo generamos utilizando la herramienta sn.exe que vienen con el FrameWork, en mi caso yo tengo uno generado (Siderys.snk) que es el que utilizo para firmar todos los Assemblies desarrollados. A continuación debemos configurar nuestro proyecto con el espacio de nombre por defecto, el nombre que llevara nuestro Assembly y cargar el archivo snk.
Una vez que nuestro proyecto está configurado, lo primero que vamos hacer es crearnos un UserControl para darle la Interfaz de usuario que nuestro tipo de columna tendrá cuando se esté ingresando un nuevo elemento en la lista o para cuando estemos editando uno nuevo. Este control de usuario tiene la particularidad que todo nuestro código Asp.Net 2.0 y JavaScript deberá estar comprendido dentro del Tag “SharePoint:RenderingTemplate” para así crear un template determinado y que el mismo sea utilizado cuando se esta renderizando nuestro campo personalizado. En nuestro caso y como primera instancia hicimos un template basados en un TextBox para que el usuario ingrese los valores que desea guardar en la lista de Sharepoint. También colocamos un control RequiredFieldValidator para controlar que se ingrese un valor si el usuario selecciona que es requerida la columna cuando se está realizando la configuración de la lista. También hemos colocado varias funciones JavaScript para trabajar con la característica de CallBack de Asp.Net 2.0 y otras para realizar tareas adicionales de validar y notificar al usuario cuando el valor esta incorrecto. En la sección 1 podemos ver el control de usuario definido para nuestra columna personalizada.

[Sección 1]

<%@Control Language="C#" %>
<%@Assembly Name="Siderys.Blog.CustomField.UniqueColumnFieldType, Version=1.0.0.0, Culture=neutral, PublicKeyToken=711eed342842acee" %>
<%@Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" namespace="Microsoft.SharePoint.WebControls"%>
<SharePoint:RenderingTemplate ID="txtUniqueColumn" runat="server">
<Template>
<asp:TextBox runat="server" ID="txtValueField" CssClass="ms-long" MaxLength="255" /><br />
<asp:RequiredFieldValidator runat="server" ID="rfvTxtValueField" ErrorMessage="Debe Ingresar un Valor" ControlToValidate="txtValueField" Enabled="false"></asp:RequiredFieldValidator>
<script type="text/javascript">
var lBtnOks = new Array();
//funcion que recibe el resultado de la ejecución en el servidor.
function OnCallbackComplete(arg, context)
{
if(arg != "")
{
alert(arg);
SetColorBorderTextBox("#ff0000");
}
else
{
SetColorBorderTextBox("");
EnabledSaveField(lBtnOks, false);
}
}
//funcin que establece el color de forde del control
function SetColorBorderTextBox(pColor)
{
lTextBox = GetTagFromIdentifierAndTitle("input","ValueField", "");
lTextBox.style.borderColor = pColor;
}
//funciona que habilita los botones de guardar
function EnabledSaveField(pButtons, pState)
{
for(iterator = 0; iterator < pButtons.length; iterator++)
{
pButtons[iterator].disabled = pState;
}
}
//funcion que llama al servidor para validar el texto ingresado por el usuario.
function OnCallbackCheckField(param)
{
lBtnOks = GetTagFromIdentifierAndTitleArray("input","SaveItem","")
EnabledSaveField(lBtnOks, true);
CheckUniqeField(param, '');
}
//funcion que devuelve un array de controles a partir del tag y el id
function GetTagFromIdentifierAndTitleArray(tagName, identifier, title)
{
var len = identifier.length;
var tags = document.getElementsByTagName(tagName);
var j = 0;
var lReturn = new Array();
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))
{
lReturn[j] = tags[i];
j++;
}
}
return lReturn;
}
//funcion que devuelve un control a partir del tag y el id
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>
</Template>
</SharePoint:RenderingTemplate>

Lo próximo que tenemos que hacer es implementar la clase que tendrá el codebeheind de nuestro control de usuario, como se puede ver en la directiva @Assembly colocada en el control de usuario, nuestro control de usuario contendrá una clase donde implementaremos todo el código necesario para realizar las validaciones del valor ingresado por el usuario. Ya comentamos que la validación la realizaremos usando un CAML contra la lista donde se creó la columna, para lo cual necesitamos saber cuál es la lista y además necesitamos saber cómo se llama la columna en Sharepoint (el nombre interno de la misma), para ello nuestro control de usuario contendrá una sobre cargar del constructor donde recibirá todos los valores necesarios para ejecutar la misma. Además esta clase es la que implementara la interface “ICallbackEventHandler” para realizar la llamada vía CallBack, vale la pena aclarar que cuando implementamos esta interface debemos implementar dos métodos “RaiseCallbackEvent” y “GetCallbackResult” que son los encargados de recibir la petición desde el cliente vía JavaScript y la encargada de enviar la notificación una vez se termine la ejecución. En la sección 2 vemos el código completo de la clase “UniqueFieldColumn” .

[Sección 2]

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint;
namespace Siderys.Blog.CustomField
{
public partial class UniqueFieldColumn : BaseFieldControl, ICallbackEventHandler
{
protected TextBox txtValueField;
protected RequiredFieldValidator rfvTxtValueField;
private bool mRequired = false;
private SPList mListField = null;
private string mInternalName = string.Empty;
private string mReturnCheckFieldCallback = string.Empty;

public UniqueFieldColumn(bool pRequired, string pInternalName, SPList pListField)
{
mRequired = pRequired;
mInternalName = pInternalName;
mListField = pListField;
}

protected override string DefaultTemplateName
{
get
{
return "txtUniqueColumn";
}
}

public override object Value
{
get
{
EnsureChildControls();
return txtValueField.Text;
}
set
{
EnsureChildControls();
txtValueField.Text = value.ToString();
}
}

public override void Focus()
{
EnsureChildControls();
txtValueField.Focus();
}

protected override void CreateChildControls()
{
if (Field == null)
{
return;
}
base.CreateChildControls();
if (ControlMode.Equals(SPControlMode.Display))
{
return;
}

txtValueField = (TextBox)TemplateContainer.FindControl("txtValueField");
txtValueField.Attributes.Add("onchange", "OnCallbackCheckField(this.value)");
rfvTxtValueField = (RequiredFieldValidator)TemplateContainer.FindControl("rfvTxtValueField");

ClientScriptManager cm = Page.ClientScript;
string cbReference = cm.GetCallbackEventReference(this, "arg", "OnCallbackComplete", "");
string callbackScript = "function CheckUniqeField(arg, context) {" + cbReference + "; }";
cm.RegisterClientScriptBlock(this.GetType(), "CheckUniqeField", callbackScript, true);

if (txtValueField == null)
{
throw new Exception("El campo es nulo");
}

if (ControlMode.Equals(SPControlMode.New))
{
txtValueField.Text = string.Empty;
rfvTxtValueField.Enabled = mRequired;
}
}

public string GetCallbackResult()
{
return mReturnCheckFieldCallback;
}

public void RaiseCallbackEvent(string eventArgument)
{
CheckUniqueField(eventArgument);
}

private void CheckUniqueField(string pValueToCheck)
{
string lQueryString = "<Where><Eq><FieldRef Name='" + mInternalName + "' /><Value Type='Text'>" + pValueToCheck + "</Value></Eq></Where>";
SPQuery lQuery = new SPQuery();
lQuery.Query = lQueryString;
SPListItemCollection lResultQuery = mListField.GetItems(lQuery);
if (lResultQuery.Count > 0)
{
mReturnCheckFieldCallback = "El valor: " + pValueToCheck + " ya existe en la lista: " + mListField.Title;
}
}

}
}

Lo próximo que haremos es extender la clase SPFieldText que es la que utiliza Sharepoint y WSS para representar una columna de tipo texto en nuestras listas. En nuestro caso estamos haciendo un campo de texto y nos basamos en esta columna para realizar la nuestra, pero si estuviéramos en otro escenario, por ejemplo creando un tipo de columna más compleja podríamos utilizar cualquieras de las otras clases provistas por Sharepoint y WSS.  Esta clase declara dos constructores personalizados y realiza la sobre carga del método “GetValidatedString” donde podríamos realiza la validación del texto introducido por el usuario si no estuviéramos utilizando CallBacks y la sobre carga de la property “FieldRenderingControl” que nos devuelve la instancia de nuestro control utilizando la clase base del mismo “BaseFieldControl”.  En la sección 3 vemos la implementación de la clase “UniqueFieldType” que es la que utilizamos para la generación de nuestro columna personalizada.

[Sección 3]

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Siderys.Blog.CustomField;

namespace Siderys.Blog.CustomField
{
public class UniqueFieldType: Microsoft.SharePoint.SPFieldText
{
public UniqueFieldType(SPFieldCollection pFields, string pFieldName):base(pFields, pFieldName)
{
}

public UniqueFieldType(SPFieldCollection pFields, string pFieldName, string pDisplayName):base(pFields,pFieldName, pDisplayName)
{
}

public override string GetValidatedString(object value)
{
return value.ToString();
}
public override Microsoft.SharePoint.WebControls.BaseFieldControl FieldRenderingControl
{
get
{
BaseFieldControl lUniqueColumn = new UniqueFieldColumn(this.Required, this.InternalName, SPContext.Current.List);
lUniqueColumn.FieldName = InternalName;
return lUniqueColumn;
}
}
}
}

Por último debemos crear un archivo XML para definir nuestra columna en Sharepoint y WSS, este XML deberá comenzar con “fldtypes_” y después podemos llamarlo como nosotros necesitemos, en nuestro caso se llama “fldtypes_UniqueColumn.xml” y el mismo deberá ser colocado en una ruta especifica dentro del servidor, para que sea tenido en cuenta, la misma es “Microsoft Sharedweb server extensions12TEMPLATEXML”. Nosotros hemos configurado las propiedades obligatorias que debe tener el elemento “FieldTypes” dentro del archivo, pero en la siguiente página del MSDN podrán ver todas las propiedades que se pueden configurar para este elemento. En la sección 4 vemos el código completo para nuestro archivo XML.

[Sección 4]

<FieldTypes>
<FieldType>
<Field Name="TypeName">UniqueColumn</Field>
<Field Name="TypeDisplayName">Unique Column</Field>
<Field Name="TypeShortDescription">Unique Column</Field>
<Field Name="ParentType">Text</Field>
<Field Name="FieldTypeClass">Siderys.Blog.CustomField.UniqueFieldType, Siderys.Blog.CustomField.UniqueColumnFieldType, Version=1.0.0.0, Culture=neutral, PublicKeyToken=711eed342842acee</Field>
<field name="UserCreatable">TRUE</field>
<Field Name="Sortable">TRUE</Field>
<Field Name="Filterable">TRUE</Field>
</FieldType>
</FieldTypes>

Una vez tenemos todo implementado, podemos proceder con la instalación de todo nuestro desarrollo, el problema acá es que todos los componente deben ser colocados en lugares diferentes dentro de nuestro servidor, así que me cree un archivo .bat para realizar la misma, en la sección 5 podrán ver el código de este instalador.

[Sección 5]

iisreset /stop 
"%programfiles%Microsoft Visual Studio 8SDKv2.0Bingacutil.exe" -uf Siderys.Blog.CustomField.UniqueColumnFieldType
"%programfiles%Microsoft Visual Studio 8SDKv2.0Bingacutil.exe" -if binDebugSiderys.Blog.CustomField.UniqueColumnFieldType.dll

copy /y FLDTYPES_UniqueColumn.xml "%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEXML"

xcopy /s /Y *.ascx "
%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATECONTROLTEMPLATES"

iisreset /start

Ahora a probar que todo funciones correctamente, lo primero que tenemos que hacer es agregar la columna a una lista, en la imagen 2 vemos la pantalla de configuración para agregar una nueva columna.

[Imagen 2]

2_Configuración_Columna 
 

Una vez creada la columna vamos a ingresar un elemento en la misma y después vamos a ingresar un segundo elemento con el mismo valor de la primera para ver como la validación del texto ingresado se realiza. Vale la pena destacar que los botones de Aceptar del formulario proporcionado por Sharepoint y WSS para ingresar un valor son deshabilitados si el valor esta repetido, esto es para que el usuario no pueda guardar el elemento antes de corregir el valor del texto, además se indica resaltando el campo en el formulario. En la imagen 3 vemos como se produce la validación del texto ingresado por el usuario.

[Imagen 3]

3_Validacion_Texto_Columna 

En la imagen 4 vemos el campo resaltado después de realizada la validación correspondiente del texto ingresado por el usuario.

[Imagen 4]

4_Campo_Resaltado_Columna_Texto  
 

En la imagen 5 vemos los valores que ya esta cargados en la lista y confirmamos que el valor “Fabián” ya está en el campo “Unique Nombre” para la lista “Unique Column”.

[Imagen 5]

5_Lista_Valores_Ingresados 

Por último vemos en la imagen 6 como se realiza la validación del campo si el usuario definió la columna como requerida y tiene que ingresar la información.

[Imagen 6]

6_Validacion_Campo_Obligatorio  

En un próximo artículo vamos a estar confeccionando un tipo de columna mas compleja y nos permita tener tipos de columnas mas complejas.

Fabián Imaz

Siderys Elite Software

Compartir
2 Comments
  1. Buenas tardes Fabián,

    La verdad es que cuando encontré tu post me llevé una alegría. Es justo lo que necesito!!! Seguí todos los pasos del tutorial, y al ir a crear la columna me aparece el tipo de columna, pero cuando le doy a ok, me da el siguiente error:

    “Field type UniqueColumn is not installed properly. Go to the list settings page to delete this field.”

    ¿Qué puede ser? ¿Me podrías ayudar?

    Muchas gracias por anticipado

  2. Mafalda,

    Ese error se puede deber a que el custom field no esta encontrando el assembly en la GAC, es decir la declaración de ensamblado que tenes en los controles de usuario no es correcta.
    Te dejo un link que explica como pode habilitar los errores personalizados en un portal de Sharepoint 2007, esto capaz te ayuda a que puedas ver la excepcióncon concreta.
    http://geekswithblogs.net/HammerTips/archive/2007/12/07/unable-to-manage-form-templates.aspx

    Saludos,