January 2011 - Artículos

En esta tercera entrega del artículo seguiremos con nuestra tarea de editar los campos del DataForm usando dos controles sobradamente conocidos por la gran mayoría de desarrolladores: ComboBox y RadioButton. El código fuente del proyecto está disponible aquí.

 

ComboBox. Seleccionando el valor del campo en una lista desplegable

Continuamos con las operaciones de selección en listas de valores de la mano de uno de los grandes clásicos entre los controles de usuario: ComboBox.

Del mismo modo que en los anteriores controles, el control ComboBox también necesita una colección de elementos para mostrar en su lista desplegable; pero en este caso, en lugar de tratarse de una colección simple de valores, emplearemos la colección de entidades de tipo Customer obtenidas a partir del control DomainDataSource ddsCustomers, que anteriormente añadimos a la página MainPage.xaml.

En primer lugar trasladaremos el control ddsCustomers al bloque de recursos de la página XAML, operación necesaria para que el ComboBox tenga acceso a los datos que ddsCustomers proporciona. Adicionalmente, ordenaremos por la propiedad CustomerName de los objetos Customer el resultado devuelto por este DomainDataSource, utilizando para ello una etiqueta SortDescriptors.

A continuación añadiremos un ComboBox a la plantilla EditTemplate del DataForm, dentro del DataField reservado a la información del cliente de la factura. El código XAML que emplearemos será el siguiente.

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

        <riaControls:DomainDataSource.SortDescriptors>
            <riaControls:SortDescriptor PropertyPath="CustomerName" />
        </riaControls:DomainDataSource.SortDescriptors>
    </riaControls:DomainDataSource>
</UserControl.Resources>
<!--....-->
<toolkit:DataForm.EditTemplate>
<!--....-->
    <toolkit:DataField Label="Cliente:">
        <StackPanel Orientation="Horizontal">
            <TextBox Text="{Binding Path=CustomerId, Mode=TwoWay}" 
                     Width="40" 
                     IsEnabled="False" />

            <ComboBox x:Name="cboCustomers" 
                      Width="200" 
                      Margin="5,0,0,0"
                      ItemsSource="{Binding Source={StaticResource ddsCustomers}, Path=Data}"
                      DisplayMemberPath="CustomerName"
                      SelectedValuePath="CustomerId" />
        </StackPanel>
    </toolkit:DataField>

Respecto a la configuración de las propiedades del ComboBox, a la propiedad ItemsSource le asignaremos una expresión de enlace a datos cuya fuente sea el DomainDataSource que hemos situado como recurso; mientras que las propiedades DisplayMemberPath y SelectedValuePath contendrán, respectivamente, los valores CustomerName y CustomerId, que corresponden a los nombres de las propiedades de los objetos Customer contenidos en la colección de entidades asignada al ComboBox. Con DisplayMemberPath le indicamos al ComboBox la propiedad a utilizar para los valores a mostrar en la lista, y SelectedValuePath es la propiedad que el control utilizará internamente para informar al DataForm del identificador de cliente seleccionado para la factura.

En el estado actual de la aplicación, cada vez que hagamos clic en el botón de edición del DataForm, el ComboBox siempre mostrará, para CustomerName, el primer valor de la colección, sin mantener la adecuada correspondencia con el valor de CustomerId.

Para corregir este comportamiento erróneo, en primer lugar, a través de la propiedad x:Name, asignaremos un nombre al control TextBox que contiene el valor del campo CustomerId.

<TextBox x:Name="txtCustomerId"
         Text="{Binding Path=CustomerId, Mode=TwoWay}"
         Width="40"
         IsEnabled="False" />

Seguidamente escribiremos en el manipulador del evento ContentLoaded del DataForm un bloque de código en el que obtendremos la instancia del mencionado TextBox y el contexto de dominio del control ddsCustomers. Ambos objetos nos permitirán construir una expresión LINQ, que tendrá como resultado el objeto Customer cuya propiedad CustomerId corresponde  al cliente actual de la factura. Como último paso de este proceso recuperaremos la instancia del ComboBox y asignaremos a su propiedad SelectedItem el objeto Customer obtenido. 

private void frmInvoices_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
{
    if (e.Mode == DataFormMode.Edit)
    {
        //....
        TextBox txtCustomerId = (TextBox)this.frmInvoices.FindNameInContent("txtCustomerId");

        MusicaGestDomainContext oDomCtxCustomers = (MusicaGestDomainContext)this.ddsCustomers.DomainContext;

        Customer oCustomerActual = (from oCustomer in oDomCtxCustomers.Customers
                         where oCustomer.CustomerId == int.Parse(txtCustomerId.Text)
                         select oCustomer).Single();

        ComboBox cboCustomers = (ComboBox)this.frmInvoices.FindNameInContent("cboCustomers");

        cboCustomers.SelectedItem = oCustomerActual;
    }
}

A partir de ahora, el elemento visualizado por el ComboBox sí corresponderá con el valor adecuado cada vez que entremos en el modo de edición del formulario de datos.

 

No obstante, el comportamiento del ComboBox dentro del formulario de datos sigue sin ser adecuado. Expliquemos esto con más detalle: cuando situados en modo de edición, el usuario modifica un campo del DataForm, éste detecta el cambio habilitando el botón OK para poder hacer clic en él y confirmar las modificaciones. Esta situación no se está produciendo actualmente para el ComboBox, ya que la selección de un nuevo valor en dicho control no hace que se active el botón OK.

Para conseguir esta funcionalidad vamos a escribir un bloque de código en el manipulador del evento SelectionChanged del ComboBox. Dentro de dicho evento recuperaremos las instancias de los controles ComboBox y TextBox, asignando a este último el valor seleccionado en la lista desplegable, lo que producirá la activación del botón OK del DataForm. 

<ComboBox x:Name="cboCustomers" 
....
          SelectionChanged="cboCustomers_SelectionChanged" />

 

private void cboCustomers_SelectionChanged(object sender, SelectionChangedEventArgs e) { ComboBox cboCustomers = (ComboBox)this.frmInvoices.FindNameInContent("cboCustomers"); if (cboCustomers != null) { string sCustomerIdComboBox = cboCustomers.SelectedValue.ToString(); TextBox txtCustomerId = (TextBox)this.frmInvoices.FindNameInContent("txtCustomerId"); txtCustomerId.Text = sCustomerIdComboBox; } }

 

Empleando esta técnica ya hemos conseguido que el control ComboBox trabaje de manera coordinada con la maquinaria del DataForm. Sin embargo, demos otra vuelta de tuerca a esta situación: supongamos que en la plantilla EditTemplate del formulario prescindimos del TextBox txtCustomerId, ¿cómo conseguimos entonces que el DataForm se percate de los cambios de selección que hagamos en el ComboBox?

La solución pasa por manipular la propiedad DataForm.CurrentItem, la cual contiene el objeto que representa a la entidad actualmente en edición en el formulario de datos; en nuestro caso un objeto Invoice.

Primeramente escribiremos el siguiente bloque de código en el evento DataForm.ContentLoaded, que nos permitirá establecer el valor correcto en el ComboBox al entrar en el modo de edición del formulario.

private void frmInvoices_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
{
    if (e.Mode == DataFormMode.Edit)
    {
        //....
        Invoice oInvoiceActual = (Invoice)this.frmInvoices.CurrentItem;

        MusicaGestDomainContext oDomCtxCustomers = (MusicaGestDomainContext)this.ddsCustomers.DomainContext;

        Customer oCustomerActual = (from oCustomer in oDomCtxCustomers.Customers
                                    where oCustomer.CustomerId == oInvoiceActual.CustomerId
                                    select oCustomer).Single();

        ComboBox cboCustomers = (ComboBox)this.frmInvoices.FindNameInContent("cboCustomers");

        cboCustomers.SelectedItem = oCustomerActual;
    }
}

A continuación procederemos de forma similar en el evento ComboBox.SelectionChanged; esta vez  para asignar el valor seleccionado en el ComboBox a la propiedad Invoice.CustomerId de la entidad actualmente en edición. 

private void cboCustomers_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ComboBox cboCustomers = (ComboBox)this.frmInvoices.FindNameInContent("cboCustomers");

    if (cboCustomers != null)
    {
        int nCustomerIdComboBox = (int)cboCustomers.SelectedValue;
        Invoice oInvoiceActual = (Invoice)this.frmInvoices.CurrentItem;
        oInvoiceActual.CustomerId = nCustomerIdComboBox;
    }
}

Como resultado, al editar ahora la entidad en el DataForm, en el campo del cliente sólo aparecerá el ComboBox.

 

 

RadioButton. Selección de opciones autoexcluyentes

RadioButton es un control que representa una alternativa más en la manera en que podemos editar/seleccionar los valores para un campo en el DataForm, ya que mediante un conjunto de controles de este tipo, podemos ofrecer al usuario varias opciones entre las cuales elegir una para asignar como valor al campo del formulario.

Vamos a emplear este control para editar el campo Region de la tabla Invoice. Dicho campo tiene cinco valores posibles en esta tabla: Asia, Europe, North America, South America y Oceania, por lo que añadiremos el mismo número de controles RadioButton a nuestra página, dentro de la plantilla EditTemplate del DataForm, usando el siguiente bloque de código XAML.

<toolkit:DataField Label="Región:">
    <StackPanel>
        <RadioButton x:Name="rbtAsia" Content="Asia" GroupName="Region" Checked="rbtRegion_Checked" />
        <RadioButton x:Name="rbtEurope" Content="Europe" GroupName="Region" Checked="rbtRegion_Checked" />
        <RadioButton x:Name="rbtNorthAmerica" Content="North America" GroupName="Region" Checked="rbtRegion_Checked" />
        <RadioButton x:Name="rbtSouthAmerica" Content="South America" GroupName="Region" Checked="rbtRegion_Checked" />
        <RadioButton x:Name="rbtOceania" Content="Oceania" GroupName="Region" Checked="rbtRegion_Checked" />
    </StackPanel>
</toolkit:DataField>

Para que el formulario considere a todos estos controles como pertenecientes a un mismo grupo, de forma que solamente uno de ellos pueda estar seleccionado a la vez, hemos asignado el mismo valor a su propiedad GroupName.

A continuación necesitamos codificar la lógica para que el RadioButton adecuado quede marcado cuando entramos en modo de edición de una entidad, para lo cual añadiremos el siguiente código al evento ContentLoaded del formulario de datos, en el que una vez obtenida la instancia de la entidad a editar, y basándonos en el valor de su propiedad Region, obtendremos del formulario el RadioButton correspondiente, para marcarlo mediante su propiedad IsChecked. Nótese que puesto que algunos nombres de región están formados por dos palabras, para componer el nombre del RadioButton, eliminamos los espacios en blanco mediante el método string.Replace.

private void frmInvoices_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
{
    if (e.Mode == DataFormMode.Edit)
    {
        //....
        Invoice oInvoiceActual = (Invoice)this.frmInvoices.CurrentItem;
        //....
        string sRegion = oInvoiceActual.Region;
        RadioButton rbtRegion = (RadioButton)this.frmInvoices.FindNameInContent("rbt" + sRegion.Replace(" ", string.Empty));
        rbtRegion.IsChecked = true;
    }
}

 

La otra parte de la funcionalidad que debemos implementar para estos controles corresponde al cambio en la selección del RadioButton mientras estamos en modo de edición, ya que debemos actualizar la propiedad Region de la entidad Invoice que estamos editando con el valor del RadioButton seleccionado. Con tal finalidad, en la declaración de los controles en el código XAML hemos incluido la llamada al método rbtRegion_Checked, que actuará como manejador del evento Checked. La labor de dicho método consiste en comprobar si el RadioButton pulsado es distinto del valor de la propiedad Region de la entidad en edición; en caso afirmativo actualizamos el valor de la propiedad.

private void rbtRegion_Checked(object sender, RoutedEventArgs e)
{
    Invoice oInvoiceActual = (Invoice)this.frmInvoices.CurrentItem;
    RadioButton rbtRegion = (RadioButton)sender;

    if (oInvoiceActual.Region != rbtRegion.Content.ToString())
    {
        oInvoiceActual.Region = rbtRegion.Content.ToString();
    }
}

Y después de esta demostración de las capacidades de edición del control RadioButton en el formulario de datos concluimos este artículo, en el cual hemos abordado una manera de potenciar las características de edición en el control DataForm, a través del uso de controles alternativos para los campos, en reemplazo del habitual TextBox, utilizado usualmente como control de edición por defecto. Espero que os resulte de interés.

En la primera parte de este artículo, sentamos las bases para empezar a trabajar en la optimización de la interfaz de usuario del DataForm construyendo el proyecto en Visual Studio 2010, la fuente de datos, y el formulario con una funcionalidad básica para las plantillas de lectura y edición. En esta segunda entrega será cuando realmente comencemos con el proceso de mejora sobre los controles de edición.

 

NumericUpDown para valores numéricos

Iniciamos nuestro periplo de optimizaciones por el campo Total del formulario. Se trata de un campo de tipo numérico que admite decimales, por lo que revisando la Barra de herramientas de Visual Studio 2010, en busca de un control más adecuado para tratar estos valores, encontramos que NumericUpDown se adapta como un guante a este propósito.

Todo lo que tenemos que hacer es añadir a la plantilla de edición del DataForm un nuevo DataField que contenga un NumericUpDown. A la propiedad Value de este control le asignaremos la expresión de enlace a datos que muestra el valor del campo. Adicionalmente, configuraremos el control para que admita decimales, el valor a incrementar cada vez que hagamos clic en los botones de aumentar/disminuir, y la alineación horizontal.

<toolkit:DataField Label="Importe:">
    <toolkit:NumericUpDown Value="{Binding Path=Total, Mode=TwoWay}" 
                           DecimalPlaces="2" Width="50" Increment="0.5" 
                           HorizontalAlignment="Left" />
</toolkit:DataField>

 

 

DatePicket y TimePicker. Combinando controles para editar campos de tipo datetime

Cuando el control DataForm construye su interfaz de usuario predeterminada, genera controles DatePicker para editar los campos de tipo datetime. Esto generalmente funciona bien para la mayoría de las situaciones, pero ¿qué ocurre si necesitamos editar la parte horaria correspondiente a este tipo de campo?

Una posible solución consiste en utilizar un control TimePicker, el cual nos permitirá editar esta información. Por lo tanto, añadiremos una copia del mismo a la plantilla EditTemplate del formulario de datos, situándolo al lado del control DatePicker ya existente, y encerrando ambos en un StackPanel.

La propiedad utilizada por TimePicker para visualizar la hora es Value, a la que asignaremos la expresión de enlace a datos que la unirá con el campo InvoiceDate. En ambos controles, DatePicker y TimePicker, dicha expresión de enlace es igual, siendo la maquinaría interna de cada control la encargada de editar la parte (fecha u hora) que le corresponda.

<toolkit:DataField Label="Fecha:">
    <StackPanel Orientation="Horizontal">
        <sdk:DatePicker SelectedDate="{Binding Path=InvoiceDate, Mode=TwoWay}" 
                        Width="110" />

        <toolkit:TimePicker Value="{Binding Path=InvoiceDate, Mode=TwoWay}" 
                            Width="110" />
    </StackPanel>
</toolkit:DataField>

Para modificar la parte de fecha del campo InvoiceDate con DatePicker, podemos editar directamente la caja de texto que contiene el valor de fecha, o hacer clic en el icono de este control que despliega el calendario.

Respecto a la edición de la parte horaria del campo con TimePicker, podemos igualmente, editar la caja de texto de este control escribiendo directamente el número, mediante las teclas de flecha arriba/abajo, o bien podemos hacer clic en el icono con forma de reloj, para desplegar una lista de horas.

 

AutoCompleteBox. Editar y seleccionar valores de una lista dinámica

En ciertas tablas de una base de datos pueden existir columnas cuyos valores se repiten a lo largo de los registros que componen la tabla, existiendo, además, la certeza de que en los nuevos registros a incorporar, tales valores se volverán a repetir, como es el caso del campo BillingCity de nuestra tabla de ejemplo Invoice.

Para ayudar al usuario en la introducción del contenido para este campo vamos a recurrir al control AutoCompleteBox, el cual, aparte de proporcionar la funcionalidad de una caja de texto, muestra de forma dinámica una lista desplegable de sugerencias con los valores más parecidos al contenido que la caja tenga en cada momento.

 

El punto principal en la configuración de este control radica en la confección y asignación a su propiedad ItemsSource de la mencionada lista de valores con la que lo alimentamos. Existen diversas técnicas para llevar a cabo esta tarea, algunas de las cuales explicaremos en los próximos apartados.

En primer lugar añadiremos a la plantilla EditTemplate del DataForm una copia de este control. La propiedad Text, al igual que en el TextBox, es la encargada de contener el valor de la caja de texto, por lo que le asignaremos la expresión de enlace a datos que obtenga el valor de la propiedad correspondiente a la entidad de la colección.

<toolkit:DataField Label="Ciudad:">
    <sdk:AutoCompleteBox x:Name="acbBillingCity" 
                         Text="{Binding Path=BillingCity, Mode=TwoWay}" 
                         Width="120" 
                         HorizontalAlignment="Left" />
</toolkit:DataField>

 

AutoCompleteBox. Creación de la lista utilizando la colección de entidades

Cada vez que editemos un elemento de la colección de entidades asignada al DataForm, se activará la plantilla EditTemplate, cargando la entidad actual en los controles del formulario de datos.

Tal acción desencadenará el evento DataForm.ContentLoaded, en cuyo código comprobaremos el modo de edición actualmente establecido en el formulario interrogando a la propiedad Mode (tipo enumerado DataFormMode) del parámetro DataFormContentLoadEventArgs que recibe el evento. En el caso de que su valor sea Edit, obtendremos del DomainDataSource el objeto que representa al contexto de dominio (MusicaGestDomainContext), y mediante su colección de entidades Invoices, empleando una expresión de LINQ, obtendremos todos los valores distintos correspondientes a la propiedad BillingCity de las entidades. A continuación recuperaremos el control AutoCompleteBox añadido al formulario usando el método DataForm.FindNameInContent, asignando a la propiedad ItemsSource la lista de valores obtenida.

<toolkit:DataForm x:Name="frmInvoices" . . . .
                  ContentLoaded="frmInvoices_ContentLoaded">

 

using ControlesDataForm.Web;
//....
private void frmInvoices_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
{
    if (e.Mode == DataFormMode.Edit)
    {
        MusicaGestDomainContext oDomainContext = (MusicaGestDomainContext)this.ddsInvoices.DomainContext;
        var oConsulta = (from oInvoice in oDomainContext.Invoices
                         select oInvoice.BillingCity).Distinct();
        AutoCompleteBox acbBillingCity = (AutoCompleteBox)this.frmInvoices.FindNameInContent("acbBillingCity");
        acbBillingCity.ItemsSource = oConsulta;
    }
}

En tiempo de ejecución, al comenzar a teclear un valor dentro de este control, se abrirá debajo del mismo una lista desplegable compuesta por los valores que acabamos de cargar, pero de los que sólo se mostrarán los que comiencen por el mismo valor que hay contenido en la caja de texto del AutoCompleteBox.

 

 

AutoCompleteBox. Creación de la lista mediante un recurso

Utilizando código XAML es posible crear declarativamente la lista de valores empleando un tipo ObjectCollection, dentro del cual incluiremos los elementos que formarán parte de la colección.

Para declarar tipos de datos de .NET Framework tales como int, double, string, etc., debemos añadir un atributo xmlns a la etiqueta UserControl de la página, que apunte al espacio de nombres System del ensamblado mscorlib. Una vez creada la colección (en la zona de recursos de la página XAML) asignaremos ésta como un recurso estático a la propiedad ItemsSource del AutoCompleteBox. El campo del formulario al que aplicaremos esta técnica será BillingState.

<UserControl x:Class="ControlesDataForm.MainPage"
....    
    xmlns:System="clr-namespace:System;assembly=mscorlib"
....    
>
    <!--....-->
    <UserControl.Resources>
        <!--....-->
        <toolkit:ObjectCollection x:Key="colEstados">
            <System:String>AB</System:String>
            <System:String>AZ</System:String>
            <System:String>BC</System:String>
            <System:String>CA</System:String>
            <System:String>DF</System:String>
            <System:String>Dublin</System:String>
            <System:String>FL</System:String>
             <!--....-->
        </toolkit:ObjectCollection>
    </UserControl.Resources>

    <toolkit:DataForm x:Name="frmInvoices"
        &lt;!--....-->
        <toolkit:DataForm.EditTemplate>
            <!--....-->
            <toolkit:DataField Label="Estado o Provincia:">
                <sdk:AutoCompleteBox Text="{Binding Path=BillingState, Mode=TwoWay}" 
                                     Width="100" HorizontalAlignment="Left"
                                     ItemsSource="{StaticResource colEstados}" />
            </toolkit:DataField>
            <!--....-->

 

 

DomainUpDown. Edición y navegación entre un conjunto de valores

El objetivo de DomainUpDown, al igual que AutoCompleteBox, consiste en seleccionar un valor de una lista (dominio de valores), aunque la diferencia en este caso reside en que dicha lista no se despliega, sino que nos movemos por ella mediante sus controles de desplazamiento.

 

El modo de creación y configuración de este control también es muy similar al de AutoCompleteBox, siendo ItemsSource la propiedad a la que tendremos que asignar la lista de valores. En nuestro ejemplo emplearemos este control para editar el campo BillingCountry, creando la lista de elementos mediante LINQ, como ya vimos anteriormente. En este caso aplicaremos también la partícula orderby a la expresión LINQ para obtener los valores ordenados.

<toolkit:DataField Label="País:">
    <toolkit:DomainUpDown x:Name="dudBillingCountry" 
                          Value="{Binding Path=BillingCountry, Mode=TwoWay}" 
                          Width="120" HorizontalAlignment="Left" />
</toolkit:DataField>
 
private void frmInvoices_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
{
    if (e.Mode == DataFormMode.Edit)
    {
        //....
        var qryConsulta = (from oInvoice in oDomainContext.Invoices
                           orderby oInvoice.BillingCountry
                           select oInvoice.BillingCountry).Distinct();

        DomainUpDown dudBillingCountry = (DomainUpDown)this.frmInvoices.FindNameInContent("dudBillingCountry");
        dudBillingCountry.ItemsSource = qryConsulta;
    }
}

 

El conjunto de valores asignado a un control DomainUpDown inicialmente es cerrado. Esto quiere decir que si escribimos un valor en la caja de texto que no esté en la lista asignada al control, y pulsamos la tecla Intro o cambiamos el foco a otro control del formulario, dicho valor será rechazado, volviendo la zona de edición a recuperar su valor original; por lo que a priori, los únicos valores admisibles son los existentes en la propiedad ItemsSource.

Una forma de alterar este comportamiento del control pasa por asignar a su propiedad InvalidInputAction el valor UseFallbackItem, y en la propiedad FallbackItem un valor igual a uno de los elementos de la lista de valores asignada al control. De esta forma, si el usuario introduce un valor incorrecto, se asignará como valor por defecto el existente en FallbackItem.

<toolkit:DomainUpDown x:Name="dudBillingCountry" 
                      Value="{Binding Path=BillingCountry, Mode=TwoWay}" 
                      Width="120" HorizontalAlignment="Left" 
                      InvalidInputAction="UseFallbackItem" 
                      FallbackItem="Germany" />

Otra técnica consiste en asignar a la propiedad InvalidInputAction el valor TextBoxCannotLoseFocus, lo que mantiene el foco en el control mientras el usuario no introduzca un valor que concuerde con alguno de los existentes en la colección asignada a ItemsSource.

Pero supongamos que el usuario necesita que el valor tecleado en el control sea añadido a su lista de valores en el caso de que no exista. En este tipo de situación, el control DomainUpDown desencadena el evento ParseError, de manera que si escribimos un manipulador para dicho evento podemos adaptar el comportamiento del control a nuestras necesidades.

En el código de este evento obtendremos la instancia del control DomainUpDown a través del parámetro sender que el manipulador recibe, y acto seguido recuperaremos la lista de valores mediante su propiedad Items, volcándola a un tipo List<object>.

Añadiremos al objeto List el valor que el usuario ha escrito en la caja de texto, que se encuentra disponible en la propiedad Text del parámetro UpDownParseErrorEventArgs que recibe el manipulador del evento. Finalmente ordenaremos la lista llamando a su método OrderBy, asignándola de nuevo a la propiedad ItemsSource del control.

Al producirse este evento se vacía la caja de texto del DomainUpDown, por lo que volveremos a asignarle a su propiedad Value el valor que el usuario había tecleado, que como ya hemos indicado, se encuentra en la propiedad UpDownParseErrorEventArgs.Text.

<toolkit:DataField Label="País:">
    <toolkit:DomainUpDown x:Name="dudBillingCountry" 
                          Value="{Binding Path=BillingCountry, Mode=TwoWay}" 
                          Width="120" HorizontalAlignment="Left" 
                          ParseError="dudBillingCountry_ParseError" />
</toolkit:DataField>

 

private void dudBillingCountry_ParseError(object sender, UpDownParseErrorEventArgs e)
{
    DomainUpDown dudBillingCountry = (DomainUpDown)sender;
    List<object> lstPaises = dudBillingCountry.Items.ToList();
    lstPaises.Add(e.Text);
    dudBillingCountry.ItemsSource = lstPaises.OrderBy(sPais => sPais);
    dudBillingCountry.Value = e.Text;
    e.Handled = true;
}

Con este control llegamos al final de la segunda parte del artículo, en el que hemos abordado varias maneras de mejorar la forma en que podemos editar los valores de los campos de un DataForm. En la tercera parte, que concluye la serie, continuaremos mostrando controles adicionales, que consigan hacer que la edición de los campos por parte de nuestros usuarios, sea una labor un poco más fácil y grata. En el siguiente enlace tenemos disponible el proyecto de ejemplo

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.