Plantillas de presentación de datos en el control DataForm de Silverlight (y 2)

Después de la introducción inicial a la plantilla ReadOnlyTemplate del control DataForm, realizada en la primera parte, en esta segunda entrega del artículo, tal y como prometimos, mostraremos algunos aspectos adicionales que contribuirán a mejorar la faceta de presentación de datos de este control.

 

Formatear los valores originales

De todos los valores obtenidos desde la fuente de datos para mostrar en el DataForm, habremos advertido que algunos campos, debido al tipo de dato subyacente con el que están definidos en la base de datos, no visualizan un valor “agradable” para el usuario. Tales campos serían InvoiceDate, Total y FirstInvoice.

Para este tipo de campos podemos implementar un convertidor, que transforme el valor original en otro formateado acorde a lo que queramos presentar al usuario.  Recomendamos la consulta del siguiente artículo en este mismo blog, para un mayor detalle acerca de la creación de convertidores.

Agregaremos por lo tanto, al proyecto Web de la solución, un archivo con el nombre Convertidores.shared.cs, dentro del cual escribiremos el código de una clase que implemente la interfaz IValueConverter, en cuyo método Convert transformaremos el valor recibido desde la interfaz de usuario por otro más conveniente de cara a su presentación. La extensión shared.cs provocará que la maquinaria de WCF RIA Services genere en el proyecto Silverlight una copia cliente de este código.

using System;
using System.Windows.Data;

namespace PlantillaReadOnly.Web
{
    public class Convertidores : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string sResultadoFormateado = string.Empty;

            if (value.GetType() == typeof(DateTime))
            {
                sResultadoFormateado = ((DateTime)value).ToLongDateString();
            }
            else if (value.GetType() == typeof(decimal))
            {
                sResultadoFormateado = ((decimal)value).ToString("C");
            }
            else if (value.GetType() == typeof(bool))
            {
                if ((bool)value)
                {
                    sResultadoFormateado = "Sí";
                }
                else
                {
                    sResultadoFormateado = "No";
                }
            }

            return sResultadoFormateado;
        }

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

La interfaz IValueConverter se encuentra dentro del ensamblado System.Windows.Data, situado en la librería PresentationFramework.dll, cuya ruta es C:Program FilesReference AssembliesMicrosoftFramework.NETFrameworkv4.0, por lo que en el proyecto Web de la solución deberemos establecer una referencia a la misma.

El siguiente paso consistirá en definir en el código XAML un apartado de recursos, y declarar una instancia del convertidor que acabamos de crear.

<UserControl.Resources>
    <domainctx:Convertidores x:Key="cnvConvertidor" />
</UserControl.Resources>

Finalmente, en los controles del DataForm que muestran los campos a formatear, añadiremos en la expresión de enlace a datos el atributo Converter, asignando a éste una expresión de acceso al convertidor que se encuentra declarado como recurso en la página.

<toolkit:DataField Label="Fecha de factura:">
    <TextBlock Text="{Binding Path=InvoiceDate, Mode=OneTime, Converter={StaticResource cnvConvertidor}}" />
</toolkit:DataField>
<!--...-->
<toolkit:DataField Label="Importe factura:">
    <TextBlock Text="{Binding Path=Total, Mode=OneTime, Converter={StaticResource cnvConvertidor}}" />
</toolkit:DataField>
<!--...-->
<toolkit:DataField Label="¿Es primer envío?:">
    <TextBlock Text="{Binding Path=FirstInvoice, Mode=OneTime, Converter={StaticResource cnvConvertidor}}" />
</toolkit:DataField>

Al volver a ejecutar la aplicación, el contenido de estos campos será transformado por el convertidor a valores más adecuados para su presentación.

 

Obtención de valores a partir de una tabla catálogo

Supongamos que además del código del cliente, también debemos visualizar su nombre en el DataForm. Puesto que el campo CustomerId está presente tanto en la tabla Invoice como en Customer, podemos obtener, para cada factura, aquellos datos que necesitemos correspondientes al cliente a quien se ha emitido. Los pasos necesarios para implementar esta funcionalidad serían los siguientes:

En primer lugar, dentro del servicio de dominio, escribiremos un nuevo método que devuelva una instancia de Customer, según el valor de CustomerId pasado como parámetro.

[EnableClientAccess()]
public class MusicaGestDomainService : LinqToEntitiesDomainService<MusicaGestEntities>
{
    //....
    public Customer GetCustomerPorId(int nCustomerId)
    {
        return this.ObjectContext.Customers.Where(oCustomer => oCustomer.CustomerId == nCustomerId).First();
    }
}

A continuación, en el código XAML de la página, añadiremos un par de nuevos controles DataField y TextBlock, asignando un nombre a éste último.

<toolkit:DataField Label="Nombre cliente:">
    <TextBlock x:Name="txtNombreCliente" />
</toolkit:DataField>

Pasando al code behind de la página XAML, escribiremos un manipulador para el evento ContentLoaded del DataForm, que se produce cada vez que se carga una nueva entidad en el formulario. En el código del manipulador obtendremos el valor de la propiedad CustomerId correspondiente a la entidad en curso (DataForm.CurrentItem).

Seguidamente obtendremos una referencia al TextBlock txtNombreCliente, que acabamos de declarar en el código XAML, empleando el método DataForm.FindInNameContent, al que pasaremos como parámetro el nombre que hemos dado al control.

Utilizando una instancia del contexto de dominio, que hemos declarado con ámbito de clase en el código de la página, invocaremos al método GetCustomerPorIdQuery, que carga la entidad Customer en base al identificador pasado como parámetro, y que nosotros recuperaremos en el evento Completed del tipo LoadOperation devuelto por el contexto de dominio. Obsérvese que el código del evento LoadOperation.Completed lo escribimos dentro de una expresión lambda, quedando por tanto incluido dentro del cuerpo de ContentLoaded.

En el interior del código de LoadOperation.Completed obtendremos la instancia de Customer resultante de la llamada al método del contexto de dominio, asignando al TextBlock del DataForm las propiedades correspondientes al nombre.

//....
using PlantillaReadOnly.Web;
using System.ServiceModel.DomainServices.Client;

namespace PlantillaReadOnly
{
    public partial class MainPage : UserControl
    {
        MusicaGestDomainContext oDomainContext = new MusicaGestDomainContext();

        public MainPage()
        {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(MainPage_Loaded);
        }

        void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            this.frmInvoices.ContentLoaded += new EventHandler<DataFormContentLoadEventArgs>(frmInvoices_ContentLoaded);
        }

        void frmInvoices_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
        {
            int nCustomerId = ((Invoice)frmInvoices.CurrentItem).CustomerId;
            TextBlock txtNombreCliente = (TextBlock)this.frmInvoices.FindNameInContent("txtNombreCliente");
            LoadOperation<Customer> oLoadOperation = oDomainContext.Load(oDomainContext.GetCustomerPorIdQuery(nCustomerId));

            oLoadOperation.Completed += (oSender, oEventArgs) =>
            {
                Customer oCustomer = oLoadOperation.Entities.First();
                txtNombreCliente.Text = oCustomer.FirstName + " " + oCustomer.LastName;
            };
        }
    }
}

Al volver a ejecutar la solución podremos observar este nuevo campo creado dinámicamente.

No obstante, es preciso hacer un pequeño retoque adicional al funcionamiento de este nuevo campo, ya que si observamos atentamente, al navegar por la colección de entidades del formulario, se produce un molesto efecto de parpadeo cuando se actualiza el nombre del cliente. Para evitarlo vamos a recurrir al evento DataForm.CurrentItemChanged, que como su nombre indica, se produce cada vez que cambia el elemento actual del formulario.

Dentro de dicho evento, ocultaremos el TextBlock que usamos para mostrar el nombre del cliente, para volver a visualizarlo en el evento ContentLoaded, una vez hayamos obtenido la instancia de Customer de la factura en curso.

public partial class MainPage : UserControl
{
    //....
    void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        this.frmInvoices.CurrentItemChanged += new EventHandler<EventArgs>(frmInvoices_CurrentItemChanged);
        this.frmInvoices.ContentLoaded += new EventHandler<DataFormContentLoadEventArgs>(frmInvoices_ContentLoaded);
    }

    void frmInvoices_CurrentItemChanged(object sender, EventArgs e)
    {
        TextBlock txtNombreCliente = (TextBlock)this.frmInvoices.FindNameInContent("txtNombreCliente");
        txtNombreCliente.Visibility = System.Windows.Visibility.Collapsed;
    }

    void frmInvoices_ContentLoaded(object sender, DataFormContentLoadEventArgs e)
    {
        //....
        oLoadOperation.Completed += (oSender, oEventArgs) =>
        {
            //....
            txtNombreCliente.Visibility = System.Windows.Visibility.Visible;
        };
    }
}

 

Redistribuyendo los campos en paneles

En un apartado anterior del artículo comentábamos que el medio para llevar a cabo la ubicación de los campos en el DataForm consistía en utilizar uno o más paneles que nos permitieran organizar su contenido.

Hasta este momento nos hemos limitado a emplear un único panel de tipo StackPanel para disponer los campos de nuestro formulario. Sin embargo, para demostrar que no estamos restringidos solamente a esta forma de diseñar la interfaz, vamos a añadir al código XAML que define el formulario y sus elementos, un conjunto de paneles StackPanel y Grid, con los que modificaremos ligeramente la disposición de los campos, sustituyendo en estos casos el uso del control DataField, el cual, como podemos ver, tampoco es estrictamente obligatorio para definir los campos del DataForm.

<toolkit:DataForm x:Name="frmInvoices" 
    ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}"
    Margin="10" 
    CommandButtonsVisibility="Navigation">
    <toolkit:DataForm.ReadOnlyTemplate>
        <DataTemplate>
            <StackPanel>
                <toolkit:DataField Label="Código factura:">
                    <TextBlock Text="{Binding Path=InvoiceId, Mode=OneTime}" />
                </toolkit:DataField>

                <!--utilizando paneles StackPanel-->
                <Border BorderThickness="3" BorderBrush="Black" Margin="0,10">
                    <StackPanel>
                        <Border BorderThickness="1" BorderBrush="Black" Margin="5">
                            <TextBlock HorizontalAlignment="Center">Datos del cliente</TextBlock>
                        </Border>

                        <StackPanel Margin="0,10" HorizontalAlignment="Center">
                            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" >

                                <StackPanel HorizontalAlignment="Center" Width="50">
                                    <TextBlock HorizontalAlignment="Center">Código</TextBlock>
                                    <TextBlock Text="{Binding Path=CustomerId, Mode=OneTime}" HorizontalAlignment="Center" />
                                </StackPanel>

                                <StackPanel HorizontalAlignment="Center" Width="150">
                                    <TextBlock HorizontalAlignment="Center">Nombre</TextBlock>
                                    <TextBlock HorizontalAlignment="Center" x:Name="txtNombreCliente" />
                                </StackPanel>

                            </StackPanel>
                        </StackPanel>
                    </StackPanel>
                </Border>

                <toolkit:DataField Label="Fecha de factura:">
                    <TextBlock Text="{Binding Path=InvoiceDate, Mode=OneTime, Converter={StaticResource cnvConvertidor}}" />
                </toolkit:DataField>

                <!--utilizando panel Grid-->
                <Border BorderThickness="3" BorderBrush="Black" Margin="0,10">
                    <Grid Width="350" Height="150" HorizontalAlignment="Left">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="50*" />
                            <RowDefinition Height="50*" />
                            <RowDefinition Height="50*" />
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="60*" />
                            <ColumnDefinition Width="70*" />
                            <ColumnDefinition Width="220*" />
                        </Grid.ColumnDefinitions>

                        <Border Grid.RowSpan="3" BorderThickness="1" BorderBrush="Black" Margin="5" HorizontalAlignment="Left" VerticalAlignment="Center">
                            <TextBlock Text="Datos de entrega" TextWrapping="Wrap" TextAlignment="Center" Margin="2" />
                        </Border>

                        <TextBlock Text="Dirección:" Grid.Column="1" VerticalAlignment="Center" />
                        <TextBlock Text="{Binding Path=BillingAddress, Mode=OneTime}" Grid.Column="2" VerticalAlignment="Center" />
                                    
                        <TextBlock Text="Ciudad:" Grid.Column="1" Grid.Row="1" VerticalAlignment="Center" />
                        <TextBlock Text="{Binding Path=BillingCity, Mode=OneTime}" Grid.Column="2" Grid.Row="1" VerticalAlignment="Center" />
                                    
                        <TextBlock Text="País:" Grid.Column="1" Grid.Row="2" VerticalAlignment="Center" />
                        <TextBlock Text="{Binding Path=BillingCountry, Mode=OneTime}" Grid.Column="2" Grid.Row="2" VerticalAlignment="Center" />
                    </Grid>
                </Border>

                <toolkit:DataField Label="Importe factura:">
                    <TextBlock Text="{Binding Path=Total, Mode=OneTime, Converter={StaticResource cnvConvertidor}}" />
                </toolkit:DataField>

                <toolkit:DataField Label="¿Es primer envío?:">
                    <TextBlock Text="{Binding Path=FirstInvoice, Mode=OneTime, Converter={StaticResource cnvConvertidor}}" />
                </toolkit:DataField>
            </StackPanel>
        </DataTemplate>
    </toolkit:DataForm.ReadOnlyTemplate>
</toolkit:DataForm>

Una vez aplicados estos cambios, al volver a ejecutar la aplicación observaremos el resultado de esta flexibilidad de que disponemos a la hora de diseñar la interfaz del formulario.

Y después de esta nueva mejora realizada al DataForm concluimos este artículo dedicado a la presentación de datos empleando este control. El código fuente del proyecto y el script de la base de datos se encuentran disponibles aquí.

Espero que os resulte de ayuda e interés. 

Sin categoría

6 thoughts on “Plantillas de presentación de datos en el control DataForm de Silverlight (y 2)

  1. Hola Marino

    Muchas gracias por tu opinión, y por supuesto que estaré encantado de que en tu próximo libro incluyas una referencia a mi blog. Será todo un honor 8-).

    Un fuerte abrazo y ánimo con ese pedazo libro que preparas ;-).

  2. me envia un error en esta linea de codigo a que se debe me podria explicar.

    domainctx:Convertidores x:Key=”cnvConvertidor”

    mucha gracias

  3. Hola kiss

    Comprueba que en el archivo de código donde has escrito el código de la clase Convertidores, la clase está contenida dentro del espacio de nombres NombreProyecto.Web. Es decir, si tu proyecto se llama PlantillaReadOnly, deberás tener un espacio de nombres PlantillaReadOnly.Web, con lo cual, al codificar la clase Convertidores tendrás que hacerlo así:

    //————————————–
    namespace PlantillaReadOnly.Web
    {
    public class Convertidores : IValueConverter
    {
    //….
    //————————————–

    Un saludo,
    Luismi

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *