domingo, 27 de noviembre de 2011

[Linq] Categoría Jerárquica múltiple niveles

 

Introducción

El objetivo del artículo es demostrar como linq puede ser verdaderamente potente a la hora de convertir estructuras simple y planas como ser entidades (o tablas) en objetos jerárquicos que faciliten la asignación de datos a controles, en este caso un treeview.

El resultado del artículo cargara un árbol como se muestra en la imagen, el cual podrá tener tantos niveles como uno necesite.

 imagen1

Recuperar los registro de forma básica

El primer paso para armar la estructura será contar con información plana y sin procesar de la base de datos.

public static List<CategoriaEntity> ObtenerCategorias()
{
    string sql = @"SELECT C.IdCategoria,
                            C.IdCategoriaPadre,
                            C.Descripcion,
                            C.Posicion
                    FROM Categorias C
                    ORDER BY C.IdCategoria, C.Posicion";

    List<CategoriaEntity> categorias = new List<CategoriaEntity>();

    using (SqlCeConnection conn = new SqlCeConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        SqlCeCommand cmd = new SqlCeCommand(sql, conn);
        IDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            categorias.Add(ConvertirCategoria(reader));
        }
        
    }

    return categorias;
}

private static CategoriaEntity ConvertirCategoria(IDataReader reder)
{
    return new CategoriaEntity()
    {
        IdCategoria = Convert.ToInt32(reder["IdCategoria"]),
        IdCategoriaPadre = reder["IdCategoriaPadre"] == DBNull.Value ? null : (int?)Convert.ToInt32(reder["IdCategoriaPadre"]),
        Descripcion = Convert.ToString(reder["Descripcion"]),
        Posicion = Convert.ToInt16(reder["Posicion"])
    };
}
Aplicar jerarquía a los datos

La información plana hay que darle jerarquía, determinando que ítem son hijos de que otros.

public static List<CategoriaJerarquica> ObtenerCategoriarJerarquia()
{
    List<CategoriaEntity> categoriasList = ObtenerCategorias();

    List<CategoriaJerarquica> query = (from item in categoriasList
                                        where item.IdCategoriaPadre == null
                                        select new CategoriaJerarquica
                                        {
                                            IdCategoria = item.IdCategoria,
                                            Descripcion = item.Descripcion,
                                            CategoriaHija = ObtenerHijos(item.IdCategoria, categoriasList)
                                        }).ToList();

    return query;
}

private static List<CategoriaJerarquica> ObtenerHijos(int idCategoria, List<CategoriaEntity> categoriasList)
{

    List<CategoriaJerarquica> query = (from item in categoriasList
                                       let tieneHijos = categoriasList.Where(o => o.IdCategoriaPadre == item.IdCategoria).Any()
                                       where item.IdCategoriaPadre == idCategoria
                                       select new CategoriaJerarquica
                                        {
                                            IdCategoria = item.IdCategoria,
                                            Descripcion = item.Descripcion,
                                            CategoriaHija = tieneHijos ? ObtenerHijos(item.IdCategoria, categoriasList) : null
                                        }).ToList();

    return query;

}

 

Con el primer linq al usar

where item.IdCategoriaPadre == null

se obtienen los nodos padres, o sea aquellos que no dependen de ningún otro nodo

 

Asignación de la jerarquía al control TreeView

Como ultimo paso y no menos importante será crear los TreeNode que requiere el control TreeView

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        List<CategoriaJerarquica> categoriaList = CategoriasDAL.ObtenerCategoriarJerarquia();

        CrearNodoHijo(categoriaList, null);
                 
    }
}


private void CrearNodoHijo(List<CategoriaJerarquica> categorias, TreeNode parentNode)
{
    categorias.ForEach(x =>
    {
        TreeNode node = new TreeNode(x.Descripcion, Convert.ToString(x.IdCategoria));

        if (x.CategoriaHija != null)
        {
            CrearNodoHijo(x.CategoriaHija, node);
        }

        if (parentNode == null)
            TreeView1.Nodes.Add(node);
        else
            parentNode.ChildNodes.Add(node);
    });   

}

Como se observa hay una invocación recursiva para armar la estructura del árbol.

 

Código de Ejemplo

El articulo fue creado con visual Studio 2008

La db es Sql Compact Edition (.sdf), por lo tanto esta se encuentra en la carpeta App_Data

 

  [C# SkyDrive]

jqGrid – Listar Orden Compra (Maestro-Detalle)

 

Introducción

La mayoría de las veces los controles que incluye una herramienta de desarrollo puedo no cumplir con las expectativas que uno busca si quiere alcanzar una interfaz rica que aproveche toda la potencia de desarrollo, es por eso que se debe recurrir a componentes externos.

Esta situación suelo encontrarla al mostrar información en un grid, es por eso que jqGrid es un control ideal para potenciar el desarrollo de la interfaz del usuario en entorno web y además se trata de un componente de libre uso.

Lo único aspecto a tener en cuenta se relaciona con la necesidad de conocer algo de javascript, concretamente jquery y de ser posible invocación a webmethods para recuperar la información del grid.

image

 

Configuración

Para poder hacer uso de jqGrid es necesario introducir en el proyecto algunas librerías de javascript.

Las cuales pueden ser descargadas de la pagina jqGrid

La referencia a estas librerías podrían hacerse de dos formas:

- usando el tag <script> en cada una de las paginas donde se requiera el grid

<script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="Scripts/jquery-ui-1.8.16.custom.min.js" type="text/javascript"></script> 

<script src="Scripts/jqGrid/grid.locale-es.js" type="text/javascript"></script>
<script src="Scripts/jqGrid/jquery.jqGrid.min.js" type="text/javascript"></script>
<link href="Scripts/jqGrid/ui.jqgrid.css" rel="stylesheet" type="text/css" />

- o usando ScriptManager.RegisterClientScriptInclude(), esto es útil cuando se quiere registrar librerías para todas las paginas de forma global, aplicándolo en el código de la Master Page

public partial class SiteMaster : System.Web.UI.MasterPage
{
    protected override void OnInit(EventArgs e)
    {

        ScriptManager.RegisterClientScriptInclude(Page, typeof(Page), "jquery", ResolveUrl(@"~/Scripts/jquery-1.6.4.min.js"));
        ScriptManager.RegisterClientScriptInclude(Page, typeof(Page), "jqueryui", ResolveUrl(@"~/Scripts/jquery-ui-1.8.16.custom.min.js"));
        ScriptManager.RegisterClientScriptInclude(Page, typeof(Page), "json2", ResolveUrl(@"~/Scripts/json2.js"));
        
        ScriptManager.RegisterClientScriptInclude(Page, typeof(Page), "gridlocale", ResolveUrl(@"~/Scripts/jqGrid/grid.locale-es.js"));
        ScriptManager.RegisterClientScriptInclude(Page, typeof(Page), "jqgrid", ResolveUrl(@"~/Scripts/jqGrid/jquery.jqGrid.min.js"));

        base.OnInit(e);
    }

}

Al usarse un Master Page este podría verse afectado por la rutas relativas de las paginas, lo que ocasionaría una incorrecta resolución de la url y el acceso a los archivos .js, el método ResolveUrl() nos ayuda a evitar este problema.

 

Definición del grid

Definir el grid con las opciones básicas no es nada difícil, para separar el código de scripting del html de la pagina facilitando así el mantenimiento verán en el ejemplo que he definido 3 .js según la operación de cada uno

En este caso en concreto, se usara el “Grid.js”, el cual define el grid maestro

$("#tbOrders").jqGrid({
    datatype: 'json',
    colNames: ['Fecha Pedido', 'Fecha Solicitud', 'Direccion', 'Ciudad', 'Pais'],
    colModel: [
             { name: 'OrderDate', index: 'OrderDate', width: 100, sortable: false },
             { name: 'RequiredDate', index: 'RequiredDate', width: 100, sortable: false },
             { name: 'ShipAddress', index: 'ShipAddress', width: 250, sortable: false },
             { name: 'ShipCity', index: 'ShipCity', width: 110, sortable: false },
             { name: 'ShipCountry', index: 'ShipCountry', width: 110, sortable: false }
              ],
    height: "300px",
    onSelectRow: function (id) {

        getDetailsOrderByOrder(id);

    }
});

y un grid detalle

$("#tbDetailsOrder").jqGrid({
    datatype: 'json',
    colNames: ['Producto', 'Cantidad', 'Precio'],
    colModel: [
             { name: 'ProductName', index: 'ProductName', width: 250, sortable: false },
             { name: 'Quantity', index: 'RequiredDate', width: 100, sortable: false },
             { name: 'UnitPrice', index: 'UnitPrice', width: 100, sortable: false }
              ],
    height: "200px",
    width:"800px"
});

La configuración es bastante estándar para un uso básico, a partir de aquí hay miles de opciones, pero básicamente se define las columnas (atributo “colNames”), así como también las propiedades de cada columnas como ser el ancho de las mismas.

En el grid maestro además se define un evento, el cual enviara el id de la entidad seleccionada para cargar así el detalle, por supuesto el id que recibe como parámetro es el valor que mas adelante veremos en la estructura json devuelta por el webmethod

 

Definición de los Page Métodos

El siguiente paso será definir la información en el servidor para poder recuperar los registros que cargaran el grid

Para esto se define dos Page Methods en la propia pagina web que implementa los grid (podría usarse una pagina adicional para esta definición)

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public static string GetOrdersByCustomer(string customer)
{

    var orders = NorthwindData.GetOrdersByCustomer(customer);

    var grid = new
    {
        page = 1,
        records = orders.Count(),
        total = orders.Count(),

        rows = from item in orders
               let orderdate = item.OrderDate.HasValue ? item.OrderDate.Value.ToShortDateString() : ""
               let requireddate = item.RequiredDate.HasValue ? item.RequiredDate.Value.ToShortDateString() : ""
              
               select new
               {
                   id = item.OrderID,
                   cell = new string[]{
                       orderdate,
                       requireddate,
                       item.ShipAddress,
                       item.ShipCity,
                       item.ShipCountry,
                       item.Customers.CompanyName

                   }
               }

    };

    return JsonConvert.SerializeObject(grid);
}

 

la estructura que requiere jqGrid es un tanto especial, y gracias a los métodos anónimos es posible armarla, y como ultimo paso serializarla usando la librería JSON.NET

La estructura es bastante simple, se define la pagina, la cantidad de registros y el total, estos valores son útiles cuando el grid esta paginado, en este caso no implementamos la paginación.

Luego se definen las filas, en donde se transforma la entidad obteniendo un identidicado en el “id”, mas una propiedad “cell” que es en definitiva un array de string con la información de cada columna requiere, es importante en este punto respetar las posiciones en que debe ir cada dato con respecto a las definición de las columnas en el paso anterior.

El mismo proceso se aplica para recuperar los detalles

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public static string GetDetailsOrdersByOrder(int order)
{

    var orders = NorthwindData.GetDetailsOrdersByOrder(order);

    var grid = new
    {
        page = 1,
        records = orders.Count(),
        total = orders.Count(),

        rows = from item in orders
               select new
               {
                   id = item.OrderID,
                   cell = new string[]{
                       item.ProductsReference.Value.ProductName,
                       item.Quantity.ToString(),
                       item.UnitPrice.ToString("N2")
                   }
               }

    };

    return JsonConvert.SerializeObject(grid);
}

 

Invocar a los Page Methods

Los grid definidos no están conectados de forma directa para que estos invoquen los servicios de datos, sino que se definieron por separado para que uno desde código controle la invocación de los servicios.

En el archivos ServiceInvoke.js se encuentra la definición

function getOrdersByCustomer(customer) {

    var params = new Object();
    params.customer = customer;

    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: "Default.aspx/GetOrdersByCustomer",
        data: JSON.stringify(params),
        dataType: "json",
        async: false,
        success: function (data, textStatus) {

            if (textStatus == "success") {

                $("#tbDetailsOrder").clearGridData();

                var grid = $("#tbOrders")[0];
                grid.addJSONData(jQuery.parseJSON(data.d));

            }

        },
        error: function (request, status, error) {
            alert(jQuery.parseJSON(request.responseText).Message);
        }
    });

}

 

Por medio de la línea

$("#tbDetailsOrder").clearGridData();

es que se limpian los registros del grid de detalle, ya que al recargarse el principal ya no hay un registro seleccionado.

function getDetailsOrderByOrder(order) {

    var params = new Object();
    params.order = order;

    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: "Default.aspx/GetDetailsOrdersByOrder",
        data: JSON.stringify(params),
        dataType: "json",
        async: false,
        success: function (data, textStatus) {

            if (textStatus == "success") {

                var grid = $("#tbDetailsOrder")[0];
                grid.addJSONData(jQuery.parseJSON(data.d));

            }

        },
        error: function (request, status, error) {
            alert(jQuery.parseJSON(request.responseText).Message);
        }
    });

}

 

Ejemplo de Código


El ejemplo fue desarrollado con visual Studio 2008 y Sql Server 2008 R2 Express

Dentro de la carpeta “DbScript” se encuentra un .sql con la estructura de la db en caso de no poder usar el .mdf adjunto en la solución

 

[C#] 
 

lunes, 10 de octubre de 2011

[GridView] Eventos de controles contenidos en el GridView (2/2)

 

Introducción

Este artículo representa una continuación de:

[GridView] Eventos de controles contenidos en el GridView (1/2)

en donde se planteara un ejemplo práctico del uso de eventos en controles contenidos en el gridview

El ejemplo representa la edición de un empleado a quien se le asigna determinadas prendas de vestir por parte de la empresa. Lo importante es que debe registrarse la fecha en que fue asigna esa indumentaria.

Un punto a que no debe dejarse pasar por alto es que la selección de la prenda asignada no debe actualizar la tabla en al db directamente, la información se encontrara registrada en memoria, cuando el usuario confirme los cambios mediante el botón de aceptar será el momento en que  toda la información, incluida las indumentarias, impactara en la base de datos.

Diseño de objetos

Las entidades estarán representadas mediante el siguiente diagrama

La entidad Empleado se relaciona con las listas de estudios he indumentarias que tiene asociada. Es por esta razón que en la capa de Acceso a datos se recuperan estas relaciones cuando se obtiene una entidad.

Esta información relacionada al empleado será utilizada en las acciones de actualización cuando se efectúen las acciones en la presentación, esto se llevara a cabo mediante el “EmpleadoManager”

 

Actualizar Entidad

Una técnica bastante utilizada para encapsular la funcionalidad de entidades en memoria cuando se debe conservar la información dentro del objeto Session, consiste en crear una clase que aplique el patrón singleton.

Este patrón usar el objeto Session para contener su información entre request.

Se puede ver en este caso la implementación en la clase “EmpleadoManager”

/// <summary>
/// El objetivo de esta clase es generar una abstraccion al uso de la Session para administrar la entidad del empleado
/// </summary>
public class EmpleadoManager
{
    private EmpleadoEntity empleado;

    private EmpleadoManager()
    {
        empleado = new EmpleadoEntity();
    }

    public static EmpleadoManager Instance()
    {
        if(HttpContext.Current.Session["empleado"] == null)
        {
            HttpContext.Current.Session["empleado"] = new EmpleadoManager();
        }

        return (EmpleadoManager)HttpContext.Current.Session["empleado"];
    }

    public static void NuevoEmpleado()
    {
        HttpContext.Current.Session.Remove("empleado");
    }

    public EmpleadoEntity Empleado
    {
        get { return empleado; }
        set { empleado = value; }
    }


    public void IndumentariaAsignar(int idIndumentaria)
    {
        if (HttpContext.Current.Session["empleado"] == null)
            return;
        
        //Se recupera la entidad lista de la session
        EmpleadoManager empleadoManager =(EmpleadoManager)HttpContext.Current.Session["empleado"];

        List<IndumentariaEntity> indumentaria = empleadoManager.Empleado.IndumentariaAsignada;

        //se recupera la entidad de la lista y se actualiza la fecha de asignacion
        if (indumentaria.Exists(x => x.IdIndumentaria == idIndumentaria))
        {
            indumentaria.Where(x => x.IdIndumentaria == idIndumentaria)
                                            .First()
                                            .FechaAsignacion = DateTime.Now;
        }
        else
        {
            indumentaria.Add(new IndumentariaEntity()
            {
                 IdIndumentaria = idIndumentaria,
                 FechaAsignacion = DateTime.Now
            });
        }

    }

    public void IndumentariaDesasignar(int idIndumentaria)
    {
        if (HttpContext.Current.Session["empleado"] == null)
            return;

        //Se recupera la entidad lista de la session
        EmpleadoManager empleadoManager = (EmpleadoManager)HttpContext.Current.Session["empleado"];

        List<IndumentariaEntity> indumentaria = empleadoManager.Empleado.IndumentariaAsignada;

        //se recupera la entidad de la lista y se actualiza la fecha de asignacion
        if (indumentaria.Exists(x => x.IdIndumentaria == idIndumentaria))
        {
            indumentaria.RemoveAll(x => x.IdIndumentaria == idIndumentaria);                           
        }

    }


}

La utilización desde la pagina es muy simple

protected void checkIndumentaria_CheckedChanged(object sender, EventArgs e)
{
    CheckBox checkBox = sender as CheckBox;

    //NamingContainer devuelve el objeto donde esta contenidos el checkbox
    //en este caso se trata de una row del gridview
    GridViewRow row = checkBox.NamingContainer as GridViewRow;
    
    //conociendo la row se puede obtener el id de la entidad que se esta seleccionando
    int idindumentaria = Convert.ToInt32(gvIndumentaria.DataKeys[row.RowIndex].Value);

    if (checkBox.Checked)
        EmpleadoManager.Instance().IndumentariaAsignar(idindumentaria);
    else
        EmpleadoManager.Instance().IndumentariaDesasignar(idindumentaria);


    AsignarIndumentaria(EmpleadoManager.Instance().Empleado);

}

Es en este punto donde se aplica la técnica que permite recuperar la fila seleccionada al marcar el checkbox contendió en el gridview.

 

Código


El ejemplo se desarrollo con VS 2008, y base de datos Sql Server Express 2008 R2

[C#]
[C# SkyDrive]

sábado, 17 de septiembre de 2011

Filtros Condicionales (2/2) – Implementar filtros múltiples

 

Introducción


Este artículo representa continuación del anterior

Filtros Condicionales (1/2)

en realidad se podría ver como una extensión, ya que aquí el objetivo consiste en poder aplicar múltiples filtro usando de cursos, y no solo uno como fue en el artículo previo

La interacción en la pantalla, para esta funcionalidad en concreto, esta dada por los siguientes pasos:

image 

Algo que hay que aclarar antes de abordar las soluciones planteadas es que la instrucción IN de sql no soporta de forma directa el uso de parámetros, es por eso que existen actualmente varios caminos a tomar ante esta situación, aquí solo expondré dos de ellos, pero existen algunos otros.

Entre los temas tratados se podrán encontrar

  1. Comunicación entre formularios
  2. Filtro IN, concatenado el string
  3. Filtro por medio de XML

 

1- Comunicación entre formularios


En el formulario de búsqueda (frmBusqueda) encontrar un código como el siguiente

private void btnBuscarCursos_Click(object sender, EventArgs e)
{
    List<CourseEntity> selectedCourses = txtCursos.Tag as List<CourseEntity>;

    using (frmSeleccionarCursos frmcursos = new frmSeleccionarCursos(selectedCourses))
    {
        if (frmcursos.ShowDialog(this) == DialogResult.OK)
        {
            txtCursos.Tag = frmcursos.CursosSeleccionados;
            txtCursos.Text = string.Join(", ", frmcursos.CursosSeleccionados.Select(x => x.Title).ToArray());
        }
    }
}

Como frmSeleccionarCursos se abre de forma modal puede esperarse en el ShowDialog() hasta tanto el Form sea cerrado, cuando esta operación se lleve a cabo y se detecte la aceptación satisfactoria del Form se procede a tomar los cursos seleccionados y asignarlos al control que contendrá la información.

En este caso se hace uso de la propiedad Tag del Textbox para mantener la información seleccionada

En el formulario de selección dispone de una propiedad para que el formulario que lo invoco pueda acceder a la información sin necesidad de recurrir directo de los controles del propio Form. A su vez solo el evento del botón Aceptar es que el cierra el form con una resultado aceptado para procesar la selección.

public List<CourseEntity> CursosSeleccionados
{
    get
    {
        return lstCursosSelected.Items.Cast<CourseEntity>().ToList();
    }
}

private void btnAceptar_Click(object sender, EventArgs e)
{
  this.DialogResult = DialogResult.OK;
}

private void btnCancelar_Click(object sender, EventArgs e)
{
  this.DialogResult = DialogResult.Cancel;
}

 

2 -Filtro IN, concatenado el string


Esta primera implementación se podría decir que es la mas estándar y directa, aunque hay que remarcar que no es la mas bonita.

En la clase PersonDAL se cuenta el método Select()

public static List<PersonEntity> Select(PersonCriteria filter)
{
    string sql = @"SELECT   P.PersonID,
                            P.LastName,
                            P.FirstName,
                            P.HireDate,
                            P.EnrollmentDate
                   FROM Person P
                        LEFT JOIN CourseInstructor CI 
                        ON P.PersonID = CI.PersonID
                   WHERE ((@FirstName IS NULL) OR (P.FirstName LIKE '%' + @FirstName + '%'))
                    AND ((@LastName IS NULL) OR (P.LastName LIKE '%' + @LastName + '%'))
                    AND ((@HireDateFrom IS NULL) OR (P.HireDate >= @HireDateFrom))
                    AND ((@HireDateTo IS NULL) OR (P.HireDate <= @HireDateTo))
                    AND ((@EnrollmentDateFrom IS NULL) OR (P.EnrollmentDate >= @EnrollmentDateFrom))
                    AND ((@EnrollmentDateTo IS NULL) OR (P.EnrollmentDate <= @EnrollmentDateTo))
                    AND ((@Course IS NULL) OR (CI.CourseID IN ({0})))";

    if (filter.Course != null)
    {
        string courseFilter = string.Join(",", filter.Course.ConvertAll(x => x.CourseID.ToString()).ToArray());
        sql = sql.Replace("{0}", courseFilter);
    }

    List<PersonEntity> list = new List<PersonEntity>();

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        SqlCommand cmd = new SqlCommand(sql, conn);

        if (string.IsNullOrEmpty(filter.FirstName))
            cmd.Parameters.AddWithValue("@FirstName", DBNull.Value);
        else
            cmd.Parameters.AddWithValue("@FirstName",  filter.FirstName);

        cmd.Parameters.AddWithValue("@LastName", string.IsNullOrEmpty(filter.FirstName) ? (object)DBNull.Value : filter.LastName);

        cmd.Parameters.AddWithValue("@HireDateFrom", filter.HireDateFrom.HasValue ? filter.HireDateFrom.Value.Date : (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@HireDateTo", filter.HireDateTo.HasValue ? filter.HireDateTo.Value.Date : (object)DBNull.Value);

        cmd.Parameters.AddWithValue("@EnrollmentDateFrom", filter.EnrollmentDateFrom.HasValue ? filter.EnrollmentDateFrom.Value.Date : (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@EnrollmentDateTo", filter.EnrollmentDateTo.HasValue ? filter.EnrollmentDateTo.Value.Date : (object)DBNull.Value);

        cmd.Parameters.AddWithValue("@Course", filter.Course == null ? (object)DBNull.Value : "");

        SqlDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            list.Add(LoadPerson(reader));
        }

        return list;
    }

}

El mismo sigue todas las reglas mencionadas en la parte 1 del articulo, solo que filtro de cursos tiene una particularidad.

Es preciso notar las líneas 19-23, en estas es cuando se une al string de la consulta principal, la lista de cursos seleccionados.

La línea 46, sigue representando la anulación o no del filtro del cursos, esto es necesario en caso de no enviarse ningún ítem en la selección.

 

3 -Filtro por medio de XML


Si bien esta implementación no es estandar para todas las base de datos, ya que requiere de soporte para xml, si es la que mejor cierra en cuanto al uso de parámetros.

En este caso la lista de cursos seleccionado es convertida a un xml, el cual se asigna al parámetro para luego unirlo al join de la consulta.

public static List<PersonEntity> SelectByXml(PersonCriteria filter)
        {
            string sql = @"
                           DECLARE @idoc  int
                           EXEC sp_xml_preparedocument @idoc OUTPUT, @Course

                           SELECT   P.PersonID,
                                    P.LastName,
                                    P.FirstName,
                                    P.HireDate,
                                    P.EnrollmentDate
                           FROM Person P
                                LEFT JOIN CourseInstructor CI 
                                ON P.PersonID = CI.PersonID
                                    LEFT JOIN OPENXML(@idoc, '/courses/course', 2)
                                    WITH (id  int 'text()') AS CL ON CI.CourseID = CL.id 
                           WHERE ((@FirstName IS NULL) OR (P.FirstName LIKE '%' + @FirstName + '%'))
                            AND ((@LastName IS NULL) OR (P.LastName LIKE '%' + @LastName + '%'))
                            AND ((@HireDateFrom IS NULL) OR (P.HireDate >= @HireDateFrom))
                            AND ((@HireDateTo IS NULL) OR (P.HireDate <= @HireDateTo))
                            AND ((@EnrollmentDateFrom IS NULL) OR (P.EnrollmentDate >= @EnrollmentDateFrom))
                            AND ((@EnrollmentDateTo IS NULL) OR (P.EnrollmentDate <= @EnrollmentDateTo))
                            AND ((@Course IS NULL) OR (CL.id IS NOT NULL))";


            List<PersonEntity> list = new List<PersonEntity>();

            using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
            {
                conn.Open();

                SqlCommand cmd = new SqlCommand(sql, conn);

                if (string.IsNullOrEmpty(filter.FirstName))
                    cmd.Parameters.AddWithValue("@FirstName", DBNull.Value);
                else
                    cmd.Parameters.AddWithValue("@FirstName", filter.FirstName);

                cmd.Parameters.AddWithValue("@LastName", string.IsNullOrEmpty(filter.FirstName) ? (object)DBNull.Value : filter.LastName);

                cmd.Parameters.AddWithValue("@HireDateFrom", filter.HireDateFrom.HasValue ? filter.HireDateFrom.Value.Date : (object)DBNull.Value);
                cmd.Parameters.AddWithValue("@HireDateTo", filter.HireDateTo.HasValue ? filter.HireDateTo.Value.Date : (object)DBNull.Value);

                cmd.Parameters.AddWithValue("@EnrollmentDateFrom", filter.EnrollmentDateFrom.HasValue ? filter.EnrollmentDateFrom.Value.Date : (object)DBNull.Value);
                cmd.Parameters.AddWithValue("@EnrollmentDateTo", filter.EnrollmentDateTo.HasValue ? filter.EnrollmentDateTo.Value.Date : (object)DBNull.Value);

                if (filter.Course != null)
                {
                    XElement root = new XElement("courses");
                    List<XElement> couseList = filter.Course.ConvertAll(x => new XElement("course", x.CourseID));
                    root.Add(couseList.ToArray());

                    cmd.Parameters.AddWithValue("@Course", root.ToString());
                }
                else
                {
                    cmd.Parameters.AddWithValue("@Course", DBNull.Value);
                }

                
                SqlDataReader reader = cmd.ExecuteReader();

                while (reader.Read())
                {
                    list.Add(LoadPerson(reader));
                }

                return list;
            }

        }

Las líneas 47-58, implementan la conversión a xml de los cursos, esta toman la forma

<courses>
  <course>1045</course>
  <course>1061</course>
</courses>

o sea cada curso seleccionado representa un tag en el xml

La consulta tiene algunas particularidades como ser las dos primeras líneas

DECLARE @idoc  int
EXEC sp_xml_preparedocument @idoc OUTPUT, @Course

cuya finalidad es inicializar el xml que luego es usado en el join

LEFT JOIN OPENXML(@idoc, '/courses/course', 2)
WITH (id  int 'text()') AS CL ON CI.CourseID = CL.id

Aquí el text() representa justamente el contenido del tag, y por medio del /courses/course (el cual es un selector de XPath), se toma cada tag de curso.

 

Para mas información sobre como trabajar el xml en T-SQL, un excelente recurso es el MSDN Library

OPEN XML

Usar OPENXML

Se puede además realizar pruebas puntuales del xml para entender su funcionamiento, por ejemplo

DECLARE  @Course As VARCHAR(1000)
SET  @Course = N'<courses>
  <course>1045</course>
  <course>1061</course>
</courses>'

DECLARE @idoc  int
EXEC sp_xml_preparedocument @idoc OUTPUT, @Course

SELECT * FROM OPENXML(@idoc, '/courses/course', 2)
          WITH (id  int 'text()')

Estas podrían ser ejecutadas en el Sql Server Management Studio

 

Links Útiles


Arrays and Lists in SQL Server 2000 and Earlier

 

Ejemplo de Código


La base de datos fue creada con Sql Server Express 2008 R2, en caso de tener problemas con al misma en el proyecto “DataAccess” esta la carpeta “script” con el .sql que puede usar para crear estructura y datos.

[C#] 
 

domingo, 11 de septiembre de 2011

Filtros Condicionales (1/2)

 

Introducción


Al desarrollar una aplicación un aspecto del cual seguramente habrá que pensar es como otorgar al usuario información que el sea útil y simple de analizar.

En este punto juega un papel fundamental como diseñar los filtros aplicado a las entidades, para poder se lo mas preciso posible en la búsqueda.

Es por eso que en este artículo se plantea la implementación de búsqueda por medio de clases que denominare Criteria, ya que esta serán las encargadas de proporcionar los datos seleccionados por el usuario en la pantalla.

Como punto ventajoso de esta técnica se podría mencionar que la clase evita tener que redefinir los parámetros del método de búsqueda cada vez que se quiera extender la funcionalidad. Esto es muy importante si se tiene la idea de aplicar algún patrón como ser el Repository.

Los requerimientos cambiantes en las aplicaciones hacen que en un primer momento un filtro simple de dos campos cubra la necesidad, pero el uso diario por parte del usuario ira cambiando esta visión, solicitando agregar nuevo filtros, el uso de una clase como témplate de filtro facilita esta tarea ya que el método no cambia en sus parámetros, solo la clase Criteria se vera afectada y por supuesto la query que aplique el filtro.

 

Estructura de la solución


Analicemos un poco como esta estructurado el ejemplo.

 image

Solo son dos capas, una de acceso a datos y otro de presentación, se realizo de esta forma para no complicar el desarrollo del ejemplo, porque aquí no se pretendía mostrar una arquitectura completa, sino solo apuntar a entender como aplicar filtros de forma correcta.

Es muy importante remarcar la clase de nombre PersonCriteria, la cual actuara solo cuando los filtros entran en accion, y la clase PersonEntity, quien representa de la entidad de negocio.

Analicemos la diferencia entre las dos entidades:

public class PersonCriteria
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public DateTime? EnrollmentDateFrom { get; set; }
    public DateTime? EnrollmentDateTo { get; set; }

    public DateTime? HireDateFrom { get; set; }
    public DateTime? HireDateTo { get; set; }

    public CourseEntity Curse { get; set; }
}
public class PersonEntity
{
    public int PersonID {get; set;}
    public string LastName { get; set; } 
    public string FirstName { get; set; } 

    public DateTime? HireDate { get; set; }
    public DateTime? EnrollmentDate { get; set; } 
}

Las propiedades de cada clase tienen unas cuantas diferencias porque sus responsabilidades y lo que representantas son diferentes.

Análisis de la Presentación


La pantalla que se presentara al usuario incluirá varios filtros que actúan de forma combinada.

image

El punto clave en al presentación será la lógica encargada de cargar el criterio de búsqueda:

private void btnFiltrar_Click(object sender, EventArgs e)
{
    PersonCriteria filter = new PersonCriteria()
    {
        FirstName = txtNombre.Text,
        LastName = txtApellido.Text,
        EnrollmentDateFrom = chkFechaInscripcionDesde.Checked ? (DateTime?)dtpFechaInscripcionDesde.Value : null,
        EnrollmentDateTo = chkFechaInscripcionHasta.Checked ? (DateTime?)dtpFechaInscripcionHasta.Value : null,
        HireDateFrom = chkFechaContratacionDesde.Checked ? (DateTime?)dtpFechaContratacionDesde.Value : null,
        HireDateTo = chkFechaContratacionHasta.Checked ? (DateTime?)dtpFechaContratacionHasta.Value : null,
        Curse = Convert.ToInt32(cmbCourse.SelectedValue)== -1 ? null : new CourseEntity() { CourseID = Convert.ToInt32(cmbCourse.SelectedValue) }
    };

    dgvPersonList.DataSource = PersonDAL.Select(filter);

}

La presentación conoce como asignar la propiedad con el dato correcto, es porque eso que allí se observan validaciones para determinar si asignar el dato o no.

 

Acceso a Datos


Esta es la capa con mayor responsabilidad, lógicamente porque será la encargada de aplicar el filtro, pero todo se reduce a un simple truco en la query que habilita o no el filtro en el WHERE

 

public static List<PersonEntity> Select(PersonCriteria filter)
{
    string sql = @"SELECT   P.PersonID,
                            P.LastName,
                            P.FirstName,
                            P.HireDate,
                            P.EnrollmentDate
                   FROM Person P
                        LEFT JOIN CourseInstructor CI 
                        ON P.PersonID = CI.PersonID
                   WHERE ((@FirstName IS NULL) OR (P.FirstName LIKE '%' + @FirstName + '%'))
                    AND ((@LastName IS NULL) OR (P.LastName LIKE '%' + @LastName + '%'))
                    AND ((@HireDateFrom IS NULL) OR (P.HireDate >= @HireDateFrom))
                    AND ((@HireDateTo IS NULL) OR (P.HireDate <= @HireDateTo))
                    AND ((@EnrollmentDateFrom IS NULL) OR (P.EnrollmentDate >= @EnrollmentDateFrom))
                    AND ((@EnrollmentDateTo IS NULL) OR (P.EnrollmentDate <= @EnrollmentDateTo))
                    AND ((@Course IS NULL) OR (CI.CourseID = @Course))";

    List<PersonEntity> list = new List<PersonEntity>();

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        SqlCommand cmd = new SqlCommand(sql, conn);

        if (string.IsNullOrEmpty(filter.FirstName))
            cmd.Parameters.AddWithValue("@FirstName", DBNull.Value);
        else
            cmd.Parameters.AddWithValue("@FirstName",  filter.FirstName);

        cmd.Parameters.AddWithValue("@LastName", string.IsNullOrEmpty(filter.FirstName) ? (object)DBNull.Value : filter.LastName);

        cmd.Parameters.AddWithValue("@HireDateFrom", filter.HireDateFrom.HasValue ? filter.HireDateFrom.Value.Date : (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@HireDateTo", filter.HireDateTo.HasValue ? filter.HireDateTo.Value.Date : (object)DBNull.Value);

        cmd.Parameters.AddWithValue("@EnrollmentDateFrom", filter.EnrollmentDateFrom.HasValue ? filter.EnrollmentDateFrom.Value.Date : (object)DBNull.Value);
        cmd.Parameters.AddWithValue("@EnrollmentDateTo", filter.EnrollmentDateTo.HasValue ? filter.EnrollmentDateTo.Value.Date : (object)DBNull.Value);

        cmd.Parameters.AddWithValue("@Course", filter.Curse == null ? (object)DBNull.Value: filter.Curse.CourseID);


        SqlDataReader reader = cmd.ExecuteReader();

        while (reader.Read())
        {
            list.Add(LoadPerson(reader));
        }

        return list;
    }

}

Analicemos una sección pequeña para entenderlo:

((@FirstName IS NULL) OR (P.FirstName LIKE '%' + @FirstName + '%'))

en esta se tienen dos parte, la primera compara el parámetro con NULL, por lo tanto si desde el código .net enviamos un DbNull.Value estaríamos anulando este filtro, porque esta comparación aplicaría siempre para todos los registros.

En la segunda mitad será donde el filtro se aplica, este al tener un valor no se vera afectado por la primer sección.

Además quise mostrar que existen varias formas desde código de asignar el parámetro, es por eso que la línea 27 la asignación del parámetro se efectúa con un if completo, mientras que el resto lo hace en sola linea, esto fue solo para remarcar que no hay una única forma de asignar el parámetro, si la comparación en una línea queda compleja (o poco clara) se puede pasar a varias líneas.

Este es todo el truco, no es nada difícil de implementar, y conocerlo puede ayudar brindar al usuario filtros mas potentes que agreguen valor a la aplicación.

Ejemplo de Código


La base de datos fue creada con Sql Server Express 2008 R2, en caso de tener problemas con al misma en el proyecto “DataAccess” esta la carpeta “script” con el .sql que puede usar para crear estructura y datos.

 

[C#] 
[VB.NET] 

lunes, 22 de agosto de 2011

Archivos de Configuración - Crear secciones propias (3/3)

 

Introducción

En esta tercer parte se extenderá el ejemplo planteado en los artículos previos

Archivos de Configuración - Una introducción (1/3)

Archivos de Configuración – Creando secciones propias (2/3)

A nivel de interfaz de usuario no ha sufrido cambios, pero si en funcionalidad, al ser esta mucho mas dinámica.

Entere los puntos tratados aquí se incluirán:

  • un modelo de configuración mas rico y extendido
  • instancia dinámica de librerías, las cuales no serán referenciadas al utilizarse

 

Analizando la estructura

Empecemos analizando como se dividieron los proyecto y como se interrelacionan.

En esta oportunidad las clases que definen la configuración están separadas del proyecto de presentación, pero unidas por medio de una referencia.

El proyecto de proveedores también esta en un proyecto separado, pero hay un detalle, el proyecto de UI no tiene referencia alguna a este. Esto es así porque se instanciara la librería se instanciara de forma dinámica.

Para poder hacer uso de la misma se copia la dll resultante usando un “Post Build Event”, para esto se accede por medio de las propiedades del proyecto de providers.

El post build copiara la dll del providers a la carpeta \bin\Debug del proyecto de UI, (donde estará el .exe), de esta forma al instanciarse dinámicamente la librería la encontrara sin problema, la carpeta local es uno de los sitios donde .net busca las instancias cuando necesita crearla.

El ejemplo se va a dividir en dos implementaciones, una en donde los proveedores no definan un interfaz común para poder se utilizados, y otra en donde la interacción es normalizada por una interfaz.

La definición de la configuración es común para ambas implementaciones, porque básicamente los datos son los mismos.

 

Definicion de la configuración

Los tag usados en esta oportunidad posee un mayor nivel de detalle comparado con los ejercicios de los artículos previos, en cuanto a funcionalidad he información que definen.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="MediosPagoSection" type="MediosPagos.UI.Configuration.MediosPagoConfigurationSection, MediosPagos.UI.Configuration" />
  </configSections>

  <MediosPagoSection>
    <assembly file="MediosPagos.Providers.dll" />
    <MediosPago>
      <MedioPago id="1" descripcion="Efectivo">
        <provider type="MediosPagos.Providers.EfectivoProvider" method="CalcularDescuento" />
      </MedioPago>
      <MedioPago id="2" descripcion="Tarjeta Credito">
        <provider type="MediosPagos.Providers.TarjetaCreditoProvider" method="CalcularRecargo">
          <params>
            <param name="recargo" value="10" type="System.Int32" />
          </params>
        </provider>
      </MedioPago>
      <MedioPago id="3" descripcion="Tarjeta Debito">
        <provider type="MediosPagos.Providers.TarjetaDebitoProvider" method="CalcularRecargo" />
      </MedioPago>
      <MedioPago id="4" descripcion="Cheque">
        <provider type="MediosPagos.Providers.ChequeProvider" method="Calcular">
          <params>
            <param name="recargo" value="10" type="System.Int32" />
            <param name="cargofijo" value="5,4" type="System.Decimal" />
          </params>
        </provider>
      </MedioPago>
      <MedioPago id="5" descripcion="Transferencia Bancaria">
        <provider type="MediosPagos.Providers.TransferenciaBancariaProvider" method="Calcular" /> 
      </MedioPago>
    </MediosPago>
  </MediosPagoSection>
  
</configuration>

Al tener el xml de configuración un tags anidados con una profundidad mayor, es lógico que la interacción entre las clases también refleje esta complejidad.

Hay algunos tag a remarcar en cuanto a utilidad:

- el tag <assembly> definido dentro de la sección de medios de pago, es utilizado para conocer el nombre de la dll donde están los providers implementados, esta información será usada para crear la instancia de la librería

- tag <provider>, ahora tiene atributos que definen el type, indicando la clase donde se implementa ese provider dentro de la librería, y el method, que se invocara. Para la versión con interfaz este atributo no será necesario, porque el contrato será único, los metodos llevaran el mismo nombre y cantidad de parametros.

- tag <params>, proporciona información adicional y dinámica necesaria por la implementación de proveedor del calculo, esta es una colección por lo que podrá agregarse cualquier numero de tag ítems que sean necesarios

Implementación sin una interfaz común

Empecemos analizando la versión que no implementa una interfaz, en donde los nombres de los métodos puedes ser variados, al igual que el numero de parámetros.

Con respecto a los parámetros, hay que mencionar una regla definida, el importe es fijo he ira siempre en primer lugar.

 

string mediopago = Convert.ToString(cmbMediosPago.SelectedValue);
ProviderElement provider = Config.Instance().GetProviderById(mediopago);

Assembly _assembly = Assembly.LoadFrom(Config.Instance().MediosPago.assembly.file);
object instance = _assembly.CreateInstance(provider.type);

//
// defino los parametros
//
List<object> param = new List<object>();
param.Add(importe);

foreach (ParamElement paramItem in provider.Params)
{
    param.Add(Convert.ChangeType(paramItem.value, Type.GetType(paramItem.type)));
}

//
// invoco al metodo
//
object result = instance.GetType().InvokeMember(provider.method, BindingFlags.InvokeMethod, null, instance, param.ToArray());


txtTotal.Text = string.Format("{0:N2}", result);

El código para realizar la invocación después de todo no parece ser tan complejo, no al menos como uno se lo imaginaba cuando observar el archivo de configuración.

Primeramente, se crea la instancia, pero como primer paso se carga el Assembly, es por eso que se requiere el nombre de la dll.

A continuación se defina la lista de parámetros en el mismo orden como se define en la implementación del método, colocando el importe en primer lugar, y a continuación los parámetros dinámicos.

Por ultimo se invoca el método, para lo cual se ha usado el InvokeMember(), porque de este solo se tiene el nombre devuelto por la configuración, pudiendo ser distinto en cada implementación.

 

[C#]
 

Implementación con interfaz Común

Si bien esta debería ser una implementación correcta del ejemplo, la anterior tenia su objetivo practico, que apuntaba a demostrar como configurar he invocar cuando todo es dinámico, pero en un desarrollo real siempre hay que tratar de llevar a la estandarización, y las interfaces permiten que esto sea simple.

Para que esto funcione fue necesario adaptar algo mas los proyecto, es por eso que ahora hay uno nuevo que actúa de intermediario entre la UI y la implementación de la proveedores, apunto al proyecto de interfaz, el cual es referenciado por los otros dos, como se observa en la imagen:

El proyecto de interfaz define el contrato entre las partes:

public interface ICalculo
{
    decimal Calcular(decimal importe);
}

Los proveedores los respetan y cumplen:

public class ChequeProvider : ICalculo
{

    /// <summary>
    /// El cheque recarga un porcentaje configurable
    /// Ademas se cobrara un cargo fijo de administracion
    /// </summary>
    /// <param name="importe"></param>
    /// <returns></returns>
    public decimal Calcular(decimal importe)
    {
        //
        // se obtiene los parametros de la configuracion
        //
        ProviderElement provider = Config.Instance().GetProviderById("4"); //Cheque
        int recargo = Convert.ToInt32(provider.Params["recargo"].value);
        decimal cargofijo = Convert.ToDecimal(provider.Params["cargofijo"].value);

        //
        // se realiza el calculo del importe final
        //
        return importe + (importe * ((decimal)recargo / 100)) + cargofijo;
    }

}
public class TarjetaCreditoProvider : ICalculo 
{

    /// <summary>
    /// La tarjeta de Credito recarga configurable por sistema
    /// </summary>
    /// <param name="importe"></param>
    /// <returns></returns>
    public decimal Calcular(decimal importe)
    {
        ProviderElement provider = Config.Instance().GetProviderById("2"); //Tarjeta Credito
        int recargo = Convert.ToInt32(provider.Params["recargo"].value);

        return importe + (importe * ((decimal)recargo / 100));
    }

}

Ahora los métodos solo reciben el importe por parámetro, si estos quieren un valor configurable ellos mismos acceden a los datos por el mismo lugar que lo hace la UI, y toman el valor directamente.

La invocación al método ahora es bien simple, porque se tiene una interfaz:

string mediopago = Convert.ToString(cmbMediosPago.SelectedValue);
ProviderElement provider = Config.Instance().GetProviderById(mediopago);

Assembly _assembly = Assembly.LoadFrom(Config.Instance().MediosPago.assembly.file);
ICalculo instance = (ICalculo)_assembly.CreateInstance(provider.type);

//
// invoco al metodo
//
decimal result = instance.Calcular(importe);

txtTotal.Text = string.Format("{0:N2}", result);

 

[C#]
 

domingo, 31 de julio de 2011

Archivos de Configuración – Creando secciones propias (2/3)

Introducción

Continuando con el planteo en la primer artículo

Archivos de Configuración - Una introducción (1/3)

avanzaremos en complejidad agregando funcionalidad que permita extender la configuración a nuestro capricho, modelando así los tags de la estructura xml que se considere adecuada para representar la información de configuración que requiere nuestra aplicación.

Continuando con la idea del artículo anterior y los medios de pago, imaginemos que dado un importe hay que aplicarle un determinado recargo (o descuento) según el medio de pago seleccionado, pero esto debería poder configurarse, porque se prevé que puede aparecer otros medios de pago en el futuro.

La interfaz es muy simple, se selecciona un medio de pago y se ingresa un importe, el botón “calcular” invocara al proveedor definido para aplicar la operación devolviendo el resultado que se muestra.

Definición de la configuración

Definiremos la configuración con la cual nos basaremos en el ejemplo

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="MediosPagoSection" type="WinformsConfigSeccionesPropias.Configuration.MediosPagoConfigurationSection, WinformsConfigSeccionesPropias" />
  </configSections>

  <MediosPagoSection>
    <MediosPago>
      <MedioPago id="1" descripcion="Efectivo" provider="WinformsConfigSeccionesPropias.EfectivoProvider" />
      <MedioPago id="2" descripcion="Tarjeta Credito" provider="WinformsConfigSeccionesPropias.TarjetaCreditoProvider"/>
      <MedioPago id="3" descripcion="Tarjeta Debito" provider="WinformsConfigSeccionesPropias.TarjetaDebitoProvider"/>
      <MedioPago id="4" descripcion="Cheque" provider="WinformsConfigSeccionesPropias.ChequeProvider"/>
      <MedioPago id="5" descripcion="Transferencia Bancaria" provider="WinformsConfigSeccionesPropias.TransferenciaBancariaProvider"/>
    </MediosPago>
  </MediosPagoSection>
  
</configuration>

Un primer punto a remarcar es la definición del una sección de configuración de nombre “MediosPagoSection”, la cual se asocia a una clase diseñada para poder interpretar la región de configuración que necesitamos.

La sección define una colección de medios de pago, en donde cada ítem cuenta con un “id”, “descripción” y el mas importante el “provider”, que no es nada mas que el nombre completo de la clase que implementa el calculo para ese medio de pago.

Definición de las clase de configuración

En el siguiente diagrama

Se define la estructura de clases utilizadas para poder mapear los tag del .config con clases que permitan manipular esta información.

La clase de nombre “Config” no seria en realidad parte de la implementación necesaria para interpretar los tag de configuración, sino mas bien es un adicional que aplica el patrón singleton para proporcionar un único acceso y directo a la información de configuración, pero su inclusión no es obligatoria.

public class Config
{
    private static Config _config;

    private Config()
    {
        this.MediosPago = (MediosPagoConfigurationSection)ConfigurationManager.GetSection("MediosPagoSection");
    }

    public static Config Instance()
    {
        if (_config == null)
            _config = new Config();

        return _config;
    }

    public MediosPagoConfigurationSection MediosPago { get; private set; }

}

El resto de los archivos si son parte del mapeo y requieren unirse uno con otro para armar la estructura.

La clase “MediosPagoConfigurationSection” que también se habrá observado en el tag del app.config, define el punto de entrada. Esta contiene una colección de ítems, es por eso que se asocia a “MediosPagoCollection”.

public class MediosPagoConfigurationSection : ConfigurationSection
{

    [ConfigurationProperty("MediosPago")]
    public MediosPagoCollection MedioPagoItems
    {
        get { return ((MediosPagoCollection)(base["MediosPago"])); }
    }
    
}

La clase que representa la colección define como se trabaja con un ítem, en esta se puede definir la propiedad “this”, para que busque tanto por índice, así como por la key definida en el elemento.

[ConfigurationCollection(typeof(MedioPagoElement), AddItemName = "MedioPago")]
public class MediosPagoCollection : ConfigurationElementCollection
{

    protected override ConfigurationElement CreateNewElement()
    {
        return new MedioPagoElement();
    }

    protected override object GetElementKey(ConfigurationElement element)
    {
        return ((MedioPagoElement)(element)).Id;
    }


    public MedioPagoElement this[int index]
    {
        get { return (MedioPagoElement)BaseGet(index); }
    }

    public MedioPagoElement this[string id]
    {
        get { return (MedioPagoElement)BaseGet(id); }
    }

}

Cada elemento representado por la clase “MedioPagoElement” solo tiene el mapeo a las propiedades del tag.

public class MedioPagoElement : ConfigurationElement
{

    [ConfigurationProperty("id", DefaultValue = "", IsKey = true, IsRequired = true)]
    public string Id
    {
        get { return ((string)(base["id"])); }
        set { base["id"] = value; }
    }

    [ConfigurationProperty("descripcion", DefaultValue = "", IsKey = false, IsRequired = true)]
    public string descripcion
    {
        get { return ((string)(base["descripcion"])); }
        set { base["descripcion"] = value; }
    }

    [ConfigurationProperty("provider", DefaultValue = "", IsKey = false, IsRequired = true)]
    public string provider
    {
        get { return ((string)(base["provider"])); }
        set { base["provider"] = value; }
    }

}

Definición de las clase de calculo de pago

En cada medio de pago se definición una clase especifica para realizar el calculo, para una mayor comodidad todas estas clases implementan una interfaz común, lo cual hace simple la instanciación.

interface ICalculoImpuesto
{
    decimal Calcular(decimal importe);
}

Cada clase concreta implementa la interfaz y aplica el calculo.

public class ChequeProvider : ICalculoImpuesto
   {

       /// <summary>
       /// El cheque recarga un 10%
       /// </summary>
       /// <param name="importe"></param>
       /// <returns></returns>
       public decimal Calcular(decimal importe)
       {
           return importe + (importe * (decimal)0.10);
       }

   }


public class EfectivoProvider : ICalculoImpuesto
   {

       /// <summary>
       /// En Efectivo se descuenta un 10%
       /// </summary>
       /// <param name="importe"></param>
       /// <returns></returns>
       public decimal Calcular(decimal importe)
       {
           return importe - (importe * (decimal)0.10);
       }

   }

public class TarjetaCreditoProvider : ICalculoImpuesto
   {

       /// <summary>
       /// La tarjeta de Credito recarga un 10%
       /// </summary>
       /// <param name="importe"></param>
       /// <returns></returns>
       public decimal Calcular(decimal importe)
       {
           return importe + (importe * (decimal)0.10);
       }

   }

public class TarjetaDebitoProvider : ICalculoImpuesto
   {

       /// <summary>
       /// La tarjeta de Debito recarga un 5%
       /// </summary>
       /// <param name="importe"></param>
       /// <returns></returns>
       public decimal Calcular(decimal importe)
       {
           return importe + (importe * (decimal)0.05);
       }

   }


public class TransferenciaBancariaProvider : ICalculoImpuesto
   {

       /// <summary>
       /// El Trsnaferencia Bancaria no afecta al importe
       /// </summary>
       /// <param name="importe"></param>
       /// <returns></returns>
       public decimal Calcular(decimal importe)
       {
           return importe;
       }

   }

 

Aplicación de todo lo definido

Bien, ahora llego el momento de poner manos a la obra y hacer uso de todo lo configurado en los pasos anteriores.

Empezaremos por cargar el combo de medios de pago, tomando la información de esta nueva estructura.

private void Form2_Load(object sender, EventArgs e)
{

    var result = (from config in Config.Instance().MediosPago.MedioPagoItems.Cast<MedioPagoElement>()
                  select new
                  {
                      key = config.Id,
                      value = config.descripcion
                  }).ToList();

    cmbMediosPago.DisplayMember = "value";
    cmbMediosPago.ValueMember = "key";
    cmbMediosPago.DataSource = result;

    cmbMediosPago.SelectedIndex = -1;

}

Como se observa no ha cambiado mucho con respecto al artículo anterior, solo que esta vez se cuenta con la ayuda de

Config.Instance().MediosPago.MedioPagoItems

el cual nos abstrae de la operación de carga de config en las clases.

 

El próximo punto involucra al calculo de impuesto.

private void btnCalcular_Click(object sender, EventArgs e)
{
    errProvider.Clear();

    if (cmbMediosPago.SelectedIndex == -1)
    {
        errProvider.SetError(cmbMediosPago, "Debe seleccionar un medio de pago");
        return;
    }

    decimal importe = 0;

    if (!decimal.TryParse(txtImporte.Text, out importe))
    {
        errProvider.SetError(txtImporte, "El importe ingresado es invalido");
        return;
    }


    string mediopago = Convert.ToString(cmbMediosPago.SelectedValue);
    string provider = Config.Instance().MediosPago.MedioPagoItems[mediopago].provider;


    ICalculoImpuesto calculo = (ICalculoImpuesto)Activator.CreateInstance(Type.GetType(provider));

    txtTotal.Text = string.Format("{0:N2}", calculo.Calcular(importe));

}

Como líneas a destacara se podría mencionar la 19, en donde se accede por medio de la key para recuperar el proveedor que se debe invocar, en este punto si es importante recuperar la información de la configuración ya que el control ComboBox no nos proporciona esta data, solo nos brinda la key.

La línea 22, tiene de interesante el uso de la clase “Activator” para crear la instancia basada en el nombre completo de la clase (namespace + nombre clase).

La línea 24, al contar con una interfaz común solo se invoca al método de la instancia creada y eso es todo lo que se necesita.

 

Código de ejemplo

[C#]
[VB.NET]

Archivos de Configuración - Una introducción (1/3)

 

Introducción


Es muy común tener que definir parámetros en las aplicaciones que se desarrollan proporcionando cierta configuración que si bien se podría considerar casi estática (porque no cambia constantemente), si sea necesario prever la posibilidad de adaptación.

Un ejemplo muy claro de estos es la cadena de conexión a la base de datos, por lo general una vez instalada la aplicación no cambia, pero cuando se esta en la etapa de implementación seguramente sea necesario su adaptación al entorno.

Por lo general se busca que sea un lugar que impacte lo menos posible en el desarrollo, algunos tienden a crear una clase y colocar constantes o variables readonly dentro del propio código, pero el problema con esto es que requieren recompilar por completo el desarrollo, además de tener que actualizar cada cliente por un simple cambio de configuración.

También se busca un lugar estándar y conocido, algunos usan la registry de windows para definir configuración, quizás era una opción se algo mas viable cuando se contaba con Win XP, pero con Sistemas Operativos como ser Vista o Win 7 esto cambio bastante, el modelo de seguridad que estos imponen aplica restricciones a estas acciones, no haciendo tan cómoda la escritura en este sitio.

Entonces porque no usar la propuesta que hace .net al respecto, si se trata de los archivos de configuración, entre las ventajas que este presenta se pueden encontrar:

  • una lectura simple, ya que se basa en xml
  • fácil acceso y modificación (se puede editar con el notepad), por lo general este archivo se encuentra junto a la aplicación por lo que la seguridad debería permitir la escritura en esta carpeta

Entre los temas que se trataran en este articulo

1- Agregar un archivo de configuración a nuestro proyecto

2- Definir una sección key-value

2a- usando la sección appSettings

2b- definiendo una sección propia

 

1- Agregar un archivo de configuración a nuestro proyecto


Si bien es una acción sencilla, si recién se esta introduciendo en el desarrollo, puede que no resulte tan intuitivo encontrar los pasos para agregar un archivo de configuración.

Estando sobre el proyecto en el “Solution Explorer”, se acciona el menú que aparece con el boton derecho del mouse, seleccionado el menu Add –> New Item…

image

en el recuadro se seleccionara la opción:

image

Esto agregar un archivo de nombre App.config, que en una primer instancia tendrá solo un tag.

Empecemos por una tarea simple, la lectura de una cadena de conexión.

Para lograr esta tarea se necesitara la ayuda de una clase en particular, me refiero al 

ConfigurationManager

con esta clase tendremos acceso a los tag de información definidos, pero para hacer uso de la misma se requiere hacer referencia a la librería System.Configuration, los pasos para esta acción serian representados en la siguiente imagen:

image

Solo queda agregar el código que tomaría la información del archivo de configuración, usándolo luego con los objetos de ado.net para conectarse

image

En el punto 1 se define el “using” a la librería referenciada en el paso anterior (definiendo así su namespace), mientras que en el 2 se hace uso de la clase ConfigurationManager para tomar la cadena de conexión.

Por supuesto en el archivo de configuración se debió agregar la key que se define en el .config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="default" connectionString="Data Source=LecturaConnectionString\TestDb.sdf;Persist Security Info=False;"/>
  </connectionStrings>
</configuration>

2- Definir una sección key-value


Definir información del tipo key-value en el archivo de configuración puede realizarse de varias formas

  • usando la sección appsetting
  • por medio de una seccion custom creada por uno

 

2a- Usando AppSetting

Esta es la forma más simple y directa de definir valores simples en la configuración, solo basta con ingresar en la sección <appSettings> el tag “add” con cada item que se requiera.

En el ejemplo se observará la sección definida como:

<appSettings>
  <add key="1" value="Efectivo" />
  <add key="2" value="Tarjeta Credito" />
  <add key="3" value="Tarjeta Debito" />
  <add key="4" value="Cheque" />
  <add key="5" value="Transferencia Bancaria" />
</appSettings>

En el código, en el Form2, se trabaja con esta información con al ayuda de linq para cargar un combo

private void Form2_Load(object sender, EventArgs e)
{


    var result = (from configKey in ConfigurationManager.AppSettings.Keys.Cast<string>()
                  let configValue = ConfigurationManager.AppSettings[configKey]
                  select new
                  {
                      key = configKey,
                      value = configValue
                  }).ToList();

    
    cmbMediosPago.DisplayMember = "value";
    cmbMediosPago.ValueMember = "key";
    cmbMediosPago.DataSource = result;
    
}

Acceder a un ítem en concreto es tan simple como usar

ConfigurationManager.AppSettings[key]

 

private void cmbMediosPago_SelectionChangeCommitted(object sender, EventArgs e)
{
    string key = Convert.ToString(cmbMediosPago.SelectedValue);
    string value = ConfigurationManager.AppSettings[key];

    lblSeleccion.Text = string.Format("Se ha seleccionado\n Key:{0} \n Value:{1}", key, value);
}

2b- Definiendo una sección propia

El uso de la sección <appSettings> en algunas circunstancia puede resultar demasiado genérico y no brindar un lugar que sea claramente identificable para el negocio que se esta programando, esta sección al ser tan común se puede llenar rápidamente de items key-value no relacionados, haciendo difícil el mantenimiento.

Pero existe una alternativa a este problema y consiste en crear una sección propia para conserva estos pares key-value de forma personalizada, otorgando una visibilidad con sentido para la aplicación

En el código del Form3 se implementa la solución usando una sección definida por uno mismo, la cual aplica el mismo concepto key-value.

<configuration>
  <configSections>
    <section name="MediosPago" type="System.Configuration.DictionarySectionHandler" />
  </configSections>
  
  
  <MediosPago>
    <add key="1" value="Efectivo" />
    <add key="2" value="Tarjeta Credito" />
    <add key="3" value="Tarjeta Debito" />
    <add key="4" value="Cheque" />
    <add key="5" value="Transferencia Bancaria" />
  </MediosPago>
  
</configuration>

Para recuperar la información y listarla

private void Form3_Load(object sender, EventArgs e)
{

    var result = (from config in ((Hashtable)ConfigurationManager.GetSection("MediosPago")).Cast<DictionaryEntry>()
                 select new 
                 {
                     key = config.Key,
                     value = config.Value
                 }).ToList();

    cmbMediosPago.DisplayMember = "value";
    cmbMediosPago.ValueMember = "key";
    cmbMediosPago.DataSource = result;

    cmbMediosPago.SelectedIndex = -1;

}

Obtener un ítem basándonos en la key requiere recuperar la sección completa para luego si acceder al valor

private void cmbMediosPago_SelectionChangeCommitted(object sender, EventArgs e)
{
    string key = Convert.ToString(cmbMediosPago.SelectedValue);
    string value = ((Hashtable)ConfigurationManager.GetSection("MediosPago"))[key].ToString();

    lblSeleccion.Text = string.Format("Se ha seleccionado\n Key:{0} \n Value:{1}", key, value);
}

Por supuesto tanto en estos ejemplo hacer uso de las clase de configuración para tomar el valor del ítem seleccionado, no tiene un sentido practico, ya que el propio combobox proporciona ambos datos key y value (usando el SelectedValue y SelectedText), solo se realiza con fines ilustrativos para poder aplicar los conceptos de programación con el archivo de configuración.

 

Código de ejemplo


[C#] 
[VB.NET]