Con ésta entrega cumpliremos con la capa de Presentación, utilizaremos todo lo que hemos visto hasta ahora aplicados a una interfaz de usuario, y como lo prometí, lo veremos implementado en winForm como en webForm.
El primer ejemplo será Desktop, crearemos un formulario con una apariencia semejante al que ven en la imagen.
Evidentemente, un sistema real no lo harán así, el botón conectar emula el comportamiento de una pantalla de login, el boton crear mandará a la BBDD los datos de la caja, Listar rellenará la grilla y Buscar By Id se encargará de devolvernos un registro a partir de lo que carguemos en la caja de Id. Otra implementación interesante sería agregarle un identity a la tabla cliente, pero para el ejemplo ya servirá esto. Para esto crearemos 2 proyectos más dentro de nuestra solución, uno será una aplicación Windows Form con Visual C#, y la otra un sitio Web.
El código que pondremos en el botón conectar es como sigue, recuerden que es conveniente armarlo dinamicamente con una pantalla de login especialmente el usuario y el pass. Con esto logramos armar toda la capa de Acceso a Datos, no se mantiene conectada la aplicación, sino solo sus valores de conexión en memoria.
1 2 3 4 5 6 7 8 9 | try { Conexion.IniciarSesion("127.0.0.1", "Queryable", "sa", "123"); MessageBox.Show(String.Format("{0}", "Se conecto exitosamente")); } catch (Exception ex) { MessageBox.Show(ex.Message); } |
Para crear un nuevo cliente instanciamos un objeto cliente del tipo Cliente, le seteamos sus atributos, a partir de los valores de la caja de texto, e invocamos el método crear enviandole el nuevo objeto cliente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var cliente = new Cliente(); try { cliente.IdCliente = Convert.ToInt16(txtIdCliente.Text); cliente.Descripcion = txtDescripcionCliente.Text; cliente.Sigla = txtSiglaCliente.Text; cliente.Crear(cliente); MessageBox.Show(String.Format("{0}", "Se creo exitosamente")); } catch (Exception ex) { MessageBox.Show(ex.Message); } |
Luego de esto escribimos el código de listado de todos los clientes, y lo cargamos en la grilla. Se dan cuenta que necesitamos muy pocas lineas de código en la capa de Presentación, y que no tiene una dependencia de la BBDD?. Si se dan cuenta, cuando vamos a devolver muchos registros no podemos utilizar un montón de instancias de Cliente, sino simplemente devolvemos un DataTable, DataSet o DataReader.
1 2 3 4 5 6 7 8 9 | var cliente = new Cliente(); try { grilla.DataSource = cliente.Listar(); } catch (Exception ex) { MessageBox.Show(ex.Message); } |
Finalmente el código de búsqueda quedaría algo asi. Cómo sabemos que si buscamos por la PK, siempre nos devolverá un sólo registro, lo podemos crear como un objeto Cliente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var cliente = new Cliente(); try { cliente = cliente.Listar(Convert.ToInt16(txtIdCliente.Text)); if (cliente != null) { txtDescripcionCliente.Text = cliente.Descripcion; txtSiglaCliente.Text = cliente.Sigla; } else { MessageBox.Show(String.Format("{0}", "No existia el cliente buscado")); } } catch (Exception ex) { MessageBox.Show(ex.Message); } |
El código completo quedaría así:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | using System; using System.Windows.Forms; using AccesoDatos; using AccesoDatos.Orm; namespace Test { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnConectar_Click(object sender, EventArgs e) { try { Conexion.IniciarSesion("127.0.0.1", "Queryable", "sa", "***"); MessageBox.Show(String.Format("{0}", "Se conecto exitosamente")); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnListar_Click(object sender, EventArgs e) { var cliente = new Cliente(); try { grilla.DataSource = cliente.Listar(); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnCrear_Click(object sender, EventArgs e) { var cliente = new Cliente(); try { cliente.IdCliente = Convert.ToInt16(txtIdCliente.Text); cliente.Descripcion = txtDescripcionCliente.Text; cliente.Sigla = txtSiglaCliente.Text; cliente.Crear(cliente); MessageBox.Show(String.Format("{0}", "Se creo exitosamente")); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void btnBuscarById_Click(object sender, EventArgs e) { var cliente = new Cliente(); try { cliente = cliente.Listar(Convert.ToInt16(txtIdCliente.Text)); if (cliente != null) { txtDescripcionCliente.Text = cliente.Descripcion; txtSiglaCliente.Text = cliente.Sigla; } else { MessageBox.Show(String.Format("{0}", "No existia el cliente buscado")); } } catch (Exception ex) { MessageBox.Show(ex.Message); } } } } |
Con respecto a la parte web, tendremos 2 partes, el código ASP.net, y el c#, veamoslo, se los dejo completamente de una:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Cliente.aspx.cs" Inherits="Cliente" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body> <form id="frmCliente" runat="server"> <div> <asp:Label ID="Id" runat="server" Text="Id: "></asp:Label> <asp:TextBox ID="txtIdCliente" runat="server"></asp:TextBox> <br /> <asp:Label ID="Label1" runat="server" Text="Descripcion: "></asp:Label> <asp:TextBox ID="txtDescripcionCliente" runat="server"></asp:TextBox> <asp:TextBox ID="txtuser" runat="server">sa</asp:TextBox> <br /> <asp:Label ID="Label2" runat="server" Text="Siglas: "></asp:Label> <asp:TextBox ID="txtSiglasCliente" runat="server"></asp:TextBox> <br /> <hr style="margin-top: 0px; margin-bottom: 0px" /> <asp:GridView ID="grilla" runat="server" CellPadding="4" ForeColor="#333333" GridLines="None"> <AlternatingRowStyle BackColor="White" /> <EditRowStyle BackColor="#2461BF" /> <FooterStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" /> <HeaderStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" /> <PagerStyle BackColor="#2461BF" ForeColor="White" HorizontalAlign="Center" /> <RowStyle BackColor="#EFF3FB" /> <SelectedRowStyle BackColor="#D1DDF1" Font-Bold="True" ForeColor="#333333" /> <SortedAscendingCellStyle BackColor="#F5F7FB" /> <SortedAscendingHeaderStyle BackColor="#6D95E1" /> <SortedDescendingCellStyle BackColor="#E9EBEF" /> <SortedDescendingHeaderStyle BackColor="#4870BE" /> </asp:GridView> <asp:Label ID="Label3" runat="server" Text="Estado:"></asp:Label> <asp:Label ID="lblEstado" runat="server"></asp:Label> <br /> <asp:Button ID="btnConectar" runat="server" Text="Conectar" onclick="btnConectar_Click" /> <asp:Button ID="btnCrear" runat="server" onclick="btnCrear_Click" Text="Crear" /> <asp:Button ID="btnListar" runat="server" onclick="btnListar_Click" Text="Listar" /> <asp:Button ID="btnListarById" runat="server" onclick="btnListarById_Click" Text="Listar By Id" /> <br /> </div> </form> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | using System; // using AccesoDatos; public partial class Cliente : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { } protected void btnConectar_Click(object sender, EventArgs e) { try { lblEstado.Text = ""; AccesoDatos.Conexion.IniciarSesion("127.0.0.1", "Queryable", "sa", "***"); lblEstado.Text = String.Format("{0}", "Se conecto exitosamente"); } catch (Exception ex) { lblEstado.Text = String.Format(ex.Message); } } protected void btnListar_Click(object sender, EventArgs e) { var cliente = new AccesoDatos.Orm.Cliente(); try { grilla.DataSource = cliente.Listar(); grilla.DataBind(); lblEstado.Text = String.Format("{0}", "Se listo exitosamentë"); } catch (Exception ex) { lblEstado.Text = ex.Message; } } protected void btnCrear_Click(object sender, EventArgs e) { var cliente = new AccesoDatos.Orm.Cliente(); try { cliente.IdCliente = Convert.ToInt16(txtIdCliente.Text); cliente.Descripcion = txtDescripcionCliente.Text; cliente.Sigla = txtSiglasCliente.Text; cliente.Crear(cliente); lblEstado.Text = String.Format("{0}", "Se creo exitosamente"); } catch (Exception ex) { lblEstado.Text = ex.Message; } } protected void btnListarById_Click(object sender, EventArgs e) { var cliente = new AccesoDatos.Orm.Cliente(); try { cliente = cliente.Listar(Convert.ToInt16(txtIdCliente.Text)); if (cliente != null) { txtDescripcionCliente.Text = cliente.Descripcion; txtSiglasCliente.Text = cliente.Sigla; } else { lblEstado.Text = String.Format("{0}", "No existia el cliente buscado"); } } catch (Exception ex) { lblEstado.Text = ex.Message; } } } |
Si pueden agregar/aportar mejoras y funcionalidad a estas clases, serán bienvenidas
de hecho éste código lo creo un MVP de Microsoft y yo le agregue algunas funcionalidades..
Parte 1 | Parte 2 | Parte 3 | Parte 4
Edit [2010/08/05]:
A pedido de JR, les subo un ejemplo práctico de las 3 capas con Firebird como Base de Datos, C# el lenguaje. Ya están incluidas dentro del proyecto el Data Provider y la BD de ejemplo. El password del fichero es www.devtroce.com, que lo disfruten!









en
en
en
Excelente Aporte GEEKZERO, en ORM haz utilizado Entity framework? linq… donde lo ubicarias en programacion a 3 capas? sabes de algun codigo de ejemplo u otro tutorial asi como el que posteaste!!
ah! otra pregunta has utilizado en este post otra persona menciono el SUBSONIC 4 has trabajado con esta libreria?
gracias! de nuevo
Excelente artículo. Voy a probarlo. Gracias por compartirlo.
Voy a dar dos enfoques por mi experiencia en el tema de programacion en capas
No tengo programado como mi primer enfoque mi solucion en capas pero pienso que seria lo ideal
de acuerdo a las experiencias que obtuve
No todo es color de rosa, adema de lidiar con la estructura que se hace de muchas formas
particular a cada programador tambien tenemos que lidiar con algunos
asuntos extras(la paginacion para bases de datos que tiene muchos registros, tratar de hacer eficiente
el programa evitando usar objetos pesados para que la aplicacion pueda correr en pc tan potentes, evitar
injection sql en controles de interfaz)
1 enfoque de una solucion en capas:
Proyectos(#):
#LibreriaHelpRB:
proyecto tipo dll de utilidad para los distintos motores de bases de datos
-HelperSql: armado de consultas y actualizaciones para el motor sql server
-HelperOracle: armado de consultas y actualizaciones para motor oracle
-HelperMySQL: armado de consultas y actualizaciones para motor MySql
-HelperDataRB: clase interface adminsitradora que instancia el helper para el motor seteado
y seria la clase de uso en la capa de datos, luego aqui en esta dll se usara la clase helper del motor correspondiente
Esta libraria deberia recibir y retornar datos en forma de datatables genericos y no tipados porque se trata de
generalizar pero entonces en datos al llamar a esta clase se deberia mapear a los objetos de transporte(orm)
Esta clase debe manejar paginacion o sea al ejecutar una consulta permitir obtenerla por paginas para evitar la sobrecarga de memoria
en la interfaz cuando se trata de muchisimos registros
Tmbien eberia controlarse injeccion en las consultas, ya que los datos de los contenedores de transporte
no deberian tener codigo ejecutable injeccion y deberia controlarse sobre los datos recibidos por parametros
o contenedores de trasporte
#Transporte:
proyecto tipo dll que actua como conteneder de datos para transportar entre metodos y capas
Los datos de transporte son los mismo cambie el motor de datos de sql a oracle
por eso de definir en una dll el trasporte, las entidades y listas de entides
de la solucion, en esta capa no hay metodos para ejecutar, solo contenedores
de datos
#Negocio:
proyecto tipo dll que declara los metodos, reglas y validaciones de negocio
Los datos que se obtienen en cada metodo de negocio son los mismos cambie o no el motor
de datos, entonces decimos que el negocio es unico, no desarrollaria aca instruciones
sql server por ejemplo, hacer select sql server particulares, porque el motor puede o no ser sql,
entonces negocio es unico para cualquier motor de datos, por ende hay que generalizar
Si quiero formar una consulta en un metodo de negocio para retornar datos
podria crear en LibreriaHelpRB, una clase administradora para cada motor
en la cual indique por separado los campos del select, el from, el where,
el having, las subconsultas etc, etc como una forma especifica de la clase adminsitradora
y esta segun sql, oracle u mysql pueda armar la consulta adecuada
comentario: algo asi hace subsonic 3, tenes una clase donde indicas todas
las partes pero despues la clase de subsonic arma la consulta final que se ejecuta en el
motor segun el motor seteado
ejemplo:
yo quiero un listado de comprobantes de ventas que fueron emitidos en cta cte filtrado por cliente
y que esten vencidos esta es una funcion de negocio, defino mi filtro para este metodo y los datos
a devolver, en la clase de negocio paso eso a datos, datos tendra que llamar al helper para el motor
correspondiente, armar el select con lso filtros, obetner los datos y en negocio antes de dslir el metodo
castear ese datatable generico devuelto al contenderor que devuelve el metodo de negocio
#Datos:
proyecto tipo dll que declara que recibe transporte de negocio y debe actualizar
o recibe consultas y debe devolver datos por medio de transporte
es unico aunque cambie el motor de datos de la solucion, ya que esta capa llama
de la dll LibreriaHelpRB a la clase HelperDataRB y esta segun el motor seteado
debe elegir entre que clases ejecutar(HelperSql,HelperOracle o HelperMySQL) y armar
la actualizacion o consulta
.Una clase de datos que consulta deberia obtener los datos por medio de la clase
administradora HelperDataRB de #LibreriaHelpRB y mapear los datos al objeto de transporte
para poder retornarlos a negocio
Una clase de datos que actualiza recibe un contendedor de transporte y llamado a
la clase administradora HelperDataRB de #LibreriaHelpRB deberia mappear primero
a datatable generico para pasarselo al metodo de actualizacion correspondiente
#WInRB:
proyecto tipo windows de entrada a la solucion, podria ser de consola o web
es la fachada para el cliente o usuario final
Usos de las capas en la solucion:
ejemplo1:
-para un formulario de alta, baja y modificacion:
en el formulario que esta en el proyecto #WInRB, hago uso de la capa de transporte
para pasar los datos a negocio al actualizar un cliente
La clase de negocio de clientes ubicada en #Negocio recibe los datos de transporte
y mediante la clase adminsitradora HelperDataRB de #LibreriaHelpRB ejecuta
la sentencia para el motor estabelcido de datos
ejemplo2:
-para un informe de comprobantes detallados:
en el formulario de filtro se piden los rangos de filtro, estos rangos se pasan como transporte
a negocio y en negocio tengo un metodo para ese informe donde llamo a la clase adminsitradora HelperDataRB
de #LibreriaHelpRB ejecuta la sentencia para el motor establecido de datos y retorna en un contenedor
de transporte con los datos
2 enfoque de una solucion en capas:(crear una capa de datos para cada motor y setear en tiempo de ejecucion la capa
que se usara deacuerdo al motor establecido)
otro enfoque es la capa de interfaz o fachada que querramos(winform, web, movil) unica(puede haber mas de una), la capa de negocio unica
que valida reglas de negocio y pasa los datos a la capa de datos
y hacemos una capa de datos fisica por cada motor luego por seteo al ejecutarse el programa
se trabaja con una u otra capa de datos
algo asi hace subssonic 3, seria un orm, donde elegis una base de datos y te genera todas las entidades
una por cada tabla para un motor específico con unas plantillas, hay una plantilla para sql server, una para oracle y asi
Resumiendo:
la meta de un sistema es cumplir las funciones de negocio requeridas por la que fue
requerido y diseñado el mismo sistema, estas funciones pueden cambiar con el tiempo,
puede cambiar el motor de bases de datos por una cuestion de gustos, o por mejoras o necesidades empresariales
tambien puede cambiar la necesidad de una fachada nueva para hacer ciertas tareas, pero debemos
conservar el negocio intacto, salvo que cambien las reglas o necesidades de negocio
tenga el motor que tenga la capa de interfaz o fachada y la capa de negocio no deberian sentirlo si cambia,
aun asi, la capa de datos tampoco deberia subrir cambios, solo fabricar el helper necesario
para el nuevo motor en el proyecto #LibreriaHelpRB
Es decir una capa de datos unica tambien donde casteo a contenedores para devolver datos hacia el cliente
y casteo a tipos genericos como datatables para actualizar en el motor de base de datos(mapeo relacional)
con las clases helper y no atar la capa de datos a un motor especifico o tendre que realizar una capa
de datos para cada motor
Si siempre usare el mismo motor de datos podria directamente escribir mi capa de datos unica
con sentencias especificas del motor elegido, por ejemplo desarrollar mi capa de datos para sql server unicamente
pero si apuntamos a la variedad por necesidades distintas podemos generalizar, aunque seguro cuesta mas
igualemnet si fueramso a desarrollar una sola capa de datos son utiles las clases helper
para evitar repeticion de codigo y nuestra solucion sera mas chica en cantidad de lineas y es importante
porque programar en capas te lleva a termianr teniendo cualuier cantidad de clases y es mejor organizarse
desde el principio, tambien puende crear una libreria para funciones y controles base ya sea para windows o form
hola rodrigob
Me gusto mucho la apreciación que le diste a este blog.tengo algunas preguntas sobre las cuales me gustaria que respondieras ya que veo que tienes una buen bagaje sobre el tema de programacion a 3 capas o n-capas.
1. la paginación: siempre hay que hacer paginación asi sea un sistema que este en crecimiento. y si es un sistema pequeño?
2. el orm es mejor seguir utilizando el modelamiento directamente en el codigo o seguir con el entity framework? y utilizar linq? sabes de algun ejemplo o codigo que utilice estas herramientas?
3. es recomendable usar interfaces en muchos ejemplos sobre programacion por capas no los utilizan y tambien se utiliza componentes como dataset como entidad. que puede diferenciar y que implicaciones en tiempo de programación las horas que me conllevarian.
4. tienes otra capa llamada transporte, realizar otra capa no implicaria mas tiempo en el desarrollo?
gracias por tus respuestas.
soy un novato en .net y llevo las malas constumbres que se desarrollaban en visual basic 6
No podia quedarme mas claro los felicito por ese gran trabajo gracias!!!!
como haria para sacar un listado en un Datagrid de WPF, ya que no tiene la propiedad DataSource
¿Saludos, que tan dificil sería adaptarlo para Access como motor de BD? ¿En que clases habria que cambiarle?
Esque en si lo que necesito es que cuando distribuya mi aplicación MonoUsuario sea access mi motor, pero cuando sea multiusuario optare por SQL Server.
¿Tendría que hacer dos proyectos distintos?
Bueno… Con este codigo funciona pero lo suyo seria que cojiese el nombre de la tabla sin ponerla tu a mano, es decir , a traves de el metodo TraerDataTable
try { var a = usu.Listar(); var b = prov.Listar(); a.TableName = "usuarios"; b.TableName = "provincias"; DS.Tables.Add(a); DS.Tables.Add(b); this.comboBox1.DataSource = DS.Tables["usuarios"]; this.comboBox1.DisplayMember = "nombre" ; this.comboBox1.ValueMember = "clave" ; }Hola. quiero meter dos tables en un dataset, pero al hacer esto me sale el siguente error: “A DataTable named ‘Table’ already belongs to this DataSet.”
Mi codigo es este:
try { DS.Tables.Add(usu.Listar()); DS.Tables.Add(prov.Listar()); this.comboBox1.DataSource = DS.Tables[0]; MessageBox.Show(DS.Tables[0].TableName); this.comboBox1.DisplayMember = "nombre"; this.comboBox1.ValueMember = "clave"; } catch (SystemException o) { MessageBox.Show(o.Message); }PD: despues de meterlos en el data set quiero relacionarlos pero eso ya es otra historia
Gracias
Hola Fran, prueba con esto, sino funciona veremos otro camino..
probando con lo que has puesto sigue sin funcionar , me aparece el siguente error: “Cannot bind to the new display member.
Parameter name: newDisplayMember”
Ahora me mete las 2 tablas en el DS pero al rellenar el combobox me da el fallo
He buscado en google y me dice que el DisplayMember que le asigno al combobox esta mal, pero si a la propiedad datasource del combo le asigno un datatable todo funciona bien,
Este es mi codigo:
try { DS.Tables.Add(usu.Listar().TableName = "usuarios"); DS.Tables.Add(prov.Listar().TableName = "provincias"); this.comboBox1.DataSource = DS.Tables[0]; this.comboBox1.DisplayMember = "nombre" ; this.comboBox1.ValueMember = "clave" ; } catch (SystemException o) { MessageBox.Show(o.Message); }El código esta sencillamente genial, yo le agregaría (de hecho voy a hacerlo para un proyecto que tengo) una abastraccion mas, la cual seria que los resultsets de mas de 1 objeto sean listas genericas con objetos de la clase que se este pidiendo, saludos y muchisimas gracias
Que bueno que te haya servido, sería interesante si compartes con la comunidad tus mejoras
Saludos..
Muchas gracias GeekZero!
Extremadamente didáctio y esclarecedor este artículo.
Acabo de terminar de leer tu post, me encuentro es pos de iniciar un proyecto personal de desarrollo en C# y por lo leido es genial, felicitaciones, espero poder aportar mas adelante cuando tenga mas avance en C#.. Gracias.
Que codigo es lo que no esta disponible? si es por el enlace de gigasize, acabo de comprobarlo, esta en linea..
que tal, acabo de revisar tu post, esta genial, quize probar tu código, pero no esta disponible, seria bueno que vuelvas a subirlo, de antemano gracias y hasta pronto,