martes, 30 de septiembre de 2008

¿Variaciones + metadatos = problema?

Todos los que hemos utilizado las variaciones de Sharepoint y hemos necesitado utilizar metadatos en nuestros contenidos localizados nos hemos dado cuenta de que es uno de los puntos mejorables de cara a la siguiente versión del producto. Ilustraré el problema con un ejemplo:

Nuestro tipo de contenido Hotel tiene un campo llamado Estilo que puede adoptar los valores negocios y turismo. El visitante del sitio tiene que poder buscar por un valor o por el otro, y puede acceder al sitio en español y en inglés. Si utilizamos un campo de tipo Choice para esto, los valores que podremos asignar estarán únicamente en un idioma.

La primera manera que se nos ocurre de lidiar con este problema es dejar de lado las características estándar de Sharepoint y utilizar código personalizado en todos los sitios que necesitemos. Así, la administración de contenidos será en un único lenguaje - esto se acepta sin grandes problemas - y en todos aquellos sitios en los que el metadato se muestre, como por ejemplo buscadores, listados, detalles, etc... utilizaremos un control desarrollado por nosotros. Perdemos toda opción de utilizar elementos tales como el ContentByQueryWebpart o las listas de Sharepoint.

Después de darle muchas vueltas, llegamos a la conclusión de que había que hacer algo para evitar tanto código repetido en cada proyecto que iniciábamos y, partiendo de la idea de los tipos de campo que venían con Sharepoint, decidimos crear un tipo de campo propio que nos diese la funcionalidad que necesitábamos: CatalogField.

La idea es simple y consiste en tener una lista que contenga toda la información de los metadatos en todos y cada uno de los lenguajes. Los campos de esta lista son:

Grupo de metadatos

Literal para administración (será el valor a guardar como metadato)

Literal en lenguaje 1

Literal en lenguaje N

El tipo de campo a crear tenía que permitirnos seleccionar un grupo de metadatos (p. ej. tipo de hotel) de

manera que en modo administración nos permitiese elegir entre todos los literales para administración y, en modo visitante, mostraría los elementos en el lenguaje en el cual se está navegando. La idea puede parecer simple, a priori, pero vamos a ver los pasos que son necesarios para llevar esto a cabo.

Lo primero es crear el fichero de definición del tipo de campo. Para ello, localizaremos el fichero FLDTYPES.XML en la carpeta 12\TEMPLATE\XML, y crearemos nuestro propio fichero FLDTYPES_custom.XML, con un contenido similar a lo siguiente:

   1: <FieldTypes> 
   2:     <FieldType> 
   3:         <Field Name="TypeName">CatalogField</Field> 
   4:         <Field Name="ParentType">Text</Field> 
   5:         <Field Name="TypeDisplayName">Catalog Field</Field> 
   6:         <Field Name="TypeShortDescription">Catalog Field Information</Field> 
   7:         <Field Name="UserCreatable">TRUE</Field> 
   8:         <Field Name="ShowInListCreate">TRUE</Field> 
   9:         <Field Name="ShowInSurveyCreate">TRUE</Field> 
  10:         <Field Name="ShowInDocumentLibrary">TRUE</Field> 
  11:         <Field Name="ShowInColumnTemplateCreate">TRUE</Field> 
  12:         <Field Name="Sortable">TRUE</Field> 
  13:         <Field Name="Filterable">TRUE</Field> 
  14:         <Field Name="FieldTypeClass"> FieldTypes.CatalogFieldOneChoice, FieldTypes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6e11e5ab5b77de2f</Field> 
  15:         <Field Name="FieldEditorUserControl">/_controltemplates/CatalogFieldControl.ascx</Field> 
  16:         <Field Name="SQLType">nvarchar</Field> 
  17:         <PropertySchema> 
  18:             <Fields> 
  19:                 <Field Name="CatalogGroup" DisplayName="Catalog group" Type="Text" Hidden="TRUE"> 
  20:                 </Field> 
  21:             </Fields> 
  22:         </PropertySchema> 
  23:     </FieldType> 
  24: </FieldTypes> 

Este fichero define, entre otras cosas, la clase que implementará la funcionalidad del tipo de campo, y el control que servirá como editor del campo. La clase que define el tipo de campo, en este caso, debería heredar de SPFieldChoice, ya que queremos que se comporte de una manera similar, y añadiremos todo el código necesario para obtener los literales en los distintos lenguajes.

   1: public class CatalogFieldOneChoice : SPFieldChoice 
   2: { 
   3:     public CatalogFieldOneChoice(SPFieldCollection fields, string fieldName) 
   4:         : base(fields, fieldName) 
   5:     { 
   6:         Init(); 
   7:     } 
   8:     public CatalogFieldOneChoice(Microsoft.SharePoint.SPFieldCollection fields, string typeName, string displayName) 
   9:         : base(fields, typeName, displayName) 
  10:     { 
  11:         Init(); 
  12:     } 
  13:     private string catalogGroup; 
  14:     public string CatalogGroup 
  15:     { 
  16:         get 
  17:         { 
  18:             try 
  19:             { 
  20:                 return updatedCatalogGroup.ContainsKey(ContextId) ? updatedCatalogGroup[ContextId] : catalogGroup; 
  21:             } 
  22:             catch { return string.Empty; } 
  23:         } 
  24:         set 
  25:         { 
  26:             this.catalogGroup = value; 
  27:         } 
  28:     } 
  29:     public void UpdateCatalogGroup(string value) 
  30:     { 
  31:         try 
  32:         { 
  33:             updatedCatalogGroup[ContextId] = value; 
  34:         } 
  35:         catch 
  36:         { 
  37:         } 
  38:     } 
  39:     public int ContextId 
  40:     { 
  41:         get 
  42:         { 
  43:             return SPContext.Current.GetHashCode(); 
  44:         } 
  45:     } 
  46:     private static Dictionary<int, string> updatedCatalogGroup = new Dictionary<int, string>(); 
  47:     private void Init() 
  48:     { 
  49:         try 
  50:         { 
  51:             this.Choices.Clear(); 
  52:             this.CatalogGroup = this.GetCustomProperty("CatalogGroup") + ""; 
  53:             using (SPSite currentSite = new SPSite(SPContext.Current.Site.ID)) 
  54:             { 
  55:                 using (SPWeb currentWeb = currentSite.OpenWeb()) 
  56:                 { 
  57:                     foreach (SPListItem catalogItem in currentWeb.Lists["Catalog"].Items) 
  58:                     { 
  59:                         if (catalogItem["CatalogGroup"].ToString().Equals(this.catalogGroup)) 
  60:                             this.Choices.Add(catalogItem.DisplayName); 
  61:                     } 
  62:                 } 
  63:             } 
  64:         } 
  65:         catch 
  66:         { 
  67:         } 
  68:     } 
  69:     public override void Update() 
  70:     { 
  71:         this.SetCustomProperty("CatalogGroup", this.CatalogGroup); 
  72:         base.Update(); 
  73:         try 
  74:         { 
  75:             if (updatedCatalogGroup.ContainsKey(ContextId)) 
  76:                 updatedCatalogGroup.Remove(ContextId); 
  77:         } 
  78:         catch { } 
  79:     } 
  80:     public override void OnAdded(SPAddFieldOptions op) 
  81:     { 
  82:         base.OnAdded(op); 
  83:         Update(); 
  84:     } 
  85: } 

Respecto al control editor, lo primero que hay que hacer es definir qué contendrá la plantilla del tipo de campo. En este caso es bastante sencillo y bastará con un simple desplegable de opciones.

   1: <wssuc:InputFormSection runat="server" id="MySections" Title="Informació del cataleg"> 
   2:    <Template_InputFormControls> 
   3:      <wssuc:InputFormControl runat="server" LabelText="Grup de cataleg"> 
   4:         <Template_Control> 
   5:            <asp:DropDownList id="ddlCatalogGroups" runat="server"> 
   6:            </asp:DropDownList> 
   7:         </Template_Control> 
   8:      </wssuc:InputFormControl> 
   9:    </Template_InputFormControls> 
  10: </wssuc:InputFormSection>

El código de servidor de este control tiene que implementar la interficie IFieldEditor y quedaría similar a lo siguiente:

   1: public class CatalogFieldOneChoiceControl : System.Web.UI.UserControl, IFieldEditor 
   2: { 
   3:     protected DropDownList ddlCatalogGroups; 
   4:     private string value = string.Empty; 
   5:     public void InitializeWithField(SPField field) 
   6:     { 
   7:         CatalogFieldOneChoice myField = field as CatalogFieldOneChoice; 
   8:         if (myField != null) 
   9:             this.value = myField.CatalogGroup+ ""; 
  10:     } 
  11:     public void OnSaveChange(SPField field, bool isNew) 
  12:     { 
  13:         string value = this.ddlCatalogGroups.SelectedValue; 
  14:         CatalogFieldOneChoice myField = field as CatalogFieldOneChoice; 
  15:         if (isNew) 
  16:             myField.UpdateCatalogGroup(value); 
  17:         else 
  18:             myField.CatalogGroup = value; 
  19:     } 
  20:     public bool DisplayAsNewSection 
  21:     { 
  22:         get 
  23:         { 
  24:             return true; 
  25:         } 
  26:     } 
  27:     protected override void CreateChildControls() 
  28:     { 
  29:         base.CreateChildControls(); 
  30:         if (!this.Page.IsPostBack) 
  31:         { 
  32:         using (SPSite currentSite = new SPSite(SPContext.Current.Site.ID)) 
  33:         { 
  34:             using (SPWeb currentWeb = currentSite.OpenWeb()) 
  35:             { 
  36:                 foreach (SPListItem catalogItem in currentWeb.Lists["Catalog"].Items) 
  37:                 { 
  38:                     ListItem group = new ListItem(catalogItem["CatalogGroup"].ToString()); 
  39:                     if (!ddlCatalogGroups.Items.Contains(group)) 
  40:                         ddlCatalogGroups.Items.Add(group); 
  41:                 } 
  42:             } 
  43:         } 
  44:             ListItem item = ddlCatalogGroups.Items.FindByText(this.value); 
  45:             if (item != null) 
  46:                item.Selected = true; 
  47:         } 
  48:     } 
  49: }

A partir de aquí, y después de desplegar todo lo que hemos creado, ya tendremos disponible en nuestro Sharepoint un nuevo

5 comentarios:

Pei dijo...

Hola David. Gracias por tu post. Justamente es lo que necesito.

Lo he intentado implementar pero al crear una columna de sitio de tipo Catalog Field Information me aparece el famoso Error desconocido de MOSS.

Para desplegarlo he añadido la DLL firmada a la GAC, he copiado el XML a 12\TEMPLATE\XML y he copiado el ASCX a 12\TEMPLATE\CONTROLTEMPLATES. Después he reiniciado. No sé si me falta algo...

¿Podrías publicar, por favor, los fuentes?

Muchas gracias y un saludo,

Paloma

David Martos dijo...

Hola Paloma,

creo que te será más útil conseguir depurar qué es lo que te está pasando. Estableciendo en el web.config los parámetros CustomErrors=Off y callstack=true deberías ver más información del error o, en última instancia, los logs de la carpeta 12\LOGS deberían decirte lo que está pasando cuando el mensaje es desconocido. Prueba a hacerlo y pon más información en un comentario y trataré de ayudarte con tu problema. Por cierto, la lista Catalog la has creado?

Pei dijo...

Gracias por tu rápida respuesta.

El error concreto es:
Exception Type: System.InvalidCastException Exception Message: No se puede convertir un objeto de tipo 'ASP._controltemplates_catalogfieldcontrol_ascx' al tipo 'Microsoft.SharePoint.WebControls.IFieldEditor'.

-------

Comenzando por el concepto: he creado una lista Catalog con cuatro campos de texto:
- CatalogGroup, Literal, Literal_ES y Literal_EN.

He creado dos items:
- Categoria, Ocio, Ocio, Leisure
- Categoria, Negocios, Negocios, Business

He creado un proyecto c# vacío donde he creado:

- fldtypes_custom.xml: igual que el de tu post, salvo por la clave del ensamblado

- CatalogFieldOneChoice.cs: igual que la de tu post, salvo que he añadido las líneas para definir el namespace FieldsType

- CatalogFieldControl.ascx: igual que el del post, salvo que he añadido las líneas para registrar las TagPrefix

- CatalogFieldOneChoiceControl.cs: igual que la de tu post, salvo por que he añadido también el namespace FieldTypes.

En las propiedades de la aplicación he puesto que el nombre del ensamblado y el espacio de nombres es FieldTypes.

He generado la dll y la he metido en la GAC. He copiado el XML y el ascx a su sitio y he reiniciado el IIS.

Si pudieras echarme una mano te lo agradecería infinito, puesto que me enfrento a la necesidad de convertir en bilingue mi portal y me he encontrado con que las variaciones no resuelven algunos de mis problemas.

Saludos.

David Martos dijo...

Hola Paloma,

el problema, al parecer, te lo está dando porque CatalogFieldOneChoiceControl.cs no implementa la interfaz IFieldEditor. Esto es el code behind del control ascx que tienes definido en el FLDTYPES_X.xml. ¿Puedes revisar esto? Por otro lado, ten una cosa en cuenta, el código que en su día probé relacionaba el nombre de las columnas de la lista Catalog con el nombre del idioma (en-US, es-ES...) así que posiblemente tengas que tocarlo para que te funcione. Déjame si quieres una dirección de contacto y te envío lo que tengo.

Pei dijo...

Hola, efectivamente, no estoy implementando la interfaz.

Te he enviado un mail a la dirección que has publicado en tu perfil.

Gracias de nuevo y perdona las molestias.