viernes, 3 de octubre de 2008

Vistas agrupadas de entradas de blog con CKS

Hasta hoy había utilizado en alguna ocasión Community Kit for Sharepoint (CKS) especialmente los componentes para gestión de FBA y para blogs (Enhanced Blog Edition - EBE) en alguno de los portales en los que he trabajado pero, hasta ahora, no había necesitado mostrar vistas agrupadas de diferentes blogs dentro de la misma colección de sitios. En cualquier caso, la idea parecía simple: utilizar un ContentByQueryWebpart (CQWP) y extenderlo para poder admitir alguno de los campos propios de los posts.

Bien, como suele suceder, la simplicidad no existe y, por alguna extraña razón, ha costado un poco más de lo esperado. Por si alguien está pensando hacer lo mismo, voy a explicar brevemente los pasos que yo he seguido.

1. Añadir a la página un CQWP, seleccionando todas las entradas de blog existentes en la colección de sitios.

2. Utilizando Sharepoint Designer, buscar el fichero ItemStyle.xsl dentro de la carpeta Style Library/XSL Style Sheets de la colección de sitios.

3. En este fichero, crear un nuevo template para mostrar entradas de blog. En mi caso, algo similar a lo siguiente:

<xsl:template name="Blogs" match="Row[@Style='Blogs']" mode="itemstyle">
    <xsl:variable name="SafeLinkUrl">
        <xsl:call-template name="OuterTemplate.GetSafeLink">
            <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
        </xsl:call-template>
    </xsl:variable>
    <xsl:variable name="SafeImageUrl">
        <xsl:call-template name="OuterTemplate.GetSafeStaticUrl">
            <xsl:with-param name="UrlColumnName" select="'ImageUrl'"/>
        </xsl:call-template>
    </xsl:variable>
    <xsl:variable name="DisplayTitle">
        <xsl:call-template name="OuterTemplate.GetTitle">
            <xsl:with-param name="Title" select="@Title"/>
            <xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
        </xsl:call-template>
    </xsl:variable>
    <xsl:variable name="LinkTarget">
        <xsl:if test="@OpenInNewWindow = 'True'" >_blank</xsl:if>
    </xsl:variable>
      <xsl:variable name="publishedDate" select="ddwrt:FormatDateTime(string(@PublishedDate),3082
                            ,'dd/MM/yyyy')" />
    <xsl:variable name="pureText">
        <xsl:call-template name="removeHtmlTags">
            <xsl:with-param name="html" select="@Body" />
        </xsl:call-template>
    </xsl:variable>
    <div>
        <a href="{$SafeLinkUrl}" target="{$LinkTarget}" title="{@LinkToolTip}">
            <p><xsl:value-of select="$publishedDate"/>  <xsl:value-of select="@Author"/></p>
            <p><strong><xsl:value-of select="$DisplayTitle"/></strong></p>
            <xsl:if test="string-length($SafeImageUrl) != 0">
                <img src="{$SafeImageUrl}" width="77" height="77" alt="{@ImageUrlAltText}"/>            
            </xsl:if>
            <p><xsl:value-of select="substring($pureText, 0, 250)" disable-output-escaping="yes" />...</p>
        </a>
    </div>
</xsl:template>

4. Editar el CQWP y establecer como estilo para los elementos el que acabamos de crear.

5. Exportar el CQWP y editar el fichero generado para buscar la siguiente línea:

<property name="CommonViewFields" type="string"/>

y cambiarla por lo siguiente:

<property name="CommonViewFields" type="string">PublishedDate,DateTime;Body,Memo;</property>

6. Eliminar el CQWP de la página e importar el fichero que acabamos de modificar.

Et voilà! La verdad: una vez hecho me doy cuenta de que he seguido los mismos pasos que he seguido siempre para añadir campos personalizados a los CQWP pero, no sé muy bien por qué, esta vez ha costado más de la cuenta.

miércoles, 1 de octubre de 2008

En casa del herrero...

... web hecha en MOSS. Bueno, tiempo para un post no técnico para informar de que por fin, después de varios años de andar utilizando Sharepoint en muchos de nuestros proyectos, hemos encontrado un hueco para traspasar la web de nuestra compañía a esta plataforma, uniéndose al conjunto de herramientas (intranet, proyectos, kb, etc...) que ya la utilizaban. En esta primera fase el objetivo ha sido poco ambicioso debido a las restricciones de tiempo. Las características básicas de lo que actualmente se puede ver son:
  • Portal de publicación con espacio para todos los contenidos que existían en la versión anterior.
  • Preparado para el salto a multi-idioma utilizando variaciones de sitio.
  • Pequeña integración con Live Maps para la sección de contacto.

A medida que se publiquen las sucesivas fases que están previstas iré informando de ello aquí mismo. Entre las futuras novedades, veremos:

  • Publicación de contenidos en más idiomas.
  • Aprovechamiento de las características Web 2.0 que proporciona Sharepoint.
  • Integraciones con nuevas tecnologías como Silverlight.
  • Etc.

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