Twitter Facebook Google + RSS Feed

Enlace a Data Objects en WPF

5
WPF

Martes, 14 de julio de 2009 a las 12:28hs por Dario Krapp

En WPF existe el concepto y la funcionalidad de enlace a datos, al igual que sucedía en tecnologías previas de Microsoft, aunque estos conceptos han evolucionado aun más permitiendo el enlace entre elementos, el enlace a recursos, a objetos ADO.NET, a Linq y a objetos de datos (también conocidos como DataObjects) en este articulo veremos el enlace a este último tipo de objeto.

Un objeto de datos representará una entidad la cual tendrá interacción con la interfaz gráfica, es muy común que dichas entidades sean persistentes, esto significa que sin importar el mecanismo de acceso a datos que se emplee, esta información terminará guardándose y recuperándose de una base de datos, aunque este es un detalle que quedará más allá del objeto de datos (o al menos así debería ser). El objeto de datos inicialmente será no más que una clase con propiedades que modelará a una entidad.
Estoy convencido que será una buena idea crear una clase lo más sencilla posible que utilizaremos como objeto de datos desde aquí en adelante.

Definiremos entonces una clase llamada Empl, que pretenderá con mucha imaginación representar a un empleado.

public class Empl 
{   
 public int IDEmpleado { set; get; }
 public long? DNI { set; get; }
 public string Apellidos { set; get; }
 public string Nombres { set; get; }
 public DateTime FechaIngeso { set; get; }
}

En nuestra intención de simplificar le hemos agregado solamente unos pocos campos, un identificador, un número de documento o cédula, sus nombres y apellidos y la fecha de ingreso.

En primera instancia utilizaremos esta clase para que los datos de un empleado puedan ser mostrados en la interfaz gráfica. Inicialmente intentaremos mostrar solo un dato, utilizaremos entonces un elemento de XAML denominado TextBox, que definiremos a continuación.

<TextBox x:Name="txtApe" Text="Apellido"></TextBox>

Esta porción de XAML definirá un elemento que mostrará el texto “Apellido” lo cual, como estarán convencidos, está bastante alejado de lo que deseamos hacer, pero sirve para hacer algunas observaciones, la primera es que XAML es un lenguaje basado en XML y diseñado para definir interfaces gráficas (espero que esto no sea una sorpresa), de aquí podemos deducir que la definición de la interfaz grafica estará constituida por nodos y atributos, el valor de cada atributo en el XAML definirá el valor de una propiedad del elemento que se está definiendo, en el ejemplo el atributo Text definirá que el elemento (un TextBox) con nombre txtApe tomará el valor “Apellido” en su propiedad Text.

Esto funcionando de esta forma estará perfectamente bien definido, pero será insuficiente debido a que todas las propiedades deberían tomar valores fijos, lo que haría a XAML muy poco práctico, en nuestro caso para dar un ejemplo deseábamos mostrar un apellido que dependerá del valor de una propiedad de un objeto y con lo que hemos visto hasta ahora hacer esto no es factible.

Es de esperarse entonces que exista una forma de definir valores de atributos dinámicos, WPF utilizará los caracteres “{” y “}” para indicar que el valor del atributo no es fijo, este concepto es denominado “Markup” y existe una amplia variedad de markups en XAML y WPF. Para nuestro caso estaremos interesados en definir un markup que indicará que la propiedad Text de nuestro TextBox estará enlazada a la propiedad Apellidos de un objeto que suministraremos posteriormente, el markup capaz de efectuar dicha operación es el siguiente:

<TextBox x:Name="txtApe" Text="{Binding Path=Apellidos}" />

Lo que indicamos en la línea previa es que deseamos asignar a la propiedad Text del elemento txtApe, un enlace a la propiedad Apellidos de un objeto. Una forma simplificada de definir lo mismo es la siguiente:

<TextBox x:Name="txtApe" Text="{Binding Apellidos}" />

Con esto hemos resuelto una parte del problema, debemos luego llevar a cabo el enlace con el objeto, en este momento es donde la propiedad DataContext se vuelve importante, la propiedad DataContext tomará el objeto (fuente de datos) que se enlazará con el elemento, de esta forma al escribir el código :

Empl objE = new Empl() 
{ 
 Apellidos = "Perez", 
 DNI = 29111222, 
 FechaIngeso = new DateTime(2000, 11, 19), 
 IDEmpleado = 123, 
 Nombres = "Juan" 
};
txtApe.DataContext = objE;  

El objeto objE se enlazará al elemento txtApe (cuyo markup se ha definido como “{Binding Apellidos}”), el resultado final será que el valor definido en la clase para la propiedad Apellidos será enlazado a la propiedad Text del elemento txtApe.

En este punto tenemos que tomar en cuenta otra consideración y es que la propiedad DataContext es una dependency property, y tratando de no entrar en muchos detalles sobre este punto destacaremos que en WPF existen un tipo de propiedades denominadas dependency properties, y la diferencia fundamental con las propiedades convencionales es que sus valores podrían ser (además de accesibles por el propio elemento que las define) ser accesibles y por todos sus elementos hijos en el árbol lógico de elementos.

Recordemos que XAML está basado sobre XML y que cada documento XML podría representarse como un árbol, en el caso de XAML, un documento XAML definirá entre otras cosas un árbol lógico de elementos, por ejemplo la definición:

<Grid x:Name="e1">
  <StackPanel x:Name="e2">
    <TextBox x:Name="e3" ...></TextBox>
    <TextBox x:Name="e4" ...></TextBox>
  </StackPanel>
</Grid>

Indicará que el elemento e1 posee un hijo denominado e2 el cual a la vez posee los hijos e3 y e4, si la propiedad DataContext es definida en el elemento e1, su valor será accesible para el elemento e2, tanto como para los elementos e3 y e4. Las dependency properties son muy comunes en WPF y su objetivo es simplificar el código y mejorar la performance.
Por lo visto anteriormente podríamos hacer los siguientes reemplazos:

<StackPanel x:Name="stackPanelEmpl">
  <TextBox x:Name="txtApe" Text="{Binding Apellidos}"></TextBox>
  </StackPanel>
stackPanelEmpl.DataContext = objE;

Y obtendríamos el mismo resultado, ya que el DataContext definido en el elemento stackPanelEmpl es accedido por el elemento txtApe.
No caben dudas de adonde queríamos llegar con estos pasos, ahora definir el resto de los campos es muy sencillo, solo deberemos agregar más elementos y enlazar las propiedades adecuadamente.

<StackPanel x:Name="stackPanelEmpl">
  <TextBox x:Name="txtIDEmpleado" Text="{Binding IDEmpleado}"></TextBox>
  <TextBox x:Name="txtDNI" Text="{Binding DNI}"></TextBox>
  <TextBox x:Name="txtApellidos" Text="{Binding Apellidos}"></TextBox>
  <TextBox x:Name="txtNombres" Text="{Binding Nombres}"></TextBox>
  <TextBox x:Name="txtFechaIngreso" Text="{Binding FechaIngeso}"></TextBox>
</StackPanel>

Con esto hemos conseguido mostrar los valores lo cual no es poco, pero tampoco es suficiente, ya que frecuentemente desearemos que el camino sea de ida y vuelta, o sea que el usuario pueda modificar valores en la interfaz gráfica y los cambios sean transferidos a la clase subyacente.

Si se verifican los valores del objeto objE, se verá que cuando los valores son modificados en los elementos TextBox, la clase será automáticamente actualizada (hay un detalle sobre esta actualización que veremos posteriormente), para verificar este comportamiento deberá definirse un elemento Button que muestre los valores de objE al ser presionado, luego explicaremos la causa de esta imposición para mostrar los valores. Hay quienes podrían preguntarse como esta magia de actualización funciona, y lo bueno es que hay una explicación relativamente simple.

Cuando hemos definido el markup Binding, si bien todo funciona, nos han quedado algunas fichas fuera del tablero, la primera es el modo de Binding (Mode) y la segunda el modo de actualización (UpdateSourceTrigger).

Antes de ver estos términos es buena idea analizar a los actores que intervienen en un enlace, los mismos son un objeto origen, un objeto destino y un valor o datos que serán transferidos desde el objeto origen hacia el objeto destino, en nuestro ejemplo el objeto objE es la fuente y el elemento TextBox txtApellidos es el destino, el Valor “Perez” representará los datos que se transferirán desde el origen (objE) hacia el destino (txtApellidos), el modo de Binding que se definirá en el markup Binding bajo el atributo Mode, podrá definirse de la forma:

  • “de ida” (OneWay)
  • “de ida y vuelta” (TwoWay)
  • “de vuelta” (OneWayToSource)
  • “Solo una vez” (oneTime)
  • “Valor definido por defecto en la dependency property” (Default)

En el caso de “de ida” (cuando el atributo Mode toma el valor OneWay) los valores podrán transferirse solo desde el origen hacia el destino, en el caso “de ida y vuelta” (TwoWay) los datos podrán transferirse en ambos sentidos, y en el caso “de vuelta” (OneTimeToSource) los datos solo podrán transferirse desde destino hacia el origen.

Viendo esto último podemos deducir que alguien está definiendo que en el caso de los Textboxes el enlace será del tipo TwoWay, la deducción el correcta y ese alguien es justamente la propiedad Text del elemento TextBox que es también una dependency property, (de hecho solo las dependency properties aceptan el markup Binding) , y una vez más sin entrar en detalles sobre las dependency properties solo comentaremos que en una dependency property es posible definir el Mode por defecto que la misma asumirá durante un enlace, pero podemos redefinir el valor por defecto de la siguiente forma:

<TextBox Margin="3" x:Name="txtApellidos" Text="{Binding Apellidos, Mode=OneWay}" />

Luego de hacer este cambio, por más que modifiquemos el apellido en el TextBox txtApellidos, la propiedad Apellidos en el objeto objE no se verá afectada.

Finalmente nos queda por ver el modo de actualización del markup Binding y una buena forma es probar una porción de código para entender cómo funciona, Dijimos previamente que la propiedad Text de elemento TextBox estaba definida por defecto con Mode TwoWay y hemos comprobado que al modificar un valor en el TextBox y luego presionar un botón, el valor de la propiedad de objE era efectivamente modificado, entonces podríamos desear hacer la misma modificación pero esta vez vía código, elegiremos el TextBox txtNombres y ejecutaremos el siguiente código, (por ejemplo al presionar un botón):

objE = new Empl() 
{ 
  Apellidos = "Perez", 
  DNI = 29111222, 
  FechaIngeso = new DateTime(2000, 11, 19), 
  IDEmpleado = 123, 
  Nombres = "Juan" 
};

stackPanelEmpl.DataContext = objE;
txtNombres.Text = "Modificado";
MessageBox.Show(objE.ToString());

Para nuestra sorpresa veremos que la propiedad Nombres de objE no ha tomado el valor “Modificado” (para que el ejemplo funcione deberá sobrescribirse el método ToString() de la clase Empl para que los valores de sus campos sean devueltos). Pero modificaremos un poco el código y agregaremos un par de líneas extra.

objE = new Empl() 
{ 
  Apellidos = "Perez", 
  DNI = 29111222, 
  FechaIngeso = new DateTime(2000, 11, 19), 
  IDEmpleado = 123, 
  Nombres = "Juan" 
};

stackPanelEmpl.DataContext = objE;
txtNombres.Focus();
txtNombres.Text = "Modificado";
txtDNI.Focus();
MessageBox.Show(objE.ToString());

En este caso el valor será modificado. De esta prueba podríamos deducir que el valor es actualizado desde el destino hacia el origen cuando el elemento destino pierde el foco y es efectivamente de esa manera (ahora quizás se entienda por que impusimos que debiera tener que presionarse un Button para que los valores fueran mostrados luego de ser modificados en algún TextBox). La propiedad UpdateSourceTrigger definirá de que manera los datos serán modificados en el objeto origen, para que esta propiedad tenga sentido es claro que el Mode deberá ser TwoWay o OneWayToSource, ya que son los casos que admiten la modificación de objeto origen.

Las opciones de UpdateSourceTrigger son:

  • “Cuando el elemento pierde el foco” (LostFocus)
  • “Cuando la propiedad cambia” (PropertyChanged)
  • “Especificado vía código” (Explicit)
  • “Valor definido por defecto en la dependency property” (Default)

Si modificamos el XAML de la siguiente forma:

<TextBox Margin="3" x:Name="txtNombres" Text="{Binding Nombres, UpdateSourceTrigger=PropertyChanged}"></TextBox>

Veremos que ya no es necesario manejar el foco del elemento txtNombres para que la propiedad sea actualizada, nuevamente es la propiedad Text del elemento TextBox es quien define que el modo de actualización por defecto será LostFocus y nadie dudará que este caso la opción mas lógica.

Si se utiliza la opción “Explicit” por ejemplo en el TexBox DNI de la siguiente forma:

<TextBox Margin="3" x:Name="txtDNI" Text="{Binding DNI, UpdateSourceTrigger=Explicit}" />

Los cambios tomarán efecto recién cuando se ejecute por código lo siguiente:

txtDNI.GetBindingExpression(TextBox.TextProperty).UpdateSource();

Ya hemos podido resolver el problema que nos habíamos propuesto, enlazar un objeto de datos a un grupo de elementos de XAML y definir de qué forma deseamos que se comporte la actualización de datos. Dejamos para el final del artículo dos puntos que no habían sido mencionados previamente relacionados con el Mode OneTime y el Mode OneWayToSource, el Mode OneTime creará un enlace desde el objeto origen hacia el objeto destino pero con la particularidad que se transferirá solamente el primer valor que el objeto origen tome, posteriores cambios en el objeto origen serán ignorados, de allí el sugerente nombre (OneTime).

En cuanto al Mode OneWayToSource parecería en primera instancia que su existencia no tiene mucho sentido, ya que podría deducirse que si se intercambian (swap) los objetos origen por destino y se establece el Mode como OneWay no habría diferencias. En realidad no es exactamente de esta forma ya que como habíamos mencionado previamente la propiedad utilizada como destino del enlace deberá ser una dependency property, pero no la propiedad origen, (por ejemplo la propiedad Apellidos de la clase Empl no es una dependency property, la hemos definido como una propiedad convencional). Entonces este tipo de enlace permitirá modificar propiedades que no son dependency properties en un solo sentido, desde la propiedad destino hacia la propiedad fuente.

Todo este esquema que hemos desarrollado funcionará bien cuando la interacción sea exclusivamente entre la interfaz gráfica y un objeto de datos, pero en escenarios de mundo real es posible que un objeto de datos que se encuentra ya enlazado a la interfaz gráfica sea modificado externamente, una forma de simular este comportamiento es agregar un botón que modifique alguna propiedad del objeto objE luego que el enlace ha sido establecido, en este caso el objeto objE no será capaz de informar a la interfaz gráfica que una de sus propiedad ha sido modificada. Esto no es fatal, en el próximo artículo veremos que existen varias posibilidades para resolver este y otros tipos de problemas de enlace de datos. Espero como siempre que este articulo haya sido de utilidad.


5 comentarios »

  1. Javi dice:

    Muchas gracias.
    Muy buena explicación

  2. Darky dice:

    Gracias, me ayudo tu explicación…

  3. caro dice:

    gracias su marterial es muy entendible. esto me servira para mi materia, me gustaria que agregen mas temas.

  4. Icastro dice:

    Muchas gracias por la explicación realmente muy clara y entendible. me dejaron muchas cosas en claro de como funcionan las cosas en wpf . nuevamente Gracias

  5. Daniel Kuperman dice:

    Muchisimas gracias por haber escrito este artículo. De todo lo que encontré en internet, fue el único que pudo explicar detalladamente como funciona el Binding. Gracias!!

Deja un comentario

Buscar