środa, 7 lipca 2010

Bindowanie danych w WPF cz. 3

Ostatni post o bindowaniu danych w WPFie. W tym poście:
- bindowanie do elementów ADO.NET (DataTable)
- bindowanie zapytań LINQ
- poprawa wydajności wyświetlania dużych kolekcji obiektów
- walidacja wprowadzanych danych


Bindowanie w ADO.NET

Co nieco o tym rodzaju było w poprzednim poście. Tam też wykorzystaliśmy ADO.NET do pobierania danych. Tym razem zbindujemy bezpośrednio wynik ADO.NET do kontrolki.
Tym razem jako bazę wybierzemy AdventureWorks. Jest dostępna do pobrania dla każdego link.
Zaczynamy. Tworzymy klasę z jedną metodą odpowiadająca za pobieranie danych o klientach:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace WpfApplication6
{
    public class Customers
    {
        private string _connectionString = @"Data Source=localhost;Initial Catalog=AdventureWorksLT;Integrated Security=True";

        public DataTable GetCustomers()
        {
            SqlConnection conn = new SqlConnection(_connectionString);
            string sqlQuery = "SELECT * FROM SalesLT.Customer";
            SqlDataAdapter da = new SqlDataAdapter(sqlQuery, conn);
            DataSet ds = new DataSet();
            da.Fill(ds, "Customers");
            return ds.Tables["Customers"];
        }
    }
}


<Window x:Class="WpfApplication6.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">
    <Window.Resources>
        <DataTemplate x:Key="CustomerTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Path=FirstName}" />
                <TextBlock Text=" " />
                <TextBlock Text="{Binding Path=LastName}" />
            </StackPanel>
        </DataTemplate> 
    </Window.Resources>
    <Grid>
        <ListBox Name="lbCustomers" Width="300" ItemTemplate="{StaticResource ResourceKey=CustomerTemplate}" />
    </Grid>
</Window>

Po otwarciu aplikacji zobaczymy imiona i nazwiska naszych klientów. Teraz to samo zadanie ale z bindowaniem w XAML'u:

<Window x:Class="WpfApplication6.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:c="clr-namespace:WpfApplication6"
       Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <ObjectDataProvider x:Key="CustomerData" MethodName="GetCustomers" ObjectType="{x:Type c:Customers}" />
        <DataTemplate x:Key="CustomerTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Path=FirstName}" />
                <TextBlock Text=" " />
                <TextBlock Text="{Binding Path=LastName}" />
            </StackPanel>
        </DataTemplate> 
    </Window.Resources>
    <Grid>
        <ListBox Name="lbCustomers" Width="300" ItemsSource="{Binding Source={StaticResource ResourceKey=CustomerData}}" ItemTemplate="{StaticResource ResourceKey=CustomerTemplate}" />
    </Grid>
</Window>

Prosto i co najważniejsze szybko :)
Ważnym aspektem jest usuwanie wierszy z takiej kolekcji. Dodamy na formatkę klawisz do usuwania:
<Button Name="bDeleteCustomer" Margin="50" Width="100" Height="30" Content="Delete Customer" VerticalAlignment="Center" HorizontalAlignment="Left" Click="bDeleteCustomer_Click" />
Najbardziej oczywisty kod usuwania jaki nam się od razu nasuwa to:
lbCustomers.Items.Remove(((DataRow)lbCustomers.SelectedItem));
jest niestety w tym przypadku kodem błędnym. Zły jest dokładnie z dwóch powodów. Po pierwsze obiekt, który znajduje się w ListBoxie to nie DataRow a DataRowView. Drugim powodem jest to, że chcemy usunąć wiersz z naszej kontrolki a nie z kolekcji wierszy w DataTable. Później podczas wysyłania zapytania powrotnego do bazy danych potrzebujemy wiedzieć co mamy usunąć. Tak więc poprawna składnia polecenia ma postać:
((DataRowView)lbCustomers.SelectedItem).Row.Delete();


Bindowanie zapytań LINQ

LINQ jak i Entity Framework pozwalają na uniknięcie pisania wielu linii kodu odpowiedzialnego za warstwe Data Access. Bindowanie zapytań wynikowych z LINQ do np. ListBoxa praktycznie nie różni się niczym od bindowania do DataTable:

            AdventrureWorksDataContext adventureWorks = new AdventrureWorksDataContext();
            var q = from a in adventureWorks.Customers
                    where a.FirstName.StartsWith("A")
                    select a;
            lbCustomers.ItemsSource = q;





Poprawa wydajności wyświetlania dużych kolekcji obiektów

Duże kolekcje obiektów (np powyżej 50 tys. elementów) sprawiają, że aplikacja traci na wydajności a użytkownik na komforcie w pracy z nią. W WPF na szczęście wprowadzono wiele różnych udogodnień dla programistów które pozwalają na osiągnięcie dużej wydajności bez zbędnego obmyślania o przeprojektowywaniu sposobu pobieraniu ich z bazy danych.


VirtualizingStackPanel - pierwszy i bardzo potężny sprzymierzeńca w uzyskaniu dużej wydajności przeglądania danych. Opiera się on o bardzo prostą strategię: twórz tyle obiektów ile użytkownik widzi plus kilka dodatkowych, aby przewijanie było płynne. Dzięki temu podczas przeglądania kolekcji gdzie mamy 10000 itemów, a widzimy tylko 30 na raz, zostanie stworzone tylko tyle ile potrzeba nam do wygodnego przeglądania. Większość kontrolek ma aktywny ten typ panelu: ListBox, ListView, DataGrid. Kontrolka ComboBox nie używa go. Można ręcznie dodać obsługę go:
        <ComboBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="100">
            <ComboBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel />
                </ItemsPanelTemplate>
            </ComboBox.ItemsPanel>
        </ComboBox>
Kolejną kontrolką, która posiada możliwość korzystania z zalet wirtualizacji jest TreeView, jednak z przyczyn wstecznej zgodności tryb ten jest w niej wyłączony. Włączyć można go w bardzo prosty sposób:
<TreeView VirtualizingStackPanel.IsVirtualizing="True" />

Wirtualizacja ma to do siebie, że można ją w bardzo prosty i banalny stracić. Przyczynami wyłączenia wirtualizacji są:
- umieszczenie kontrolki w kontenerze typu ScrollViewer lub StackPanel zamiast Grid;
- zmiana templatu kontrolki i zastąpienie VirtualizingStackPanel innym panelem
- grupowanie elementów - konfiguruje kontrolkę do przeglądania po pixelach a nie itemach (jest to wymagane w przypadku wirtualizacji)
- wypełnianie programistyczne podczas wypełniania danymi kontrolki (wirtualizacja działa tylko z bindowaniem - nie mylić z bindowaniem w kodzie)


Item Container Recycling - domyślnie wyłączone - podczas przeglądania obiektów za pomocą VirtualizingStackPanel obiekty są tworzone i niestety po opuszczeniu widoku z nich - oznaczane do usunięcia. Powoduje to duże obciążenie garbage collectora jak również większego zapotrzebowania mocy na ponowne tworzenie obiektów. Dzięki Item Container Recycling obiekty nie zostają oznaczone do usunięcia i mogą być ponownie użyte przy przeglądaniu. Włączyć ten tryb można za pomocą komendy:
<ListBox VirtualizingStackPanel.VirtualizationMode="Recycling" />


Deferred Scrolling - podczas przesuwania suwakiem obiekty są tworzone niejako od razu. Kidy uaktywnimy Deferred Scrolling tworzone są wtedy, kiedy użytkownik zwolni suwak. Przydaje się to zwłaszcza wtedy kiedy używamy bogatych wizualnie stylów w poszczególnych Itemach np. ListBoxa. Włączanie:
<ListBox ScrollViewer.IsDeferredScrollingEnabled="True" />



Walidacja wprowadzanych danych

Walidacja pełni kluczową rolę w aplikacjach zorientowanych na przetwarzane i wprowadzanie dużych ilości danych. Oprócz tego, że zapobiega i przychwytuje wprowadzeniu błędnych danych to dzięki WPFowi jesteśmy w stanie wprowadzić ją na poziomie bindowania. Zyskujemy dzięki temu możliwość wykorzystania jej niezależnie od użytej kontrolki.

ExceptionValidationRule - powiadamia o występującym wyjątku poprzez zmianę koloru np. ramki TextBoxa na czerwony. Do testu wykorzystamy prostą klasę Person:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WpfApplication7
{
    public class Person
    {
        private int _age = 10;

        public int Age
        {
            get { return _age; }
            set
            {
                if (value > 100 || value < 1)
                {
                    throw new ArgumentOutOfRangeException("The age value must be in range 1 and 99");
                }
                _age = value;
            }
        }

        private string _firstName = "Ala";

        public string FirstName
        {
            get { return _firstName; }
            set { _firstName = value; }
        }

    }
}

<Window x:Class="WpfApplication7.MainWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:c="clr-namespace:WpfApplication7"
       Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <ObjectDataProvider x:Key="Person" ObjectType="{x:Type c:Person}"/>
    </Window.Resources>
    <Grid>
        <StackPanel Width="200" Orientation="Vertical" HorizontalAlignment="Left">
            <TextBox Name="tbAge" Width="100" Height="25" Margin="5" >
                <TextBox.Text>
                    <Binding Source="{StaticResource ResourceKey=Person}" Path="Age">
                        <Binding.ValidationRules>
                            <ExceptionValidationRule/>
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
            <TextBox Name="tbName" Width="100" Height="25" Margin="5" Text="{Binding Source={StaticResource ResourceKey=Person}, Path=FirstName}" />
        </StackPanel>
    </Grid>
</Window>

Po uruchomieniu i wprowadzeniu błędnej wartości zobaczymy następujący wynik:




DataErrorValidationRule - sprawdza czy błąd nie został wyrzucony przez implementację interfejsu DataErrorValidationRule. Zobaczymy jak zaimplementować ten interfejs do naszej testowej klasy:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace WpfApplication7
{
    public class Person : IDataErrorInfo
    {
        private int _age = 10;

        public int Age
        {
            get { return _age; }
            set
            {
                if (value > 100 || value < 1)
                {
                    throw new ArgumentOutOfRangeException("The age value must be in range 1 and 99");
                }
                _age = value;
            }
        }

        private string _firstName = "Ala";

        public string FirstName
        {
            get { return _firstName; }
            set { _firstName = value; }
        }

        //WPF doesn't use this property
        public string Error
        {
            get { return null; }
        }

        public string this[string columnName]
        {
            get
            {
                string notification = string.Empty;
                if (columnName == "FirstName")
                {
                    bool valid = false;
                    for (int i = 0; i < FirstName.Length; i++)
                    {
                        if (!char.IsLetterOrDigit(FirstName[i]))
                        {
                            valid = true;
                            break;
                        }
                    }
                    if (valid)
                    {
                        return "First Name can be only letters and numbers";
                    }
                }

                return notification;
            }
        }
    }
}


            <TextBox Name="tbName" Width="100" Height="25" Margin="5" >
                <TextBox.Text>
                    <Binding Source="{StaticResource ResourceKey=Person}" Path="FirstName">
                        <Binding.ValidationRules>
                            <DataErrorValidationRule />
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
 
powyższy kod można także skrócić do zapisu:

            <TextBox Name="tbName" Width="100" Height="25" Margin="5" >
                <TextBox.Text>
                    <Binding Source="{StaticResource ResourceKey=Person}" Path="FirstName" ValidatesOnDataErrors="True" />
                </TextBox.Text>
            </TextBox>

Po wprowadzeniu znaku innego niż litera lub cyfra, obwódka TextBoxu zmieni kolor jak w poprzednim przykładzie:


Ostatnią możliwością walidacji danych jest własna jej implementacja. Aby zaimplementować własną walidację należy dziedziczyć po klasie ValidationRule:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;

namespace WpfApplication7
{
    public class AgeValidationRule : ValidationRule
    {
        private int _min;

        public int Min
        {
            get { return _min; }
            set { _min = value; }
        }

        private int _max;

        public int Max
        {
            get { return _max; }
            set { _max = value; }
        }


        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            int age;
            try
            {
                age = int.Parse(value.ToString(), System.Globalization.NumberStyles.Any, cultureInfo);
            }
            catch (Exception ex)
            {
                return new ValidationResult(false, "The character isn't valid");
            }
            if (age <= _min || age >= _max)
            {
                return new ValidationResult(false, string.Format("The age must be between {0} and {1}", _min, _max));
            }
            else
            {
                return new ValidationResult(true, null);
            }
        }
    }
}

Natępnie w kodzie kontrolki:

            <TextBox Name="tbAge" Width="100" Height="25" Margin="5" >
                <TextBox.Text>
                    <Binding Source="{StaticResource ResourceKey=Person}" Path="Age" UpdateSourceTrigger="PropertyChanged">
                        <Binding.ValidationRules>
                            <c:AgeValidationRule Min="0" Max="100" />
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
Własną implementację błędu możemy wykorzystać nie tylko do walidacji pojedyńczego TextBoxa ale wielu.

Przejdźmy teraz do prezentacji wizualnej błędu użytkownikowi. Do tej pory jedynym rezultatem błędnie wprowadzonych danych była zmiana koloru ramki TextBoxa. Dzięki przechwyceniu eventa Validation.Error jesteśmy w stanie dokładniej pomóc użytkownikowi naprawić źle wprowadzane dane:

          <TextBox Name="tbAge" Width="100" Height="25" Margin="5" Validation.Error="tbAge_Error" >
                <TextBox.Text>
                    <Binding Source="{StaticResource ResourceKey=Person}" Path="Age" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
                        <Binding.ValidationRules>
                            <c:AgeValidationRule Min="0" Max="100" />
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>

        private void tbAge_Error(object sender, ValidationErrorEventArgs e)
        {
            if (e.Action == ValidationErrorEventAction.Added)
            {
                MessageBox.Show(e.Error.ErrorContent.ToString());
            }
        }
Efekt:

Taki sposób dostarczania informacji jest już połową drogi do sukcesu - nikt nie lubi latających MessageBoxów wokoło :). W WPF-ie mamy możliwość stworzenia szablonu kontrolki w momencie gdy walidacja nie odniosła pozytywnego rezultatu. Utworzymy dodatkowo styl, który będzie przypisany wszystkim kontrolką TextBox:

    <Window.Resources>
        <ObjectDataProvider x:Key="Person" ObjectType="{x:Type c:Person}"/>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel LastChildFill="True">
                            <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold">*</TextBlock>
                            <Border BorderBrush="Green" BorderThickness="1">
                                <AdornedElementPlaceholder></AdornedElementPlaceholder>
                            </Border>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

Efekt wpisania złych danych będzie zielona obwódka i gwiazdka:


Możemy jeszcze dołożyć ToolTipa który wyświetli informacje co zostało niepoprawnie wprowadzone:

        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel LastChildFill="True">
                            <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold" ToolTip="{Binding ElementName=adornerPlaceholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">*</TextBlock>
                            <Border BorderBrush="Green" BorderThickness="1">
                                <AdornedElementPlaceholder Name="adornerPlaceholder"></AdornedElementPlaceholder>
                            </Border>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
Po najechaniu na gwiazdkę dostajemy komunikat, co zostało źle wprowadzone:


Jak widać WPF daje duże pole do popisu w kwestii walidacji danych wprowadzanych przez użytkownika. Wykorzystując je odpowidnio można stworzyć system, który wygląda nie tylko ładnie wizualnie, ale daje też realne korzyści dla użytkownika końcowego.



Źródło:
http://msdn.microsoft.com/en-us/library/system.windows.controls.dataerrorvalidationrule.aspx
http://msdn.microsoft.com/en-us/library/system.windows.controls.exceptionvalidationrule.aspx
http://msdn.microsoft.com/en-us/library/system.windows.controls.scrollviewer.isdeferredscrollingenabled.aspx
Pro WPF in C# 2010: Windows Presentation Foundation in .NET 4.0

Brak komentarzy:

Prześlij komentarz