Small Bdc for Wss – Campos de Lista desde Base Datos

Hace tiempo que me estaba dando vuelta esta idea en la cabeza, la había empezado a codificar, pero por razones de tiempo la tuve que abandonar. En el día de ayer la retome y termine de implementar. La idea había surgido para un proyecto, que después no se llevo a cabo por diferentes motivos y a mí la idea me había gustado, dado que tener la posibilidad de cargar una columna de una lista con datos de una base de datos en Windows Sharepoint Services 3.0 me seducía mucho ya que esta funcionalidad (mucho más robusta y escalable) viene con la versión de MOSS 2007.

La vez pasada habíamos publicado un artículo sobre tener Campos Únicos en una lista de Sharepoint, nos permitía controlar que los datos que cargaba el usuario fueran diferentes entre sí. Basándonos en ese mismo artículo “Valores únicos en una lista” lo que hicimos acá fue llenar campos de una lista desde una base de datos. Para darle un poco más de aplicabilidad, no nos concentramos en solo un motor de Base de Datos, como podría haber sido SQL Server, si no que usamos OLEDB para realizar la conexión con la base y ejecución de la consulta. Como se trata de un campo de una lista, y lo que se busca es cargar el valor de la lista con registros almacenados en una base de datos, lo que hicimos fue simplemente contemplar la primera columna que se devuelve en el origen de datos, es decir que si se carga una consulta compleja como ser “Select * from tabla o Select Campo1, Campo2 from tabla” solo se tendrá en cuenta la primera columna del schema que devuelva la ejecución de la consulta.

Vale la pena realizar una aclaración antes de meternos de lleno al código, cuando estaba realizando la codificación del tipo de campo, me tope con un problema en la secuencia de cómo WSS y Sharepoint manejan el guardado de los valores del campo, la primera vez (cuando e crea) y cuando se modifica el mismo. Por eso, realizando un searching en google me tope con este artículo “Custom Field Type Properties” que explica que es lo que sucede y nos da una solución alternativa para salir del paso, la cual aplique con éxito. Cuando se dispara el método OnAdded del campo que estamos creando la instancia del campo que se nos pasa es diferente a la que teníamos en determinado momento (cuando lo estábamos configurando) y los valores de las custom properties utilizadas para almacenar la cadena de conexión y consulta select se pierden, es por ello que tuvimos que utilizar un Diccionario (Dictionary) estático y almacenar los valores de forma provisoría, para que cuando se dispara el método OnAdded tuviéramos los valores para guardarlos en nuestras custom properties correspondientes.

Hecha esta aclaración, lo que vamos hacer en primer lugar es crearnos un proyecto de Sharepoint vacio, podríamos utilizar un proyecto de biblioteca de clases, de no tener instalado la extensiones de WSS para visual studio 2005, sigo trabajando con visual studio 2005, pero próximamente me estaré migrando a visual studio 2008 dado que me estoy armando una maquina virtual con todo el ambiente, Windows 2008, Sql Server, Sharepoint con SP1 y visual studio 2008.

Una vez tenemos el proyecto creado vamos a configurar el mismo, lo que vamos hacer es editar las propiedades y colar un nombre para el Assembly que se va a generar, un espacio de nombre por defecto para todas las clases que se creen y vamos a firmar el mismo utilizando un arhivo .snk, en mi caso este archivo se llama “Siderys.snk”

Una vez configurado el proyecto, lo primero que vamos agregar al mismo es el archivo Xml que define a nuestro tipo de campo. En este archivo que lo llamaremos “fldtypes_BdcWss.xml” indicaremos cual es el nombre, descripción, la clase que lo implemente, el Assembly que contiene esa clase, el tipo de campo base que vamos a estar utilizando y algunas propiedades más que se pueden ver en la sección 1. También crearemos dos propiedades personalizadas, ocultas, que serán las que van almacenar la cadena de conexión con la base de datos y la consulta que vamos a estar ejecutando.

[Sección 1]

<FieldTypes>
    <FieldType>
        <Field Name="TypeName">BdcWss</Field>
        <Field Name="TypeDisplayName">Bdc Wss</Field>
        <Field Name="TypeShortDescription">Bdc Wss</Field>
        <Field Name="ParentType">Choice</Field>
        <Field Name="FieldTypeClass">Siderys.Blog.CustomField.BdcWss, Siderys.Blog.CustomField.BdcWss, 
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> <Field Name="FieldEditorUserControl">/_controltemplates/BdcWssEditFieldType.ascx</Field> <PropertySchema> <Fields> <Field ID="StringConnection" Hidden="TRUE" Name="StringConnection"
DisplayName="StringConnection" Type="Text" ></Field> <Field ID="StringSelect" Hidden="TRUE" Name="StringSelect"
DisplayName="StringSelect" Type="Text" ></Field> </Fields> </PropertySchema> </FieldType> </FieldTypes>

Una vez el archivo Xml está creado, lo próximo que vamos hacer es crear el primer control de usuario. Este control de usuario es el que se utilizara cuando estemos creando un nuevo elemento en la lista y nos desplegara un DrodDownList con los datos cargados para que el usuario seleccione uno de los valores recogidos de la base de datos. En la sección 2 vemos el código de nuestro ASCX llamado “BdcWssFieldType.ascx” y en la sección 3 vemos el código fuente de la clase que implementa la lógica del control.

[Sección 2]

<%@ Control Language="C#"%>
<%@Assembly Name="Siderys.Blog.CustomField.BdcWss, 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="BdcWss" runat="server"> <Template> <asp:DropDownList ID="ddlResultSelect" runat="server"> </asp:DropDownList> </Template> </SharePoint:RenderingTemplate>

[Sección 3]

using System;
using System.Data;
using System.Configuration;
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;
using Microsoft.SharePoint.WebControls;
using System.Data.OleDb;

/// <summary>
/// Summary description for BdcWssFieldType
/// </summary>
namespace Siderys.Blog.CustomField
{
    public class BdcWssFieldType : BaseFieldControl
    {
        protected DropDownList ddlResultSelect;
        private OleDbConnection mConnection = null;
        private OleDbCommand mCommand = null;
        private OleDbDataReader mReader = null;

        public BdcWssFieldType()
        {
            //
            // TODO: Add constructor logic here
            //
        }

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

        public override object Value
        {
            get
            {
                return ddlResultSelect.SelectedItem.Text;
            }

            set
            {
                EnsureChildControls();
                ddlResultSelect.Items.FindByText(value.ToString()).Selected = true;
            }
        }

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

        private void FillDropDownListFromDataBase()
        {
            try
            {
                //obtengo los valores para la conexion y la consulta almacenados en las propiedades del campo.
                string lConnectionString = this.Field.GetCustomProperty("StringConnection").ToString();
                string lSelectString = this.Field.GetCustomProperty("StringSelect").ToString();

                mConnection = new OleDbConnection(lConnectionString);
                mConnection.Open();
                mCommand = new OleDbCommand(lSelectString, mConnection);
                mReader = mCommand.ExecuteReader();
                ddlResultSelect.Items.Clear();
                while (mReader.Read())
                {
                    ddlResultSelect.Items.Add(new ListItem(mReader.GetString(0)));
                }               
            }
            catch (OleDbException ex)
            {
                throw ex;
            }
            finally
            {
                mReader.Close();
                mConnection.Close();
            }
        }

        protected override void CreateChildControls()
        {
            if (this.Field == null) return;
            base.CreateChildControls();
            if (this.ControlMode == Microsoft.SharePoint.WebControls.SPControlMode.Display)
                return;
            ddlResultSelect = (DropDownList)(TemplateContainer.FindControl("ddlResultSelect"));
            if (ddlResultSelect == null)
            {
                throw new ArgumentException("DropDowList es nulo.......");
            }

            if (!Page.IsPostBack)
            {
                try
                {
                    FillDropDownListFromDataBase();
                }
                catch (Exception ex)
                {
                    ddlResultSelect.Items.Add(ex.Message);
                }
            }
        }
    }
}

Lo próximo que vamos a realizar es un control de usuario que será utilizado cuando estemos creando el campo en la lista y nos permitirá ingresar la cadena de conexión a la base de datos y probar la misma. También nos permitirá ingresar la consulta que queremos ejecutar y probarla, donde se cargara un DropDownList de ejemplo con los datos sacados de la base de datos. En la sección 4 vemos el código del control ASCX llamado “BdcWssEditFieldType.ascx” y en la sección 5 vemos el código fuente de la clase que implementa la lógica del control de usuario.

[Sección 4]

<%@ Control Language="C#" AutoEventWireup="false" Inherits="Siderys.Blog.CustomField.BdcWssEditFieldType, 
Siderys.Blog.CustomField.BdcWss, Version=1.0.0.0, Culture=neutral, PublicKeyToken=711eed342842acee" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormControl" Src="~/_controltemplates/InputFormControl.ascx" %> <%@ Register TagPrefix="wssuc" TagName="InputFormSection" Src="~/_controltemplates/InputFormSection.ascx" %> <%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> <%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint,
Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %> <wssuc:InputFormSection runat="server" id="BdcSection" Title="Bdc - Configuracion Base de Datos"> <template_inputformcontrols> <wssuc:InputFormControl runat="server" LabelText="Ingrese una cadena de conexion y una consulta select."> <Template_Control> <table> <tr class="ms-authoringcontrols"> <td> <asp:Label ID="lblConnectionString" runat="server" Text="Connection String">
</
asp:Label> </td> </tr> <tr> <td> <asp:TextBox ID="txtConnectionString" runat="server" TextMode="MultiLine"
Rows="5" Columns="40"></asp:TextBox> </td> </tr> <tr> <td> <asp:Button ID="cmdTestConnection" runat="Server" Text="Test Connection" /> </td> </tr> <tr> <td> <asp:Label ID="lblTestConnection" runat="server" ForeColor="Red"></asp:Label> </td> </tr> <tr class="ms-authoringcontrols"> <td> <asp:Label ID="lblSelect" runat="server" Text="Select Command"></asp:Label> </td> </tr> <tr> <td> <asp:TextBox ID="txtSelect" runat="server" TextMode="MultiLine" Rows="5"
Columns="40"></asp:TextBox> </td> </tr> <tr> <td> <asp:Button ID="cmdTestSelect" runat="Server" Text="Test Select" /> </td> </tr> <tr> <td> <asp:Label ID="lblTestSelect" runat="server" ForeColor="Red"></asp:Label> </td> </tr> <tr> <td> <asp:DropDownList ID="ddlSelectResult" runat="server" Visible=false> </asp:DropDownList> </td> </tr> </table> </Template_Control> </wssuc:InputFormControl> </template_inputformcontrols> </wssuc:InputFormSection>

[Sección 5]

using System;
using System.Data;
using System.Configuration;
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;
using Microsoft.SharePoint.WebControls;
using System.Data.OleDb;

/// <summary>
/// Summary description for BdcWssEditFieldType
/// </summary>
namespace Siderys.Blog.CustomField
{
    public class BdcWssEditFieldType : UserControl, IFieldEditor
    {
        protected TextBox txtConnectionString = null;
        protected Button cmdTestConnection = null;
        protected Label lblTestConnection = null;
        protected TextBox txtSelect = null;
        protected Button cmdTestSelect = null;
        protected Label lblTestSelect = null;
        protected DropDownList ddlSelectResult = null;
        private OleDbConnection mConnection = null;
        private OleDbCommand mCommand = null;
        private OleDbDataReader mReader = null;

        public BdcWssEditFieldType()
        {
            //
            // TODO: Add constructor logic here
            //
        }

        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            //agrego el metodo para manejar el evento click
            this.cmdTestConnection.Click += new EventHandler(cmdTestConnection_Click);
            //agrego el metodo para manejar el evento click
            this.cmdTestSelect.Click += new EventHandler(cmdTestSelect_Click);
        }

        #region IFieldEditor Members

        public bool DisplayAsNewSection
        {
            get
            {
                return true;
            }
        }

        public void InitializeWithField(SPField field)
        {
            BdcWss lBdc = (BdcWss)field;
            if (lBdc != null)
            {
                EnsureChildControls();
                string lStringConnection = lBdc.GetCustomProperty("StringConnection").ToString();
                string lStringSelect = lBdc.GetCustomProperty("StringSelect").ToString();

                txtConnectionString.Text = lStringConnection;
                txtSelect.Text = lStringSelect;
            }
        }

        public void OnSaveChange(SPField field, bool isNewField)
        {
            BdcWss lBdcWss = (BdcWss)field;
            if (isNewField)
            {
                lBdcWss.UpdateConnectionStringProperty(this.txtConnectionString.Text);
                lBdcWss.UpdateSelectStringProperty(this.txtSelect.Text);
            }
            else
            {
                //Almaceno la cadena de conexion.
                lBdcWss.ConeectionString = this.txtConnectionString.Text;
                //Almaceno la consulta Select.
                lBdcWss.SelectString = this.txtSelect.Text;
            }
        }

        #endregion

        protected void cmdTestConnection_Click(object sender, EventArgs e)
        {
            try
            {
                mConnection = new OleDbConnection(this.txtConnectionString.Text);
                mConnection.Open();
                lblTestConnection.Text = "Se conecto correctamente";
            }
            catch (OleDbException ex)
            {
                lblTestConnection.Text = ex.Message;
            }
            finally
            {
                mConnection.Close();
            }
        }

        protected void cmdTestSelect_Click(object sender, EventArgs e)
        {
            try
            {
                mConnection = new OleDbConnection(this.txtConnectionString.Text);
                mConnection.Open();
                mCommand = new OleDbCommand(this.txtSelect.Text, mConnection);
                mReader = mCommand.ExecuteReader();
                ddlSelectResult.Visible = true;
                ddlSelectResult.Items.Clear();
                while (mReader.Read())
                {
                    ddlSelectResult.Items.Add(new ListItem(mReader.GetString(0)));
                }
                lblTestSelect.Text = "Se ejecuto correctamente";
            }
            catch (OleDbException ex)
            {
                lblTestSelect.Text = ex.Message;
            }
            finally
            {
                mReader.Close();
                mConnection.Close();
            }
        }
    }
}

Por último nos queda implementar la clase que representara a nuestro tipo de campo, esta clase extiende de “SPFieldText” como ya comentamos en el artículo anterior y dado que lo que vamos a guardar es un dato simple, seleccionado por el usuario no necesitamos implementar ningún otro tipo de campo más complejo. En la sección 6 vemos el código de la clase llamada “BdcWss”.

[Sección 6]

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace Siderys.Blog.CustomField
{
    public class BdcWss : SPFieldText
    {
        private string mConnectonString = string.Empty;
        private string mSelectString = string.Empty;

        private static Dictionary<int, string> mDicConnectionStriong = new Dictionary<int, string>();
        private static Dictionary<int, string> mDicSelectString = new Dictionary<int, string>();

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

        public BdcWss(SPFieldCollection pFields, string pTypeName, string pDisplayName)
            : base(pFields, pTypeName, pDisplayName)
        {
            
        }

        public void Init()
        {
            mConnectonString = this.GetCustomProperty("StringConnection").ToString();
            mSelectString = this.GetCustomProperty("StringSelect").ToString();
        }

        private int GetContextId
        {
            get
            {
                return SPContext.Current.GetHashCode();
            }
        }
        public string ConeectionString
        {
            get
            {
                return mDicConnectionStriong.ContainsKey(GetContextId) ? mDicConnectionStriong[GetContextId] : mConnectonString;
            }
            set
            {
                mConnectonString = value;
            }
        }

        public string SelectString
        {
            get
            {
                return mDicSelectString.ContainsKey(GetContextId) ? mDicSelectString[GetContextId] : mSelectString;
            }
            set
            {
                mSelectString = value;
            }
        }

        public void UpdateConnectionStringProperty(string pValue)
        {
            mDicConnectionStriong[GetContextId] = pValue;
        }

        public void UpdateSelectStringProperty(string pValue)
        {
            mDicSelectString[GetContextId] = pValue;
        }

        public override void OnAdded(SPAddFieldOptions op)
        {
            base.OnAdded(op);
            Update();
        }

        public override void Update()
        {
            this.SetCustomProperty("StringConnection", ConeectionString);
            this.SetCustomProperty("StringSelect", SelectString);
            base.Update();
            if (mDicConnectionStriong.ContainsKey(GetContextId))
            {
                mDicConnectionStriong.Remove(GetContextId);
            }
            if (mDicSelectString.ContainsKey(GetContextId))
            {
                mDicSelectString.Remove(GetContextId);
            }
        }

        public override Microsoft.SharePoint.WebControls.BaseFieldControl FieldRenderingControl
        {
            get
            {
                Microsoft.SharePoint.WebControls.BaseFieldControl BdcWssControl = new BdcWssFieldType();
                BdcWssControl.FieldName = InternalName;
                return BdcWssControl;
            }
        }
    
    }
}

Una vez implementado, lo que nos resta es compilarlo e instalar todo en nuestro servidor de Sharepoint 2007, para lo cual en mi caso me cree un archivo .Bat para instalar todo. El Assembly lo debemos colocar en la GAC y los demás archivos debemos colocarlo en la carpeta “CONTROLTEMPLATES” donde está instalado Sharepoint. En la sección 7 podemos ver el código del archivo .bat creado para ejecutar la instalación.

[Sección 7]

iisreset /stop
"%programfiles%Microsoft Visual Studio 8SDKv2.0Bingacutil.exe" -uf Siderys.Blog.CustomField.BdcWss
"%programfiles%Microsoft Visual Studio 8SDKv2.0Bingacutil.exe" 
-if binDebugSiderys.Blog.CustomField.BdcWss.dll copy /y fldtypes_BdcWss.xml "%CommonProgramFiles%Microsoft Sharedweb server extensions12TEMPLATEXML" xcopy /s /Y BdcWssFieldType.ascx "%CommonProgramFiles%Microsoft Sharedweb server
extensions12TEMPLATECONTROLTEMPLATES" xcopy /s /Y BdcWssEditFieldType.ascx "%CommonProgramFiles%Microsoft Sharedweb server
extensions12TEMPLATECONTROLTEMPLATES" iisreset /start pause

Bien una vez instalado todo, lo que nos resta es probar nuestro nuevo tipo de columna. Para testearla me voy a conectar a una base de datos creada en SQL Server 2005, con una tabla llamada productos, en la imagen 1 podemos ver dicha tabla cargada con datos de pruebas.

[Imagen 1]

1_TablaProductos

Lo que debemos hacer ahora es ir a una lista de Sharepoint y acceder a la configuración de la misma. Una vez en la sección de configuración lo que vamos hacer es crear una nueva columna en la misma, para ello seleccionamos el link “Crear Columna” . Una vez en dicha página lo que vamos hacer es colocar un nombre a esta nueva columna, en mi caso la llame “Productos”, en la imagen 2 vemos la pantalla para crear una nueva columna en la lista y en la lista de campos disponibles vemos nuestra nueva columna, cargamos el nombre y seleccionamos dicho tipo de campo.

[Imagen 2]

2_Configuracion_Columna_1

Una vez que seleccionamos el tipo de campo que queremos, la página se vuelve a cargar y nos muestra la sección codificada por nosotros en el control de usuario “BdcWssEditFieldType.ascx” donde vamos a carga la cadena de conexión y la consulta que queremos ejecutar. La imagen 3 nos muestra estos campos cargados y probados y lo próximo que debemos realizar es guardar nuestra nueva columna, para lo cual vamos a presionar el botón “Aceptar” de la página.

[Imagen 3]

3_Configuracion_Columna_2

Una vez guarda la nueva columna en la lista, lo próximo que debemos de realizar es crear un nuevo elemento en la misma y seleccionar un valor del DropDownList correspondiente. En la imagen 4 vemos el formulario para dar de altas nuevos elementos en la lista, con la nueva columna cargada.

[Imagen 4]

4_Nuevo_Elemnto_Lista

Una vez cargados los valores del nuevo elemento, presionamos el botón “Aceptar” para guardar los mismos en la lista. En la imagen 5 vemos la página de todos los elementos de la lista y en nuevo valor ingresado en la misma.

[Imagen 5]

5_Elemento_Vista_All

También quiero destacar que el comportamiento del nuevo campo es similar a todos los campos en una lista de Sharepoint, es decir que podemos editar la configuración y modificar en cualquier momento los valores de conexión y la consulta que estamos ejecutando. Como también podemos editar cualquier elemento creado y cambiar el valor almacenado en la lista por cualquier otro valor de la base de datos. En la imagen 6 vemos la edición del elemento recientemente creado y el nuevo valor seleccionado.

[Imagen 6]

6_Valor_Modificado

Espero que este artículo les sea de utilidad, en lo que respecta al uso que le podemos dar, creo que podemos tener valores en nuestras listas desde un origen de datos y sobre WSS 3.0 principalmente y también lo podemos utilizar en Sharepoint 2007 si no queremos configurar la característica BDC que este nos brinda. De todas formas vale la pena aclarar nuevamente que esto no intenta ser un sustituto de esa gran característica que nos provee MOSS, si no una versión de un tipo de campo que nos permite mostrar datos desde una base de datos.

Les pido disculpas por lo extenso de este artículo, pero hacerlo de una forma más corta hubiera implicado tener que dejar cosas afuera.

Aquí puede descargar el código fuente de este articulo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *