Editando Datos en un DataTemplate

En el anterior post vimos como cambiar el DataTemplate dinámicamente, en este vamos a ver como podemos hacer que el usuario pueda editar datos y modificarlos. Siguiendo con la aplicación de ejemplo imaginar que le ponemos un campo de rating de la persona y que el usuario pueda cambiar este rating, pulsando a un botón de editar.

 

image

De manera que cuando pulsemos editar podamos cambiar el Rating de la persona. El control de rating es muy sencillo, crearemos un control de usuario cuyo xaml seran las 5 estrellas

<UserControl x:Class="WPFDataTemplates.FiveStarRating"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >
    <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
        <StackPanel Orientation="Horizontal">
            <Path x:Name="Star1" Margin="0,15,5,0" Stroke="Black" StrokeThickness="2"  StrokeLineJoin="Round" Data="M 0,0 l 10,0 l 5,-10 l 5,10 l 10,0 l -7,10 l 2,10 l -10,-5 l -10,5 l 2,-10 Z" />
            <Path x:Name="Star2" Margin="0,15,5,0" Stroke="Black" StrokeThickness="2"  StrokeLineJoin="Round" Data="M 0,0 l 10,0 l 5,-10 l 5,10 l 10,0 l -7,10 l 2,10 l -10,-5 l -10,5 l 2,-10 Z" />
            <Path x:Name="Star3" Margin="0,15,5,0" Stroke="Black" StrokeThickness="2"  StrokeLineJoin="Round" Data="M 0,0 l 10,0 l 5,-10 l 5,10 l 10,0 l -7,10 l 2,10 l -10,-5 l -10,5 l 2,-10 Z" />
            <Path x:Name="Star4" Margin="0,15,5,0" Stroke="Black" StrokeThickness="2"  StrokeLineJoin="Round" Data="M 0,0 l 10,0 l 5,-10 l 5,10 l 10,0 l -7,10 l 2,10 l -10,-5 l -10,5 l 2,-10 Z" />
            <Path x:Name="Star5" Margin="0,15,5,0" Stroke="Black" StrokeThickness="2"  StrokeLineJoin="Round" Data="M 0,0 l 10,0 l 5,-10 l 5,10 l 10,0 l -7,10 l 2,10 l -10,-5 l -10,5 l 2,-10 Z" />
        </StackPanel>
    </Canvas>

</UserControl>

 

Crearemos una DependencyProperty donde le pasaremos el rating y rellenara las estrellas dependiendo del valor

 

  public partial class FiveStarRating : UserControl
    {
        public int Rating
        {
            get { return (int)this.GetValue(RatingProperty); }
            set
            {
                if (value > 5)
                    value = 5;
                if (value < 0)
                    value = 0;

                this.SetValue(RatingProperty, value);
            }
        }

        public static readonly DependencyProperty RatingProperty = DependencyProperty.Register("Rating",
                    typeof(int), typeof(FiveStarRating),
                    new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(RatingValueChanged)
                    ));

        public FiveStarRating()
        {
            InitializeComponent();


        }

        private static void RatingValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {

            FiveStarRating x = sender as FiveStarRating;
            int ratingValue = (int)e.NewValue;

            for (int i = 1; i <= 5; i++)
            {
                Path clearStar = (Path)x.FindName("Star" + i);
                clearStar.Fill = new SolidColorBrush(Colors.Gray);
            }
            for (int i = 1; i <= ratingValue; i++)
            {

                Path starFilled = (Path)x.FindName("Star" + i);
                starFilled.Fill = new SolidColorBrush(Colors.Goldenrod);

            }

        }
    }

 

 

Una vez que tenemos el control añadimos al objeto persona la propiedad de rating

 private int _imageRating;
        public int ImageRating
        {
            get { return _imageRating; }
            set { _imageRating = value; }
        }

Creamos un converter para el Binding del rating ya que para mostrar el rating vamos a utilizar un label en el cual enlazaremos la propiedad Content del label con la propiedad ImageRating de la persona y a través del converter devolveremos el control de usuario de rating creado anteriormente. Tranquilos viendo el código se entiende.

El DataTemplate quedaria

 

 <DataTemplate x:Key="TemplatePerson">
        <DataTemplate.Resources>
            <LinearGradientBrush x:Key="backBrush" StartPoint="0,0.5" EndPoint="1,0.5">
                <GradientStop Color="#1100CC22" Offset="0" />
                <GradientStop Color="#8800CC22" Offset="0.97" />
                <GradientStop Color="#AA10FF18" Offset="0.999" />
                <GradientStop Color="#44FFFFFF" Offset="1" />
            </LinearGradientBrush>
            <local:PersonImageConverter x:Key="imageConverter" />
        </DataTemplate.Resources>

        <Border x:Name="personsBorder" Style="{StaticResource PersonBorderStyle}" Width="450">
            <Grid>
                <StackPanel Orientation="Horizontal">

                    <Image Width="40" Height="40" 
                           Source="{Binding Path=ImageRef, 
                        Converter={StaticResource imageConverter}}">
                        <Image.BitmapEffect>
                            <DropShadowBitmapEffect />
                        </Image.BitmapEffect>

                    </Image>

                    <TextBlock x:Name="personName" 
                               Text="{Binding Name}"  
                               Padding="15,15"
                               Foreground="Black" />
                    <Label Content="{Binding Converter={StaticResource RatingConverter}}" Width="200" />
                    <Button Height="30"  Command="local:MyCommands.ChangeRating" Content="Editar"
                                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}">
                       
                    </Button>


                </StackPanel>
            </Grid>
        </Border>
    </DataTemplate>

Si nos fijamos en las nuevas líneas

 

<Label Content="{Binding Converter={StaticResource RatingConverter}}" Width="200" />

<Button Height="30" Command="local:MyCommands.ChangeRating" Content="Editar" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}">

Observamos el Converter en el binding sobre el content del label , este converter es muy sencillo

public class RatingConverter : IValueConverter
    {
        #region IValueConverter Members

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            //in order to handle design time problems, handle null value case
            if (value == null)
                return new FiveStarRating();

            Person obj = (Person)value;

            FiveStarRating uc = new FiveStarRating();
            uc.Rating = (int)obj.ImageRating;

            return uc;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            FiveStarRating obj = (FiveStarRating)value;
            return obj.Rating;
        }

        #endregion
    }

La segunda línea importante es el Botón de edición al cual en vez de utilizar el evento Click, no podríamos porque estamos en un fichero de recursos, utilizamos un comando en este caso MyCommandsChangeRating

public class MyCommands
    {
        public static RoutedCommand ChangeRating =
     new RoutedCommand("ChangeRating", typeof(WpfWindow));

    }

En la ventana creamos el binding al comando

 <Window.CommandBindings>
    <CommandBinding Command="local:MyCommands.ChangeRating" Executed="OnChangeRating" />
    </Window.CommandBindings>

 

De manera que cuando se pulse un botón se ejecute el método OnChangeRating con el parámetro del item de la lista donde esta el botón y cambiaremos el DataTemplate para que el usuario pueda meter el Rating

 

private void OnChangeRating(object sender, ExecutedRoutedEventArgs e)
        {

            ListBoxItem itm = (ListBoxItem)e.Parameter;
            itm.ContentTemplate = this.FindResource("TemplatePersonEdit") as DataTemplate;
        }

 

El DataTemplate en este caso

 

<DataTemplate x:Key="TemplatePersonEdit">
        <DataTemplate.Resources>
            <LinearGradientBrush x:Key="backBrush" StartPoint="0,0.5" EndPoint="1,0.5">
                <GradientStop Color="#1100CC22" Offset="0" />
                <GradientStop Color="#8800CC22" Offset="0.97" />
                <GradientStop Color="#AA10FF18" Offset="0.999" />
                <GradientStop Color="#44FFFFFF" Offset="1" />
            </LinearGradientBrush>
            <local:PersonImageConverter x:Key="imageConverter" />
        </DataTemplate.Resources>

        <Border x:Name="personsBorder" Style="{StaticResource PersonBorderStyle}" BorderThickness="8" CornerRadius="5" BorderBrush="#FF201F1F" Width="450">
            <Grid>
                <StackPanel Orientation="Horizontal">

                    <Image Width="60" Height="60" 
                           Source="{Binding Path=ImageRef, 
                        Converter={StaticResource imageConverter}}">
                        <Image.BitmapEffect>
                            <DropShadowBitmapEffect />
                        </Image.BitmapEffect>

                    </Image>

                    <TextBlock x:Name="personName" 
                               Text="{Binding Name}"  
                               Padding="15,15"
                               Foreground="Black" FontSize="20" />
                    <TextBox  Height="30"   FontSize="20" Foreground="Blue" Text="{Binding Path=ImageRating}" HorizontalAlignment="Left" />

                    <Button Height="30"  Margin="10,0,0,0" Command="local:MyCommands.SaveRating" Content="Salvar"
                                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}">
                    </Button>

                </StackPanel>
            </Grid>
        </Border>
    </DataTemplate>

 

Quedándonos

 

image

 

En el botón salvar haríamos los mismos pasos que con el de editar, crear un comando, enlazarlo….

 

El código lo podéis bajar, por cierto tiene un comportamiento extraño cuando….?

 

Animo que queda poco para las vacaciones!!!!!!!

 

Cambiar el DataTemplate de un ListBox con DataTrigger

En el anterior post Sergi se dio cuenta que era mejor realizar el cambio del DataTemplate a través de un DataTrigger, en el anterior el cambio del DataTemplate en el ListBox se realizaba a través del evento SelectionChanged y la propiedad ItemTemplateSelector había un problema que detecto Julio y es que se producía un comportamiento raro con las teclas de subir y bajar, realmente no funcionaba bien.

Ele ejemplo lo puse porque cuando vienes del desarrollo tradicional lo primero que piensas es en hacer todo con eventos como hacíamos con WinForms, pero ahora con WPF tenemos que cambiar de mentalidad y aprovecharnos de toda la potencia del XAML, en este caso con DataTriggers.

Para enseñarlo no voy a utilizar el ejemplo del anterior post sino un nuevo ejemplo con dos listas una con SelectionChanged y la otra con DataTrigger, el otro ejemplo lo dejo para que lo cambies vosotros a modo de ejercicio.

La aplicación es fea, habría que cambiarla con MultiBinding… solo quiero demostrar lo de este post

 

image

En este caso el XAML de los DataTemplates de SelectionChanged es

 

 

 <DataTemplate x:Key="PeopleTemplateSimple">
            <StackPanel Orientation="Horizontal" Background="AliceBlue">
                <TextBlock Text="{Binding Path=FirstName}"/>
                <TextBlock Text="{Binding Path=LastName}"/>
            </StackPanel>
        </DataTemplate>

        <DataTemplate x:Key="PeopleTemplateComplex">
            <Border BorderBrush="#FFE7FF12" BorderThickness="5,0,5,0" CornerRadius="5,5,5,5" Width="Auto" Height="61.34" Margin="0,0,0,10">
                <Border.Background>
                    <LinearGradientBrush EndPoint="0.02,0.5" StartPoint="0.982,0.5">
                        <GradientStop Color="#FF5C3610" Offset="0"/>
                        <GradientStop Color="#FFC88848" Offset="1"/>
                    </LinearGradientBrush>
                </Border.Background>
                <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.39*"/>
                        <ColumnDefinition Width="0.61*"/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="0.343*"/>
                        <RowDefinition Height="0.327*"/>
                        <RowDefinition Height="0.33*"/>
                    </Grid.RowDefinitions>
                    <TextBlock Margin="0,0,4,0" Text="Nombre Completo" TextWrapping="Wrap" HorizontalAlignment="Right" VerticalAlignment="Center"/>
                    <TextBlock Margin="0,0,4,0" Grid.Row="1" Text="SSN" TextWrapping="Wrap" HorizontalAlignment="Right" VerticalAlignment="Center"/>
                    <TextBlock HorizontalAlignment="Right" Margin="0,1.951,4,2.331" VerticalAlignment="Stretch" Text="Fecha" TextWrapping="Wrap" Grid.Row="2" d:LayoutOverrides="Height"/>
                    <TextBlock Grid.Column="1" Text="{Binding Path=FirstName, Mode=Default}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
                    <TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding Path=SSN, Mode=Default}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
                    <TextBlock Grid.Column="1" Grid.Row="2" Text="{Binding Path=DateOfBirth, Mode=Default}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
                </Grid>
            </Border>
        </DataTemplate>

Como en el anterior post, dos DataTemplates uno para el item sin selección y otro mas completo para el item seleccionado

El DataTemplate para el DataTrigger es

 

 

<ControlTemplate x:Key="PeopleTemplateSimple2">
            <StackPanel Orientation="Horizontal" Background="AliceBlue">
                <TextBlock Text="{Binding Path=FirstName}"/>
                <TextBlock Text="{Binding Path=LastName}"/>
            </StackPanel>
        </ControlTemplate>

        <ControlTemplate x:Key="PeopleTemplateComplex2">
            <Border BorderBrush="#FFE7FF12" BorderThickness="5,0,5,0" CornerRadius="5,5,5,5" Width="Auto" Height="61.34" Margin="0,0,0,10">
                <Border.Background>
                    <LinearGradientBrush EndPoint="0.02,0.5" StartPoint="0.982,0.5">
                        <GradientStop Color="#FF5C3610" Offset="0"/>
                        <GradientStop Color="#FFC88848" Offset="1"/>
                    </LinearGradientBrush>
                </Border.Background>
                <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.39*"/>
                        <ColumnDefinition Width="0.61*"/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="0.343*"/>
                        <RowDefinition Height="0.327*"/>
                        <RowDefinition Height="0.33*"/>
                    </Grid.RowDefinitions>
                    <TextBlock Margin="0,0,4,0" Text="Nombre Completo" TextWrapping="Wrap" HorizontalAlignment="Right" VerticalAlignment="Center"/>
                    <TextBlock Margin="0,0,4,0" Grid.Row="1" Text="SSN" TextWrapping="Wrap" HorizontalAlignment="Right" VerticalAlignment="Center"/>
                    <TextBlock HorizontalAlignment="Right" Margin="0,1.951,4,2.331" VerticalAlignment="Stretch" Text="Fecha" TextWrapping="Wrap" Grid.Row="2" d:LayoutOverrides="Height"/>
                    <TextBlock Grid.Column="1" Text="{Binding Path=FirstName, Mode=Default}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
                    <TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding Path=SSN, Mode=Default}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
                    <TextBlock Grid.Column="1" Grid.Row="2" Text="{Binding Path=DateOfBirth, Mode=Default}" TextWrapping="Wrap" HorizontalAlignment="Left" VerticalAlignment="Center"/>
                </Grid>
            </Border>
        </ControlTemplate>

Lo mismo pero en este caso cambiamos el DataTemplate por ControlTemplate que permite cambiar la plantilla visual de un control de WPF, es decir de cualquier control que herede de Control, que es donde está definida la propiedad Template

Para nuestro caso hacemos el DataTemplate

 <DataTemplate x:Key="PeopleTemplateUsingTriggers">
            <Control x:Name="theControl" Focusable="False" Template="{StaticResource PeopleTemplateSimple2}" />
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" Value="True">
                    <Setter TargetName="theControl" Property="Template" Value="{StaticResource PeopleTemplateComplex2}" />
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>

Si os fijáis tenemos un control al cual le aplicamos el Template simple, pero tenemos un DataTrigger que enlazamos con el Item de la lista y la propiedad IsSelected del item, si es true cambiamos el Template del control por el complejo, bastante mas sencillo y eficaz que subscribirnos al evento SelectionChanged

Por ultimo la lista quedaría

 

 <ListBox Margin="0,10,0,0" x:Name="PeopleListBox2"  Background="#FFF9F0C3" 
                 ItemsSource="{Binding Path=People, Mode=Default}" 
                 ItemTemplate="{StaticResource PeopleTemplateUsingTriggers}" Height="300" />

La conclusión que tenemos que sacar es que al cambiarnos a WPF debemos de sacar el máximo rendimiento al XAML y olvidarnos un poco de la manera de programa WinForms, os animo a que hagáis el ejemplo del post anterior

Cambiar el Template del item seleccionado en una lista de WPF

Otra de las preguntas recurrentes de la gente es en la listas, combox, items control como cambiar el aspecto de la información que se muestra cuando el usuario selecciona una fila. Esto es muy sencillo de realizar en WPF. Para hacer esto lo primero que intenta la gente es subscribirse al evento SelectionChanged de la lista y cambiar un diferente DataTemplate, pero esto no funciona ya que cambia a toda la lista.

Para realizar esto debemos basarnos en la clase System.Windows.Controls.DataTemplateSelector, debemos de crear una clase que herede de esta y sobrescribir el método SelectTemplate. Basándonos en la aplicación del post DataTemplates en WPF, creamos la clase PersonDataTemplateSelector y heredamos de DataTemplateSelector

public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
           

 

En este método como vemos tenemos el parámetro item, que nos va a pasar el objeto seleccionado, la propiedad container que es ContentPresenter de la lista.  En este método lo que tenemos que implementar es cambiar el DataTemplate dependiendo si el item esta seleccionado o no, yo he creado en recursos un nuevo DataTemplate para cuando este seleccionado, mostrando la imagen y el texto mas grande

<DataTemplate x:Key="TemplatePersonSelect">
        <DataTemplate.Resources>
            <LinearGradientBrush x:Key="backBrush" StartPoint="0,0.5" EndPoint="1,0.5">
                <GradientStop Color="#1100CC22" Offset="0" />
                <GradientStop Color="#8800CC22" Offset="0.97" />
                <GradientStop Color="#AA10FF18" Offset="0.999" />
                <GradientStop Color="#44FFFFFF" Offset="1" />
            </LinearGradientBrush>
            <local:PersonImageConverter x:Key="imageConverter" />
        </DataTemplate.Resources>

        <Border x:Name="personsBorder" Style="{StaticResource PersonBorderStyle}" BorderThickness="8" CornerRadius="5" BorderBrush="#FF201F1F" Width="450">
            <Grid>
                <StackPanel Orientation="Horizontal">

                    <Image Width="60" Height="60" 
                           Source="{Binding Path=ImageRef, 
                        Converter={StaticResource imageConverter}}">
                        <Image.BitmapEffect>
                            <DropShadowBitmapEffect />
                        </Image.BitmapEffect>

                    </Image>

                    <TextBlock x:Name="personName" 
                               Text="{Binding Name}"  
                               Padding="15,15"
                               Foreground="Black" FontSize="20" />

                </StackPanel>
            </Grid>
        </Border>
    </DataTemplate>

Una vez que tenemos el DataTemplate implementamos el método SelectTemplate

 

public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
        {
            Person person = item as Person;
            DataTemplate dataTemplate = null;
            if (person != null)
            {
                Window window = System.Windows.Application.Current.MainWindow;
                ListBox list = window.FindName("personItems") as ListBox;

                Person selectedPerson = list.SelectedItem as Person;
                ContentPresenter pres = container as ContentPresenter;

                if (selectedPerson != null && selectedPerson.Name == person.Name)
                    dataTemplate = pres.FindResource("TemplatePersonSelect") as DataTemplate;
                else
                    dataTemplate = pres.FindResource("TemplatePerson") as DataTemplate;
            }
            return dataTemplate;
        }

 

 

Como podéis observar si la persona seleccionada es la misma que el item enviado por parámetro devolvemos el DataTemplate que queremos para los objetos seleccionado, sino el de por defecto, esto no solo sirve para los seleccionados, sino por ejemplo si la comprobación hubiera sido que el nombre fuese Oscar o lo que queramos, este método nos permite cambiar el DataTemplate dinámicamente.

Ahora lo que tenemos que hacer es implementar el using de XAML para poder referenciar la clase PersonDataTemplateSelector , en nuestra aplicacion

 

xmlns:local="clr-namespace:WPFDataTemplates"

Crear el recurso para el DataTemplate Selector

 <Window.Resources>
        <local:PersonDataTemplateSelector x:Key="PersonTemplateSelector" />
    </Window.Resources>

 

Implementarlo en la lista

<ListBox x:Name="personItems" Grid.Row="1" 
            ItemTemplateSelector="{StaticResource PersonTemplateSelector}"
            SelectionChanged="personItems_SelectionChanged"
            HorizontalAlignment="Stretch"   
            Margin="10" 
            VerticalAlignment="Center"
        />

Nos tenemos que fijar en dos cosas, primero debemos de  tener en cuenta es que no podemos tener asignada la propiedad ItemTemplate que especificabamos en el post anterior junto con ItemTemplateSelector. Segundo debemos de implementar el evento SelectionChanged que en nuestro caso seria

private void personItems_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            personItems.ItemTemplateSelector = new PersonDataTemplateSelector();
        }

 

La aplicación nos quedaría:

 

image

DataTemplates en WPF

Los DataTemplates en WPF nos van a permitir definir uListViewna plantilla para una clase de datos, es decir como vamos a mostrar visualmente al usuario. Generalmente se suelen utilizar en los controles de listas como son ItemsControl, ListBox, ListView…

Si no utilizamos nuestro propio DataTemplate en estos controles el tiene por defecto como DataTemplate un TextBlock. Si enlazamos un objeto complejo como Persona llamará a ToString() de esta clase y lo enlazara al TetBloxk por defecto, mostrandonos

 

image

Pero donde definimos el DataTemplate?, esta definición puede estar en varios sitios en el propio control como por ejemplo

 

<ListBox ItemsSource="{Binding}" BorderBrush="Transparent" 
         Grid.IsSharedSizeScope="True"
         HorizontalContentAlignment="Stretch">
    <ListBox.ItemTemplate>
        <DataTemplate>
            
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Como un recurso, en el cual se le dice el tipo de objeto que se va a enlazar

<DataTemplate x:Key="itemsTemplate">
        <Border x:Name="personsBorder" Style="{StaticResource PersonBorderStyle}">
            <Grid>
                <StackPanel Orientation="Horizontal">

                                     <Image Width="40" Height="40" 
                           Source="{Binding Path=ImageRef, 
                        Converter={StaticResource imageConverter}}">
                        <Image.BitmapEffect>
                            <DropShadowBitmapEffect />
                        </Image.BitmapEffect>

                    </Image>

                    <TextBlock x:Name="personName" 
                               Text="{Binding Name}"  
                               Padding="15,15"
                               Foreground="Black" />
        
                </StackPanel>
            </Grid>
        </Border>
    </DataTemplate>

A la hora de asociarlo a la lista es como cualquier recurso en este caso a la propiedad ItemTemplate


 
<ItemsControl x:Name="personItems" Grid.Row="1" 
            HorizontalAlignment="Stretch"   
            Margin="10" 
      ItemTemplate="{StaticResource itemsTemplate}" 
      VerticalAlignment="Center"
        />
 

 

Aquí tenéis un ejemplo en el que mostramos una listado de personas y de ha definido un DataTemplate

 

image

 

Primera aplicación en Gestalt

El otro día Eugenio nos presento Gestalt Beta, azúcar para la web una nueva herramienta en beta que te permite escribir código Ruby, Python y XAML en tus páginas (X)HTML. hoy me encuentro que Karsten Januszewski ha escrito un post write up on his XAML Playground en el que nos muestra una aplicación para diseñar escrita en silverligth utilizando Gestalt

 

 

En channel9 tenéis un video de la explicación del proyecto http://channel9.msdn.com/posts/ContinuumNews/XAML-Playground-A-Designer-built-in-Silverlight-using-Gestalt/

Evento DoubleClick en WPF

Una pregunta de los foros preguntaba porque no podía coger el evento DoubleClick en WPF en un objeto TextBox. La razón es que en Winforms todos los controles que derivaban de  System.Windows.Forms.Control tenían el evento DoubleClick y en WPF los elementos que derivan de System.Windows.Controls.Control también lo tienen, pero no todos los controles en WPF derivan de esta clase y TextBlock es uno de ellos y no tiene el evento DoubleClick.

Para solucionar esto utilizamos un pequeño truco, en nuestro TextBlock recogemos el evento MouseDown y en este evento ponemos las siguientes líneas

 

 private void MyTextBlock_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ClickCount == 2)
            {
                Console.WriteLine("DocubleClick!!!");
            }
        }

Sencillo!!!!

Multithreading en WPF: BackgroundWorker y como comunicarse con la Interfaz de Usuario

Muchas veces en nuestras aplicaciones necesitamos realizar tareas largas que deben ser ejecutados en un thread separado, pero al mismo tiempo mostrar al usuario una barra de progreso o cualquier tipo de notificación para que el usuario sienta que la aplicación no ha dejado de funcionar sino que esta ejecutando una tarea larga y que además puede abortar esa tarea.

Para ello utilizaremos la clase BackgroundWorker, La clase BackgroundWorker permite ejecutar una operación en un subproceso dedicado e independiente, pudiendo el usuario interactuar con la interfaz de usuario mientras se ejecuta la tarea.

Para lanzar una tarea en otro thread utilizaremos el evento DoWork de la clase BackgroundWorker, el código en el event handler del evento DoWork se jecutara en un thread diferente

int maxRecords = 1000;
 
BackgroundWorker worker = new BackgroundWorker();
 
worker.DoWork += delegate(object s, DoWorkEventArgs args)
{
    for (int x = 1; x < maxRecords; x++)
    {
        System.Threading.Thread.Sleep(10);
    }
};
 
worker.RunWorkerAsync();

 

De esta manera lanzaremos en otro Thread totalmente diferentes el recorrido de los registros

Si queremos pasar parámetros a la tarea , nosotros podemos pasar esos parámetros al método RunWorkerAsync a través de la propiedad Argument del evento DoWork

 

BackgroundWorker worker = new BackgroundWorker();
 
worker.DoWork += delegate(object s, DoWorkEventArgs args)
{
    string path = (string)args.Argument;
    //hacer algo
};
 
worker.RunWorkerAsync("c:\myFile.txt");

Si queremos retornar un valor como el resultado de un calculo, debemos retornar ese valor a través de la propiedad Result de DoWorkEventArgs en el event handler de DoWork. Este valor será recogido por nosotros en el evento RunWorkerCompleted al que tenemos que suscribirnos

 

BackgroundWorker worker = new BackgroundWorker();
 
worker.DoWork += delegate(object s, DoWorkEventArgs args)
{
    args.Result = CalculationMethod();
};
 
worker.RunWorkerCompleted += delegate(object s, RunWorkerCompletedEventArgs args)
{
    object result = args.Result;
};
 
worker.RunWorkerAsync();

 

Si queremos cancelar el proceso mientras se esta ejecutando debemos de poner la propiedad WorkerSupportsCancellation a True, de esta manera podemos llamar al metodo CancelAsync para que cancele el proceso. Este método pone la propiedad CancellationPending a true y en ele evnt handler DoWork deberemos comprobarla para abortar el proceso poniendo la propiedad Cancel a true

 

    for (int x = 1; x < maxRecords; x++)
    {
        //check if there is a cancelat
        if (worker.CancellationPending)
        {
            args.Cancel = true;
            return;
        }
 
        System.Threading.Thread.Sleep(10);
    }
};
 
worker.RunWorkerAsync();

 

Si queremos notificar el progreso al usuario a través de la interfaz de usuario, debemos poner primero la propiedad WorkerReportsProgess a true, suscribirnos al evento ProgressChanged que se lanzara por cada cambio de progreso que nosotros notifiquemos en el event handler DoWork a través del método ReportProgress

int maxRecords = 1000;
 
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
 
worker.DoWork += delegate(object s, DoWorkEventArgs args)
{
    for (int x = 1; x < maxRecords; x++)
    {
        System.Threading.Thread.Sleep(10);
        worker.ReportProgress(Convert.ToInt32(((decimal)x / (decimal)maxRecords) * 100));
    }
};
 
worker.ProgressChanged += delegate(object s, ProgressChangedEventArgs args)
{
    int percentage = args.ProgressPercentage;
};
 
worker.RunWorkerAsync();

 

Si queremos notificar este porcentaje al usuario necesitamos utilizar la clase Dispatcher que nos permite comunicarnos con otro Thread , si queremos coger la referencia de un objeto de la interfaz de usuario como un botón para habilitarlo o deshabilitarlo lo tenemos que referenciar

System.Windows.Threading.Dispatcher aDisp = Button1.Dispatcher;

 

Aquí os dejo un ejemplo de como utilizarlo todo junto para que podáis jugar con ello

 

Validation Rules en WPF

Siguiendo con el DataBinding de WPF vamos ha hablar de ValidationRules.Una regla de validación es una porción de código usada para validar un dato en el target, antes de que se actualice el source, es decir se utiliza para que se realice la validación en la Interfaz de Usuario antes de que se modifique el source (por ejemplo una propiedad enlazada), pero mejor vamos a verlo con ejemplos

El código de validación es realizado en una clase que extiende ValidationRule, haciendo un override del método Validate. Existe una regla de fabrica denominada ExceptionValidationRule, que nos da la posibilidad de controlar la introducción de datos erróneos como en este caso.

<Window ... xmlns:local="clr-namespace:WithBinding">
   <Window.Resources>
      ...
      <local:AgeToForegroundConverter x:Key="ageConverter" />
   </Window.Resources>
   ...
   <TextBox ...
       Foreground="
          {Binding
             Path=Age,
             Converter={StaticResource ageConverter}}">
       <TextBox.Text>
          <Binding Path="Age">
             <Binding.ValidationRules>
                <ExceptionValidationRule />
             </Binding.ValidationRules>
          </Binding>
       </TextBox.Text>
   </TextBox>
   ...

En este caso en el Textbox de Edad hemos puesto la regla de validacion ExceptioValidationRule nos comprobara que lo que metamos pueda ser transformado a int porque la propiedad Age a la que esta enlazado es de tipo int y no puede hacer la conversión.

Cuando un error de validación ocurre, un objeto ValidationError es creado.Este contiene el objeto usado para desplegar el mensaje de error, en general a nivel de la

UI. En el caso de la ExceptionValidationRule, el error de validación esta en la propiedad ErrorContent de este objeto que a su vez contiene la propiedad Message de la excepción capturada por esta regla. Para lograr acceder a esta información sobre los errores, podemos escuchar por el evento ValidationError, attacheado al elemento de UI en cuestión que causa el error.

 

// Window1.cs
...
public Window1( ) {
   InitializeComponent( );
   this.birthdayButton.Click += birthdayButton_Click;
   Validation.AddErrorHandler(
      this.ageTextBox,
      ageTextBox_ValidationError);
}
void ageTextBox_ValidationError(
      object sender, 
      ValidationErrorEventArgs e) {
   MessageBox.Show((string)e.Error.ErrorContent, 
                   "Validation Error");
}
 

Debemos de decir que el mecanismo anterior funciona, solo si la propiedad NotifyOnValidationError esta con valor True en el Binding del elemento en cuestión

<!-- Window1.xaml -->
...
<TextBox Name="ageTextBox" ...>
   <TextBox.Text>
      <Binding Path="Age" 
               NotifyOnValidationError="True">
         <Binding.ValidationRules>
            <ExceptionValidationRule />
         </Binding.ValidationRules>
      </Binding>
   </TextBox.Text>
</TextBox>
...

Al ejecutar esto con un error de validación, obtenemos…

image

Si bien lo anterior funciona bien, el mensaje no es muy amigable para el usuario final. Podemos mejorarlo un poco customizando la manera de mostrar el error, como vemos en el siguiente código, en el que creamos una regla de validación para comprobar el rango de edad:

public class NumberRangeRule : ValidationRule {
   int min;
   public int Min {
      get { return min; }
      set { min = value; }
   }
   int max;
   public int Max {
      get { return max; }
      set { max = value; }
   }
   ...
   public override ValidationResult Validate(
      object value, 
      System.Globalization.CultureInfo cultureInfo) {
      int number;
      if( !int.TryParse((string)value, out number) ) {
         return new ValidationResult(
            false,
            "Invalid number format");
      }
      if( number < min || number > max ) {
         return new ValidationResult(
            false,
            string.Format(
                "Number out of range ({0}-{1})", 
                min, max));
      }
      return new ValidationResult(true, null);
   }
}

Enganchamos la regla en el XAML como antes…

 

<TextBox ...
   Foreground="
     {Binding Path=Age,
        Converter={StaticResource ageConverter}}">
   <TextBox.Text>
      <Binding Path="Age">
          <Binding.ValidationRules>
              <local:NumberRangeRule Min="0" 
                                     Max="128" />
          </Binding.ValidationRules>
      </Binding>
   </TextBox.Text>
</TextBox>

 

image

Podemos mejorar aun mas, colocando el mensaje de error en un tooltip sobre el elemento que causo el error. Utilizando databinding sobre la propiedad ErrorContent podemos mostrar el error:

<TextBox
   Name="ageTextBox" ...
   ToolTip=
      "{Binding
         ElementName=ageTextBox,
         Path=(Validation.Errors)[0].ErrorContent}">
   <TextBox.Text>
     <Binding Path="Age">
        <Binding.ValidationRules>
           <local:NumberRangeRule Min="0" Max="128" />
        </Binding.ValidationRules>
     </Binding>
   </TextBox.Text>
</TextBox>

En el ejemplo anterior, el tooltip esta asociado al primer elemento de la attached property Errors. Si no hay errores, el tooltip esta vacío, cuando hay errores, se toma la información y se despliega. Ya no necesitamos el NotifyOnValidationError en true, ya que el cambio en la colección y el binding, mantiene los elementos sincronizados

 

image

Wikipedia en WPF

Dot Net Solutions junto con Monochrome y platform evangelism team de Redmond han lanzado Wikipedia Explorer una aplicación realizada en WPF que nos presenta otra forma de ver la Wikipedia creando una nueva experiencia al usuario.

Cada vez me gustan mas estas aplicaciones de WPF que nos permiten utilizar datos de la nube y visualizarlos de forma tan espectacular que con una aplicación web seria imposible.

Aquí tenéis unos pantallazos y podéis instalarla desde aquí

 

Wikipedia Explorer Screenshot 3

Wikipedia Explorer Screenshot 4

Debugeando los Binding de WPF

Cuando desarrollamos en WPF, los binding son indispensables y a veces tenemos problemas con ellos que no somos capaces de detectar y deseamos con todo el alma poder poner un breakpoint y tener la posibilidad de debugear con todas las herramientas que nos da Visual Studio.

Pero esto no es posible y para poder ver los errores de binding y poderlos analizar tenemos diferentes alternativas

Output window

En la ventana Output de Visual Studio donde se displayan todos los mensajes, cuando se produce un error de binding se muestra en esta ventana el error como por ejemplo

System.Windows.Data Error: 35 : BindingExpression path error: ‘Password1’ property not found on ‘object’ »LoginViewModel’ (HashCode=28756230)’. BindingExpression:Path=Password1; DataItem=’LoginViewModel’ (HashCode=28756230); target element is ‘TextBox’ (Name=’Password’); target property is ‘Text’ (type ‘String’)

 

Usar un converter

Si con el mensaje no conseguimos ver el problema una opción es crear un converter y cambiar el bindign al converter. En el converter si podemos poner un punto de ruptura y ver el valor que entra al Binding

 

Crear una Dependency Property

Otra opción es crear una DependencyProperty y poner un punto de ruptura

 

TraceLevel

PresentationTraceSources.TraceLevel es una clase que te permite enviar a la ventana de Output mucha mas información del DataBinding, podemos monitorizar todos los pasos que se producen en un Binding, para habilitarlo tenemos que añadir el namespace System.Diagnostics y en el binding añadir PresentationTraceSources.TraceLevel=High. Esta opción aparece a partir de la versión 3.5

 

<Window x:Class="DebugBindings.Window1"... xmlns:trace="clr-namespace:System.Diagnostics;assembly=WindowsBase">
<StackPanel>
<TextBox Text="{Binding ElementName=slider, Path=Value, Mode=OneWay, trace:PresentationTraceSources.TraceLevel=High}" Height="30" Width="100" Margin="10"/>
<Slider Minimum="0" Maximum="100" Value="20" Margin="10" x:Name="slider" />
StackPanel>
Window>

En la ventana de Output aparecerá

Created BindingExpression (hash=17654054) for Binding (hash=44624228)

Path: ‘Value’

BindingExpression (hash=17654054): Default update trigger resolved to LostFocus

BindingExpression (hash=17654054): Attach to System.Windows.Controls.TextBox.Text (hash=52727599)

BindingExpression (hash=17654054): Resolving source

BindingExpression (hash=17654054): Found data context element: (OK)

Lookup name slider: queried TextBox (hash=52727599)

BindingExpression (hash=17654054): Resolve source deferred

BindingExpression (hash=17654054): Resolving source

BindingExpression (hash=17654054): Found data context element: (OK)

Lookup name slider: queried TextBox (hash=52727599)

BindingExpression (hash=17654054): Activate with root item Slider (hash=54371668)

BindingExpression (hash=17654054): At level 0 – for Slider.Value found accessor DependencyProperty(Value)

BindingExpression (hash=17654054): Replace item at level 0 with Slider (hash=54371668), using accessor DependencyProperty(Value)

BindingExpression (hash=17654054): GetValue at level 0 from Slider (hash=54371668) using DependencyProperty(Value): ’20’

BindingExpression (hash=17654054): TransferValue – got raw value ’20’

BindingExpression (hash=17654054): TransferValue – implicit converter produced ’20’

BindingExpression (hash=17654054): TransferValue – using final value ’20’

If I now enter text in the TextBox, I can check the Output Window to confirm that the binding was indeed broken!

BindingExpression (hash=17654054): Deactivate

BindingExpression (hash=17654054): Replace item at level 0 with {NullDataItem}

BindingExpression (hash=17654054): Detach