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#]