MSDN Magazine > Inicio > Todos los números > 2008 > Julio >  Datos y WPF: Personalización de la visualizació...
Datos y WPF
Personalización de la visualización de datos con enlace de datos y WPF
Josh Smith
En este artículo se describen los siguientes temas:
  • Enlace de datos en WPF
  • Visualización de datos y datos jerarquizados
  • Uso de plantillas
  • Validación de entradas
En este artículo se usan las siguientes tecnologías:
WPF, XAML, C#

Descarga de código disponible en: AdvancedWPFDatabinding2008_07.exe (171 KB)
Examinar el código en línea
Cuando Windows® Presentation Foundation (WPF) apareció por primera vez en la historia de .NET, la mayoría de los artículos y de las aplicaciones de demostración destacaban su espléndido motor de representación y sus capacidades 3D. Aunque son muy entretenidos, dichos ejemplos no muestran toda la eficacia de la funcionalidad real de WPF. La mayoría de nosotros no necesita crear aplicaciones con cubos de vídeo giratorios que explotan en forma de fuegos artificiales al hacer clic en ellos. La mayoría de nosotros se gana la vida mediante la creación de software para mostrar y editar grandes cantidades de datos profesionales o científicos.
La buena noticia es que WPF ofrece una excelente compatibilidad para la administración de la visualización y la edición de datos complejos. En el número de diciembre de 2007 de MSDN® Magazine, John Papa escribió "Enlace de datos en WPF" (msdn.microsoft.com/magazine/cc163299), donde se explicaban a la perfección los conceptos fundamentales del enlace de datos en WPF. En este artículo, estudiaremos escenarios de enlace de datos más avanzados, a partir de lo que John presentó en la columna de Puntos de datos mencionado. Al final, conocerá diferentes formas de implementar los requisitos del enlace de datos más habituales que se pueden encontrar en la mayoría de las aplicaciones de línea de negocio.

Enlace en código
Uno de los principales cambios que WPF presenta para los desarrolladores de aplicaciones de escritorio consiste en el amplio uso de la programación declarativa y la compatibilidad con ésta. Las interfaces de usuario y los recursos de WPF se pueden declarar mediante el lenguaje marcado de aplicaciones extensible (XAML), un lenguaje marcado basado en XML. La mayoría de las explicaciones del enlace de datos en WPF sólo muestra una forma de trabajar con los enlaces en XAML. Dado que todo lo que se pueda hacer en XAML también se puede conseguir en código, es importante que los desarrolladores profesionales de WPF descubran cómo trabajar con el enlace de datos mediante programación y de forma declarativa.
Muchas veces, resulta más útil y conveniente declarar los enlaces en XAML. A medida que los sistemas se vuelven más complejos y dinámicos, a veces, resulta más lógico trabajar con enlaces en código. Antes de avanzar, echemos un vistazo a algunas clases y métodos habituales relacionados con el enlace de datos mediante programación.
Los elementos WPF heredan los métodos SetBinding y GetBinding­Expression de FrameworkElement o Framework­ContentElement. Éstos son unos métodos muy útiles que llaman a métodos con los mismos nombres en la clase de utilidad BindingOperations. En el código que aparece a continuación, se muestra cómo usar la clase Binding­Operations para enlazar la propiedad Text de un cuadro de texto a una propiedad de otro objeto:
static void BindText(TextBox textBox, string property)
{
      DependencyProperty textProp = TextBox.TextProperty;
      if (!BindingOperations.IsDataBound(textBox, textProp))
      {
          Binding b = new Binding(property);
          BindingOperations.SetBinding(textBox, textProp, b);
      }
} 
Puede desenlazar fácilmente una propiedad mediante el código que puede encontrar aquí:
static void UnbindText(TextBox textBox)
{
    DependencyProperty textProp = TextBox.TextProperty;
    if (BindingOperations.IsDataBound(textBox, textProp))
    {
        BindingOperations.ClearBinding(textBox, textProp);
    }
}
Al cancelar el enlace, se quita también el valor enlazado de la propiedad de destino.
Al declarar el enlace de datos en XAML se ocultan algunos de los detalles subyacentes. Cuando empieza a trabajar con enlaces en código, dichos detalles comienzan a aparecer. Uno de ellos es el hecho de que la encargada de mantener la relación entre un origen y un destino de enlace es una instancia de la clase BindingExpression, en lugar de Binding. La clase Binding contiene información de alto nivel que pueden compartir varias BindingExpressions, pero el cumplimiento de un vínculo entre dos propiedades enlazadas surge de una expresión subyacente. En el código que aparece a continuación puede ver cómo se usa BindingExpression para comprobar mediante programación si se valida la propiedad Text de un cuadro de texto:
static bool IsTextValidated(TextBox textBox)
{
    DependencyProperty textProp = TextBox.TextProperty;

    var expr = textBox.GetBindingExpression(textProp);
    if (expr == null)
        return false;

    Binding b = expr.ParentBinding;
    return b.ValidationRules.Any();
} 
Dado que una clase BindingExpression no contiene información acerca de si se va a validar, deberá preguntar al enlace primario. Examinaremos las técnicas de validación de entradas más adelante.
Práctica virtual: Enlace de datos en WPF avanzado
WPF le ofrece una excelente compatibilidad para la administración de la visualización y la edición de datos complejos. Mediante relativamente pocas líneas de XAML, puede mostrar una estructura de datos jerárquica o validar la entrada del usuario. Puede practicar el enlace de datos en WPF en nuestra práctica virtual preconfigurada. Todo está instalado y preparado, incluidos los proyectos de los que se habla en el artículo. Sólo tiene que iniciar el laboratorio y usar el código conforme lee el artículo.

Práctica en el laboratorio virtual:

Trabajo con plantillas
Una interfaz de usuario eficaz presenta datos sin procesar de forma que el usuario pueda obtener de forma intuitiva información importante. Esta es la esencia de la visualización de datos. Los enlaces de datos son sólo una pieza del rompecabezas de la visualización de datos. Todos, salvo los programas WPF más triviales, necesitan un medio para presentar los datos de una forma más eficaz que mediante el enlace de una propiedad a un control con una propiedad en un objeto de datos. Los objetos de datos reales tienen muchos valores relacionados, que se deberían agregar en una representación visual unida. Por ello, WPF dispone de plantillas de datos.
La clase System.Windows.DataTemplate es sólo una forma de plantilla en WPF. Por lo general, una plantilla es como un molde que usa el marco de WPF para crear elementos visuales que ayuden a representar objetos sin representación visual intrínseca. Cuando un elemento intenta mostrar un objeto que no dispone de una representación visual intrínseca, tal como un objeto de negocios personalizado, puede indicarle al elemento cómo representar el objeto mediante un DataTemplate.
DataTemplate puede generar tantos elementos visuales como sean necesarios para mostrar el objeto de datos. Dichos elementos usan enlaces de datos para mostrar los valores de propiedad del objeto de datos. Si un elemento no sabe cómo mostrar el objeto que debe representar, se limita a llamar al método ToString y a mostrar el resultado en un TextBlock.
Imagine que tiene una única clase llamada FullName, en la que se almacena el nombre de alguien. Desea mostrar una lista de nombres y hacer que los apellidos aparezcan más destacados que el resto del nombre. Para ello, puede crear un DataTemplate en el que se describa cómo representar un objeto FullName. En el código que aparece en la figura 1, se muestra la clase FullName y el código subyacente para una ventana en la que se representará una lista de nombres.
public class FullName
{
    public string FirstName { get; set; }
    public char MiddleInitial { get; set; }
    public string LastName { get; set; }
}

public partial class WorkingWithTemplates : Window
{
    // This is the Window's constructor.
    public WorkingWithTemplates()
    {
        InitializeComponent();

        base.DataContext = new FullName[]
        {
            new FullName 
            { 
                FirstName = "Johann", 
                MiddleInitial = 'S', 
                LastName = "Bach" 
            },
            new FullName 
            { 
                FirstName = "Gustav",
                MiddleInitial = ' ',
                LastName = "Mahler" 
            },
            new FullName 
            { 
                FirstName = "Alfred", 
                MiddleInitial = 'G', 
                LastName = "Schnittke" 
            }
        };
    }
}
Como se puede ver en la figura 2, hay un ItemsControl en el archivo XAML de la ventana. Se crea una lista de elementos que el usuario no puede seleccionar ni quitar. ItemsControl tiene un DataTemplate asignado a su propiedad ItemTemplate, con el que se representa cada instancia FullName creada en el constructor de la ventana. Puede ver cómo la mayoría de los elementos TextBlock del DataTemplate tienen su propiedad Text enlazada a propiedades del objeto FullName que representan.
<!-- This displays the FullName objects. -->
<ItemsControl ItemsSource="{Binding Path=.}">
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock FontWeight="Bold" Text="{Binding LastName}" />
        <TextBlock Text=", " />
        <TextBlock Text="{Binding FirstName}" />
        <TextBlock Text=" " />
        <TextBlock Text="{Binding MiddleInitial}" />
      </StackPanel>
    </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>
Al ejecutar esta aplicación de demostración, tiene el aspecto que se muestra en la figura 3. Al usar un DataTemplate para representar el nombre, resulta fácil destacar el apellido, ya que la propiedad FontWeight de TextBlock es negrita. En este sencillo ejemplo se puede observar la relación fundamental entre el enlace de datos en WPF y las plantillas. A medida que profundicemos en este tema, combinaremos estas características para obtener formas más eficaces de visualizar objetos complejos.
Figura 3 FullNames representados mediante un DataTemplate

Trabajo con un DataContext heredado
A menos que se indique lo contrario, todos los enlaces se enlazan de forma implícita a la propiedad DataContext de un elemento. El DataContext de un elemento hace referencia a su origen de datos, por decirlo así. Hay que ser consciente de algo especial con respecto al funcionamiento de DataContext. Una vez que haya comprendido este sutil aspecto de DataContext, se simplifica en gran medida el diseño de las interfaces de usuario vinculadas a datos complejos.
No es necesario establecer la propiedad DataContext para hacer referencia a un objeto de origen de datos complejo. Si el DataContext de un elemento antecesor del árbol de elementos (técnicamente, el árbol lógico) recibe un valor para su DataContext, todos los elementos descendentes de la interfaz de usuario heredarán el valor de forma automática. En otras palabras, si el DataContext se ha establecido para hacer referencia a un objeto Foo, el DataContext de todas las ventanas harán referencia al mismo objeto Foo de forma predeterminada. Puede otorgarle fácilmente un valor DataContext diferente a cada elemento, lo que hace que todos los elementos descendientes de dicho elemento hereden ese nuevo valor DataContext. Esto es algo similar a una propiedad de ambiente en Windows Forms.
En la sección anterior, vimos cómo usar DataTemplates para crear visualizaciones de los objetos de datos. Los elementos creados por la plantilla en la figura 2 tienen sus propiedades enlazadas a las propiedades un objeto FullName. Dichos elementos están vinculados de forma implícita a su DataContext. El DataContext de los elementos creados por Data­Template hace referencia al objeto de datos para el que se usa la plantilla, tal como un objeto FullName.
No hay nada extraordinario en la herencia del valor de la propiedad DataContext. Se aprovecha la compatibilidad con las propiedades de dependencia heredadas que están integradas en WPF. Todas las propiedades de dependencia pueden ser una propiedad heredada, sólo con especificar un marcador en los metadatos proporcionados al registrar dicha propiedad con el sistema de propiedad de dependencia de WPF.
Otro ejemplo de propiedad de dependencia heredada es Font­Size (todos los elementos la tienen). Al establecer la propiedad de dependencia FontSize en una ventana, todos los elementos de dicha ventana mostrarán el texto en ese tamaño. La misma infraestructura usada para propagar el valor FontSize por el árbol de elementos es la que propaga DataContext.
Este uso del término "herencia" es diferente de su significado en el sentido orientado a los objetos, donde una subclase hereda los miembros de su clase principal. La herencia del valor de la propiedad sólo hace referencia a la propagación de valores en el árbol de elementos en tiempo de ejecución. Naturalmente, una clase puede heredar una propiedad de dependencia que pueda heredar el valor, en el sentido orientado hacia el objeto.

Trabajo con vistas de recopilación
Cuando los controles WPF se enlazan a una recopilación de datos, no se enlazan directamente a la conexión en sí. En cambio, se enlazan de forma implícita a una vista contenida de forma automática en dicha colección. La vista implementa la interfaz ICollectionView y puede ser una de las distintas implementaciones concretas, como por ejemplo, ListCollectionView.
Una vista de recopilación incluye varias responsabilidades. Realiza un seguimiento del elemento actual de la recopilación, lo que se suele traducir en el elemento activo/seleccionado en una lista de control. Las vistas de recopilación también proporcionan un medio genérico para ordenar, filtrar y agrupar los elementos de una lista. Varios controles pueden enlazar la misma vista en torno a una recopilación, por lo que todos están coordinados entre sí. En el código que aparece a continuación se muestran algunas características de ICollectionView:
// Get the default view wrapped around the list of Customers.
ICollectionView view = CollectionViewSource.GetDefaultView(allCustomers);

// Get the Customer selected in the UI.
Customer selectedCustomer = view.CurrentItem as Customer;

// Set the selected Customer in the UI.
view.MoveCurrentTo(someOtherCustomer);
Todos los controles de la lista, tales como lista desplegable, cuadro combinado y vista de lista, deben tener su propiedad IsSynchronizedWithCurrentItem configurada como verdadera, para así permanecer sincronizada con la propiedad Current-Item de la vista de la recopilación. La clase abstracta Selector define dicha propiedad. Si no se establece en verdadera, la selección de un elemento del control de lista no actualizará el CurrentItem de la vista de la recopilación y la asignación de un nuevo valor a CurrentItem no aparecerá reflejada en dicho control de lista.

Trabajo con datos jerárquicos
El mundo real está lleno de datos jerárquicos. Un cliente realiza varios pedidos, una molécula está formada por varios átomos, un departamento consiste en muchos empleados y un sistema solar contiene un conjunto de cuerpos celestes. Seguramente esté familiarizado con esta organización general/en detalle.
WPF proporciona varias formas de trabajar con estructuras de datos jerárquicas, que son adecuadas para diferentes situaciones. Las opciones se limitan esencialmente a usar muchos controles para mostrar datos o a mostrar varios niveles de la jerarquía de datos en un control. A continuación, estudiaremos ambos métodos.

Uso de muchos controles para visualizar datos XML
Una forma muy frecuente de trabajar con datos jerárquicos es hacer que un control independiente muestre cada nivel de jerarquía. Por ejemplo, imagine que tenemos un sistema que representa a los clientes, los pedidos y los detalles de pedido. En esa situación, podríamos usar un cuadro combinado para mostrar los clientes, una lista desplegable para los pedidos de los clientes seleccionados y un ItemsControl para mostrar los detalles del pedido seleccionado. Es una forma fantástica de mostrar datos jerárquicos y bastante fácil de implementar en WPF.
Según el escenario que describimos anteriormente, en la figura 4 se puede ver un ejemplo simplificado de los datos con los que puede trabajar una aplicación, incluida en el componente XmlDataProvider de WPF. Una interfaz de usuario similar a la de la figura 5 puede mostrar dichos datos. Observe cómo puede seleccionar los clientes y los pedidos, pero los detalles de los pedidos aparecen en una lista de sólo lectura. Esto tiene sentido ya que un objeto visual sólo debe poder seleccionarse si afecta al estado de la aplicación o si es editable.
<XmlDataProvider x:Key="xmlData">
  <x:XData>
    <customers >
      <customer name="Customer 1">
        <order desc="Big Order">
          <orderDetail product="Glue" quantity="21" />
          <orderDetail product="Fudge" quantity="32" />
          </order>
          <order desc="Little Order">
            <orderDetail product="Ham" quantity="1" />
            <orderDetail product="Yarn" quantity="2" />
          </order>
        </customer>
        <customer name="Customer 2">
          <order desc="First Order">
            <orderDetail product="Mousetrap" quantity="4" />
          </order>
        </customer>
      </customers>
    </x:XData>
</XmlDataProvider>
Figura 5 Una forma de mostrar los datos XML
El XAML que aparece en la figura 6 describe cómo usar esos controles para mostrar los datos jerárquicos que acabamos de ver. Esta ventana no necesita código, existe completamente en XAML.
<Grid DataContext=
    "{Binding Source={StaticResource xmlData},
    XPath=customers/customer}"
    Margin="4" 
  >
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition />
    <RowDefinition />
  </Grid.RowDefinitions>

  <!-- CUSTOMERS -->
  <DockPanel Grid.Row="0">
    <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers" />
    <ComboBox
      IsSynchronizedWithCurrentItem="True"
      ItemsSource="{Binding}"
      >
      <ComboBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding XPath=@name}" />
        </DataTemplate>
      </ComboBox.ItemTemplate>
    </ComboBox>
  </DockPanel>

  <!-- ORDERS -->
  <DockPanel Grid.Row="1">
    <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
    <ListBox
      x:Name="orderSelector" 
      DataContext="{Binding Path=CurrentItem}"
      IsSynchronizedWithCurrentItem="True" 
      ItemsSource="{Binding XPath=order}"
      >
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding XPath=@desc}" />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </DockPanel>

  <!-- ORDER DETAILS -->
  <DockPanel Grid.Row="2">
    <TextBlock DockPanel.Dock="Top" FontWeight="Bold" 
      Text="Order Details" />
    <ItemsControl 
      DataContext=
         "{Binding ElementName=orderSelector, Path=SelectedItem}"
      ItemsSource="{Binding XPath=orderDetail}">
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <TextBlock>
            <Run>Product:</Run>
            <TextBlock Text="{Binding XPath=@product}" />
            <Run>(</Run>
            <TextBlock Text="{Binding XPath=@quantity}" />
            <Run>)</Run>
          </TextBlock>
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
  </DockPanel>
</Grid>
Observe el uso extensivo de las consultas XPath cortas para indicar a WPF de dónde puede obtener los valores enlazados. La clase Binding expone una propiedad XPath a la que puede asignar cualquier consulta XPath admitida por el método XmlNode.SelectNodes. De forma subyacente, WPF usa dicho método para ejecutar consultas XPath. Lamentablemente, esto significa que, dado que XmlNode.SelectNodes no son compatibles actualmente con las funciones XPath, WPF tampoco lo es.
El cuadro combinado de clientes y la lista desplegable de pedidos se enlazan al conjunto de nodos de la consulta XPath que realiza el enlace DataContext de la cuadrícula raíz. El DataContext de la lista desplegable se devolverá de forma automática el CurrentItem de la vista de colección colocada en torno a la colección de los XmlNodes generados para el DataContext de la cuadrícula. En otras palabras, el DataContext de la lista desplegable es el Customer seleccionado. Dado que el ItemsSource de la lista está enlazado a su propio DataContext (debido a que no se especificó otro origen) y su enlace ItemsSource ejecuta una consulta XPath para recibir los elementos <order> de DataContext, ItemsSource se enlaza de forma eficaz con la lista de pedidos del cliente seleccionado.
Recuerde que al enlazar a datos XML, en realidad, crea los enlaces a los objetos creados por una llamada a XmlNode.SelectNodes. Si no tiene cuidado, puede acabar con varios controles enlazados a su XmlNodes equivalente de forma lógica, pero físicamente diferentes. Esto se debe a que cada llamada a XmlNode.SelectNodes genera un conjunto nuevo de XmlNodes, incluso aunque envíe la consulta XPath al mismo XmlNode cada vez. Se trata de una preocupación específica de los enlaces de datos XML, por lo que puede estar tranquilo al enlazar a objetos de negocios.

Uso de muchos controles para visualizar objetos de negocios
Imagine que quiere enlazar a los mismos datos del ejemplo anterior, pero éstos existen como objetos de negocios en lugar de XML. ¿Cómo cambiaría eso la forma en que enlaza a los distintos niveles de la jerarquía de datos? ¿La técnica empleada sería diferente?
En el código de la figura 7 se muestran las clases sencillas que se usan para crear objetos de negocios que almacenan los datos a los que vamos a enlazar. Estas clases conforman el mismo esquema lógico que en los datos XML que se usan en la sección anterior.
public class Customer
{
    public string Name { get; set; }
    public List<Order> Orders { get; set; }

    public override string ToString()
    {
        return this.Name;
    }
}

public class Order
{
    public string Desc { get; set; }
    public List<OrderDetail> OrderDetails { get; set; }

    public override string ToString()
    {
        return this.Desc;
    }
}

public class OrderDetail
{
    public string Product { get; set; }
    public int Quantity { get; set; }
}
Se puede ver el XAML de la ventana que muestra estos objetos en la figura 8. Es muy parecido al XAML que se puede ver en la figura 6, pero hay una serie de diferencias importantes que conviene destacar. Algo que no se puede ver en el XAML es que el constructor de la ventana crea los objetos de datos y establece DataContext, en lugar del XAML que le hace referencia como recurso. Observe que ninguno de los controles tiene establecido de forma explícita su Data­Context. Todos heredan el mismo DataContext, que es una instancia de List<Customer>.
<Grid Margin="4">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition />
    <RowDefinition />
  </Grid.RowDefinitions>

   <!-- CUSTOMERS -->
   <DockPanel Grid.Row="0">
     <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers"
     />
     <ComboBox 
       IsSynchronizedWithCurrentItem="True" 
       ItemsSource="{Binding Path=.}" 
       />
   </DockPanel>

   <!-- ORDERS -->
   <DockPanel Grid.Row="1">
     <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
     <ListBox 
       IsSynchronizedWithCurrentItem="True" 
       ItemsSource="{Binding Path=CurrentItem.Orders}" 
       />
   </DockPanel>

   <!-- ORDER DETAILS -->
   <DockPanel Grid.Row="2">
     <TextBlock DockPanel.Dock="Top" FontWeight="Bold" 
        Text="Order Details" />
     <ItemsControl
       ItemsSource="{Binding Path=CurrentItem.Orders.CurrentItem.
       OrderDetails}"
       >
       <ItemsControl.ItemTemplate>
         <DataTemplate>
           <TextBlock>
             <Run>Product:</Run>
             <TextBlock Text="{Binding Path=Product}" />
             <Run>(</Run>
             <TextBlock Text="{Binding Path=Quantity}" />
             <Run>)</Run>
           </TextBlock>
         </DataTemplate>
       </ItemsControl.ItemTemplate>
     </ItemsControl>
   </DockPanel>
</Grid>
Otra diferencia importante al enlazar a objetos de negocios en lugar de XML es que el ItemsControl que hospeda los detalles del pedido no necesita estar enlazado al SelectedItem de la lista desplegable del pedido. Dicho método era necesario en el escenario de enlace XML ya que no existe una forma genérica de hacer referencia al elemento actual de una lista cuyos elementos proceden de una consulta XPath local.
Al enlazar a objetos de negocios en lugar de XML, su enlace a niveles anidados de los elementos seleccionados resulta insignificante. El enlace de ItemsSource de ItemsControl usa esta característica al especificar CurrentItem en la ruta de enlace dos veces: una para el cliente seleccionado y otra para el pedido seleccionado. La propiedad CurrentItem es miembro de la ICollectionView subyacente que rodea el origen de datos, como ya hemos descrito en este artículo.
Hay otro punto de interés con respecto a la diferencia en el funcionamiento de los métodos XML y del objeto de negocios. Dado que el ejemplo de XML se enlaza a XmlElements, debe ofrecer DataTemplates para explicar cómo representar los clientes y los pedidos. Al enlazar a los objetos de negocios personalizados, puede evitar esta sobrecarga si invalida el método ToString de las clases Customer y Order, y permite a WPF mostrar el resultado de dicho método para esos objetos. Este truco sólo resulta suficiente al trabajar con objetos que pueden tener representaciones textuales sencillas. Podría no tener sentido usar esta técnica al trabajar con objetos de datos complejos.

Un control para visualizar una jerarquía completa
Hasta ahora, sólo hemos visto formas de mostrar datos jerárquicos al mostrar cada nivel de la jerarquía en controles independientes. Suele resultar útil y necesario mostrar todos los niveles de una estructura de datos jerárquicos en el mismo control. El ejemplo canónico de este método es el control TreeView, que ofrece compatibilidad con la visualización y el recorrido de niveles arbitrarios de datos anidados.
Puede rellenar el TreeView de WPF con elementos de una de las siguientes formas. Una opción es agregar los elementos de XAML de forma manual y la otra es crearlas a través del enlace de datos.
En el XAML que aparece a continuación puede ver cómo agregar de forma manual algunos TreeViewItems a un TreeView de XAML:
<TreeView>
  <TreeViewItem Header="Item 1">
    <TreeViewItem Header="Sub-Item 1" />
    <TreeViewItem Header="Sub-Item 2" />
  </TreeViewItem>
  <TreeViewItem Header="Item 2" />
</TreeView>
La técnica manual para crear elementos en un TreeView tiene sentido en situaciones en las que el control siempre muestra un conjunto de elementos pequeños y estáticos. Cuando necesite mostrar grandes cantidades de datos que puedan variar con el tiempo, es necesario usar un método más dinámico. Llegados a ese punto, dispone de dos opciones. Puede escribir código que modifique una estructura de datos, cree TreeViewItems basados en los objetos de datos que encuentre y los agregue al TreeView. De forma alternativa, puede aprovechar las plantillas de datos jerárquicas y dejar que WPF haga todo el trabajo.

Uso de plantillas de datos jerárquicos
Puede expresar de manera declarativa la forma en que WPF debería representar los datos jerárquicos a través de las plantillas de datos jerárquicos. La clase HierarchicalData­Template es una herramienta que cubre el vacío que existe entre la estructura de datos compleja y una representación visual de dichos datos. Es muy parecida a un DataTemplate normal, pero, además, le permite especificar de dónde proceden los elementos secundarios de un objeto de datos. Asimismo, puede proporcionar a HierarchicalDataTemplate una plantilla con la que representar dichos elementos secundarios.
Imagine que desea mostrar los datos que se presentan en la figura 7 en un control TreeView. TreeView tendría un aspecto similar al de la figura 9. Esta implementación implica el uso de dos Hierarchical­DataTemplates y de un DataTemplate.
Figura 9 Representación de una jerarquía de datos completa en un TreeView
Las dos plantillas jerárquicas muestran los objetos Customer y Order. Dado que los objetos OrderDetail no disponen de elementos secundarios, puede representarlos mediante un DataTemplate no jerárquico. La propiedad ItemTemplate del TreeView usa la plantilla para objetos de tipo Customer, ya que Customers son los objetos de datos incluidos en el nivel de raíz de TreeView. El XAML que aparece en la figura 10 muestra cómo todas las piezas de este rompecabezas encajan entre sí.
<Grid>
  <Grid.DataContext>
    <!-- 
    This sets the DataContext of the UI
    to a Customers returned by calling
    the static CreateCustomers method. 
    -->
    <ObjectDataProvider 
      xmlns:local="clr-namespace:VariousBindingExamples"
      ObjectType="{x:Type local:Customer}"
      MethodName="CreateCustomers"
      />
  </Grid.DataContext>

  <Grid.Resources>
    <!-- ORDER DETAIL TEMPLATE -->
    <DataTemplate x:Key="OrderDetailTemplate">
      <TextBlock>
        <Run>Product:</Run>
        <TextBlock Text="{Binding Path=Product}" />
        <Run>(</Run>
        <TextBlock Text="{Binding Path=Quantity}" />
        <Run>)</Run>
      </TextBlock>
    </DataTemplate>

    <!-- ORDER TEMPLATE -->
    <HierarchicalDataTemplate 
      x:Key="OrderTemplate"
      ItemsSource="{Binding Path=OrderDetails}"
      ItemTemplate="{StaticResource OrderDetailTemplate}"
      >
      <TextBlock Text="{Binding Path=Desc}" />
    </HierarchicalDataTemplate>

    <!-- CUSTOMER TEMPLATE -->
    <HierarchicalDataTemplate 
      x:Key="CustomerTemplate"
      ItemsSource="{Binding Path=Orders}"
      ItemTemplate="{StaticResource OrderTemplate}"
      >
      <TextBlock Text="{Binding Path=Name}" />
    </HierarchicalDataTemplate>
  </Grid.Resources>

  <TreeView
    ItemsSource="{Binding Path=.}"
    ItemTemplate="{StaticResource CustomerTemplate}"
    />

</Grid>
Asignamos una recopilación de objetos Customer al Data­Context de una cuadrícula que contenga el TreeView. Esto se puede hacer en XAML mediante el ObjectDataProvider, que es una manera práctica de llamar a un método desde XAML. Dado que el DataContext se hereda a lo largo del árbol de elementos, el DataContext de TreeView hace referencia a ese conjunto de objetos Customer. Por ese motivo podemos darle a la propiedad ItemsSource un enlace de "{Binding Path=.}", que es una forma de indicar que la propiedad ItemsSource está enlazada al DataContext de TreeView.
Si no asignó la propiedad ItemTemplate de TreeView, éste sólo mostrará los objetos Customer de nivel superior. Dado que WPF no sabe cómo representar un Customer, llamará a ToString en cada Customer y mostrará dicho texto para cada elemento. No tendría manera de saber que cada Customer está asociado con una lista de objetos Order y que cada Order tiene una lista de objetos OrderDetail. Dado que WPF no puede comprender por arte de magia su esquema de datos, debe explicar el esquema a WPF para que pueda representar la estructura de datos correctamente.
En la explicación de la estructura y del aspecto de los datos a WPF es donde los HierarchicalDataTemplates entran en acción. Las plantillas que se usan en la demostración contienen árboles de elementos visuales muy sencillos, principalmente TextBlocks con una pequeña cantidad de texto en ellos. En una aplicación más compleja, las plantillas podrían tener modelos 3D giratorios interactivos, imágenes, dibujos de gráficos vectoriales, UserControls complejos o cualquier contenido WPF para visualizar el objeto de datos subyacente.
Es importante tener en cuenta el orden en que se declaran las plantillas. Debe declarar una plantilla antes de hacer referencia a ella a través de la extensión StaticResource. Hay un requisito que el lector XAML impone y que se aplica a todos los recursos, no sólo a las plantillas.
Se puede hacer referencia a las plantillas mediante la extensión Dynamic­Resource, en cuyo caso el orden léxico de las declaraciones de la plantilla es irrelevante. No obstante, el uso de una referencia Dynamic­Resource, en oposición a una referencia StaticResource, conlleva cierta sobrecarga en tiempo de ejecución ya que supervisan el sistema de recursos para comprobar si se producen cambios. Dado que las plantillas no se reemplazan en tiempo de ejecución, dicha sobrecarga es innecesaria, por lo que es mejor usar referencias StaticResource y organizar adecuadamente el orden de las declaraciones de las plantillas.

Trabajo con entradas de usuario
Para la mayoría de los programas, la representación de datos es sólo la mitad del trabajo. El otro gran desafío es analizar, aceptar y rechazar los datos del usuario. En un mundo ideal, donde todos los usuarios siempre aportan datos lógicos y precisos, sería una tarea muy sencilla. No obstante, en el mundo real no es así. Los usuarios reales crean errores tipográficos, olvidan especificar los valores necesarios, escriben valores en el sitio equivocado, eliminan o agregan registros que no deberían y, por lo general, cumplen la Ley de Murphy siempre que pueden.
Es nuestro trabajo, como desarrolladores y arquitectos, luchar contra las inevitables entradas erróneas y malintencionadas que llevan a cabo los usuarios. La infraestructura de enlaces WPF es compatible con la validación de entradas. En las siguientes secciones del artículo, estudiaremos cómo usar la compatibilidad de WPF para la validación, así como la forma de mostrar al usuario mensajes de error de validación.

Validación de entradas a través de ValidationRules
La primera versión de WPF, que formaba parte de Microsoft® .NET Framework 3.0, sólo contaba con compatibilidad limitada para la validación de entradas. La clase Binding cuenta con una propiedad ValidationRules, que puede almacenar cualquier cantidad de clases derivadas de ValidationRule. Todas esas reglas pueden contener cierta lógica que intenta comprobar si el valor enlazado es válido.
En aquellos tiempos, WPF sólo incluía una subclase ValidationRule, llamada ExceptionValidationRule. Los desarrolladores podían agregar dicha regla a las ValidationRules de un enlace y ésta captaba las excepciones producidas durante las actualizaciones del origen de datos, lo que permitía a la interfaz de usuario mostrar el mensaje de error de la excepción. La utilidad de este método para la validación de entradas es cuestionable, ya que la base de una buena experiencia de usuario consiste en intentar no revelar de forma innecesaria detalles técnicos al usuario. Los mensajes de error de las excepciones de análisis de datos suelen ser demasiado técnicos para la mayoría de los usuarios, pero me estoy apartando del tema.
Imagine que tiene una clase que representa una era de tiempo, como la clase Era que hemos visto:
public class Era
{
    public DateTime StartDate { get; set; }
    public TimeSpan Duration { get; set; }
}
Si desea permitir que el usuario modifique la fecha de inicio y la duración de una era, puede usar dos controles TextBox y enlazar las propiedades Text a las propiedades de una instancia Era. Dado que el usuario puede escribir el texto que desee en un TextBox, no puede estar seguro de que el texto de entrada se pueda convertir en una instancia de DateTime o TimeSpan. En este escenario, puede usar ExceptionValidationRule para notificar errores de conversión de datos y, a continuación, mostrar dichos errores de conversión en la interfaz de usuario. El XAML que aparece en la figura 11 muestra cómo conseguirlo.
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
  <TextBox.Text>
    <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
      <Binding.ValidationRules>
        <ExceptionValidationRule />
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox 
  Grid.Row="3" 
  Text="{Binding 
         Path=Duration, 
         UpdateSourceTrigger=PropertyChanged, 
         ValidatesOnExceptions=True}" 
  />
Esos dos cuadros de texto muestran las dos formas en que se puede agregar un ExceptionValidationRule a las ValidationRules de un enlace en XAML. El cuadro de texto Start Date usa la sintaxis detallada del elemento de propiedad para agregar la regla de forma explícita. El cuadro de texto Duration usa la sintaxis abreviada al establecer la propiedad ValidatesOnExceptions del enlace en true. Los dos enlaces tienen su propiedad UpdateSourceTrigger establecida en PropertyChanged de forma que la entrada se valida cada vez que la propiedad Text del cuadro de texto recibe un valor nuevo, en lugar de esperar hasta que el control pierda el enfoque. En la figura 12 se muestra una captura de pantalla del programa.
Figura 12 ExceptionValidationRule muestra los errores de validación

Visualización de los errores de validación
Como puede ver en la figura 13, el cuadro de texto Duration contiene un valor no válido. La cadena que contiene no se puede convertir en una instancia TimeSpan. En la información del cuadro de texto aparece un mensaje de error y se puede ver un pequeño icono de error rojo a la derecha del control. Este comportamiento no se produce de forma automática, pero es muy fácil de implementar y personalizar.
<!-- 
The template which renders a TextBox 
when it contains invalid data. 
-->
<ControlTemplate x:Key="TextBoxErrorTemplate">
  <DockPanel>
    <Ellipse 
      DockPanel.Dock="Right" 
      Margin="2,0"
      ToolTip="Contains invalid data"
      Width="10" Height="10"   
      >
      <Ellipse.Fill>
        <LinearGradientBrush>
          <GradientStop Color="#11FF1111" Offset="0" />
          <GradientStop Color="#FFFF0000" Offset="1" />
        </LinearGradientBrush>
      </Ellipse.Fill>
    </Ellipse>
    <!-- 
    This placeholder occupies where the TextBox will appear. 
    -->
    <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>

<!-- 
The Style applied to both TextBox controls in the UI.
-->
<Style TargetType="TextBox">
  <Setter Property="Margin" Value="4,4,10,4" />
  <Setter 
    Property="Validation.ErrorTemplate" 
    Value="{StaticResource TextBoxErrorTemplate}" 
    />
  <Style.Triggers>
    <Trigger Property="Validation.HasError" Value="True">
      <Setter Property="ToolTip">
        <Setter.Value>
          <Binding 
            Path="(Validation.Errors)[0].ErrorContent"
            RelativeSource="{x:Static RelativeSource.Self}"
            />
        </Setter.Value>
      </Setter>
    </Trigger>
  </Style.Triggers>
</Style>
La clase estática Validation establece una relación entre un control y todos los errores de validación que contiene debido al uso de algunas propiedades adjuntas y métodos estáticos. Puede hacer referencia a esas propiedades adjuntas en XAML para crear descripciones de sólo marcado de cómo la interfaz de usuario debe presentar al usuario los errores de validación de entradas. El XAML de la figura 13 es responsable de explicar cómo se representan los mensajes de error de entradas para los dos controles TextBox.
Style en la figura 13 afecta a todas las instancias de un cuadro de texto de la interfaz de usuario. Se aplican tres parámetros a un cuadro de texto. El primer Setter afecta a la propiedad Margin del cuadro de texto. La propiedad Margin se establece como un valor que proporciona suficiente espacio para mostrar el icono de error a la derecha.
El siguiente Setter de Style asigna el ControlTemplate que se usa para representar el cuadro de texto cuando éste contiene datos no válidos. Establece la propiedad Validation.ErrorTemplate adjunta como el Control­Template declarado sobre Style. Cuando la clase Validation notifica que el cuadro de texto tiene uno o más errores de validación, el cuadro de texto se representa con esa plantilla. Aquí es donde entra en escena el icono de error rojo, tal como se muestra en la figura 12.
Style también contiene un desencadenador que supervisa la propiedad Validation.HasError adjunta en el cuadro de texto. Cuando la clase Validation establece la propiedad HasError adjunta en true para el cuadro de texto, el desencadenador de Style activa y asigna información al cuadro de texto. El contenido de la información está enlazado al mensaje de error de la excepción que se muestra al intentar analizar el texto del cuadro de texto en una instancia del tipo de datos de la propiedad de origen.

Validación de entradas a través de IDataErrorInfo
Con la llegada de Microsoft .NET Framework 3.5, la compatibilidad de WPF para la validación de entradas ha mejorado significativamente. El método Validation­Rule es de utilidad para las aplicaciones sencillas, pero las aplicaciones del mundo real se enfrentan a la complejidad de los datos y las reglas de negocio. La codificación de reglas de negocio en objetos Validation­Rule no sólo vincula dicho código a la plataforma WPF, sino que además no permite que haya lógica de negocio ahí donde debe existir: en los objetos de negocios.
Muchas aplicaciones tienen una capa de negocio, en la que la complejidad del procesamiento de reglas de negocio se incluye en un conjunto de objetos de negocios. Al compilar en Microsoft .NET Framework 3.5, puede usar la interfaz IDataErrorInfo para que WPF pregunte a los objetos de negocios si están en un estado válido o no. De esta forma, se elimina la necesidad de agregar a los objetos una lógica de negocio independiente desde la capa de negocio, y permite crear objetos de negocios independientes de la plataforma de la interfaz de usuario. Dado que la interfaz IDataErrorInfo existe desde hace años, se facilita en gran medida el uso repetido de objetos de negocios de una aplicación heredada de Windows Forms o ASP.NET.
Imagine que necesita ofrecer una validación para una era que vaya más allá de garantizar que la entrada de texto del usuario se pueda convertir al tipo de datos de la propiedad de origen. Podría tener sentido que la fecha de inicio de una era no puede estar en el futuro, ya que no tenemos información de las eras que aún no existen. Además, también podría tener sentido exigir que una era tenga una duración de al menos un milisegundo.
Estos tipos de reglas son parecidos a la idea genérica de lógica de negocio, en el sentido de que ambos son ejemplos de reglas de dominio. Es mejor implementar reglas de dominio en los objetos que almacenar su estado: objetos de dominio. El código que aparece en la figura 14 muestra la clase SmartEra, en la que se muestran los mensajes de error de validación a través de la interfaz IData­ErrorInfo.
public class SmartEra 
    : System.ComponentModel.IDataErrorInfo
{
    public DateTime StartDate { get; set; }
    public TimeSpan Duration { get; set; }

    #region IDataErrorInfo Members

    public string Error
    {
        get { return null; }
    }

    public string this[string property]
    {
        get 
        {
            string msg = null;
            switch (property)
            {
                case "StartDate":
                    if (DateTime.Now < this.StartDate)
                        msg = "Start date must be in the past.";
                    break;

                case "Duration":
                    if (this.Duration.Ticks == 0)
                        msg = "An era must have a duration.";
                    break;

                default:
                    throw new ArgumentException(
                        "Unrecognized property: " + property);
            }
            return msg;
        }
    }

    #endregion // IDataErrorInfo Members
}
El aprovechamiento de la compatibilidad para la validación de la clase SmartEra de una interfaz de usuario WPF es muy sencillo. Sólo tiene que indicarle a los enlaces que deben distinguir la interfaz IDataErrorInfo en el objeto al que están enlazados. Puede hacerlo de dos formas, tal como se puede ver en la figura 15.
<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
  <TextBox.Text>
    <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
      <Binding.ValidationRules>
        <ExceptionValidationRule />
        <DataErrorValidationRule />
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox 
  Grid.Row="3" 
  Text="{Binding 
         Path=Duration, 
         UpdateSourceTrigger=PropertyChanged, 
         ValidatesOnDataErrors=True,
         ValidatesOnExceptions=True}" 
  />
De forma similar al proceso de agregar la ExceptionValidationRule de forma explícita o implícita a una colección ValidationRules, puede agregar DataErrorValidationRule directamente a ValidationRules de un enlace o, simplemente, establecer la propiedad ValidatesOnDataErrors en true. Ambos métodos tienen como resultado el mismo efecto final: el sistema de enlaces realiza consultas a la interfaz IDataErrorInfo del origen de datos para buscar errores de validación.

Conclusión
Hay un motivo por el que muchos desarrolladores dicen que su característica preferida de WPF es su compatibilidad enriquecida para el enlace de datos. El uso de enlaces en WPF es tan eficaz y persuasivo que requiere que muchos desarrolladores de software vuelvan a valorar su pensamiento acerca de la relación entre datos e interfaces de usuario. Muchas de las características principales de WPF funcionan conjuntamente para admitir escenarios complejos enlazados a datos, tales como las plantillas, los estilos y las propiedades adjuntas.
Con relativamente pocas líneas de XAML, puede expresar cómo desea mostrar una estructura de datos jerárquica y cómo validar las entradas de usuario. En las situaciones avanzadas, puede aprovechar toda la efectividad del sistema de enlaces al obtener acceso a éste mediante programación. Al disponer de una infraestructura tan eficaz, el objetivo eterno de crear mejores experiencias de usuario y visualizaciones de datos atractivas finalmente está al alcance de los desarrolladores que intentan crear aplicaciones empresariales.

Josh Smith está a favor de usar WPF para crear fantásticas experiencias de usuario. Se le otorgó el título de MVP de Microsoft por su trabajo en la comunidad WPF. Josh trabaja para Infragistics en el grupo Experience Design. Cuando no está frente al PC, se dedica a tocar el piano, a leer temas de historia y a explorar Nueva York con su novia. Puede visitar el blog de Josh en joshsmithonwpf.wordpress.com.

Page view tracker