poniedziałek, 5 lipca 2010

Bindowanie danych w WPF cz. 1

Technologia WPF, o której już wspominałem wcześniej, oprócz bardzo dużych możliwości tworzenia wspaniałego i bogatego interfejsu wspiera potężny mechanizm bindowania ze źródłami danych.

Mechanizm jest znacznie lepiej dopracowany niż ten znany z WindowsForms. Umożliwia tworzenie źródeł danych jak i wzorców dla wyświetlanych danych. Wielu ekspertów uważa, że przesiadając się z WindowsForms warto właśnie ten mechanizm poznać jako pierwszy i najważniejszy, gdyż z niego będziemy korzystali najczęściej w naszych aplikacjach.

Tak samo jak w WindowsForms mamy proste wiązanie. Jednak oprócz wiązania w jedną i dwie strony mamy także inne możliwości. Zaczniemy jednak od bardzo prostego przykładu kolejno idąc w stronę coraz to bardziej złożonych.

A więc zaczniemy od bardzo prostego bindowania w jedną stronę. Na początek stworzymy formatkę z odpowiednimi kontrolkami:

<Window x:Class="WpfApplication4.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Label Name="label" Grid.Column="0" Grid.Row="0" Content="Ala ma kota" />
        <TextBox Name="textBox" Grid.Column="1" Grid.Row="0" Width="100" Height="25" HorizontalAlignment="Left" VerticalAlignment="Top" Text="10" />
    </Grid>
</Window>


Teraz w pliku źródłowym wprowadzamy kod:

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Binding binding = new Binding();
            binding.Source = textBox;
            binding.Path = new PropertyPath("Text");
            label.SetBinding(Label.FontSizeProperty, binding);
        }
    }

Uruchamiamy program i zobaczmy jak działa. Po wprowadzeniu liczby do TextBoxa rozmiar czcionki Labela odpowiednio się zwiększa lub zmniejsza. Zobaczmy jak zostało to osiągnięte. Na początku został stworzony obiekt typu Binding, który reprezentuje wiązanie z danymi. Następnie ustawiamy parametr Source, który odpowiada za źródło danych. Właściwość Path mówi do jakiej właściwości będziemy się odnosili (w naszym wypadku jest to po prostu Text). Na końcu bindujemy label z naszym obiektem Binding. Bindingowi podlega każda DependencyProperty.
Zaprezentowano tutaj sposób bindowania za pomocą kodu. W większości przypadków stosuje się metodę poprzez bindowanie bezpośrednio w XAMLu. Dlaczego? Przede wszystkim jest to dużo łatwiejsze i przyjemniejsze. Wiązanie w kodzie przypadje się w kilku sytuacjach:
- kiedy chcemy dynamicznie bindować kontrolki z różnymi źródłami danych
- jeżeli chcemy usunąć bindowanie (za pomocą metod ClearBinding() lub ClearAllBindings()
- kiedy tworzymy własne kontrolki

Zobaczmy teraz na poprzedni przykład w wersji XAML (kod C# dla bindowania usuwamy lub komentujemy):
<Window x:Class="WpfApplication4.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Label Name="label" Grid.Column="0" Grid.Row="0" Content="Ala ma kota" FontSize="{Binding ElementName=textBox, Path=Text}"/>
        <TextBox Name="textBox" Grid.Column="1" Grid.Row="0" Width="100" Height="25" HorizontalAlignment="Left" VerticalAlignment="Top" Text="10" />
    </Grid>
</Window>

Jak widać z kilku linijek, które musialiśmy poprzednio napisać zrobiło się 5 wyrazów:
FontSize="{Binding ElementName=textBox, Path=Text}"
Czyli w tym wypadku Bindujemy właściwość FontSize z obiektem textBox (ElementName) i jego właściwością Text (Path).

Powyższy przykład pokazuje bindowanie w jedną stronę (OneWay). Zobaczmy teraz jak działa bindowanie w dwie strony. Tworzymy nowy przykład, który bardzo ładnie pokaże zastosowanie tego rodzaju bindowania:

<Window x:Class="WpfApplication4.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Slider Name="slider" Grid.Column="0" Grid.Row="0" Minimum="0" Maximum="100" Value="{Binding ElementName=textBox, Path=Text, Mode=TwoWay}" />
        <TextBox Name="textBox" Grid.Column="1" Grid.Row="0" Width="100" Height="25" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="5" />
    </Grid>
</Window>


Po uruchomieniu aplikacji zobaczymy:


Zmieniając pozycję na suwaku zmieniamy wartość wyświetlaną w TextBoxie, ale równocześnie zmieniając wartość w TextBoxie zmieniamy pozycję suwaka Slidera. Tak więc jest to klasyczny przykład bindowania w dwie strony. Tak więc podsumowując mamy do dyspozycji następujące tryby bindowania:
- TwoWay - uaktualnia zarówno wartość właściwości kontrolki celowej jak i źródłowej
- OneWay - uaktualnia wartość kontrolki docelowej tylko w przypadku zmiany wartości w źródle
- OneTime - dokonuje zmiany w kontrolce docelowej tylko raz (np. w momencie uruchomienia programu)
- OneWayToSource - uaktualnia tylko wartość źródłową
- Default - zależy od konkretnej sytuacji i właściwości na którą się ją nakłada

Podczas pracy z ostatnim przykładem, podczas jakichkolwiek zmian położenia suwaka czy wprowadzania danych do TextBoxa, zmiany następują natychmiastowo. Można to zmienić za pomocą typu wyliczeniowego UpdateSourceTrigger. Do wyboru mamy takie tryby jak:
- LostFocus - czyli kiedy kontrolka straci aktywność
- PropertyChanged - kiedy właściwość się zmieni
- Default - dla większości właściwości jest to PropertyChanged
- Explicit - czyli na wyraźne życzenie użytkownika

W przypadku ostatniej możliwości chodzi o to, że np. użytkownik wprowadza jakieś dane, a następnie klika w przycisk OK który dokonuje uaktualnienia. Wymaga to napisania następującego kodu (na formatkę dokładamy przycisk):

        private void button_Click(object sender, RoutedEventArgs e)
        {
            BindingExpression be = slider.GetBindingExpression(Slider.ValueProperty);
            be.UpdateSource();
        }
krótko mówiąc po naciśnięciu przycisku w TextBoxie pojawi się wartość Slidera.


Do tej pory widzieliśmy w jaki sposób wiązać proste właściwości obiektów. Teraz zobaczymy w jaki sposób można związać kontrolkę z obiektem. Na potrzeby przykładu stworzymy prostą klasę reprezentującą osobę:
    class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int YearOfBirth { get; set; }
    }

Ważne jest tutaj, że właściwości do których bindujemy są publiczne. Należy podkreślić słowo właściwości. Do zwykłych pól klasy (chociaż byłyby publiczne) bindowanie nie działa. Tak więc najpierw w Resourcach okna tworzymy obiekt typu Person, inicjujemy jego pola, a następnie kolejnym Labelom przypisujemy odpowiednie właściwości:

<Window x:Class="WpfApplication4.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:c="clr-namespace:WpfApplication4"
       Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <c:Person x:Key="Marek" FirstName="Marek" LastName="Kowalski" YearOfBirth="1980" />
    </Window.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Label Content="{Binding Source={StaticResource ResourceKey=Marek}, Path=FirstName}" Grid.Column="0" Grid.Row="0" />
        <Label Content="{Binding Source={StaticResource ResourceKey=Marek}, Path=LastName}" Grid.Column="0" Grid.Row="1" />
        <Label Content="{Binding Source={StaticResource ResourceKey=Marek}, Path=YearOfBirth}" Grid.Column="0" Grid.Row="2" />
    </Grid>
</Window>


Kilka linijek powyższego kodu należy wyjaśnić:
       xmlns:c="clr-namespace:WpfApplication4"
linijka ta umożliwia zmapowanie zwykłej klasy CLR na XAML;
    <Window.Resources>
        <c:Person x:Key="Marek" FirstName="Marek" LastName="Kowalski" YearOfBirth="1980" />
    </Window.Resources>
tworzymy globalny Resource o nazwie Marek (tak jest idenyfikowany w programie) i wypełniamy jego właściwości;
{Binding Source={StaticResource ResourceKey=Marek}, Path=FirstName}
Source jest używany kiedy odnosimy się do obiektu, StaticResource (StaticResource możemy traktować jako miejsce do przechowywania informacji które będziemy wykorzystywali w innych miejscach naszego programu) zawiera klucz do stworzonego obiektu, Path do właściwości którą bindujemy.

Zbindowaliśmy jeden obiekt? To teraz czas na całą listę obiektów. Stworzymy listę obiektów klasy Person a następnie wyświetlimy ją w ListBoxie. Na początek kod a później wyjaśnienia:

    public class PersonList : ObservableCollection<Person>
    {

    }

<Window x:Class="WpfApplication4.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:c="clr-namespace:WpfApplication4"
       Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <c:PersonList x:Key="PersonList">
            <c:Person FirstName="Ala" LastName="Kowalska" YearOfBirth="1920" />
            <c:Person FirstName="Marek" LastName="Kowalski" YearOfBirth="1980" />
        </c:PersonList>  

        <DataTemplate x:Key="listBoxItem">
            <Border BorderBrush="Red" BorderThickness="2" CornerRadius="5" >
            <StackPanel Name="itemPanel" Orientation="Vertical" Margin="5">
                <TextBlock Text="{Binding Path=FirstName}" Foreground="Blue" />
                <TextBlock Text="{Binding Path=LastName}" Foreground="Green" />
                <TextBlock Text="{Binding Path=YearOfBirth}" Foreground="Red" />
            </StackPanel>
            </Border>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <ListBox Name="lb" Width="100" Height="250" ItemsSource="{StaticResource ResourceKey=PersonList}" ItemTemplate="{StaticResource ResourceKey=listBoxItem}" >
        </ListBox>
    </Grid>
</Window>

Po uruchomieniu programu zobaczymy następujący efekt na ekranie:


Jak widać składowe właściwości klasy Person występują tutaj jako jeden item ListBoxa. A więc teraz po kolei co zrobiliśmy aby osiągnąć taki efekt. Na początek stworzyliśmy klasę PersonList która dziedziczy po ObservableCollection. Dziedziczymy po tej klasie gdyż zapewnia ona powiadamianie w momencie dodawania nowych elementów do kolekcji (świetnie się też sprawdza w czasie bindowania).


        <c:PersonList x:Key="PersonList">
            <c:Person FirstName="Ala" LastName="Kowalska" YearOfBirth="1920" />
            <c:Person FirstName="Marek" LastName="Kowalski" YearOfBirth="1980" />
        </c:PersonList>
Powyższy kod powinien być raczej łatwo zrozumiały dla wszystkich. Tworzymy listę a następnie dodajemy do niej kolejne elementy.


<ListBox Name="lb" Width="100" Height="250" ItemsSource="{StaticResource ResourceKey=PersonList}" ItemTemplate="{StaticResource ResourceKey=listBoxItem}" >
ItemSource wskazuje na źródło danych dla naszego ListBoxa. W naszym przypadku jest to PersonList znajdujący się w Resource. Ciekawsza jest dalsza część minowicie DataTemplate. Odpowiada ona za sposób wyświetlania bindowanych danych. Możemy zdecydować w jaki sposób będą wyświetlane itemy w naszym ListBoxie. Kod odpowiadający za wygląd wyświetlanych elementów:

        <DataTemplate x:Key="listBoxItem">
            <Border BorderBrush="Red" BorderThickness="2" CornerRadius="5" >
            <StackPanel Name="itemPanel" Orientation="Vertical" Margin="5">
                <TextBlock Text="{Binding Path=FirstName}" Foreground="Blue" />
                <TextBlock Text="{Binding Path=LastName}" Foreground="Green" />
                <TextBlock Text="{Binding Path=YearOfBirth}" Foreground="Red" />
            </StackPanel>
            </Border>
        </DataTemplate>
A więc mamy ramkę i StackPanel. W StackPanel o orientacji pionowej kolejno TextBlock do wyświetlania imienia, nazwiska i roku urodzenia. Każdy z TextBlock jest zbindowany z odpowiednią właściwością klasy Person. Nie ma tutaj użytego słowa kluczowego ElementName czy też Source, gdyż to już zostało wcześniej zdefiniowane w ItemSource.
Tak więc bardzo małym nakładem pracy udało się osiągnąć ciekawe rezultaty w sposobie wyświetlania danych.
Teraz można jeszcze dodać możliwość edytowania danych zawatych w ListBoxie. Na formatkę dokładamy TextBoxy i bindujemy je z itemem ListBoxa:

<Window x:Class="WpfApplication4.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:c="clr-namespace:WpfApplication4"
       Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <c:PersonList x:Key="PersonList">
            <c:Person FirstName="Ala" LastName="Kowalska" YearOfBirth="1920" />
            <c:Person FirstName="Marek" LastName="Kowalski" YearOfBirth="1980" />
        </c:PersonList>  

        <DataTemplate x:Key="listBoxItem">
            <Border BorderBrush="Red" BorderThickness="2" CornerRadius="5" >
            <StackPanel Name="itemPanel" Orientation="Vertical" Margin="5">
                <TextBlock Text="{Binding Path=FirstName}" Foreground="Blue" />
                <TextBlock Text="{Binding Path=LastName}" Foreground="Green" />
                <TextBlock Text="{Binding Path=YearOfBirth}" Foreground="Red" />
            </StackPanel>
            </Border>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <ListBox HorizontalAlignment="Left" Name="lb" Width="100" Height="250" ItemsSource="{StaticResource ResourceKey=PersonList}" ItemTemplate="{StaticResource ResourceKey=listBoxItem}" SelectedIndex="0" />
        <StackPanel Width="100" Orientation="Vertical">
            <TextBox Width="100" Text="{Binding ElementName=lb, Path=SelectedItem.FirstName}" />
            <TextBox Width="100" Text="{Binding ElementName=lb, Path=SelectedItem.LastName}" />
            <TextBox Width="100" Text="{Binding ElementName=lb, Path=SelectedItem.YearOfBirth}" />
        </StackPanel>
    </Grid>
</Window>
Linijka:

Text="{Binding ElementName=lb, Path=SelectedItem.FirstName}"
Oznacza tyle, że pobierając Item pobieramy typ object. Skoro wiemy że pojedyńczy Item jest typu Person, znamy jego właściwości. Dlatego możemy bezpośrednio zbindować TextBoxy z odpowiednimi właściwościami klasy Person:




Ponieważ wiązanie tutaj jest domyślnie TwoWay zmieniając dane w TextBoxach zmieniają się także w źródle danych.



Tyle na temat bindowania do obiektów. W następnej części pokażę w jaki sposób wykorzystać poznane tutaj narzędzia w pracy z bazą danych.

Źródło:
http://msdn.microsoft.com/en-us/library/ms752347.aspx
http://msdn.microsoft.com/en-us/magazine/cc163299.aspx
http://coredotnet.blogspot.com/2006/05/wpf-data-binding-tutorial.html
http://msdn.microsoft.com/en-us/library/ms617928.aspx
http://msdn.microsoft.com/en-us/library/system.windows.dependencyproperty.aspx
http://msdn.microsoft.com/en-us/library/system.windows.data.binding.updatesourcetrigger.aspx
http://msdn.microsoft.com/en-us/library/system.windows.data.binding.elementname.aspx
http://msdn.microsoft.com/en-us/library/ms613642.aspx

1 komentarz:

  1. Świetny artykuł, na pewno jeszcze nie raz mi się przyda ;)

    OdpowiedzUsuń