viernes, 7 de mayo de 2010

.NET 4 WPF: Descubriendo la potencia de su enlace a datos.

Hola!

Una de las cosas que más ha cambiado desde Windows Forms a WPF es la forma en la que mostramos nuestros datos, y no solo en al aspecto visual, lo que realmente a cambiado es como enlazamos nuestros controles a nuestras fuentes de datos y se los mostramos al usuario.

Para ilustrar un poco el nuevo paradigma de enlace a datos de WPF vamos a hacer una pequeña aplicación (pequeña porque entre Xaml y Código no tiene más de 85 líneas contando espacios en blanco) que nos permitirá mostrar en un datagrid los datos de pedidos de nuestros clientes, al seleccionar cualquier pedido, veremos en un listbox a su lado los productos pedidos por estos clientes y debajo de este listbox tendremos información para contactar con este cliente, aquí os dejo una captura de la aplicación en funcionamiento:

app_running

Preparación de Nuestra Fuente de Datos

En mi caso he usado la base de datos NorthWind en access para hacer más sencillo el trabajo con ella, la tenéis incluida en el proyecto de ejemplo que se encuentra al final de esta página, pero también la podéis descargar aquí.

Una vez que tengáis la base de datos Nwind.mdb copiadla al directorio de vuestro proyecto e incluidla en el mismo, desde el Menú Project –> Add Existing Item, acto seguido Visual Studio 2010 abrirá el Data Source Configuration Wizard:

Add database

Seleccionamos DataSet y presionamos Siguiente, donde el wizard nos pedirá que seleccionemos los objetos de la base de datos que queremos adquirir a nuestro dataset:

Select Tables

Yo he seleccionado todas las tablas, pero para nuestro proyecto solo necesitaremos Customers, Order Details, Orders, Products y Shippers. Seleccionad las tablas, dadle un nombre al dataset en el textbox DataSet name y presionad Finish, con esto la base de datos estará incluida en nuestro proyecto y Visual Studio creará un Dataset tipado (archivo .xsd), Si lo abrís, veréis algo parecido a esto (depende de las tablas que hayáis seleccionado):

screenshot_dataset

Esta es una representación visual de vuestro dataset, cada recuadro tiene dos secciones la superior es la tabla con sus campos y la inferior (llamada igual que la tabla + TableAdapter) es el objeto que usaremos para recuperar los registros de esa tabla. También Veréis líneas que unen las tablas, estas son las relaciones entre ellas, si pincháis sobre ellas se ponen en azul, borradlas todas, puesto que en el siguiente paso las crearemos.

Vamos a crear una nueva relación entre las tablas Orders (Tabla Parent) y Order Details (Tabla Child) para esto, presionad el botón derecho del ratón sobre el encabezado de Order Details, del menú contextual seleccionad Add y en el desplegable pinchad en Relation, se mostrará una ventana como esta:

newrelation

Relación Tablas Orders y Order Details

Propiedad

Valor

Name

OrderDetails

Parent Table

Orders

Child Table

Order Details

Key Columns

OrderID

Foreign Key Columns

OrderID

Ahora crearemos la relación entre la tabla Order Details y Products de la misma forma y con los siguientes parámetros:

Relación Tablas Orders y Products

Propiedad

Valor

Name

ProductsOrderDetails

Parent Table

OrderDetails

Child Table

Products

Key Columns

ProductID

Foreign Key Columns

ProductID

También la relación entra la tabla Orders y Customers:

Relación Tablas Orders y Customers

Propiedad

Valor

Name

CustomersOrders

Parent Table

Orders

Child Table

Customers

Key Columns

CustomerID

Foreign Key Columns

CustomerID

Y por último la relación entre la tabla Orders y Shippers:

Relación Tablas Orders y Shippers

Propiedad

Valor

Name

ShippersOrders

Parent Table

Orders

Child Table

Shippers

Key Columns

ShipVia

Foreign Key Columns

ShipperID

Con esto ya hemos terminado de preparar nuestras fuentes de datos, ahora vamos a diseñar nuestra ventana.

Diseñar nuestra ventana

Vamos a diseñar nuestra ventana, esta se compone de una grid dividida en 2 rows y 2 columnas, la primera row la usaremos como Titulo y la segunda row tendrá el contenido. En la primera Columna de la segunda row insertaremos una datagrid que mostrará las ordenes de pedidos y en la segunda columna insertaremos controles que mostrarán los detalles del pedido seleccionado en cada momento:

window_design

El código Xaml de la grid es este:

    <Grid>

<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75*"></ColumnDefinition>
<ColumnDefinition Width="25*"></ColumnDefinition>
</Grid.ColumnDefinitions>

</Grid>


Es muy sencillo, simplemente indicamos la definición de rows y columnas, con tamaño variable o fijo.



Ahora vamos a insertar el titulo de la grid de datos y la propia datagrid, especificándole a que columna y row pertenecen:



<ContentPresenter Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Content="Grid de datos."/>



<DataGrid Name="grdDatos" Grid.Row="1" Grid.Column="0" Margin="5"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
AutoGenerateColumns="False" IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding}">

</DataGrid>


Como veis en mi caso he optado por no definir las columnas automáticamente (AutoGenerateColumns=”False”) para poder definir el enlace de las columnas que quiero mostrar, en cada uno de los dos controles especifico las propiedades Attached Grid.Row y Grid.Column, para colocar el control donde le corresponde, en la datagrid también establezco la propiedad IsSyncronizedWithCurrentItem a True, esta propiedad es muy importante pues es la que hará posible que el resto de controles usen los datos actualmente seleccionados.



Dentro de la Datagrid, debemos especificar las columnas que queremos mostrar, para ello incluimos el siguiente código xaml:



<DataGrid.Columns>
<DataGridTextColumn Header="Cod.Pedido" Binding="{Binding Path=OrderID}" MinWidth="80"/>
<DataGridTextColumn Header="Cliente" Binding="{Binding Path=CustomersOrders/CompanyName}" MinWidth="200"/>
<DataGridTextColumn Header="Fecha Pedido" Binding="{Binding Path=OrderDate}" MinWidth="100"/>
<DataGridTextColumn Header="Transportista" Binding="{Binding Path=ShippersOrders/CompanyName}" MinWidth="200"/>
</DataGrid.Columns>




Y Aquí es donde nos empezamos a beneficiar del enlace a datos de WPF, como veis cada columna tiene una propiedad Binding, esta propiedad apunta al campo que queremos mostrar en ella, en el campo de la primera columna (Cod.Pedido) apuntamos directamente al campo OrderID de la tabla Orders, que va a ser nuestro DataContext principal. Esto es igual en Fecha Pedido, pero veréis que Cliente y transportista son distintos, esto se debe a que, usando las relaciones anteriormente creadas estamos dirigiendo nuestra columna por esas relaciones a la tabla Hija, y escogiendo un campo de la misma para mostrar, con lo  cual no tenemos que preocuparnos de realizar consultas específicas con Joins, simplemente usamos las relaciones entre Tablas para mostrar los campos que nos interesen.



Ahora vamos a crear la parte de la ventana encargada de mostrar el detalle del pedido:



<ContentPresenter Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" Content="Información Extra."/>

<StackPanel Grid.Row="1" Grid.Column="1" Margin="5">
<ContentPresenter Content="Detalle:"></ContentPresenter>
<ListBox Name="lstDetallePedidos" Height="125"
ItemsSource="{Binding Path=OrderDetails}"
DisplayMemberPath="ProductsOrderDetails/ProductName"
IsSynchronizedWithCurrentItem="True" >
</ListBox>

<ContentPresenter Content="Cod.Cliente:"></ContentPresenter>
<TextBox Name="txtCodCliente" Text="{Binding Path=CustomerID}"></TextBox>
<ContentPresenter Content="Compañía:"></ContentPresenter>
<TextBox Name="txtNombreCia" Text="{Binding Path=CustomersOrders/CompanyName}"></TextBox>
<ContentPresenter Content="Contacto:"></ContentPresenter>
<TextBox Name="txtContactoCliente" Text="{Binding Path=CustomersOrders/ContactName}"></TextBox>
<ContentPresenter Content="Teléfono:"></ContentPresenter>
<TextBox Name="txtTfnoCliente" Text="{Binding Path=CustomersOrders/Phone}"></TextBox>
</StackPanel>


Veréis que en si es muy sencillo, al igual que anteriormente usamos un ContentPresenter en la row 0 y columna 1 para definir el título y después en la row 1 columna 1 introducimos un StackPanel, puesto que queremos un Layout sencillo de elementos en Pila. Al igual que en las columnas de la Datagrid aquí enlazamos la propiedad Text de cada TextBox al campo de la tabla Orders que queremos mostrar y nos servimos de las relaciones para mostrar campos de otras tablas.



El control que difiere un poco es el ListBox, pues en este establecemos 2 propiedades para realizar el enlace a datos, primero el ItemsSource, en este caso lo establecemos a la relación OrderDetails, de esta forma obtendremos tantos items como relaciones existan entre la tabla Orders y OrderDetails (es decir, si una orden contiene 2 OrderDetails, obtendremos esos dos items) pero, al establecer la propiedad DisplayMemberPath usamos una segunda relación, ProductsOrderDetails y el campo ProductName de la tabla producto, puesto que no nos interesa mostrar el ID del producto seleccionado, que se encuentra en la tabla OrderDetails, queremos el nombre que se encuentra en la tabla Productos, de esta forma usamos la relación ProductsOrderDetails para movernos hacia la tabla Productos para cada Item del ListBox.



Y con esto hemos terminado de diseñar y preparar nuestra ventana, ahora solo nos queda escribir el código que haga que todo esto funcione y digo solo, porque es realmente sencillo.



Escribiendo el Código



Bien, lo primero que tenemos que hacer es abrir el archivo de código asociado a nuestro xaml y declarar los miembros privados que contendrán nuestros datos y las instancias de los tableadapter que usaremos para rellenarlos:



'Definimos los objetos con los que vamos a trabajar.
Private dsDatos As New NwindDataSet
Private custAdap As New NwindDataSetTableAdapters.CustomersTableAdapter
Private ordeAdap As New NwindDataSetTableAdapters.OrdersTableAdapter
Private detailAdap As New NwindDataSetTableAdapters.Order_DetailsTableAdapter
Private shipAdap As New NwindDataSetTableAdapters.ShippersTableAdapter
Private prodAdap As New NwindDataSetTableAdapters.ProductsTableAdapter


Definimos nuestro Dataset, dsDatos y TableAdapters que rellenarán las tablas que necesitamos Customers, Orders, Order_Details, Shippers y Products.



Ahora en el Constructor de nuestra ventana vamos a cargar los datos y establecer el origen de datos de nuestros controles:



Public Sub New()
' This call is required by the designer.
InitializeComponent()

' Add any initialization after the InitializeComponent() call.
' Obtenemos nuestros datos.
custAdap.Fill(dsDatos.Customers)
detailAdap.Fill(dsDatos.Order_Details)
shipAdap.Fill(dsDatos.Shippers)
prodAdap.Fill(dsDatos.Products)
ordeAdap.Fill(dsDatos.Orders)

'Establecemos la tabla Orders como el DataContext de la ventana.
Me.DataContext = dsDatos.Orders
End Sub


Este código no es nada complicado, tras la llamada a InitializeComponent (creada automáticamente por Visual Studio al crear la Sub New de la ventana) simplemente usamos nuestros TableAdapters para rellenar las tablas de nuestro DataSet dsDatos.



La línea más importante de nuestra aplicación es la penúltima:



    Me.DataContext = dsDatos.Orders


Esta línea establece el contexto de datos de nuestra ventana a la tabla Orders, de la cual obtendremos todas las relaciones y campos necesarios. Aquí es donde ocurre la magia del enlace a datos en WPF, en el código xaml hemos definido que campo de que tabla va a mostrar cada control, pero en ningún momento le hemos dicho de que fuente de datos va a obtener estas tablas, al hacer esto, cada control en ejecución vera que no tiene fuente de datos y empezará a preguntar a sus controles padres que fuente de datos usar, en el caso de la DataGrid consultará a la Grid principal, en el caso del ListBox consultará al StackPanel, que a su vez consultará a la grid principal, como la grid principal tampoco tiene fuente de datos preguntará a su control padre, la ventana en este caso, y así al establecer la fuente de datos en la ventana, todos los controles de la misma que no especifiquen una fuente de datos propia usarán la misma fuente de datos, lo que permite que todos los controles estén sincronizados, usando propiedad IsSynchronizedWithCurrentItem que hace que si el control selecciona una row distinta a la actual esta se establezca en la fuente de datos como el Item actual.



Y… ya no hay más código este es todo el código necesario para que la ventana funcione perfectamente, aquí tenéis un listado completo del archivo MainWindow.xaml.vb:



Class MainWindow
'Definimos los objetos con los que vamos a trabajar.
Private dsDatos As New NwindDataSet
Private custAdap As New NwindDataSetTableAdapters.CustomersTableAdapter
Private ordeAdap As New NwindDataSetTableAdapters.OrdersTableAdapter
Private detailAdap As New NwindDataSetTableAdapters.Order_DetailsTableAdapter
Private shipAdap As New NwindDataSetTableAdapters.ShippersTableAdapter
Private prodAdap As New NwindDataSetTableAdapters.ProductsTableAdapter


    Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
' Obtenemos nuestros datos.
custAdap.Fill(dsDatos.Customers)
detailAdap.Fill(dsDatos.Order_Details)
shipAdap.Fill(dsDatos.Shippers)
prodAdap.Fill(dsDatos.Products)
ordeAdap.Fill(dsDatos.Orders)
'Establecemos la tabla Orders como el DataContext de la ventana.
Me.DataContext = dsDatos.Orders
End Sub
End Class


A continuación tenéis la solución completa en .NET 4 & Visual Studio 2010 para descargarla y poder jugar con ella.



Un gran saludo y hasta la próxima!


6 comentarios:

  1. Muchisimas gracias por el ejemplo, no es facil encontrar ejemplos de este tipo, completos de principio a fin, sin presuponer los conocimientos de la gente que pueda leerlos, Y EN VB, lo dicho, muchas gracias.

    ResponderEliminar
  2. Disculpa como haces para que los datos del detalle cambien cada que cambias la fila del datagrid

    ResponderEliminar
  3. muchisimas gracias. ahora solo me queda traducirlo a c#

    ResponderEliminar
  4. Exelente pero veo en el ejemplo que la base de datos tiene imagenes, hay alguna forma de que tambien se muestren cuando se selecciona un producto distinto?

    ResponderEliminar