martes, febrero 22, 2011

C# - Aplicación de Escritorio, Sistemas de Ventas Parte II - Creación de la Capa de Datos


"Si usa algún código del siguiente tutorial, den el icono de ME GUSTA del Facebook que se encuentra en su mano derecha, para que se vuelva Seguidor del Blog y también comentenos que tal les pareció el tutorial"

1. Entorno


  • SQL Server 2008
  • Visual Studio 2008

2. Introducción


2.1. Programación por capas

La programación por capas es un estilo de programación en el que el objetivo primordial es la separación de la lógica de negocios de la lógica de diseño. La ventaja principal de este estilo es que el desarrollo se puede llevar a cabo en varios niveles y, en caso de que sobrevenga algún cambio, sólo se ataca al nivel requerido sin tener que revisar entre código mezclado. Un buen ejemplo de este método de programación sería el modelo de interconexión de sistemas abiertos

2.2. Programación en tres capas

  • Capa de presentación: es la que ve el usuario (también se la denomina "capa de usuario"), presenta el sistema al usuario, le comunica la información y captura la información del usuario en un mínimo de proceso (realiza un filtrado previo para comprobar que no hay errores de formato). Esta capa se comunica únicamente con la capa de negocio. También es conocida como interfaz gráfica y debe tener la característica de ser "amigable" (entendible y fácil de usar) para el usuario.
  • Capa de negocio: es donde residen los programas que se ejecutan, se reciben las peticiones del usuario y se envían las respuestas tras el proceso. Se denomina capa de negocio (e incluso de lógica del negocio) porque es aquí donde se establecen todas las reglas que deben cumplirse. Esta capa se comunica con la capa de presentación, para recibir las solicitudes y presentar los resultados, y con la capa de datos, para solicitar al gestor de base de datos para almacenar o recuperar datos de él. También se consideran aquí los programas de aplicación.
  • Capa de datos: es donde residen los datos y es la encargada de acceder a los mismos. Está formada por uno o más gestores de bases de datos que realizan todo el almacenamiento de datos, reciben solicitudes de almacenamiento o recuperación de información desde la capa de negocio.

3. Desarrollo


3.1. Creando el proyecto

Primero debemos de crear un proyecto con Visual Studio 2008, para eso abrimos el Visual Studio 2008 y nos vamos al menú de "Archivo-->Nuevo Proyecto". A nuestro proyecto le pondremos de nombre "SistemaVentas"

3.2. Agregando la Capa de Datos

Debemos de agregar a nuestro proyecto la capa de datos, para eso nos vamos al menu de "Archivo-->Agregar Nuevo Proyecto"


Y le pondremos como nombre "CapaDatos"


3.3. La clase Conexion

Para agregar una clase en C# debemos hacer clic derecho en la Capa de Datos y seleccionar la opción "Agregar-->Clase" y la clase que creamos se llamara "Conexion", que se encargara de guardar la cadena de conexión para poder conectarnos con nuestra base de datos que esta en SQL Server 2008 y la cual se llama BDTutorial.
























La clase Conexion tendrá el siguiente código en C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

//Desarrollado por Henry Joe Wong Urquiza

namespace CapaDatos
{
  public class Conexion
  {
    //La base de datos se llama BDTutorial
    //La ubicacion de base de datos esta de modo local y en una instancia que se llama SQL2008
    //Utiliza seguridad integrada para conectarse a la base de datos
    public static string cn = "Data Source=.\\SQL2008;Initial Catalog=BDTutorial;Integrated Security=True";
  }
}

3.2. La clase Producto

Esta clase se encarga de conectar la tabla Producto con C#

using System;
using System.Collections.Generic;
using System.Text;
//Impotaciones necesarias
using System.Data;
using System.Data.SqlClient;

//Desarrollado por Henry Joe Wong Urquiza

namespace CapaDatos
{

  public class Producto
  {
    private int var_codigoProducto;
    private string var_nombre;
    private decimal var_precio;

    //Constructor vacio
    public Producto()
    {

    }

    //Constructor con parametros
    public Producto(
    int codigoProducto,
    string nombre,
    decimal precio
    )
    {
      this.var_codigoProducto = codigoProducto;
      this.var_nombre = nombre;
      this.var_precio = precio;
    }

    //Metodo utilizado para insertar un Producto
    public string Insertar(Producto varProducto)
    {
      string rpta = "";
      SqlConnection sqlCon = new SqlConnection();
      try
      {
        //1. Establecer la cadena de conexion
        sqlCon.ConnectionString = Conexion.cn;
        //2. Abrir la conexion de la BD
        sqlCon.Open();
        //3. Establecer el comando
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.Connection = sqlCon;
        sqlCmd.CommandText = "spI_Producto";
        sqlCmd.CommandType = CommandType.StoredProcedure;

        //4. Agregar los parametros al comando
        //Establecemos los valores para el parametro @codigoProducto del Procedimiento Almacenado
        SqlParameter sqlParcodigoProducto = new SqlParameter();
        sqlParcodigoProducto.ParameterName = "@codigoProducto";
        sqlParcodigoProducto.SqlDbType = SqlDbType.Int;
        //Le declaramos que el parametro es de salida, porque obtendremos el codigo generado por la base de datos
        sqlParcodigoProducto.Direction = ParameterDirection.Output;
        sqlCmd.Parameters.Add(sqlParcodigoProducto); //Agregamos el parametro al comando
        //Establecemos los valores para el parametro @nombre del Procedimiento Almacenado
        SqlParameter sqlParnombre = new SqlParameter();
        sqlParnombre.ParameterName = "@nombre";
        sqlParnombre.SqlDbType = SqlDbType.VarChar;
        sqlParnombre.Size = 100;
        sqlParnombre.Value = varProducto.nombre;
        sqlCmd.Parameters.Add(sqlParnombre); //Agregamos el parametro al comando
        //Establecemos los valores para el parametro @precio del Procedimiento Almacenado
        SqlParameter sqlParprecio = new SqlParameter();
        sqlParprecio.ParameterName = "@precio";
        sqlParprecio.SqlDbType = SqlDbType.Decimal;
        sqlParprecio.Precision=18;
        sqlParprecio.Scale=2;
        sqlParprecio.Value = varProducto.precio;
        sqlCmd.Parameters.Add(sqlParprecio); //Agregamos el parametro al comando

        //5. Ejecutamos el commando
        rpta = sqlCmd.ExecuteNonQuery() == 1 ? "OK" : "No se inserto el producto de forma correcta";

      }
      catch (Exception ex)
      {
        rpta = ex.Message;
      }
      finally
      {
        //6. Cerramos la conexion con la BD
        if (sqlCon.State == ConnectionState.Open) sqlCon.Close();
      }
      return rpta;                
    }


    //Metodo utilizado para actualizar un Producto
    public string Actualizar(Producto varProducto)
    {
      string rpta = "";
      SqlConnection sqlCon = new SqlConnection();
      try
      {
        //1. Establecer la cadena de conexion
        sqlCon.ConnectionString = Conexion.cn;
        //2. Abrir la conexion de la BD
        sqlCon.Open();
        //3. Establecer el comando
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.Connection = sqlCon;
        sqlCmd.CommandText = "spU_Producto";
        sqlCmd.CommandType = CommandType.StoredProcedure;

        //4. Agregar los parametros al comando
        //Establecemos los valores para el parametro @codigoProducto del Procedimiento Almacenado
        SqlParameter sqlParcodigoProducto = new SqlParameter();
        sqlParcodigoProducto.ParameterName = "@codigoProducto";
        sqlParcodigoProducto.SqlDbType = SqlDbType.Int;
        sqlParcodigoProducto.Value = varProducto.codigoProducto;
        sqlCmd.Parameters.Add(sqlParcodigoProducto); //Agregamos el parametro al comando
        //Establecemos los valores para el parametro @nombre del Procedimiento Almacenado
        SqlParameter sqlParnombre = new SqlParameter();
        sqlParnombre.ParameterName = "@nombre";
        sqlParnombre.SqlDbType = SqlDbType.VarChar;
        sqlParnombre.Size = 100;
        sqlParnombre.Value = varProducto.nombre;
        sqlCmd.Parameters.Add(sqlParnombre); //Agregamos el parametro al comando
        //Establecemos los valores para el parametro @precio del Procedimiento Almacenado
        SqlParameter sqlParprecio = new SqlParameter();
        sqlParprecio.ParameterName = "@precio";
        sqlParprecio.SqlDbType = SqlDbType.Decimal;
        sqlParprecio.Precision = 18;
        sqlParprecio.Scale = 2;
        sqlParprecio.Value = varProducto.precio;
        sqlCmd.Parameters.Add(sqlParprecio); //Agregamos el parametro al comando

        //5. Ejecutamos el commando
        rpta = sqlCmd.ExecuteNonQuery() == 1 ? "OK" : "No se actualizo el producto de forma correcta";

      }
      catch (Exception ex)
      {
        rpta = ex.Message;
      }
      finally
      {
        //6. Cerramos la conexion con la BD
        if (sqlCon.State == ConnectionState.Open) sqlCon.Close();
      }
      return rpta;                
    }

    //Metodo utilizado para obtener todos los productos de la base de datos
    public DataTable ObtenerProducto()
    {
      DataTable dtProducto = new DataTable("Producto");
      SqlConnection sqlCon = new SqlConnection();
      try
      {
        //1. Establecer la cadena de conexion
        sqlCon.ConnectionString = Conexion.cn;

        //2. Establecer el comando
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.Connection = sqlCon;//La conexion que va a usar el comando
        sqlCmd.CommandText = "spF_Producto_All";//El comando a ejecutar
        sqlCmd.CommandType = CommandType.StoredProcedure;//Decirle al comando que va a ejecutar una sentencia SQL

        //3. No hay parametros

        //4. El DataAdapter que va a ejecutar el comando y es el encargado de llena el DataTable
        SqlDataAdapter sqlDat = new SqlDataAdapter(sqlCmd);
        sqlDat.Fill(dtProducto);//Llenamos el DataTable
      }
      catch (Exception ex)
      {
        dtProducto = null;
      }
      return dtProducto;
    }

    #region Metodos Get y Set
    public int codigoProducto
    {
      get { return var_codigoProducto; }
      set { var_codigoProducto = value; }
    }
    public string nombre
    {
      get { return var_nombre; }
      set { var_nombre = value; }
    }
    public decimal precio
    {
      get { return var_precio; }
      set { var_precio = value; }
    }
    #endregion

  }
}

3.3. Clase DetalleVenta

Esta clase se encarga de conectar la tabla DetalleVenta con C#

using System;
using System.Collections.Generic;
using System.Text;
//Impotaciones necesarias
using System.Data;
using System.Data.SqlClient;

//Desarrollado por Henry Joe Wong Urquiza

namespace CapaDatos
{
  public class DetalleVenta
  {
    private int var_codigoVenta;
    private int var_codigoProducto;
    private decimal var_cantidad;
    private decimal var_descuento;

    //Constructor vacio
    public DetalleVenta()
    {

    }

    //Constructor con parametros
    public DetalleVenta(
    int codigoVenta ,
    int codigoProducto ,
    decimal cantidad ,
    decimal descuento 
    )
    {
      this.var_codigoVenta=codigoVenta;
      this.var_codigoProducto=codigoProducto;
      this.var_cantidad=cantidad;
      this.var_descuento=descuento;
    }

    //Metodo utilizado para insertar un DetalleVenta
    //Le pasamos la conexion y la transaccion por referencia, debido a que esos datos lo obtenemos
    //de la clase Venta y no deberiamos crear una nueva Conexion o una nueva Transaccion
    //sino la creada por la clase Venta
    public string Insertar(DetalleVenta varDetalleVenta, ref SqlConnection sqlCon, ref SqlTransaction sqlTra)
    {
      string rpta = "";
      try
      {
        //1. Establecer el comando
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.Connection = sqlCon;
        sqlCmd.Transaction = sqlTra;
        sqlCmd.CommandText = "spI_DetalleVenta";
        sqlCmd.CommandType = CommandType.StoredProcedure;

        //4. Agregar los parametros al comando
        //Establecemos los valores para el parametro @codigoVenta del Procedimiento Almacenado
        SqlParameter sqlParcodigoVenta = new SqlParameter();
        sqlParcodigoVenta.ParameterName = "@codigoVenta";
        sqlParcodigoVenta.SqlDbType = SqlDbType.Int;
        sqlParcodigoVenta.Value = varDetalleVenta.codigoVenta;
        sqlCmd.Parameters.Add(sqlParcodigoVenta); //Agregamos el parametro al comando
        //Establecemos los valores para el parametro @codigoProducto del Procedimiento Almacenado
        SqlParameter sqlParcodigoProducto = new SqlParameter();
        sqlParcodigoProducto.ParameterName = "@codigoProducto";
        sqlParcodigoProducto.SqlDbType = SqlDbType.Int;
        sqlParcodigoProducto.Size = 4;
        sqlParcodigoProducto.Value = varDetalleVenta.codigoProducto;
        sqlCmd.Parameters.Add(sqlParcodigoProducto); //Agregamos el parametro al comando
        //Establecemos los valores para el parametro @cantidad del Procedimiento Almacenado
        SqlParameter sqlParcantidad = new SqlParameter();
        sqlParcantidad.ParameterName = "@cantidad";
        sqlParcantidad.SqlDbType = SqlDbType.Decimal;
        sqlParcantidad.Precision = 18;
        sqlParcantidad.Scale = 2;
        sqlParcantidad.Value = varDetalleVenta.cantidad;
        sqlCmd.Parameters.Add(sqlParcantidad); //Agregamos el parametro al comando
        //Establecemos los valores para el parametro @descuento del Procedimiento Almacenado
        SqlParameter sqlPardescuento = new SqlParameter();
        sqlPardescuento.ParameterName = "@descuento";
        sqlPardescuento.SqlDbType = SqlDbType.Decimal;
        sqlParcantidad.Precision = 18;
        sqlParcantidad.Scale = 2;
        sqlPardescuento.Value = varDetalleVenta.descuento;
        sqlCmd.Parameters.Add(sqlPardescuento); //Agregamos el parametro al comando

        //5. Ejecutamos el commando
        rpta = sqlCmd.ExecuteNonQuery() == 1 ? "OK" : "No se inserto el detalle de venta de forma correcta";

      }
      catch (Exception ex)
      {
        rpta = ex.Message;
      }
      return rpta;                
    }

    #region Metodos Get y Set
    public int codigoVenta
    {
      get { return var_codigoVenta; }
      set { var_codigoVenta = value; }
    }
    public int codigoProducto
    {
      get { return var_codigoProducto; }
      set { var_codigoProducto = value; }
    }
    public decimal cantidad
    {
      get { return var_cantidad; }
      set { var_cantidad = value; }
    }
    public decimal descuento
    {
      get { return var_descuento; }
      set { var_descuento = value; }
    }
    #endregion

  }
}

3.4. Clase Venta

Esta clase se encarga de conectar la tabla Venta con C#

using System;
using System.Collections.Generic;
using System.Text;
//Impotaciones necesarias
using System.Data;
using System.Data.SqlClient;

//Desarrollado por Henry Joe Wong Urquiza

namespace CapaDatos
{
  public class Venta
  {
    private int var_codigoVenta;
    private string var_cliente;
    private DateTime var_fecha;

    //Constructor vacio
    public Venta()
    {

    }

    //Constructor con parametros
    public Venta(int codigoVenta,string cliente,DateTime fecha)
    {
      this.var_codigoVenta=codigoVenta;
      this.var_cliente=cliente;
      this.var_fecha=fecha;
    }

    //Metodo utilizado para insertar un Venta
    public string Insertar(Venta varVenta, List<DetalleVenta> detalles)
    {
      string rpta = "";
      SqlConnection sqlCon = new SqlConnection();

      try
      {
        //1. Establecer la cadena de conexion
        sqlCon.ConnectionString = Conexion.cn;
        //2. Abrir la conexion de la BD
        sqlCon.Open();
        //3. Establecer la transaccion
        SqlTransaction sqlTra = sqlCon.BeginTransaction();
        //4. Establecer el comando
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.Connection = sqlCon;
        sqlCmd.Transaction = sqlTra;
        sqlCmd.CommandText = "spI_Venta";
        sqlCmd.CommandType = CommandType.StoredProcedure;
        //5. Agregar los parametros al comando
        //Establecemos los valores para el parametro @codigoVenta del Procedimiento Almacenado
        SqlParameter sqlParcodigoVenta = new SqlParameter();
        sqlParcodigoVenta.ParameterName = "@codigoVenta";
        sqlParcodigoVenta.SqlDbType = SqlDbType.Int;
        sqlParcodigoVenta.Direction = ParameterDirection.Output;
        sqlCmd.Parameters.Add(sqlParcodigoVenta); //Agregamos el parametro al comando
        //Establecemos los valores para el parametro @cliente del Procedimiento Almacenado
        SqlParameter sqlParcliente = new SqlParameter();
        sqlParcliente.ParameterName = "@cliente";
        sqlParcliente.SqlDbType = SqlDbType.VarChar;
        sqlParcliente.Size = 100;
        sqlParcliente.Value = varVenta.cliente;
        sqlCmd.Parameters.Add(sqlParcliente); //Agregamos el parametro al comando
        //6. Ejecutamos el commando
        rpta = sqlCmd.ExecuteNonQuery() == 1 ? "OK" : "No se inserto el detalle de venta de forma correcta";
        if (rpta.Equals("OK"))
        {
          //Obtenemos el codigo de la venta que se genero por la base de datos
          this.codigoVenta=Convert.ToInt32(sqlCmd.Parameters["@codigoVenta"].Value);
          foreach(DetalleVenta det in detalles){
            //Establecemos el codigo de la venta que se autogenero
            det.codigoVenta = this.codigoVenta;
            //Llamamos al metodo insertar de la clase DetalleVenta
            //y le pasamos la conexion y la transaccion que debe de usar
            rpta = det.Insertar(det, ref sqlCon, ref sqlTra);
            if (!rpta.Equals("OK"))
            {
              //Si ocurre un error al insertar un detalle de venta salimos del for
              break;
            }
          }
        }
        if (rpta.Equals("OK"))
        {
          //Se inserto todo los detalles y confirmamos la transaccion
          sqlTra.Commit();
        }
        else
        {
          //Algun detalle no se inserto y negamos la transaccion
          sqlTra.Rollback();
        }

      }
      catch (Exception ex)
      {
        rpta = ex.Message;
      }
      finally
      {
        //6. Cerramos la conexion con la BD
        if (sqlCon.State == ConnectionState.Open) sqlCon.Close();
      }
      return rpta;                
    }

    //Obtenemos la venta por el codigo generado
    public DataTable ObtenerVenta(int codigoVenta)
    {
      DataTable dtVenta = new DataTable("Venta");
      SqlConnection sqlCon = new SqlConnection();
      try
      {
        //1. Establecer la cadena de conexion
        sqlCon.ConnectionString = Conexion.cn;

        //2. Establecer el comando
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.Connection = sqlCon;//La conexion que va a usar el comando
        sqlCmd.CommandText = "spF_Venta_One";//El comando a ejecutar
        sqlCmd.CommandType = CommandType.StoredProcedure;//Decirle al comando que va a ejecutar una sentencia SQL

        //3. Agregar los parametros al comando
        //Establecemos los valores para el parametro @codigoVenta del Procedimiento Almacenado
        SqlParameter sqlParcodigoVenta = new SqlParameter();
        sqlParcodigoVenta.ParameterName = "@codigoVenta";
        sqlParcodigoVenta.SqlDbType = SqlDbType.Int;
        sqlParcodigoVenta.Value = codigoVenta;
        sqlCmd.Parameters.Add(sqlParcodigoVenta); //Agregamos el parametro al comando

        //4. El DataAdapter que va a ejecutar el comando y es el encargado de llena el DataTable
        SqlDataAdapter sqlDat = new SqlDataAdapter(sqlCmd);
        sqlDat.Fill(dtVenta);//Llenamos el DataTable
      }
      catch (Exception ex)
      {
        dtVenta = null;
      }
      return dtVenta;
    }

    //Obtener todas las ventas
    public DataTable ObtenerVenta()
    {
      DataTable dtVenta = new DataTable("Venta");
      SqlConnection sqlCon = new SqlConnection();
      try
      {
        //1. Establecer la cadena de conexion
        sqlCon.ConnectionString = Conexion.cn;

        //2. Establecer el comando
        SqlCommand sqlCmd = new SqlCommand();
        sqlCmd.Connection = sqlCon;//La conexion que va a usar el comando
        sqlCmd.CommandText = "spF_Venta_All";//El comando a ejecutar
        sqlCmd.CommandType = CommandType.StoredProcedure;//Decirle al comando que va a ejecutar una sentencia SQL

        //3. No hay parametros

        //4. El DataAdapter que va a ejecutar el comando y es el encargado de llena el DataTable
        SqlDataAdapter sqlDat = new SqlDataAdapter(sqlCmd);
        sqlDat.Fill(dtVenta);//Llenamos el DataTable
      }
      catch (Exception ex)
      {
        dtVenta = null;
      }
      return dtVenta;
    }

    #region Metodos Get y Set
    public int codigoVenta
    {
      get { return var_codigoVenta; }
      set { var_codigoVenta = value; }
    }
    public string cliente
    {
      get { return var_cliente; }
      set { var_cliente = value; }
    }
    public DateTime fecha
    {
      get { return var_fecha; }
      set { var_fecha = value; }
    }
    #endregion

  }
}

4. Resumen

Al final deberíamos tener las siguientes clases

30 comentarios:

MUY BUENO TU TUTORIAL AMIGO, LO ESTOY ANALIZANDO COMPLETAMENTE Y ME PARECE MUY INTERESANTE, HASTA DA TRISTEZA VER QUE NO HAY COMENTARIOS... TAL VEZ SEA PORQUE AL PONER UN COMENTARIO TE REDIRECCIONA A OTRA PÁGINA.... SALUDOS!!

Muchas gracias por lo saludos ... Y síguenos por facebook para que te avise cada vez que subimos un nuevo tutorial.

esta muy padre el tutorial me a ayudado mucho, muchas gracias

Excelente tutorial, me ah sido de gran ayuda para aprender a implementar capas y para algunos problemas que tenia para usar store procedures, gracias Henry.

Gracias a ustedes por la confianza :D

me gustaria implementar en la logica de negocio algo para en una tabla de la base de datos almacenar los errores
menejo de errores graicas
ojala me ayuden

Interesante aunque porque ocupar DataSet y DataTables ??? por que no uno o lo otro

saludos y gracias.

Es por motivos didácticos, para que aprendan a usar los dos

Exelente tutorial Henry mira estoy probando el codigo y esta exelente para nosotros los novatos en el desarrollo solo tengo una duda no se sime podrias decir que signnifica esta barible
rpta = "";
y anteMano muchas gracias aprendi cosas nuevas

Excelente tutorial, me ah sido de gran ayuda para comprender a la implementacion del procesode las capas y el uso de los procedimientos almacenados solo tengo una duda no si me podrias decir que significa la variable string rpta = ""; si no es mucha molestia, gracias Henry

rpta viene a ser como una variable que dice la RESPUESTA de lo que esta sucediendo con ese método.

Revisando el tutorial, me ha parecido interesante el enfoque que le das, tambien que lo haces facil para los que talves no entendamos mucho..:D. Muy buen tutorial, continua asi. Saludos

Como Te comente en la primera parte, excelente tutorial la verdad me dio todas las luces porque estoy apenas iniciando con asp.net y sql server. Gracias por dedicar tiempo a este tema...

hey muy buen aporte me servira mucho para mi proyecto Grasias
!!!!!1

Buelvo acomentar exelente tutorial,pero analizandolo me surgio una duda como le ago para realizar esta misma aplicacion pero en web haciendo uso de ASP Con SQl Server.Espero una respuesta.

Solo tienes que cambiar la capa de presentación por un proyecto web nada mas

excelente tutorial!...

Muy bueno el tutorial, lo estoy probando.

Muchas gracias por la información es justo lo que estaba buscando..

Muy bueno, simple y didáctico.

Muy bueno, simple y didáctico.

Muy bueno, simple y didáctico.

Muchas Gracias por el aporte....gran ayuda de tu parte

mil gracias .... lo voy a revizar.. (rock)

Muy bueno el tutorial, realizándolo y comprendiéndolo de a poco gracias amigo te pasaste.

Muy bueno el tutorial, realizándolo y comprendiéndolo de a poco gracias amigo te pasaste.

Excelente tutorial, es justo lo que necesitaba, Gracias.

Gran pero gran tutorial!! hace tiempo que buscaba algo explicado detalladamente para poder entender de que se trata!! Gracias

gracias amigo un buen tuto sigue pa adelante

muy buen tuto hombre lo felicito y de antemano gracias siga haciendo tutoriales como este que para quienes estamos aprendiendo es la herramineta mas util que podemos encontrar enb la web B
ENDICIONES