martes, 9 de octubre de 2012

Crear Recordset desconectado a partir de Colección (C#)

A veces se tiene que dar mantenimiento a aplicaciones escritas con lenguajes que ya tienen varios años como visual basic 6 en el cual se trabaja con recordsets. Este recordset se creará a partir de una colección de objetos, y debido que se ha hecho para un fin muy específico solo soportará tipos de datos primitivos y el tipo string-que no es primitivo- en las propiedades de los objetos, todos los objetos de la colección deben ser del mismo tipo y la colección deberá implementar a IEnumerable. Para poder instanciar a un Recordset es necesario importar la referencia COM:
Microsoft ActiveX Data Objects 2.6 Library
y usar la directiva:
using ADODB;
El método que crea el recordset:
public static Recordset CrearRecordsetDesdeLista<T>(IEnumerable<T> lista)
{
    Recordset rsParaVB6 = new Recordset();
    rsParaVB6.CursorLocation = CursorLocationEnum.adUseClient;
    Fields rsColumnas = rsParaVB6.Fields;

    if (lista != null && lista.Count() > 0) 
    {
        //Se elije un objeto no nulo. Si todos los objetos son nulos se obtendrá un null.
        T objNoNulo = lista.FirstOrDefault(x=> x != null);
        if (objNoNulo != null)
        {
            PropertyInfo[] arrPi = objNoNulo.GetType().GetProperties();

            //Agrega las columnas
            for (int i = 0; i < arrPi.Length; i++)
            {
                Type tipoDotNet = arrPi[i].PropertyType;
                DataTypeEnum tipoADODB = Met_TraduceTipoDato(tipoDotNet);
                int lengthColumna = ObtenerMaxLenghtColumna(tipoDotNet, arrPi[i].GetValue(objNoNulo, null));
                rsColumnas.Append(arrPi[i].Name, tipoADODB, lengthColumna
                                    , FieldAttributeEnum.adFldUpdatable, Missing.Value);
            }

            //Abre el recordset:
            rsParaVB6.Open(Missing.Value, Missing.Value, CursorTypeEnum.adOpenUnspecified, LockTypeEnum.adLockUnspecified, 0);

            //Agregando filas
            foreach (var obj in lista)
            {
                if (obj != null)
                {
                    rsParaVB6.AddNew(Missing.Value, Missing.Value);
                    //Agrega los valores a cada columna:                 
                    for (int i = 0; i < arrPi.Length; i++)
                    {
                        rsColumnas[i].Value = arrPi[i].GetValue(obj, null);
                    }
                    rsParaVB6.Update(Missing.Value, Missing.Value);
                }
            }
        }
    }

    return rsParaVB6;
}

En este método se llaman a otros 2 métodos:

/// <summary>
/// Traduce el tipo de dato .Net al tipo de dato que usa ADODB.Recordset y retorna éste último.
/// </summary>
/// <param name="tipoDatoNet">Tipo de dato .Net a ser traducido.</param>
/// <returns>
/// Retorna un tipo de dato apropiado para ADODB.Recordset.
/// </returns>
protected static DataTypeEnum TraduceTipoDato(Type tipoDatoNet)
{
    switch (tipoDatoNet.UnderlyingSystemType.ToString())
    {
        case "System.Boolean":
            return ADODB.DataTypeEnum.adBoolean;

        case "System.Byte":
            return ADODB.DataTypeEnum.adUnsignedTinyInt;

        case "System.Char":
            return ADODB.DataTypeEnum.adChar;

        case "System.DateTime":
            return ADODB.DataTypeEnum.adDate;

        case "System.Decimal":
            return ADODB.DataTypeEnum.adCurrency;

        case "System.Double":
            return ADODB.DataTypeEnum.adDouble;

        case "System.Int16":
            return ADODB.DataTypeEnum.adSmallInt;

        case "System.Int32":
            return ADODB.DataTypeEnum.adInteger;

        case "System.Int64":
            return ADODB.DataTypeEnum.adBigInt;

        case "System.SByte":
            return ADODB.DataTypeEnum.adTinyInt;

        case "System.Single":
            return ADODB.DataTypeEnum.adSingle;

        case "System.UInt16":
            return ADODB.DataTypeEnum.adUnsignedSmallInt;

        case "System.UInt32":
            return ADODB.DataTypeEnum.adUnsignedInt;

        case "System.UInt64":
            return ADODB.DataTypeEnum.adUnsignedBigInt;

        case "System.String":
        default:
            return ADODB.DataTypeEnum.adVarChar;
    }//switch
}

/// <summary>
/// Devuelve el MaxLength para una columna ADODBRecordset, en los DataColumn de .Net el MaxLength es considerado 
/// solo si el tipo es String de lo contrario no se toma en cuenta y el valor predeterminado es -1.
/// <para>
/// Ver: http://msdn.microsoft.com/es-es/library/system.data.datacolumn.maxlength%28v=vs.80%29.aspx
/// </para>
/// Si el tipo de dato es String entonces se devuelve el Length del parámetro <paramref name="valorDatoNet"/>
/// </summary>
/// <param name="tipoDatoNet">Tipo de dato .Net a verificar</param>
/// <param name="valorDatoNet">Valor del tipo de dato a verificar</param>
/// <returns>
/// Retorna el Length de un valor siempre y cuando este sea de tipo 
/// String(si la cadena está vacía o nula el length a devolver será 1), de lo contrario devolverá -1.
/// </returns>
protected static int ObtenerMaxLenghtColumna(Type tipoDatoNet, object valorDatoNet)
{
    int maxLength = -1;
    if ("System.String" == tipoDatoNet.UnderlyingSystemType.ToString())
    {
        maxLength = valorDatoNet != null ? valorDatoNet.ToString().Length : 1;
    }
    return maxLength;
}

La clase completa:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ADODB;
namespace MyDLL
{
    public class ADODBRecordsetCreator
    {
       //aquí van los 3 métodos expuestos líneas arriba.
    }
}

Si asumimos que la clase ADODBRecordsetCreator-la cual contiene a los 3 métodos listados arriba- se encuentra en la librería MyDLL, podemos utilizar dicha librería más o menos así:

using System.Collections.Generic;
using TestDataAccess;
using ADODB;
using MyDLL;
namespace TestRecordSet
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Producto> lista = new List<Producto>()
            {
              new Producto(){Codigo = "000001",Nombre="Producto 1",Precio=100},
              new Producto(){Codigo = "000022",Nombre="Producto 2",Precio=220},
              new Producto(){Codigo = "000333",Nombre="Producto 3",Precio=333}
            };

            Recordset rsParaVB6;
            rsParaVB6 = MyHelpers.CrearRecordsetDesdeLista(lista);
        }
    }

    public class MyHelpers
    {
        public static Recordset CrearRecordsetDesdeLista<T>(IEnumerable<T> lista)
        {
            return ADODBRecordsetCreator.CrearRecordsetDesdeLista(lista);
        }
    }
    
    [Serializable]
    public class Producto
    {
        public string Codigo { get; set; }
        public string Nombre { get; set; }
        public decimal Precio { get; set; }
    }
}