[Windows Phone 7.5] Creando un DataContextProxy

Hola a todos!

Hoy vamos a ver un tema muy interesante y que puede salvarnos en alguna ocasión. Hablamos del DataContextProxy pero… ¿Qué es esto Yeray? Vamos allá!

¿Qué es un DataContextProxy y para que sirve?

Cuando creamos una aplicación Windows Phone usando el patrón MVVM (esto es, siempre. Porque siempre usamos MVVM ¿Verdad?) Establecemos nuestra ViewModel en la propiedad DataContext de nuestra View, de forma que todos nuestros controles lo usan como fuente de datos y nos permite enlazarnos a las propiedades de la VievModel, algo así:

image

Un caso muy simple, un ComboBox que mediante enlace a datos y apoyándose en el DataContext de la View accede a una propiedad expuesta por el ViewModel. No existe ninguna complejidad en este escenario.

Ahora, vamos a imaginar un caso más complejo: Tenemos el ComboBox incluido como parte de un DataTemplate de un ListBox:

image

Con esta configuración, el DataContext del ComboBox corresponde a cada Item del ListBox, es decir, una instancia de la clase Customer, por lo que no podemos acceder a las propiedades expuestas por la ViewModel. Esta situación es muy común, por ejemplo si queremos tener un botón como parte de un DataTemplate y que ese botón use un ICommand expuesto en nuestra ViewModel. Normalmente, podríamos acceder al DataContext de la página, usando la propiedad del Binding ElementName:

<ListBox Name="lstCustomers" ItemsSource="{Binding Customers}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding CustomerName}"></TextBlock>
                <Button Content="Show" Command="{Binding DataContext.ShowCustomerCommand, ElementName=lstCustomers}"></Button>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Esto es totalmente correcto y nos permitiría acceder a la propiedad DataContext del elemento lstCustomers, con lo que ya tendríamos acceso a la ViewModel, pero no es algo que me parezca demasiado elegante de usar. Como las expresiones de enlace a datos no se validan en tiempo de diseño, a no ser que enlacemos con un StaticResource donde se valida su nombre, si cambiamos el nombre de la lista, aunque sea en una mayúscula… no lo encontrará y pasaremos un buen rato buscando el porqué no funciona nuestro comando.

Es aquí donde entra en juego el DataContextProxy. Básicamente se trata de un objeto que hereda de FrameworkElement (la clase base que incorpora a los controles, entre otras, la funcionalidad DataContext) y que es capaz de exponer mediante una propiedad el DataContext de su objeto padre, aprovechándonos de la herencia de propiedades que tiene FrameworkElement:

image

 

Siempre y cuando no establezcamos directamente la propiedad DataContext de un elemento, se heredará el valor de su contenedor, es por esto que podemos establecer la propiedad DataContext solo en la View y no tenemos que ir estableciéndola en cada control de la página.

Nuestro DataContextProxy se aprovechará de esta característica para exponer a través de una propiedad el DataContext de nuestra página. Podremos incluirlo en la misma como un recurso y acceder a el mediante la expresión StaticResource, lo que nos dará validación del nombre fuente en tiempo de diseño.

Muy bien, ¿Como lo implementamos?

Lo mejor de todo es que crear y usar un DataContextProxy es realmente trivial y sencillo. Lo primero que necesitamos es crear una nueva clase en nuestro proyecto Windows Phone que herede de FrameworkElement:

public class DataContextProxyService : FrameworkElement
{
    public DataContextProxyService()
    {
        this.Loaded += new RoutedEventHandler(DataContextProxyService_Loaded);
    }

    void DataContextProxyService_Loaded(object sender, RoutedEventArgs e)
    {
    }
}

Lo primero que haremos en el constructor de nuestra clase es manejar el evento Loaded para poder ejecutar el código necesario cuando nuestro servicio haya sido inicializado por la página que lo contenga.

A continuación vamos a crear una DependencyProperty que será el contenedor a través del cual expondremos nuestro DataContext:

public static readonly DependencyProperty DataSourceProperty =
    DependencyProperty.Register("DataSource", typeof(Object), typeof(DataContextProxyService), null);

public Object DataSource
{
    get { return (Object)GetValue(DataSourceProperty); }
    set { SetValue(DataSourceProperty, value); }
}

Ahora solo nos queda un último paso: Cuando recibamos el evento Loaded, vamos a crear un Binding que exponga nuestro DataContext a través de la propiedad DataSource:

void DataContextProxyService_Loaded(object sender, RoutedEventArgs e)
{
    Binding binding = new Binding();
    binding.Source = this.DataContext;
    this.SetBinding(DataContextProxyService.DataSourceProperty, binding);
}

Y con esto ya tenemos terminada nuestra clase DataContextProxy, ahora vamos a probarla, para empezar vamos a crear una página que tenga un Listbox con un DataTemplate que muestre un TextBlock con el nombre de un Item y un Button:

<ListBox ItemsSource="{Binding Customers}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBlock Text="{Binding Name}"></TextBlock>
                <Button Content="Click Me!"
                        Command="{Binding SayhelloCommand}"></Button>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Vamos a enlazar el DataContext de nuestra página a una ViewModel que contenga una Lista de Customers y un comando que se ejecute al presionar el botón:

public class VMMainPage : INotifyPropertyChanged
{
    private List<Customer> customers;
    public List<Customer> Customers 
    {
        get
        {
            return customers;
        }
        set 
        {
            customers = value;
            RaisePropertyChanged("Customers");
        }
    }

    private ICommand sayHelloCommand = null;
    public ICommand SayHelloCommand
    { 
        get
        {
            if (sayHelloCommand == null)
                sayHelloCommand = new DelegateCommand(() => { MessageBox.Show("Hello!"); });
            return sayHelloCommand;
        }
    }

    public VMMainPage()
    {
        var newCustomers = new List<Customer>()
                                        {
                                            new Customer() { Name = "Yeray" },
                                            new Customer() { Name = "Vicenç"},
                                            new Customer() { Name = "Ibon"}
                                        };

        Customers = newCustomers;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged(string propertyName)
    { 
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Si ejecutamos la aplicación veremos el siguiente resultado, el nombre del customer se muestra correctamente pero el comando no funciona:

image

Si miramos la ventana de Output veremos los siguientes errores:

image

Efectivamente, el sistema de enlace de Silverlight está buscando el comando SayHelloCommand dentro de cada item de la lista, por supuesto no lo encuentra. Aquí es donde entra en acción nuestro DataContextProxy: Lo primero es añadir una referencia a nuestro ensamblado en la página para que podamos acceder a los objetos de nuestra aplicación:

xmlns:dataproxy="clr-namespace:WP75DataContextProxy"

A continuación, vamos a definir una instancia de nuestra clase DataContextProxyService como recurso de la página en la que nos encontramos:

<phone:PhoneApplicationPage.Resources>
    <dataproxy:DataContextProxyService x:Key="DataProxy"></dataproxy:DataContextProxyService>
</phone:PhoneApplicationPage.Resources>

Y por último vamos a modificar el Binding del comando en el botón para que haga uso de la propiedad DataSource de nuestro DataContextProxyService:

<Button Content="Click Me!"
        Command="{Binding DataSource.SayHelloCommand, Source={StaticResource DataProxy}}">
</Button>

Esta vez, si iniciamos la aplicación y presionamos alguno de los botones, el resultado será distinto, el sistema de enlace a datos encontrará el comando en nuestro DataContext principal, el asignado a la ventana y no lo buscará en cada Item:

image

 

Conclusión

Usando esta técnica tenemos una forma sencilla y clara de poder obtener nuestro DataContext principal dentro de listas, combos, plantillas y otros elementos. Todo esto sin tener que referenciar elementos por nombre, usando un recurso estático y sin depender de elementos que mañana pueden no ser necesarios y puedan ser eliminados de la aplicación. Como siempre aquí os dejo el código de ejemplo que he usado en el artículo.

Espero que os sea muy útil. Happy Coding!

Published 28/3/2012 10:00 por Josué Yeray Julián Ferreiro
Comparte este post:

Comentarios

# re: [Windows Phone 7.5] Creando un DataContextProxy

Thursday, March 29, 2012 12:22 PM por Omar del Valle Rodríguez

Hola Josue...

Cuando me encontré con este problema lo resolví así:

Command="{Binding Main.MyCommand, Source={StaticResource Locator}}"

Main es el ViewModel que inyecta el Locator a la vista por IoC...

¿Ventajas? ¿Desventajas? ¿Es lo mismo?

Gracias! :)

# re: [Windows Phone 7.5] Creando un DataContextProxy

Thursday, March 29, 2012 12:58 PM por Josué Yeray Julián Ferreiro

Hola Omar

En tu caso al usar el Locator como un StaticResource para resolver tu viewmodel, lo tienes perfectamente resuelto, siempre y cuando tengas controlado que no se devuelva una nueva instancia de la viewmodel cuando vuelves a llamar al locator.

La ventaja de esto es que, si tuvieses varias controles anidados con distintos DataContext, podrías introducir el DataContextProxy en la jerarquía y obtener el DataContext de su padre, sin que tenga que ser exactamente la viewmodel.

Un saludo campeón!

# re: [Windows Phone 7.5] Creando un DataContextProxy

Thursday, March 29, 2012 1:31 PM por Omar del Valle Rodríguez

Creo tener algún test por ahí al estilo: .Verify(loc => .....), Times.Once());

De todas maneras me lo miro porque no había caido en que eso podría crear una nueva instancia del ViewModel... :)

Gracias mil y gran post!!! :)