Al igual que el resto de elementos de la interfaz de usuario en una aplicación Silverlight, el control DomainDataSource se define declarativamente utilizando código XAML. No obstante, también será posible implementar determinados aspectos de su comportamiento desde code behind, como veremos en la aplicación de ejemplo que vamos a desarrollar.
Por otro lado, se trata de un control que no se limita simplemente a presentar los datos obtenidos desde el contexto de dominio, sino que está dotado de una serie de características adicionales que le permiten ordenar, filtrar, agrupar, etc. dichos datos.
Para adaptar esta interfaz prefabricada a nuestras necesidades, acomodando los estilos y recursos asignados a tipos de letra, literales, controles, etc., debemos editar el archivo de estilos Styles.xaml, situado en la carpeta Assets de la estructura del proyecto; y el archivo de recursos ApplicationStrings.resx, situado en la carpeta Assets/Resources. Cuando necesitemos añadir nuevas páginas al proyecto lo haremos dentro de la carpeta Views (figura 1).

Por otro lado, esta plantilla de proyecto también proporciona ya activado el soporte de WCF RIA Services, necesario para la interacción de los datos entre las capas cliente e intermedia de la aplicación. Como requisito previo, necesitaremos tener instalado Silverlight 4 Tools for Visual Studio 2010. En cuanto a los datos de ejemplo, utilizaremos la base de datos Chinook, que podemos descargar desde CodePlex.
La primera tarea a realizar será la creación, en el proyecto Web de nuestra solución, de un "ADO.NET Data Model" (modelo de datos) con el nombre ChinookModel, donde a partir de la tabla Invoice de la base de datos se generará una entidad del mismo nombre. A continuación, crearemos un "Domain Service Class" (servicio de dominio) con el nombre ChinookDomainService, que configuraremos para que genere el código para las operaciones CRUD sobre la entidad Invoice.
Comenzaremos, como indica el título de este apartado, por las operaciones elementales de recuperación, visualización y edición de datos. Para ello, añadiremos al proyecto una página con el nombre Presentacion.xaml, a la que accederemos desde la página principal de la aplicación (MainPage.xaml) utilizando un control HyperlinkButton, como vemos en el listado 1.
Listado 1
<HyperlinkButton x:Name="lnkPresentacion"
Style="{StaticResource LinkStyle}"
NavigateUri="/Presentacion"
TargetName="ContentFrame"
Content="{Binding Path=ApplicationStrings.PresentacionPageTitle,
Source={StaticResource ResourceWrapper}}"/>
Dentro del disenador de la pagina Presentacion.xaml arrastraremos un DomainDataSource y un DataGrid desde la Caja de herramientas, asignandoles respectivamente los nombres ddsInvoices y grdInvoices. Debido a que el DomainDataSource necesita acceder al contexto de dominio, tenemos que especificar declarativamente dicho dominio anadiendo un espacio de nombres (xmlns) con el nombre domainctx, en la etiqueta UserControl de la pagina.
Pasando a la configuracion del control DomainDataSource, la propiedad Query] Name contendra el nombre del metodo perteneciente al servicio de dominio (GetInvoices), que devuelve la coleccion de entidades al contexto de dominio, y este a su vez al control. Para que el DomainDataSource tenga acceso al contexto de dominio, añadiremos al código de este control la etiqueta DomainContext, especificando el espacio de nombres domainctx, declarado en la etiqueta UserControl.
Finalmente, para que el DataGrid pueda mostrar datos, asignaremos a su propiedad ItemsSource una expresión de enlace a datos que conecte este control con el DomainDataSource (listado 2). El resultado lo vemos en la figura 2.

Listado 2
<navigation:Page x:Class="PruebasDDS.Views.Presentacion"
xmlns:domainctx="clr-namespace:PruebasDDS.Web"
<!-- .... -->
<riaControls:DomainDataSource x:Name="ddsInvoices" QueryName="GetInvoices">
<riaControls:DomainDataSource.DomainContext>
<domainctx:ChinookDomainContext />
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
<!-- .... -->
<sdk:DataGrid x:Name="grdInvoices"
ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}"
Margin="5" Height="350" />
En tiempo de ejecución, además de consultar los datos también es posible editarlos. Una vez finalizados los cambios, podemos grabarlos en la base de datos o descartarlos mediante los métodos SubmitChanges o RejectChanges del DomainDataSource, lo que en este ejemplo llevaremos a cabo desde el evento Click de sendos controles Button (listado 3).
Listado 3
<Button x:Name="btnGrabar" Content="Grabar cambios"
Width="110" Margin="5" Click="btnGrabar_Click" />
<Button x:Name="btnDeshacer" Content="Deshacer cambios"
Width="110" Margin="5" Click="btnDeshacer_Click" />
//-----------
private void btnGrabar_Click(object sender, RoutedEventArgs e)
{
this.ddsInvoices.SubmitChanges();
}
private void btnDeshacer_Click(object sender, RoutedEventArgs e)
{
this.ddsInvoices.RejectChanges();
}
Para ordenar los datos en el DomainDataSource emplearemos la etiqueta SortDescriptors, que representa una colección de objetos SortDescriptor; este tipo de objeto contiene el modo de ordenación por uno de los campos de la colección de entidades devuelta por el DomainDataSource.
En esta parte del ejemplo ordenaremos los resultados por los campos BillingCountry, Billing] City y Total (listado 4), estableciendo tambien un sentido de la ordenacion distinto para cada uno, mediante el atributo Direction de los objetos SortDescriptor.
Listado 4
<riaControls:DomainDataSource x:Name="ddsInvoices" QueryName="GetInvoices"
AutoLoad="True">
<riaControls:DomainDataSource.DomainContext>
<domainctx:ChinookDomainContext />
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.SortDescriptors>
<riaControls:SortDescriptor PropertyPath="BillingCountry" Direction="Descending" />
<riaControls:SortDescriptor PropertyPath="BillingCity" Direction="Ascending" />
<riaControls:SortDescriptor PropertyPath="Total" Direction="Descending" />
</riaControls:DomainDataSource.SortDescriptors>
</riaControls:DomainDataSource>
<!-- .... -->
<sdk:DataGrid x:Name="grdInvoices"
ItemsSource="{Binding ElementName=ddsInvoices, Path=Data}"
AutoGenerateColumns="False" Margin="5" Height="350">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Header="Factura" Binding="{Binding InvoiceId}" />
<sdk:DataGridTextColumn Header="Cliente" Binding="{Binding CustomerId}" />
<sdk:DataGridTextColumn Header="País" Binding="{Binding BillingCountry}" />
<sdk:DataGridTextColumn Header="Ciudad" Binding="{Binding BillingCity}" />
<sdk:DataGridTextColumn Header="Importe" Binding="{Binding Total}" />
</sdk:DataGrid.Columns>
</sdk:DataGrid>
Apreciaremos los campos ordenados porque en su cabecera se muestra un pequeño indicador del sentido de la ordenación (figura 3). Si bien el propio control DataGrid permite por defecto ordenar una columna haciendo clic en su cabecera, la ordenación desde el DomainDataSource, como acabamos de comprobar, presenta como ventaja la posibilidad de ordenar por más de una columna simultáneamente.

Listado 5
<StackPanel Width="300" >
<StackPanel Orientation="Horizontal">
<TextBlock Text="Orden 1" Margin="0,2,5,0"
VerticalAlignment="Center" />
<TextBox x:Name="txtOrden1" Margin="0,2,5,0" Width="100" />
<CheckBox x:Name="chkDescendente1" Content="Descendente"
Margin="0,2,10,0" VerticalAlignment="Center" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Orden 2" Margin="0,2,5,0"
VerticalAlignment="Center" />
<TextBox x:Name="txtOrden2" Margin="0,2,5,0" Width="100" />
<CheckBox x:Name="chkDescendente2" Content="Descendente"
Margin="0,2,10,0" VerticalAlignment="Center" />
</StackPanel>
<Button x:Name="btnOrdenar" Content="Ordenar" Margin="0,2,0,0"
Width="100" HorizontalAlignment="Center"
Click="btnOrdenar_Click" />
</StackPanel>
<!-- .... -->

Listado 6
private void btnOrdenar_Click(object sender, RoutedEventArgs e)
{
this.ddsInvoices.SortDescriptors.Clear();
SortDescriptor oSortDesc1 = new SortDescriptor(this.txtOrden1.Text,
this.chkDescendente1.IsChecked.Value
? System.ComponentModel.ListSortDirection.Descending
: System.ComponentModel.ListSortDirection.Ascending);
this.ddsInvoices.SortDescriptors.Add(oSortDesc1);
SortDescriptor oSortDesc2 = new SortDescriptor(this.txtOrden2.Text,
this.chkDescendente2.IsChecked.Value
? System.ComponentModel.ListSortDirection.Descending
: System.ComponentModel.ListSortDirection.Ascending);
this.ddsInvoices.SortDescriptors.Add(oSortDesc2);
}
Para poner en práctica esta característica, después añadir una página con el nombre Filtros.xaml a nuestro proyecto y configurarla como en los anteriores casos, agregaremos una combinación de filtros por tres campos de las entidades manejadas por el DomainDataSource (listado 7).
Listado 7
<riaControls:DomainDataSource x:Name="ddsInvoices"
QueryName="GetInvoices" AutoLoad="True">
<riaControls:DomainDataSource.DomainContext>
<domainctx:ChinookDomainContext />
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.FilterDescriptors>
<riaControls:FilterDescriptor PropertyPath="BillingCountry"
Operator="IsEqualTo" Value="Canada" />
<riaControls:FilterDescriptor PropertyPath="BillingCity"
Operator="IsContainedIn" Value="Halifax,Ottawa" />
<riaControls:FilterDescriptor PropertyPath="Total"
Operator="IsLessThan" Value="5" />
</riaControls:DomainDataSource.FilterDescriptors>
</riaControls:DomainDataSource>
Al poner en ejecución esta página, comprobaremos la efectividad del código que acabamos de escribir, ya que el número de registros mostrados por el DataGrid se reducirá de acuerdo con las condiciones establecidas en los filtros (figura 5).

Como hemos comprobado, existen diversos modos de combinar los filtros y sus propiedades, a fin de acotar los resultados devueltos por el DomainDataSource: un filtro único, varios filtros sobre distintos campos, una colección de valores separados por comas, etc. Pero supongamos que necesitamos crear un filtro sobre un mismo campo numérico que contemple varios valores; si intentamos poner dichos valores en una lista separada por comas obtendremos un error, aunque esto no significa que no podamos establecer dicho filtro, ya que la solución estriba en emplear un filtro independiente por cada valor, añadiendo en la declaración del control DomainDataSource el valor Or al atributo FilterOperator (listado 8).
Listado 8
<riaControls:DomainDataSource x:Name="ddsInvoices" QueryName="GetInvoices"
FilterOperator="Or" AutoLoad="True">
<riaControls:DomainDataSource.DomainContext>
<domainctx:ChinookDomainContext />
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.FilterDescriptors>
<riaControls:FilterDescriptor PropertyPath="Total" Operator="IsEqualTo" Value="3,96" />
<riaControls:FilterDescriptor PropertyPath="Total" Operator="IsEqualTo" Value="7,92" />
<riaControls:FilterDescriptor PropertyPath="Total" Operator="IsEqualTo" Value="10,90" />
</riaControls:DomainDataSource.FilterDescriptors>
</riaControls:DomainDataSource>
No obstante, los ejemplos anteriores resultan muy rígidos, ya que establecen un valor fijo para el filtro sin poder cambiarlo, mientras que lo deseable en este tipo de situación sería dar al usuario la posibilidad de introducir dicho valor.
Podemos lograr este comportamiento desde el code behind de la página, como vimos en la ordenación de datos; sin embargo, en este caso optaremos por una solución distinta, pero igualmente efectiva (listado 9), consistente en enlazar la propiedad FilterDescriptor. Value con un control de la página que sea el que proporcione el valor de filtro, por ejemplo, un TextBox; empleando el operador de filtro StartsWith, para que los datos sean filtrados dinámicamente según escribimos (figura 6).

Listado 9
<riaControls:DomainDataSource.FilterDescriptors>
<riaControls:FilterDescriptor
PropertyPath="BillingCity"
Operator="StartsWith"
Value="{Binding ElementName=txtBillingCity,
Path=Text}" />
</riaControls:DomainDataSource.FilterDescriptors>
<!-- .... -->
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="Ciudad:"
VerticalAlignment="Center" />
<TextBox x:Name="txtBillingCity" Width="200" />
</StackPanel>
<!-- .... -->

Listado 10
<riaControls:DomainDataSource x:Name="ddsInvoices"
QueryName="GetInvoices"
AutoLoad="True">
<riaControls:DomainDataSource.DomainContext>
<domainctx:ChinookDomainContext />
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.GroupDescriptors>
<riaControls:GroupDescriptor
PropertyPath="BillingCountry" />
</riaControls:DomainDataSource.GroupDescriptors>
</riaControls:DomainDataSource>
Si añadimos varios objetos GroupDescriptor (listado 11) conseguiremos un agrupamiento anidado a varios niveles (figura 8).

Listado 11
<riaControls:DomainDataSource.GroupDescriptors>
<riaControls:GroupDescriptor
PropertyPath="BillingCountry" />
<riaControls:GroupDescriptor
PropertyPath="BillingCity" />
</riaControls:DomainDataSource.GroupDescriptors>
También podemos realizar la operación de expandir/ contraer los grupos desde code behind, lo cual confiere a nuestra aplicación una mayor flexibilidad. Si por ejemplo añadimos un CheckBox a la página (listado 12), en su evento Click (listado 13) obtenemos, de la propiedad DomainDataSource.Data, los datos en forma del tipo ICollectionView. Recorriendo la colección ICollectionView. Groups, por cada uno de los objetos CollectionViewGroup que ésta contiene podremos expandir o contraer el grupo en el DataGrid llamando a los métodos CollapseRowGroup o ExpandRowGroup de este control, pasándoles como parámetro el objeto CollectionViewGroup. Previamente ejecutaremos el método DataGrid.ScrollIntoView para posicionarnos en el grupo requerido (figura 9).
Listado 12
<CheckBox x:Name="chkContraer"
Content="Contraer grupos"
HorizontalAlignment="Center"
Margin="5"
Click="chkContraer_Click" />
Listado 13
private void chkContraer_Click(object sender, RoutedEventArgs e)
{
ICollectionView oCollectionView = (ICollectionView)this.ddsInvoices.Data;
if ((bool)this.chkContraer.IsChecked)
{
foreach (CollectionViewGroup oCollectionViewGroup in oCollectionView.Groups)
{
this.grdInvoices.ScrollIntoView(oCollectionViewGroup, null);
this.grdInvoices.CollapseRowGroup(oCollectionViewGroup, true);
}
}
else
{
foreach (CollectionViewGroup oCollectionViewGroup in oCollectionView.Groups)
{
this.grdInvoices.ScrollIntoView(oCollectionViewGroup, null);
this.grdInvoices.ExpandRowGroup(oCollectionViewGroup, true);
}
}
}

Para crear un parametro utilizaremos la clase Parameter, asignando a la propiedad ParameterName el nombre del parametro perteneciente al metodo, mientras que en la propiedad Value asignaremos el valor que recibira el parametro. Los parametros tendran que estar contenidos dentro de la coleccion DomainDataSource.QueryParameters.
Para ilustrar el uso de parámetros mediante un ejemplo, añadiremos al servicio de dominio el método GetInvoicesBillingCountry (listado 14), que recibe un parámetro con el que devuelve las entidades Invoice que tengan dicho valor en la propiedad BillingCountry. A continuación, añadiremos al proyecto la página Parametros.xaml, agregando a ésta un DomainDataSource con el nombre del método recién creado en la propiedad QueryName, y la definición de un parámetro para dicho método (listado 15). Los datos resultantes se ajustarán al valor del parámetro (figura 10).
Listado 14
public IQueryable<Invoice> GetInvoicesBillingCountry(string sBillingCountry)
{
return this.ObjectContext.Invoices.Where(oInvoice => oInvoice.BillingCountry == sBillingCountry);
}
Listado 15
<riaControls:DomainDataSource x:Name="ddsInvoices" QueryName="GetInvoicesBillingCountry" AutoLoad="True">
<riaControls:DomainDataSource.DomainContext>
<domainctx:ChinookDomainContext />
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.QueryParameters>
<riaControls:Parameter ParameterName="sBillingCountry" Value="Canada" />
</riaControls:DomainDataSource.QueryParameters>
</riaControls:DomainDataSource>

Por otro lado, al igual que ocurre con los filtros, mediante una expresión de enlace a datos (listado 16) podemos conseguir que el usuario, a través de un control de la página, sea quien introduzca el valor del parámetro (figura 11).
Listado 16
<riaControls:DomainDataSource x:Name="ddsInvoices"
QueryName="GetInvoicesBillingCountry"
AutoLoad="True">
<riaControls:DomainDataSource.DomainContext>
<domainctx:ChinookDomainContext />
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.QueryParameters>
<riaControls:Parameter
ParameterName="sBillingCountry"
Value="{Binding
ElementName=txtBillingCountry,Path=Text}" />
</riaControls:DomainDataSource.QueryParameters>
</riaControls:DomainDataSource>
<!-- .... -->
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center">
<TextBlock Text="País" Margin="5" />
<TextBox x:Name="txtBillingCountry" Width="200"
Margin="5" />
</StackPanel>

En las propiedades PageSize y LoadSize (listado 17), pertenecientes al DomainDataSource, estableceremos respectivamente la cantidad de elementos a mostrar por página y la cantidad de elementos a cargar. El DataPager se encargará de proporcionar la interfaz de usuario para navegar por las páginas mostradas en el DataGrid. Precisamente por el hecho de utilizar el DataPager, hemos de definir un orden en el DomainDataSource para que la paginación funcione adecuadamente, debido a que Entity Framework no soporta paginación si no tiene establecido previamente un orden sobre los datos (figura 12).
Listado 17
<riaControls:DomainDataSource x:Name="ddsInvoices"
QueryName="GetInvoices" AutoLoad="True"
PageSize="10" LoadSize="30">
<riaControls:DomainDataSource.DomainContext>
<domainctx:ChinookDomainContext />
</riaControls:DomainDataSource.DomainContext>
<riaControls:DomainDataSource.SortDescriptors>
<riaControls:SortDescriptor
PropertyPath="InvoiceId" />
</riaControls:DomainDataSource.SortDescriptors>
</riaControls:DomainDataSource>
<!-- .... -->
<sdk:DataGrid x:Name="grdInvoices"
<!-- .... -->
<sdk:DataPager x:Name="pgrInvoices"
Source="{Binding ElementName=ddsInvoices,Path=Data}"
Width="200" />
