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 Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.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.
El control DataForm, perteneciente al paquete de controles y componentes Silverlight Toolkit, permite al programador crear formularios de consulta y edición de datos de un modo muy sencillo y rápido; cualidad muy de agradecer cuando desarrollamos la interfaz de usuario de nuestra aplicación.
Si durante el diseño de una página XAML, definimos una fuente de datos mediante un control DomainDataSource y asignamos éste a la propiedad ItemsSource de un DataForm, tendremos listo un formulario con toda la lógica necesaria para realizar la navegación y edición sobre la colección de entidades obtenidas a partir de un origen de datos.
Puede suceder, no obstante, que a pesar de ser un buen candidato para implementar nuestros procesos de manipulación de datos, el modo de presentación o edición predeterminado empleado por el DataForm no se ajuste totalmente a nuestras necesidades, ya sea por los controles utilizados para las operaciones de manipulación con los datos; por la disposición de dichos controles en la interfaz del formulario; o por cualquier otro motivo que dificulte su integración en nuestra interfaz de usuario.
Plantillas en el control DataForm
Para resolver esta situación, el control DataForm ofrece un sistema de plantillas, mediante las cuales obtendremos un alto grado de flexibilidad a la hora de establecer la disposición (layout) de los elementos integrantes del formulario, tanto en lo referente a las tareas de visualización como a la edición de datos.
En función del tipo de operación que vayamos a realizar utilizaremos una de las siguientes plantillas, bien de forma independiente o combinadas entre sí, dependiendo en este último caso de si necesitamos que el usuario pueda consultar y editar los datos simultáneamente. Todas ellas heredan de la clase DataTemplate, la cual se utiliza para describir la estructura visual de un objeto de datos.
--EditTemplate. Plantilla para realizar la edición de un elemento existente.
--HeaderTemplate. Plantilla para definir la cabecera del formulario de datos.
--NewItemTemplate. Plantilla para realizar la edición de un nuevo elemento.
--ReadOnlyTemplate. Plantilla para consultar/visualizar el elemento actual.
El objetivo del presente artículo consistirá en explicar el proceso de creación de un DataForm de sólo lectura, utilizando la ayuda, como es lógico, de la plantilla ReadOnlyTemplate. El código fuente del proyecto y el script de la base de datos se encuentran disponibles aquí.
Preparación de la fuente de datos
Como datos de ejemplo a mostrar desde el DataForm, crearemos una base de datos con el nombre MusicaGest, que contendrá dos tablas: Invoice y Customer. En el siguiente bloque de código tenemos el script de creación de la base de datos y tablas, así como la inserción de los registros.
CREATE DATABASE MusicaGest
GO
USE MusicaGest
GO
CREATE TABLE Invoice
(
InvoiceId int IDENTITY(1,1) NOT NULL,
CustomerId int NOT NULL,
InvoiceDate datetime NULL,
BillingAddress varchar(70) NULL,
BillingCity varchar(50) NULL,
BillingCountry varchar(50) NULL,
Total numeric(10, 2) NULL,
FirstInvoice bit NULL,
CONSTRAINT PK_Invoice PRIMARY KEY CLUSTERED (InvoiceId ASC)
)
GO
CREATE TABLE Customer
(
CustomerId int NOT NULL,
FirstName nvarchar(40) NOT NULL,
LastName nvarchar(20) NOT NULL,
Country nvarchar(40) NULL,
CONSTRAINT PK_Customer PRIMARY KEY CLUSTERED (CustomerId ASC)
)
GO
INSERT INTO Invoice VALUES (42,'03/27/2007','9, Place Louis Barthou','Bordeaux','France',7.92,1)
INSERT INTO Invoice VALUES (39,'05/09/2007','4, Rue Milton','Paris','France',6.93,0)
INSERT INTO Invoice VALUES (41,'08/23/2007','11, Place Bellecour','Lyon','France',2.97,1)
INSERT INTO Invoice VALUES (13,'10/04/2007','Qe 7 Bloco G','Brasília','Brazil',5.95,0)
INSERT INTO Invoice VALUES (3,'10/12/2007','1498 rue Bélanger','Montréal','Canada',10.93,1)
INSERT INTO Invoice VALUES (14,'07/07/2008','8210 111 ST NW','Edmonton','Canada',6.93,0)
INSERT INTO Invoice VALUES (31,'08/24/2008','194A Chain Lake Drive','Halifax','Canada',3.96,1)
INSERT INTO Invoice VALUES (22,'08/27/2008','120 S Orange Ave','Orlando','USA',2.97,0)
INSERT INTO Invoice VALUES (24,'11/13/2009','162 E Superior Street','Chicago','USA',5.95,1)
INSERT INTO Invoice VALUES (10,'12/01/2009','Rua Dr. Falcão Filho, 155','São Paulo','Brazil',9.90,0)
INSERT INTO Invoice VALUES (54,'01/09/2010','110 Raeburn Pl','Edinburgh ','United Kingdom',2.97,1)
INSERT INTO Invoice VALUES (52,'01/11/2010','202 Hoxton Street','London','United Kingdom',1.98,0)
INSERT INTO Invoice VALUES (37,'03/22/2010','Berger Straße 10','Frankfurt','Germany',2.97,1)
INSERT INTO Invoice VALUES (50,'03/27/2010','C/ San Bernardo 85','Madrid','Spain',4.95,0)
INSERT INTO Invoice VALUES (18,'05/15/2010','627 Broadway','New York','USA',1.98,1)
INSERT INTO Invoice VALUES (23,'09/18/2010','69 Salem Street','Boston','USA',2.97,0)
INSERT INTO Invoice VALUES (50,'11/03/2010','C/ San Bernardo 85','Madrid','Spain',2.97,1)
INSERT INTO Invoice VALUES (36,'11/09/2010','Tauentzienstraße 8','Berlin','Germany',2.97,0)
INSERT INTO Customer VALUES (3,'François','Tremblay','Canada')
INSERT INTO Customer VALUES (10,'Eduardo','Martins','Brazil')
INSERT INTO Customer VALUES (13,'Fernanda','Ramos','Brazil')
INSERT INTO Customer VALUES (14,'Mark','Philips','Canada')
INSERT INTO Customer VALUES (18,'Michelle','Brooks','USA')
INSERT INTO Customer VALUES (22,'Heather','Leacock','USA')
INSERT INTO Customer VALUES (23,'John','Gordon','USA')
INSERT INTO Customer VALUES (24,'Frank','Ralston','USA')
INSERT INTO Customer VALUES (31,'Martha','Silk','Canada')
INSERT INTO Customer VALUES (36,'Hannah','Schneider','Germany')
INSERT INTO Customer VALUES (37,'Fynn','Zimmermann','Germany')
INSERT INTO Customer VALUES (39,'Camille','Bernard','France')
INSERT INTO Customer VALUES (41,'Marc','Dubois','France')
INSERT INTO Customer VALUES (42,'Wyatt','Girard','France')
INSERT INTO Customer VALUES (50,'Enrique','Muñoz','Spain')
INSERT INTO Customer VALUES (52,'Emma','Jones','United Kingdom')
INSERT INTO Customer VALUES (54,'Steve','Murray','United Kingdom')
Estas tablas están basadas en la base de datos Chinook, disponible en CodePlex, pero en nuestro caso el número de registros es notablemente inferior, existiendo, por otra parte, algunas diferencias entre sus estructuras, debido a la necesidad de adaptarlas al proyecto que vamos a desarrollar.
Creación del proyecto
A continuación abriremos Visual Studio 2010 y crearemos un nuevo proyecto de tipo Silverlight llamado PlantillaReadOnly, con la opción de WCF RIA Services habilitada. Agregaremos un modelo de datos a partir de nuestra base de datos MusicaGest, que genere sendas entidades en base a las tablas que contiene. También crearemos un servicio de dominio, que proporcione la lógica para la manipulación de las mencionadas entidades.

Presentando datos con el control DataForm. La vía fácil y rápida
Como siguiente paso, añadiremos al código de la página XAML un control DomainDataSource, que obtenga la colección de entidades Invoice llamando al método GetInvoices del servicio de dominio.
<UserControl x:Class="PlantillaReadOnly.MainPage"
xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices"
xmlns:domainctx="clr-namespace:PlantillaReadOnly.Web"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<riaControls:DomainDataSource x:Name="ddsInvoices" QueryName="GetInvoices">
<riaControls:DomainDataSource.DomainContext>
<domainctx:MusicaGestDomainContext />
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
<StackPanel Background="Turquoise">
</StackPanel>
</Grid>
</UserControl>
Finalmente agregaremos un DataForm, que enlazaremos con el DomainDataSource.
<UserControl x:Class="PlantillaReadOnly.MainPage"
xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices"
xmlns:domainctx="clr-namespace:PlantillaReadOnly.Web"
xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<riaControls:DomainDataSource x:Name="ddsInvoices" QueryName="GetInvoices">
<riaControls:DomainDataSource.DomainContext>
<domainctx:MusicaGestDomainContext />
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
<StackPanel Background="Turquoise">
<toolkit:DataForm x:Name="frmInvoices"
ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}"
Margin="10" />
</StackPanel>
</Grid>
</UserControl>
Al ejecutar la aplicación obtendremos un formulario de datos que utiliza el modo de manipulación predeterminado. Esta interfaz de usuario, generada automáticamente por el DataForm, cumple el doble objetivo de servir como medio de visualización y edición de la colección de entidades.

Empleando la plantilla ReadOnlyTemplate
Pero supongamos que el formulario tuviera que limitarse a visualizar los datos, sin que existiera la posibilidad de editarlos. Éste sería un aspecto de la aplicación que también podríamos resolver de forma rápida asignando el valor True a la propiedad IsReadOnly del DataForm.
<toolkit:DataForm x:Name="frmInvoices"
ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}"
Margin="10"
IsReadOnly="True">
<!--....-->

No obstante, nuestro objetivo es lograr un mayor control sobre la interfaz de usuario haciendo uso de la plantilla ReadOnlyTemplate del DataForm; tarea que comenzaremos a desarrollar a partir de ahora.
Para aplicar esta plantilla debemos utilizar las etiquetas ReadOnlyTemplate y DataTemplate en el código XAML de la siguiente manera:
<toolkit:DataForm x:Name="frmInvoices"
ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}"
Margin="10">
<toolkit:DataForm.ReadOnlyTemplate>
<DataTemplate>
<!--....-->
</DataTemplate>
</toolkit:DataForm.ReadOnlyTemplate>
</toolkit:DataForm>
Dentro de la etiqueta DataTemplate, la organización de los campos la realizaremos combinando uno o varios paneles, en función de la complejidad con que vayamos a diseñar nuestra interfaz de usuario.
Por cada campo que queramos agregar al formulario incluiremos un control DataField, que servirá para definir una etiqueta con el título del campo. En el interior del DataField insertaremos el control que se ocupará de visualizar, mediante una expresión de enlace a datos (data binding), el valor de uno de los campos de la fuente de datos. Dado que la finalidad de nuestro ejemplo consiste en desarrollar un formulario de sólo lectura, para mostrar el valor de los campos emplearemos controles TextBlock.
<toolkit:DataForm x:Name="frmInvoices"
ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}"
Margin="10">
<toolkit:DataForm.ReadOnlyTemplate>
<DataTemplate>
<StackPanel>
<toolkit:DataField Label="Código factura:">
<TextBlock Text="{Binding Path=InvoiceId, Mode=OneTime}" />
</toolkit:DataField>
<toolkit:DataField Label="Código cliente:">
<TextBlock Text="{Binding Path=CustomerId, Mode=OneTime}" />
</toolkit:DataField>
<toolkit:DataField Label="Fecha de factura:">
<TextBlock Text="{Binding Path=InvoiceDate, Mode=OneTime}" />
</toolkit:DataField>
<toolkit:DataField Label="Dirección de entrega:">
<TextBlock Text="{Binding Path=BillingAddress, Mode=OneTime}" />
</toolkit:DataField>
<toolkit:DataField Label="Ciudad de entrega:">
<TextBlock Text="{Binding Path=BillingCity, Mode=OneTime}" />
</toolkit:DataField>
<toolkit:DataField Label="País de entrega:">
<TextBlock Text="{Binding Path=BillingCountry, Mode=OneTime}" />
</toolkit:DataField>
<toolkit:DataField Label="Importe factura:">
<TextBlock Text="{Binding Path=Total, Mode=OneTime}" />
</toolkit:DataField>
<toolkit:DataField Label="¿Es primer envío?:">
<TextBlock Text="{Binding Path=FirstInvoice, Mode=OneTime}" />
</toolkit:DataField>
</StackPanel>
</DataTemplate>
</toolkit:DataForm.ReadOnlyTemplate>
</toolkit:DataForm>
Al ejecutar nuevamente la solución, el DataForm aparecerá con la configuración de controles que acabamos de definir en su plantilla ReadOnlyTemplate. Un detalle a destacar, relacionado con esta técnica de trabajo, consiste en la plena libertad de que disponemos para situar los campos en el orden y/o ubicación que mejor se adapte a los requisitos de la aplicación. Si bien es cierto que este es un aspecto que también podemos tratar desde la clase de metadatos de la entidad, mediante el atributo Display, la libertad y flexibilidad que nos aporta el uso de las plantillas es muy superior.

Un pequeño inconveniente del que podemos habernos percatado tras aplicar la plantilla, reside en que a pesar de no poder editar los campos del formulario, en la barra de herramientas, junto a los botones de navegación, siguen apareciendo los botones añadir y eliminar. El primero no reviste problema, ya que permanece deshabilitado, pero el segundo sí se encuentra activo, permitiendo eliminar elementos de la colección.

Puesto que la única operación que queremos ofrecer al usuario dentro del formulario es la navegación por la colección de entidades, en el código XAML que usamos para declarar el DataForm, asignaremos a la propiedad CommandButtonsVisibility el valor Navigation, indicando así que solamente los botones de navegación estarán visibles.
<toolkit:DataForm x:Name="frmInvoices"
ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}"
Margin="10"
CommandButtonsVisibility="Navigation">
La propiedad DataForm.CommandButtonsVisibility pertenece al tipo enumerado DataFormCommandButtonsVisibility. Podemos combinar los miembros de esta enumeración para especificar qué botones de operación deseamos ofrecer al usuario en el DataForm.

Y llegados a este punto finalizamos la primera parte del artículo, en el cual hemos realizado una introducción a las plantillas de datos del control DataForm, ilustrando las diferencias entre la creación de un formulario de datos mediante el modo predeterminado y aplicando la plantilla ReadOnlyTemplate. En la segunda entrega abordaremos otros interesantes aspectos relacionados con la presentación de datos mediante este control, tales como el formateo de los valores de los campos y el acceso a una tabla catálogo auxiliar.