Mejorando la experiencia de usuario en el control DataForm de Silverlight (1)

En el artículo dedicado a la edición de datos con plantillas en el DataForm, apuntábamos la posibilidad de mejorar la interfaz de usuario para este control, debido a que los controles de edición que se proporcionan por defecto pueden no ser los más indicados en todos los escenarios a desarrollar.

Las características de los valores a manipular hacen que en ciertas situaciones, un TextBox, por ejemplo, no resulte suficiente si además de escribir el valor del campo queremos ofrecer al usuario una lista de posibles valores para evitar que tenga que teclearlos.

Por tales razones, a través de las diversas entregas que componen este artículo, nos dedicaremos a intentar optimizar la experiencia de usuario en algunos aspectos susceptibles de ser mejorados con respecto al comportamiento predeterminado que ofrece el DataForm. El código fuente del ejemplo que desarrollaremos puede descargarse aquí.

 

Elaboración de la fuente de datos

Al igual que en otros artículos, el primer paso fundamental consiste en preparar un conjunto de datos de prueba. Como en otras ocasiones, nuestro punto de partida será la base de datos Chinook, que podemos descargar desde CodePlex.

Empleando, con algunas variaciones, los datos de las tablas Invoice y Customer pertenecientes a la base de datos Chinook, crearemos sendas tablas de igual nombre en una nueva base de datos que llamaremos MusicaGest, utilizando las sentencias SQL del siguiente script.

CREATE DATABASE MusicaGest
GO

USE MusicaGest
GO

CREATE TABLE Invoice
(
    InvoiceId int NOT NULL,
    CustomerId int NOT NULL,
    InvoiceDate datetime NULL,
    BillingAddress varchar(70) NULL,
    BillingCity varchar(50) NULL,
    BillingState varchar(50) NULL,
    BillingCountry varchar(50) NULL,
    Region varchar(50) NULL,
    Total numeric(10, 2) NULL,
    FirstInvoice bit NULL,
    CONSTRAINT PK_Invoice PRIMARY KEY CLUSTERED (InvoiceId ASC)
)
GO

INSERT INTO Invoice
SELECT InvoiceId,CustomerId,InvoiceDate,
BillingAddress,BillingCity,BillingState,BillingCountry,
CASE
    WHEN BillingCountry IN ('Austria','Belgium','Czech Republic','Denmark','Finland','France',
        'Germany','Hungary','Ireland','Italy','Netherlands','Norway','Poland','Portugal','Spain',
        'Sweden','United Kingdom') THEN 'Europe'
    WHEN BillingCountry IN ('Argentina','Brazil','Chile') THEN 'South America'
    WHEN BillingCountry IN ('Canada','USA') THEN 'North America'
    WHEN BillingCountry IN ('Australia') THEN 'Oceania'
    WHEN BillingCountry IN ('India') THEN 'Asia'
END,
Total,
(CustomerId % 2)
FROM Chinook.dbo.Invoice
GO

CREATE TABLE Customer
(
    CustomerId int NOT NULL,
    Name varchar(61) NOT NULL,
    CONSTRAINT PK_Customer PRIMARY KEY CLUSTERED (CustomerId ASC)
)
GO

INSERT INTO Customer
SELECT CustomerId, FirstName + ' ' + LastName AS CustomerName 
FROM Chinook.dbo.Customer
GO

 

Creación del proyecto en Visual Studio 2010

De igual forma que en otros artículos dedicados al DataForm, iniciaremos Visual Studio 2010 y crearemos un nuevo proyecto de tipo Silverlight con el nombre DataFormUX, en el que activaremos WCF RIA Services.

Los primeros pasos que daremos en el desarrollo de este proyecto serán la creación de un modelo de datos (ADO .NET Data Model) con el nombre MusicaGestModel, al que añadiremos las tablas Invoice y Customer, que serán convertidas en entidades dentro del modelo. También agregaremos un servicio de dominio (Domain Service) con el nombre MusicaGestDomainService que contendrá las operaciones de manipulación de dichas entidades. Consulte el lector el siguiente artículo para un mayor detalle acerca de la creación del modelo de datos y del servicio de dominio.

 

El formulario de datos

Nuestro siguiente paso consistirá en escribir, dentro de la página MainPage.xaml, el código necesario para crear un DataForm y su fuente de datos correspondiente, representada por un control DomainDataSource.

Aprovecharemos igualmente para incluir sendas plantillas ReadOnlyTemplate y EditTemplate, con las que respectivamente presentaremos y editaremos los elementos de la colección de entidades conectada al formulario de datos.

<UserControl x:Class="DataFormUX.MainPage" xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices"
xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit" xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" xmlns:domainctx="clr-namespace:DataFormUX.Web"
.... <UserControl.Resources> <Style TargetType="TextBox" > <Setter Property="HorizontalAlignment" Value="Left" /> </Style> </UserControl.Resources> <!--....--> <riaControls:DomainDataSource x:Name="ddsInvoices" QueryName="GetInvoices"> <riaControls:DomainDataSource.DomainContext> <domainctx:MusicaGestDomainContext /> </riaControls:DomainDataSource.DomainContext> </riaControls:DomainDataSource> <!--....--> <toolkit:DataForm x:Name="frmInvoices" Margin="10" ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}" AutoEdit="False" AutoCommit="False"> <toolkit:DataForm.ReadOnlyTemplate> <DataTemplate> <StackPanel> <toolkit:DataField Label="Código factura:" Description="Número identificador de la factura"> <TextBlock Text="{Binding Path=InvoiceId, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="Código cliente:" Description="Identificador del cliente"> <TextBlock Text="{Binding Path=CustomerId, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="Fecha:"> <TextBlock Text="{Binding Path=InvoiceDate, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="Dirección:"> <TextBlock Text="{Binding Path=BillingAddress, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="Ciudad:"> <TextBlock Text="{Binding Path=BillingCity, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="Estado o Provincia:"> <TextBlock Text="{Binding Path=BillingState, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="País:"> <TextBlock Text="{Binding Path=BillingCountry, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="Region:"> <TextBlock Text="{Binding Path=Region, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="Importe:"> <TextBlock Text="{Binding Path=Total, Mode=OneWay}" /> </toolkit:DataField> <toolkit:DataField Label="Primera factura?:"> <TextBlock Text="{Binding Path=FirstInvoice, Mode=OneWay}" /> </toolkit:DataField> </StackPanel> </DataTemplate> </toolkit:DataForm.ReadOnlyTemplate> <toolkit:DataForm.EditTemplate> <DataTemplate> <StackPanel> <toolkit:DataField Label="Código factura:"> <TextBlock Text="{Binding Path=InvoiceId, Mode=TwoWay}" Width="40" HorizontalAlignment="Left" /> </toolkit:DataField> <toolkit:DataField Label="Código Cliente:"> <TextBox Text="{Binding Path=CustomerId, Mode=TwoWay}" Width="40" /> </toolkit:DataField> <toolkit:DataField Label="Fecha:"> <sdk:DatePicker SelectedDate="{Binding Path=InvoiceDate, Mode=TwoWay}" Width="110" HorizontalAlignment="Left" /> </toolkit:DataField> <toolkit:DataField Label="Dirección:"> <TextBox Text="{Binding Path=BillingAddress, Mode=TwoWay}" Width="150" /> </toolkit:DataField> <toolkit:DataField Label="Ciudad:"> <TextBox Text="{Binding Path=BillingCity, Mode=TwoWay}" Width="120" /> </toolkit:DataField> <toolkit:DataField Label="Estado o Provincia:"> <TextBox Text="{Binding Path=BillingState, Mode=TwoWay}" Width="100" /> </toolkit:DataField> <toolkit:DataField Label="País:"> <TextBox Text="{Binding Path=BillingCountry, Mode=TwoWay}" Width="80" /> </toolkit:DataField> <toolkit:DataField Label="Región:"> <TextBox Text="{Binding Path=Region, Mode=TwoWay}" Width="100" /> </toolkit:DataField> <toolkit:DataField Label="Importe:"> <TextBox Text="{Binding Path=Total, Mode=TwoWay}" Width="50" /> </toolkit:DataField> <toolkit:DataField Label="Primera factura?:"> <CheckBox IsChecked="{Binding Path=FirstInvoice, Mode=TwoWay}" /> </toolkit:DataField> </StackPanel> </DataTemplate> </toolkit:DataForm.EditTemplate> </toolkit:DataForm>

 

Comenzando con la mejora

Llegados a este punto habremos logrado un formulario de datos con una interfaz de usuario muy similar a la que el DataForm hubiera generado automáticamente, pero en la que el control TextBox queda algo escaso de funcionalidad para la edición de los valores de ciertos campos.

 

A partir de la segunda entrega de este artículo propondremos soluciones puntuales para cada uno de los campos que consideremos mejorables, sustituyendo el control TextBox inicial por otro más adecuado para los valores que el usuario deba editar. Sin embargo, antes de entrar en el desarrollo de tales funcionalidades, modificaremos en primer lugar la plantilla ReadOnlyTemplate, de forma que, por un lado, además del código del cliente (campo CustomerId) también se visualice el nombre (campo CustomerName de la tabla Customers); mientras que por otra parte, haremos que el campo InvoiceDate muestre la fecha con un formato más adecuado. Toda esta funcionalidad la lograremos utilizando convertidores de tipo, como vimos en los artículos dedicados a las plantillas de presentación en el DataForm y uso de convertidores en el DataGrid, publicados anteriormente en este blog.

Primeramente añadiremos a la página XAML un nuevo control DomainDataSource que obtenga la colección de entidades Customer.

<!--....-->
<riaControls:DomainDataSource x:Name="ddsCustomers" QueryName="GetCustomers">
    <riaControls:DomainDataSource.DomainContext>
        <domainctx:MusicaGestDomainContext />
    </riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>

<StackPanel Background="SkyBlue">
    <toolkit:DataForm x:Name="frmInvoices"
....

 

El código de los convertidores lo escribiremos en un archivo con el nombre Convertidores.cs, que agregaremos al proyecto Silverlight de la solución que estamos desarrollando. Dentro de este archivo crearemos las clases FechaConvertidor y CustomerConvertidor, que utilizaremos respectivamente para formatear la fecha de la factura y obtener el nombre del cliente.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Data;

namespace DataFormUX.Web
{
    public class FechaConvertidor : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return ((DateTime)value).ToString("dd \de MMMM \de yyyy; HH:mm");
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return null;
        }
    }

    public class CustomerConvertidor : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            MusicaGestDomainContext oDomainContext = (MusicaGestDomainContext)((MainPage)App.Current.RootVisual).ddsCustomers.DomainContext;
            Customer oCustomer;

            if (oDomainContext.Customers.Count > 0)
            {
                oCustomer = (from oCust in oDomainContext.Customers
                             where oCust.CustomerId == (int)value
                             select oCust).Single();
            }
            else
            {
                oCustomer = new Customer() { CustomerId = -1, CustomerName = "--" };
            }
            
            return oCustomer.CustomerName;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return null;
        }
    }
}

 

Centrándonos en la clase CustomerConvertidor, la técnica que hemos empleado para obtener el nombre del cliente consiste en tomar el objeto App, que representa a la aplicación en ejecución (deriva de la clase Application), y a través de sus propiedades, acceder al DomainDataSource ddsCustomers situado en el objeto MainPage, que deriva de UserControl y representa a la página XAML en la que construimos la interfaz de usuario.

Desde ddsCustomers obtenemos el contexto de dominio (propiedad DomainContext), y comprobamos que la colección de entidades contiene elementos interrogando a su propiedad Count, ya que hemos detectado que, aleatoriamente, este DomainDataSource tarda un cierto tiempo en cargarse, y en algunas ocasiones, a la hora de mostrar la primera entidad en el DataForm, todavía se encuentra vacío.

En el caso de que ddsCustomers contenga valores, para obtener el objeto Customer adecuado utilizamos una expresión LINQ, en la que empleamos el parámetro value del método Convert, que contiene el valor del campo CustomerId. Si ddsCustomers no tiene elementos, creamos un objeto Customer sin nombre. Como último paso devolvemos la propiedad CustomerName del objeto Customer como valor de retorno, la cual será visualizada en un campo de la plantilla ReadOnlyTemplate del DataForm. Previamente, tendremos que haber instanciado los convertidores en la zona de recursos de la página XAML.

<UserControl.Resources>
    <domainctx:FechaConvertidor x:Key="cnvFechaConvertidor" />
    <domainctx:CustomerConvertidor x:Key="cnvCustomerConvertidor" />
    <!--....-->
</UserControl.Resources>

<toolkit:DataForm x:Name="frmInvoices" 
....
<toolkit:DataForm.ReadOnlyTemplate>
<!--....-->
<toolkit:DataField Label="Nombre cliente:" Description="Nombre del cliente">
    <TextBlock Text="{Binding Path=CustomerId, Mode=OneWay, Converter={StaticResource cnvCustomerConvertidor}}" />
</toolkit:DataField>

<toolkit:DataField Label="Fecha:">
    <TextBlock Text="{Binding Path=InvoiceDate, Mode=OneWay, Converter={StaticResource cnvFechaConvertidor}}" />
</toolkit:DataField>
<!--....-->

 

Después de haber realizado estas modificaciones, el DataForm en modo de lectura mostrará las mejoras aplicadas, como vemos en la siguiente figura.

 

Y con esta mejora sobre la plantilla de lectura del DataForm termina la primera parte del artículo. En la siguiente entrega comenzaremos con las optimizaciones a nivel de la plantilla de edición del formulario, a través del uso de controles alternativos al TextBox. 

2 Comentarios

  1. anonymous

    En la primera parte de este artículo, sentamos las bases para empezar a trabajar en la optimización

  2. cynthiay

    Hola LuisMi, estoy siguiendo este ejemplo, y ya puse en el xaml de una página de mi proyecto:
    xmlns:toolkit=»http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit»
    pero cuando agrego:

Deja un comentario

Tema creado por Anders Norén