poniedziałek, 31 grudnia 2012

70-511 Rozdział 9 - kontrolki Windows Forms w WPF

WPF oferuje bardzo dużo kontrolek. Są jednak takie, jak np. MaskedTextBox których w WPF nie ma a Windows Forms są. Dzięki mechanizmowi umożliwiającemu używanie kontrolek Windows Forms w WPF, nie musimy ich implementować od nowa. Przyjrzyjmy się aspektom, wykorzystania okien i kontrolek Windows Forms w aplikacji WPF

Okna dialogowe
Okna OpenFileDialog (Otwóz plik) oraz SaveFileDialog (Zapisz plik) to najczęściej wykorzystywane okna dialogowe w aplikacjach. Aby użyć je w aplikacji WPF należy:
  • Dodać referencję do DLL System.Windows.Forms
  • W kodzie tworzymy obiekt klasy OpenFileDialog, lub SaveFileDialog
Inne kontrolki występujące w Windows Forms, możemy dodawać do aplikacji WPF poprzez komponent WindowsFormsHost. Aby można było używać tego komponentu należy dodać referencję w XAML do DLL System.Windows.Forms.Integration. Następnie przeciągamy element WindowsFormsHost, w którym możemy umieścić kontrolkę Windows Forms. Właściwości i zdarzenia ustawiamy tak samo jak dla kontrolek WPF-owych. Aby pobrać referencję do kontrolki należy skorzystać z właściwości Child kontrolki WindowsFormsHost:

Code:
        <WindowsFormsHost Name="WindowsFormsHost">
            <forms:MaskedTextBox Name="mtbTextBox1"></forms:MaskedTextBox>
        </WindowsFormsHost>

Code:
            var maskedTextBox = (MaskedTextBox) WindowsFormsHost.Child;
            maskedTextBox.Mask = "00000";



Użycie kontrolek WPF w aplikacji Windows Forms
W aplikacji Windows Formst można użyć kontrolek z WPF dzięki kontrolce ElementHost.
Jeżeli kontrolka jest częścią tworzonego projektu, można ją przypisać w designerze. Jeżeli nie należy ją przypisać w kodzie.

niedziela, 30 grudnia 2012

70-511 Rozdział 9 - globalizacja i lokalizacja

Terminy globalizacja i lokalizacja w przypadku tworzenia oprogramowania oznaczają różne terminy:
  • Globalizacja - wyświetlanie kwot, oraz liczb z odpowiednim formatowaniem
  • Lokalizacja - język programu dostosowany do ustawień systemu operacyjnego
Aktualna kultura aplikacji jest reprezentowana przez obiekt typu CultureInfo. Aktualną kulturę możemy zmienić w prosty sposób:

Code:
Thread.CurrentThread.CurrentCulture = new CultureInfo("en");

Globalizacja obsługiwana jest automatycznie dla dat. W przypadku cen należy korzystać w celu ich wyświetlania z automatycznego formatowania:

Code:
var price = 500.ToString("C");

Jeżeli przyjrzymy się klasie Thread, znajdują się w niej dwie podobne właściwości: CurrentCulture oraz CurrentUICulture. Różnica pomiędzy nimi jest taka, iż pierwsza właściwość determinuje w jaki sposób będą formatowane daty, a druga determinuje tłumaczenie aplikacji.

Lokalizacja w Windows Forms
Aby włączyć lokalizację w Windows Forms, należy w ustawieniach formy zaznaczyć flagę Localizable. Następnie tłumaczymy interfejs na poszczególne języki. Po przetłumaczeniu formatki (np. na angielski) zmieniamy właściwość Language na następny język i ponownie tłumaczymy zawartość interfejsu. 



Lokalizacja w WPF
Podczas uruchamiania aplikacji WPF, szukane są pliki dll zawierające zasoby lokalizacji. Kroki które należy wykonać, aby mechanizm lokalizacji działał poprawnie:
1. Dodajemy do projektu atrybut UICulture
Atrybut UICulture specyfikuje domyślny język aplikacji. Po dodaniu tego atrybutu, zostanie stworzony katalog dla danej kultury, do którego zostanie zapisana biblioteka dll z zasobami językowymi.
Na początek otwieramy za pomocą notatnika plik <nazwa_projektu>.csproj. Wyszukujemy pierwszy element PropertyGroup i dodajemy w nim element: <UICulture>en-US</UICulture>. Zapisujemy plik i przebudowujemy projekt.

2. Oznaczenie lokalizowanych elementów
Lokalizowane elementy możemy oznaczyć za pomocą właściwości x:Uid:

Code:
<Button x:Uid="Button1"></Button>

Powyższą operację można zautomatyzować za pomocą aplikacji Msbuild.exe.

3. Wyodrębnienie lokalizowanej zawartości.
Aby wyodrębnić lokalizowaną zawartość potrzebujemy narzędzia LocBaml. Narzędzie uruchamiamy z linii poleceń: locbaml /parse en-US\myApplication.resources.dll. Na wyjściu otrzymamy plik w formacie .csv, który zawiera wszystkie lokalizowane właściwości z elementów oznaczonych atrybutem x:Uid

4. Tłumaczenie
Tłumaczenie polega na przetłumaczeniu zawartości pliku CSV. Tłumaczymy tylko zawartość kolumny Value.

5. Tworzenie podfolderów dla języków
Podfoldery tworzymy w miejscu, którym znajduje się wykonywalna wersja naszej aplikacji. Foldery muszą mieć nazwy kultur dla jakich są tworzone, np. pl

6. Tworzenie bibliotek dll z przetłumaczoną zawartością
Za pomocą LocBaml możemy stworzyć biblioteki dll z przetłumaczonymi zasobami za pomocą polecenia:

locbaml /generate en-US\myApplication.resources.dll
/trans:myApplication.resources.FrenchCan.csv /cul:fr-CA /out:fr-CA

Elementy tego polecenia:
  • /generate - LocBaml stworzy zasób na podstawie podanej dll
  • /trans - plik csv z tłumaczeniem
  • /cul - kultura dla której tworzymy plik zasobów
  • /out - folder do którego zostanie skopiowany stworzony zasób
Jeżeli chcemy zmienić język, musimy zrobić to przed startem aplikacji, najlepiej zrobić to w zdarzeniu Application.Startup

sobota, 29 grudnia 2012

70-511 Rozdział 9 - programowanie asynchroniczne

Programowanie asynchroniczne ma na celu polepszenie interakcji aplikacji z użytkownikiem. Przenosząc długotrwałe operacje na inny wątek, zapewniamy, że interfejs naszej aplikacji nie zostanie zamrożony.
Komponentem, który umożliwia proste przeniesienie długotrwałych operacji na inny wątek, znany także z Windows Forms jest BackgroundWorker.
W momencie, gdy na obiekcie klasy BackgroundWorker wywołamy metodę RunWorkerAsync zostaje wywołane zdarzenie DoWork. Kod podpięty pod to zdarzenie zostanie wykonany na osobnym wątku. Niektóre z właściwości obiektu BackgroundWorker:
  • CancellationPending - flaga czy anulowano zadanie
  • IsBusy - flaga czy BW wykonuje zadanie
  • WorkerReportsProgress - flaga czy BW może powiadamiać o postępie pracy
  • WorkerSupportsCancellation - flaga czy operacja wykonywana przez BW może zostać anulowana
  • CancelAsync - przerywa aktualnie wykonywaną operację
  • ReportProgress - wywołuje zdarzenie ProgressChanged
  • RunWorkerAsync - uruchamia operacje
  • DoWork - zdarzenie, które jest wywoływane w momencie wywołania metody RunWorkerAsync 
  • ProgressChanged - zdarzenie wywoływane w momencie wywołania metody ReportProgress
  • RunWorkerCompleted - zdarzenie wywoływane w momencie zakończenia operacji przez BW
Podczas pracy z BW możemy mieć następujące zadania do zrealizowania:
  • przesłanie danych do naszego nowego wątku - parametr przekazujemy w wywołaniu metody RunWorkerAsync, a odebranie parametru następuje przez właściwość DoWorkEventArgs.Argument
  • powiadomienie, że proces zakończył się - realizowane poprzez obsłużenie zdarzenia RunWorkerCompleted
  • zwrócenie obliczanej wartości - przypisujemy wartość do właściwości - DoWorkEventArgs.Result, a odbieramy ją w zdarzeniu RunWorkerCompleted.Result
  • anulowanie operacji - ustawiamy flagę WorkerSupportsCancellation; w zdarzeniu DoWork implementujemy logikę, która sprawdza co jakiś czas flagę CancellationPending i jeżeli flaga jest ustawiona, przerywa operację
  • raportowanie postępów - ustawiamy flagę WorkerReportsProgress; w kodzie, metody która wykonuje długotrwałe obliczenia wywołujemy metodę ReportProgress do której przekazujemy informację o procencie wykonanej pracy
Przykładowa realizacja opisanych powyżej punktów:

Code:
public partial class MainWindow : Window
    {
        private BackgroundWorker backgroundWorker;

        public MainWindow()
        {
            InitializeComponent();

            backgroundWorker = new BackgroundWorker {WorkerReportsProgress = true, WorkerSupportsCancellation = true};
            backgroundWorker.DoWork += (o, args) =>
            {
                var number = double.Parse(args.Argument.ToString());
                double tmp = 0;
                for (int i = 1; i < 101; i++)
                {
                    if (backgroundWorker.CancellationPending)
                    {
                        args.Cancel = true;
                        return;
                    }
                    tmp += Math.Sqrt(number*i);
                    Thread.Sleep(50);
                    backgroundWorker.ReportProgress(i);
                }

                args.Result = tmp;
            };

            backgroundWorker.RunWorkerCompleted += (sender, args) =>
                                                       {
                                                           if (!args.Cancelled)
                                                           {
                                                               tbResult.Text = "Zadanie ukończone. Wynik: " +
                                                                               args.Result;
                                                           }
                                                           else
                                                           {
                                                               tbResult.Text = "Zadanie zostało przerwane";
                                                           }
                                                       };

            backgroundWorker.ProgressChanged += (sender, args) =>
                                                    {
                                                        pbProgress.Value = args.ProgressPercentage;
                                                    };
        }

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.RunWorkerAsync(tbNumber.Text);
        }

        private void btnStop_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.CancelAsync();
        }
    }






Aplikacja do pobrania z linka: http://sdrv.ms/V0bXkg



Delegaty
Delegaty umożliwiają implementację asynchronicznych metod. Każda delegata posiada metody: BeginInvoke oraz EndInvoke. Wywołanie pierwszej z nich powoduje stworzenie osobnego wątku i wykonania w nim wskazywanej metody. Druga metoda pozwala na pobranie rezultatu wykonania metody.
Delegaty możemy użyć na trzy sposoby w celu tworzenia metod asynchronicznych:

  • wywołujemy BeginInvoke, wykonujemy zadanie a następnie wywołujemy EndInvoke na tym samym wątku
  • wywołujemy BeginInvoke a następnie oczekując na zakończenie wywołania, wykonujemy inne operacje
  • wywołanie metody po zakończeniu pracy wątku w tle
Pierwsze rozwiązanie jest najprostsze w wykonaniu, jednak nie jest idealne. W momencie wywołania metody EndInvoke nastąpi zablokowanie wątku głównego i oczekiwanie aż wątek w tle zakończy swoją pracę:

Code:
            MyDelegate myDelegate = DoSomething;
            myDelegate(null);
            IAsyncResult result = myDelegate.BeginInvoke(5, null, null);
            lblOnTheSameThreadResult.Text = myDelegate.EndInvoke(result).ToString();

Drugi przypadek zakład iż, w czasie oczekiwania na rezultat wykonania się operacji w innym wątku, będziemy wykonywać inne rzeczy w głównym wątku:

Code:
            MyDelegate myDelegate = DoSomething;
            myDelegate(null);
            IAsyncResult result = myDelegate.BeginInvoke(5, null, null);
            while (!result.IsCompleted)
            {
                //Do some operations here
            }
            lblPoolingResult.Text = myDelegate.EndInvoke(result).ToString();

Ostatnia możliwość zakłada, iż rezultat wykonania operacji asynchronicznej nie będzie nam potrzebny w wątku, który go wykonał. Po zakończeniu obliczeń, zostanie wywołana metoda, w której możemy odebrać naszą wartość:

Code:
            MyDelegate myDelegate = DoSomething;
            myDelegate(null);
            IAsyncResult result = myDelegate.BeginInvoke(5,
                            new AsyncCallback(ar =>
                                                {
                                                    var d = (MyDelegate) ar.AsyncState;
                                                    double res = d.EndInvoke(ar);
                                                    Dispatcher.Invoke(() =>
                                                                          {
                                                                              lblCallbackResult.Text = res.ToString();
                                                                          });
                                                }), myDelegate);

Kod do pobrania: http://sdrv.ms/RPK8iZ



Wątki
Chcąc mieć 100% kontrolę nad wątkami, możemy skorzystać z klasy Thread. Podstawowe operacje w przypadku pracy z wątkami:
1. Tworzenie wątku:

Code:
            var thread = new Thread(() =>
                                        {
                                            //...Do something
                                        });
            thread.Start();

2. Niszczenie wątku:
Wątek można zniszczyć za pomocą metody Abort. Wywołanie tej metody na działającym wątku spowoduje rzucenie wyjątku ThreadAbortException:

Code:
            try
            {
                thread.Abort();
            }
            catch (ThreadAbortException e)
            {
                Console.WriteLine(e);
            }

3. Synchronizacja wątków
Zakleszczenia (deadlock) - występują gdy procesy czekają nawzajem na siebie - żaden nie jest w stanie ukończyć pracy
Sytuacje wyścigu (race conditions) - występują w momencie gdy więcej niż jeden proces odwołuje się do niezabezpieczonej pamięci współdzielonej
Do synchronizacji możemy wykorzystać słowo kluczowe lock:

Code:
            lock (aobj)
            {
                
            }



Wątki a kontrolki
Kontrolki na formatce obsługuje wątek nazywany UI Thread. Dostęp do kontrolek z innych wątków nie jest bezpieczny. Windows Forms jak i WPF definiują wzorce dostępu do wątku interfejsu:
Windows Forms:
Za pomocą delegaty odwołujemy się do wątku interfejsu:

Code:
        public delegate void SetTextDelegate(string t);
        public void SetText(string text)
        {
            if (textBox1.InvokeRequired)
            {
                var del = new SetTextDelegate(SetText);
                del.Invoke(text);
            }
            else
            {
                textBox1.Text = text;
            }
        }

WPF:
Za pomocą obiektu Dispatcher, wykonujemy zmiany w interfejsie:

Code:
            Dispatcher.Invoke(() => { });
            Dispatcher.BeginInvoke(() => { }, DispatcherPriority.Normal);

Pierwsza metoda jest blokująca. Druga nie zablokuje wątku interfejsu w czasie jego aktualizacji.

piątek, 28 grudnia 2012

70-511 Rozdział 8 - implementacja IDataErrorInfo

Poprzednio zostały przedstawione reguły walidacji - ValidationRules. Mechanizm ten umożliwiał łatwe tworzenie reguł, według których były walidowane dane. Interfejs IDataErrorInfo implementujemy na poziomie obiektu. Dodaje to dodatkowy poziom walidacji - na poziomie poszczególnych obiektów.
Interfejs ten posiada dwie składowe:
  • Error - informacja o błędzie w obiekcie
  • Item - informacja o błędzie dla danego pola w klasie
Właściwość Error w większości przypadków nie jest implementowana (aby dostać jej wartość, należy bezpośrednio odwołać się do danego pola klasy). Składowa Item, integruje się z istniejącym systemem walidacji.

Po zaimplementowaniu interfejsu w obiekcie, należy włączyć powiadamianie o błędach w definicji bindowania (ValidatesOnDataErrors=True). Kolejnym elementem jest ustawienie właściwości UpdateSourceTrigger. Jeżeli będzie ona ustawiona na tryb PropertyChanged (walidacja po każdej zmianie wartości) może spowodować to duże obciążenie dla procesora. Jeżeli podczas walidacji wystąpi błąd, zostanie on dodany do kolekcji Validation.Errors. Aby go wyświetlić, możemy stworzyć odpowiedni styl dla kontrolki, w którym uwzględnimy sposób wyświetlania błędów.

Przykładowa implementacja z zastosowaniem w walidacji obiektu:

Code:
<Window x:Class="IDataErrorInfo_Implementation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:IDataErrorInfo_Implementation" Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:Person x:Key="personObj"></local:Person>
        <Style TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self},
                                                    Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80" />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        
        <TextBlock>First Name:</TextBlock>
        <TextBox HorizontalAlignment="Left" VerticalAlignment="Center"
            Text="{Binding FirstName, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True}" 
            Grid.Column="1" Width="100"/>

        <TextBlock Grid.Row="1">Last Name:</TextBlock>
        <TextBox HorizontalAlignment="Left" VerticalAlignment="Center"
            Text="{Binding Source={StaticResource personObj}, Path=LastName, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True}" 
            Grid.Column="1" Width="100" Grid.Row="1"/>
    </Grid>
</Window>

Code:
using System.ComponentModel;

namespace IDataErrorInfo_Implementation
{
    public class Person : IDataErrorInfo, INotifyPropertyChanged
    {
        private int id;
        private string firstName;
        private string lastName;
        private string city;

        public int Id
        {
            get { return id; }
            set
            {
                if (value == id) return;
                id = value;
                OnPropertyChanged("Id");
            }
        }

        public string FirstName
        {
            get { return firstName; }
            set
            {
                if (value == firstName) return;
                firstName = value;
                OnPropertyChanged("FirstName");
            }
        }

        public string LastName
        {
            get { return lastName; }
            set
            {
                if (value == lastName) return;
                lastName = value;
                OnPropertyChanged("LastName");
            }
        }

        public string City
        {
            get { return city; }
            set
            {
                if (value == city) return;
                city = value;
                OnPropertyChanged("City");
            }
        }

        public string this[string columnName]
        {
            get
            {
                string result = "";
                switch (columnName)
                {
                    case "FirstName":
                        if (!string.IsNullOrWhiteSpace(FirstName) && FirstName.Length < 3)
                        {
                            result = "First name must be specified";
                        }
                        break;
                    case "LastName":
                        if (string.IsNullOrWhiteSpace(LastName))
                        {
                            result = "Last name must be specified";
                        }
                        break;
                }

                return result;
            }
        }

        public string Error
        {
            get { throw new SystemNotImplementedException(); }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}




Tworzony kod do pobrania: http://sdrv.ms/12GZQ0P

czwartek, 27 grudnia 2012

70-511 Rozdział 8 - Walidacja Windows Forms

Walidacja w WPF została omówiona w poście poświęconym bindowaniu danych. Tym razem przyjrzymy się w jaki sposób można obsłużyć walidację w Windows Forms.

Walidacja na poziomie pojedynczej kontroki
Walidacja pojedynczych pól odbywa się w momencie gdy użytkownik wprowadza dane np. do TextBox-a. Jeżeli chodzi o tę kontrolkę, mamy szereg właściwości ułatwiających ten proces:
  • MaxLength - maksymalna długość tekstu w kontrolce
  • PasswordChar - jaki znak zostanie użyty podczas zasłaniania zawartości kontrolki - przeważnie używane w polach w które wprowadzamy hasło
  • ReadOnly - czy jest tylko do odczytu
  • MultiLine - czy można wprowadzać wiele linii tekstu
Oprócz właściwości, mamy dostępne trzy zdarzenia wywoływane w czasie wprowadzania danych do kontrolek:
  • KeyDow
  • KeyPres
  • KeyUp
KeyDown i KeyUp wywoływane są odpowiednio w momencie naciśnięcia i zwolnienia klawisza. Zdarzenia te są najczęściej wykorzystywane do determinacji czy został wciśnięty jedne z klawiszy: Alt, Shift, Ctrl. Klawiszom tym odpowiadają właściwości logiczne które możemy sprawdzić w metodzie podpiętej pod zdarzenie:

Code:
sender, KeyEventArgs e)
        {
            var alt = e.Alt;
            var ctrl = e.Control;
            var shift = e.Shift;
        }

        private void textBox1_KeyUp(object sender, KeyEventArgs e)
        {
            var alt = e.Alt;
            var ctrl = e.Control;
            var shift = e.Shift;
        }

Za pomocą właściwości KeyCode możemy sprawdzić, który klawisz został naciśnięty:

Code:
            if (e.KeyCode == Keys.A)
            {
                MessageBox.Show("Naciśnięto klawisz");
            }


Zdarzenie KeyPress wywoływane jest, gdy naciśnięty klawisz reprezentuje wartość z tablicy ASCI (są to klawisze liter a - z, liczb, znaków specjalnych. Jeżeli naciśnięte klawisze/klawisz nie tworzą kodu ASCII, zdarzenie nie zostanie wywołane. Przykładem są klawisze Alt, Ctrl. Za pomocą właściwości KeyChar mamy możliwość sprawdzenia jaki klawisz został naciśnięty. Naciśnięty klawisz możemy następnie sprawdzić jawnie (przyrównując go bezpośrednio do żądanego klawisza) bądź skorzystać z metod zawartych w klasie Char:
  • Char.IsDigit(e.KeyChar);
  • Char.IsLetter(e.KeyChar);
  • Char.IsLetterOrDigit(e.KeyChar);
  • Char.IsPunctuation(e.KeyChar);
  • Char.IsLower(e.KeyChar);      
  • Char.IsUpper(e.KeyChar);


Aktywność
Aktywowanie kontrolki, czy też ustawienie fokusa na niej powoduje, że możemy z nią prowadzić interakcje wprowadzając i zmieniając zawartość za pomocą myszki i klawiatury.
Każda kontrolka implementuje metodę Focus, która zwraca wartość logiczną czy udało się ją aktywować. Zdarzenia odpowiedzialne za aktywację kontrolki wywoływane są w następującej kolejności:
  • Enter - wywoływane w momencie gdy kontrolka uzyskała fokus
  • GotFocus - wywoływane w momencie gdy kontrolka po raz pierwszy uzyskała fokus
  • Leave - wywoływane w momencie gdy kontrolka straciła fokus
  • Validating - w momencie walidacji
  • Validated - po zakończeniu walidacji
  • LostFocus - wywoływane w momencie gdy kontrolka po raz pierwszy straciła fokus
Walidację najlepiej przeprowadzić w zdarzeniu Validating. Zdarzenie to jest wywoływane jeżeli zostanie ustawiona flaga CausesValidation kontrolki. Domyślnie ta wartość jest ustawiona, więc nie ma potrzeby tego zmieniać ręcznie. Metoda podpięta pod zdarzenie Validating przyjmuje jako jeden z parametrów obiekt klasy CancelEventArgs. Możemy skorzystać z właściwości Cancel - spowoduje to, iż kontrolka nie straci fokusa w przypadku np. niepowodzenia walidacji.
Przykładowy kod:

Code:
        private void textBox1_Validating(object sender, CancelEventArgs e)
        {
            if (textBox1.Text.Length < 3)
            {
                e.Cancel = true;
            }
        }

Walidację całej formatki można zaimplementować w następujący sposób: klawisz akceptujący wartości czy też przechodzący do następnego kroku przeprowadzi walidację wszystkich pól i w razie niepowodzenia, ustawia fokus na pierwszej kontrolce z nieprawidłową wartością. Przykład:

Code:
        private void button1_Click(object sender, EventArgs e)
        {
            foreach (Control control in Controls)
            {
                if (control is TextBox)
                {
                    if (string.IsNullOrWhiteSpace(control.Text))
                    {
                        control.Focus();
                        return;
                    }
                }
            }
        }

Błędy należy w graficzny sposób przedstawić użytkownikowi. Przydatna do tego jest kontrolka ErrorProvider. Za pomocą metody SetError ustawiamy na danej kontrolce błąd. Aby usunąć powiadomienie o błędzie, należy do metody SetError podać jako błąd pusty ciąg znaków. Przykład:

Code:
            if (textBox1.Text.Length < 3)
            {
                e.Cancel = true;
                errorProvider1.SetError(sender as TextBox, "Błąd walidacji!");
            }
            else
            {
                errorProvider1.SetError(sender as TextBox, "");
            }




Kod do pobrania: http://sdrv.ms/12GZW8A

środa, 26 grudnia 2012

70-511 Rozdział 8 - DataGrid

Kontrolka ta jest analogiczna do DataGridView z WindowsForms, tylko przeznaczona dla WPF. Dane do których chcemy się bindować muszą implementować interfejs IEnumerable. Domyślnie zostaną stworzone kolumny na podstawie nazw pól klasy której obiekty są bindowane do DataGrid. Źródło danych ustawiamy przez właściwość ItemsSource. Możemy ją przypisać zarówno w kodzie jak i XAML-u:

Code:
dgCustomers.ItemsSource = customerRepository.GetAllCustomers();

Code:
        <DataGrid Name="dgCustomers" ItemsSource="{Binding}">
            
        </DataGrid>

Podobnie jak w DGV mamy predefiniowane typy kolumn:
  • DataGridTextBoxColumn - dane tekstowe
  • DataGridComboBoxColumn - dane do wyboru z listy (ComboBox)
  • DataGridCheckBoxColumn - dane logiczne
  • DataGridHyperlinkColumn - linki do danych
Jeżeli dostępne typy kolumn nam nie wystarczają możemy skorzystać z DataGridTemplateColumn. Szablon pozwala zdefiniować wygląd każdego z elementów kolumny. Możemy także skorzystać z właściwości TemplateSelector, wybierając szablon na podstawie zdefiniowanej logiki.
Przykładowa kolumna z własnym stylem:

Code:
<DataGrid Name="dgCustomers" ItemsSource="{Binding}">
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="TemplateColumn" >
                    <DataGridTemplateColumn.CellTemplate>
                        <ItemContainerTemplate>
                            <Grid>
                                <Rectangle>
                                    <Rectangle.Fill>
                                        <SolidColorBrush Color="Blue"></SolidColorBrush>
                                    </Rectangle.Fill>
                                </Rectangle>
                                <TextBlock Text="{Binding FirstName}" Foreground="Chartreuse"></TextBlock>
                            </Grid>
                        </ItemContainerTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>



Ciekawą opcją jest możliwość wyświetlenia dodatkowych informacji o zaznaczonym rekordzie (RowDetailsTemplate). Jeżeli dana tabela ma wiele kolumn, możemy wyświetlić najczęściej używane dane, a pozostałe wyświetlać w momencie kiedy tego sobie zażyczy użytkownik.
Przykład:

Code:
            <DataGrid.RowDetailsTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="25" />
                            <RowDefinition Height="25" />
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="50" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="Imię: " Grid.Row="0" />
                        <TextBlock Text="{Binding FirstName}" Grid.Column="1" />
                        <TextBlock Text="Imię: " Grid.Row="1" />
                        <TextBlock Text="{Binding LastName}" Grid.Row="1" Grid.Column="1" />
                    </Grid>
                </DataTemplate>
            </DataGrid.RowDetailsTemplate>



Dodatkowe właściwości które możemy ustawić:
  • RowDetailsVisibilityMode - tryb wyświetlania wiersza szczegółów, możliwe tryby:
    • VisibleWhenSelected - domyślny tryb - wiersz widoczny dla zaznaczonego rekordu
    • Visible - widoczny dla wszystkich rekordów
    • Collapsed - brak widoczności wiersza szczegółów
  •  RowDetailsTemplateSelector - logika wyboru szablonu dla wiersza szczegółów
 Kody: http://sdrv.ms/UHcTum

wtorek, 25 grudnia 2012

70-511 Rozdział 8 - Bindowanie w Windows Forms

Windows Forms także umożliwia bindowanie danych do kontrolek. Mechanizm ten nie jest tak bardzo rozwinięty jak w przypadku WPF, jednak umożliwia obsłużenie najczęstszych scenariuszy.
Omówione zostaną także kontrolki DataGridView (Windows Forms) umożliwiająca wyświetlanie danych tabelarycznych.


Bindowanie proste
Bindowanie to polega na przypisaniu pojedynczemu elementowi wartości z danej kolumny tabeli.
Przykładowe przypisanie
Przykładowe przypisanie wartości kolumny FirstName do kontrolki Label:

Code:
            var customers = new DataService().GetSampleDataTable();
            label1.DataBindings.Add("Text", customers, "FirstName");


Bindowanie złożone
Występuje gdy więcej niż jedna kolumna (np. obiektu DataTable) jest bindowana do więcej niż jednej właściwości kontrolki. Przykładami mogą być tutaj DataGridView wyświetlający całą tabelę, bądź kontrolka List wyświetlająca więcej niż jedną kolumnę. Przykład bindowania do DataGridView w dwóch wersjach: za pomocą BindingSource oraz DataSet:

Code:
            var dataSet = new DataService().GetSampleDataSet();
            //1. Bindowanie do BindingSource
            var bindingSource = new BindingSource(dataSet, "Customers");
            dataGridView1.DataSource = bindingSource;

            //2. Bindowanie do DataSet
            dataGridView1.DataSource = dataSet;
            dataGridView1.DataMember = "Customers";



Nawigacja między rekordami
Dzięki kontrolce BindingNavigator można szybko stworzyć prostą nawigację po rekordach. Wystarczy przeciągnąć tę kontrolkę na formatkę i przypisać źródło danych do właściwości BindingSource:

Code:
bindingNavigator1.BindingSource = customersBindingSource;

Efekt:




DataGridView - Windows Forms
Kontrolka służąca do wyświetlania danych tabelarycznych pobranych najczęściej z bazy danych. Wyżej zostało przedstawione w jaki sposób bindować do niej dane.
DataGridView (w skrócie będę pisać DGV) posiada sześć wbudowanych typów kolumn:
  •  DataGridViewTextBoxColumn - typ wykorzystywany do wyświetlania danych łańcuchowych i numerycznych
  • DataGridViewCheckBoxColumn - wyświetlanie danych logicznych
  • DataGridViewImageColumn - wyświetlanie grafiki
  • DataGridViewButtonColumn - komórka, której zawartością jest przycisk
  • DataGridViewComboBoxColumn - zawiera kontrolkę ListBox 
  • DataGridViewLinkColumn - tworzy linki, np. do innych danych
  • Custom - jeżeli wbudowane typy kolumn nam nie wystarczają, możemy stworzyć naszą poprzez dziedziczenie z klasy DataGridViewColumn
 Podstawowymi operacjami wykonywanymi na kontrolce DGV są: dodawanie kolumn, usuwanie kolumn, pobieranie wartości aktualnie zaznaczonej komórki.
Przykłady tych operacji w C#

Code:
            //1. Dodawanie kolumny
            var firstNameDataColumn = new DataGridViewTextBoxColumn();
            firstNameDataColumn.Name = "FirstName";
            firstNameDataColumn.HeaderText = "Imię";
            firstNameDataColumn.ValueType = typeof (string);
            dataGridView2.Columns.Add(firstNameDataColumn);

            //2. Usuwanie kolumny
            dataGridView2.Columns.Remove("FirstName");

            //3. Informacja o aktualnie wybranej komórce
            var currentSelectedCell = dataGridView2.CurrentCell;
            var dataGridViewColumn = currentSelectedCell.OwningColumn; //Kolumna
            var dataGridViewRow = currentSelectedCell.OwningRow; //Wiersz
            var value = currentSelectedCell.Value; //wartość wybranej komórki

Komórki DGV mogą być edytowane przez użytkownika. Ważna jest więc walidacja danych wprowadzanych przez użytkownika. Walidację przeprowadzamy przez podpięcie się do zdarzenia CellValidating:

Code:
            dataGridView2.CellValidating += (sender, e) =>
                                                {
                                                    //Jeżeli kolumna byłaby zbindowana z źródłem danych wtedy zamaist właściwości Name, sprawdzamy DataPropertyName
                                                    if (dataGridView2.Columns[e.ColumnIndex].Name == "FirstName")
                                                    {
                                                        if (string.IsNullOrWhiteSpace(e.FormattedValue.ToString()))
                                                        {
                                                            dataGridView2.Rows[e.RowIndex].ErrorText =
                                                                "Pole imię musi być uzupełnione";
                                                            e.Cancel = true;
                                                        }
                                                        else
                                                        {
                                                            dataGridView2.Rows[e.RowIndex].ErrorText = null;
                                                        }
                                                    }
                                                };

Ważne jest tutaj, że  w przypadku gdy kolumna była tworzona przez nas (ma podaną nazwę) - używamy właściwości Name, a gdy DGV jest bindowany z obiektem typu DataSet należy skorzystać z właściwości DataPropertyName.


Komórki w których użytkownik błędnie wprowadził dane, zostaną oznaczone czerwoną ikoną wykrzyknika, a dodatkowo jeżeli na niego najedziemy otrzymamy zawartość pola ErrorText. 

Ciekawą opcją jest możliwość nadawania wyglądu poszczególnym komórką. Obsługa polega na przypięciu się do zdarzenia CellPainting i napisaniu odpowiedniej logiki. Przykład:

Code:
            dataGridView1.CellPainting += (sender, e) =>
                                              {
                                                  if (e.Value != null && e.Value.ToString() == "Liu")
                                                  {
                                                      e.Graphics.FillRectangle(Brushes.Aqua ,e.CellBounds);
                                                      e.Graphics.DrawString(e.Value.ToString(), e.CellStyle.Font, Brushes.Black, e.CellBounds.X, e.CellBounds.Y);
                                                      e.Handled = true;
                                                  }
                                              };




Kod: http://sdrv.ms/UHd8Wi

poniedziałek, 24 grudnia 2012

70-511 Rozdział 7 - Binding - Sortowanie, Grupowanie oraz Filtrowanie

Sortowanie
Sortowanie zbindowanych danych z kontrolką umożliwia interfejs ICollectionView. Po pobraniu do niego referencji mamy dostęp do kolekcji SortDescriptions, która zawiera obiekty typu SortDescription. Obiekty te definiują po której właściwości dotyczą oraz kierunek sortowania. Umieszczając więcej niż jedną definicję sposobu sortowania, będzie ono miało takie samo znaczenie jak w przypadku zapytań SQL: kolekcja najpierw zostanie posortowana według pierwszego parametru a następnie według drugiego.

Code:
            var collectionView = CollectionViewSource.GetDefaultView(DataContext);
            collectionView.SortDescriptions.Add(new SortDescription("Price", ListSortDirection.Ascending));
            collectionView.SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending));

Efekt:



Jeżeli bindujemy do źródła, które nie implementuje interfejsu IBindingList, możemy napisać własne porównywanie obiektów a następnie przypisać je do właściwości CustomSort.

Własny mechanizm porównywania opiera się stworzenie klasy która implementuje interfejs IComparer. następnie rzutujemy ICollectionView na ListCollectionView i przypisujemy do właściwości CustomSort obiekt naszej klasy:

Code:
            var collectionView = (ListCollectionView)CollectionViewSource.GetDefaultView(DataContext);
            collectionView.CustomSort = new CustomComparer();



Grupowanie
Kolejną operacją którą oferuje ICollectionView jest grupowanie. Grupy tworzymy poprzez dodanie do kolekcji GroupDescriptions obiektu klasy PropertyGroupDescription. Aby zobaczyć efekt należy jeszcze stworzyć styl wyświetlania dla zgrupowanych obiektów GroupStyle:
Obiekt GroupStyle zawiera następujące właściwości:
  • ContainerStyle - styl dla grupy
  • ContainerStyleSelector - logika wyboru stylu dla grupy
  • HeaderTemplate - styl nagłówka dla grupy
  • HeaderTemplateSelector - logika wybierania stylu dla nagłówka grupy
  • Panel - styl dla panelu, na którym są prezentowane grupowane elementy

Prosty przykład grupowania książek na podstawie ich ceny:

Code:
        <ListBox ItemsSource="{Binding}" Name="lbBooks" ItemTemplate="{StaticResource BooksTemplate}">
            <ListBox.GroupStyle>
                <GroupStyle>
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <Label Content="{Binding Path=Name}" Foreground="Purple" Background="Red" Padding="4" />
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </ListBox.GroupStyle>
        </ListBox>

Code:
collectionView.GroupDescriptions.Add(new PropertyGroupDescription("Price"));


Efekt:


W HeaderTemplate Label bindowany jest do właściwości Name. Właściwość ta jest pobierana z obiektu klasy PropertyGroupDescription.



Filtrowanie
ICollectionView posiada wbudowany mechanizm filtrowania poprzez właściwość Filter. Jest to delegata ktra przyjmuje jako parametr obiekt i zwraca wartość logiczną.
Przykładowa implementacja, odfiltrowująca książki które kosztują ponad 5 zł:

Code:
            collectionView.Filter += o =>
                                         {
                                             var book = (Book) o;
                                             return book.Price < 5;
                                         };

Efekt:




Filtrowanie obiektów ADO.NET
Metoda wyżej opisana nie zadziała na obiekty związane z ADO.NET (DataSet, Table). Jest to spowodowane tym, iż tak naprawdę bindujemy się do obiektu typu DataView. DataView posiada własny mechanizm filtrowania. Możemy z niego skorzystać poprzez właściwość BindingListCollectionView.CustomFilter. Właściwość ta przyjmuje parametr jako łańcuch w formacie:
<ColumnName> <Operator> <Value>
Przykładowo jeżeli chcemy odfiltrować osoby których imię to Lucy, wystarczy napisać:
FirstName = 'Lucy'
W przypadku wartości łańcuchowych zamykamy je w apostrofach.
Przykład w kodzie:

Code:
            var customersCollectionView =
                (BindingListCollectionView)CollectionViewSource.GetDefaultView(((DataContextContent) DataContext).Customers);
            customersCollectionView.CustomFilter = "FirstName = 'Lucy'";


Kod prezentowanych przykładów: http://sdrv.ms/Wl1Yb7

niedziela, 23 grudnia 2012

70-511 Rozdział 7 - Binding - Hierarchical Data Templates

W tej części o szablonach dla danych hierarchicznych.

Hierarchical Data Templates
Wyświetlając hierarchiczne dane w kontrolkach takich jak TreeView czy Menu możemy użyć HierarchicalDataTemplate. Klasa ta posiada wszystkie właściwości znane z klasy DataTemplate oraz kilka dodatkowych właściwości:
  • ItemBindingGroup - wartość tej właściwości jest kopiowana do wszystkich podrzędnych elementów danego elementu 
  • ItemContainerStyle - ustawia styl dla każdego z elementów podrzędnych
  • ItemContainerStyleSelector - logika wybierania stylu dla elementów
  • ItemsSource - ścieżka do następnego poziomu elementów względem obecnego
  • ItemStringFormat - formatowanie tekstu
  • ItemTemplate - szablon elementu
  • ItemTemplateSelector - logika wybierania szablonu prezentacji
Dwa przykłady obrazujące w jaki sposób poradzić sobie z szablonami dla danych hierarchicznych (pełny kod źródłowy do pobrania z linka podanego poniżej):

Definicje klas dla przykładu z firmami:

Code:
    [Serializable]
    public class Company
    {
        public string Name { get; set;}

        public List<CompanyState> CompanyStates { get; set; }
    }

    [Serializable]
    public class CompanyState
    {
        public string Name { get; set; }

        public List<Unit> Units { get; set; }
    }

    public class Unit
    {
        public string Name { get; set; }

        public List<Department> Departments { get; set; }
    }

    public class Department
    {
        public string Name { get; set; }

        public List<Employee> Employees { get; set; }
    }

    public class Employee
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }


Definicje hierarchii:

Code:
<Window x:Class="HierarchicalDataTemplates.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:bindingDataTemplates="clr-namespace:Binding_DataTemplates;assembly=SampleDataProvider" Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <HierarchicalDataTemplate x:Key="TreeViewDataTemplate" ItemsSource="{Binding Friends}">
            <TextBlock Text="{Binding Name}"></TextBlock>
            <HierarchicalDataTemplate.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding FirstName}"/>
                            <TextBlock Text=" "></TextBlock>
                            <TextBlock Text="{Binding LastName}"/>
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>
        </HierarchicalDataTemplate>

        <HierarchicalDataTemplate x:Key="CompanyDataTemplate" ItemsSource="{Binding CompanyStates}" DataType="{x:Type bindingDataTemplates:Company}">
            <TextBlock Text="{Binding Name}" />
            <HierarchicalDataTemplate.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Units}">
                    <TextBlock Text="{Binding Name}" />
                    <HierarchicalDataTemplate.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding Departments}">
                            <TextBlock Text="{Binding Name}" />
                            <HierarchicalDataTemplate.ItemTemplate>
                                <HierarchicalDataTemplate ItemsSource="{Binding Employees}">
                                    <TextBlock Text="{Binding Name}" />
                                    <HierarchicalDataTemplate.ItemTemplate>
                                        <DataTemplate>
                                            <StackPanel Orientation="Horizontal">
                                                <TextBlock Text="{Binding Id}" />
                                                <TextBlock Text=" "></TextBlock>
                                                <TextBlock Text="{Binding FirstName}" />
                                                <TextBlock Text=" " />
                                                <TextBlock Text="{Binding LastName}" />
                                            </StackPanel>
                                        </DataTemplate>
                                    </HierarchicalDataTemplate.ItemTemplate>
                                </HierarchicalDataTemplate>
                            </HierarchicalDataTemplate.ItemTemplate>
                        </HierarchicalDataTemplate>
                    </HierarchicalDataTemplate.ItemTemplate>
                </HierarchicalDataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>
        </HierarchicalDataTemplate>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <TreeView ItemTemplate="{StaticResource TreeViewDataTemplate}" ItemsSource="{Binding StatesList}" Grid.Column="0" />
        <TreeView ItemTemplate="{StaticResource CompanyDataTemplate}" ItemsSource="{Binding Company}" Grid.Column="1" />
    </Grid>
</Window>

Efekt:



Cały kod aplikacji można pobrać z linku: http://sdrv.ms/ZCHejE

sobota, 22 grudnia 2012

70-511 Rozdział 7 - Binding - Data Templates

Data Templates - czyli szablon prezentacji danych określa w jaki sposób użytkownik zobaczy dane prezentowane np. w ListBox-ie.
Do tej pory wykorzystywaliśmy dla ListBoxa właściwość DisplayMemberPath. Teraz możemy napisać szablon:

Code:
        <ListBox ItemsSource="{Binding}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Title}"></TextBlock>
                            <TextBlock Text=" - "></TextBlock>
                            <TextBlock Text="{Binding Author}"></TextBlock>
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

Efekt:


W zależności od tego czy mamy do czynienia z kontrolką typu Item (np. ListBox) czy też Content (Label) przypisujemy nasz szablon do odpowiedniej właściwości:
  • Items control - ItemTemplate
  • Content control - ContentTemplate
 Większość szablonów definiujemy jako zasoby, które następnie przypisujemy do odpowiedniej kontrolki:

Code:
<Window x:Class="Binding_DataTemplates.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="BookTemplate">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Title}"></TextBlock>
                    <TextBlock Text=" - "></TextBlock>
                    <TextBlock Text="{Binding Author}"></TextBlock>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ListBox ItemsSource="{Binding}" ItemTemplate="{StaticResource BookTemplate}">
            <!--
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Title}"></TextBlock>
                            <TextBlock Text=" - "></TextBlock>
                            <TextBlock Text="{Binding Author}"></TextBlock>
                        </StackPanel>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
            -->
        </ListBox>
    </Grid>
</Window>




Konwertery
Konwertery świetnie nadają się do warunkowego formatowania danych, np. zmiana koloru tekstu w TextBlock jeżeli cena książki jest wyższa od 10 złotych:

Code:
    [ValueConversion(typeof(decimal), typeof(Brush))]
    public class PriceForegroundConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var price = (decimal) value;
            if (price > 10)
            {
                return Brushes.Red;
            }

            return Brushes.Black;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

Code:
    <Window.Resources>
        <local:PriceForegroundConverter x:Key="ForegroudPriceConverter" />
        <DataTemplate x:Key="BookTemplate">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Title}" Foreground="{Binding Price, Converter={StaticResource ForegroudPriceConverter}}"></TextBlock>
                    <TextBlock Text=" - "></TextBlock>
                    <TextBlock Text="{Binding Author}"></TextBlock>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </Window.Resources>




DataTemplateSelector
Pozwala przypisać szablon wyświetlania danych w zależności od bindowanych danych. Dla przykładu możemy zrealizować poniższy scenariusz:
Jeżeli cena książki jest wyższa od 5 zł, chcemy wyświetlić jej tytuł na czerwono, oraz po autorze dodać informację o cenie - także wyświetloną na czerwono. W tym celu najpierw tworzymy dwa szablony wyświetlania danych:

Code:
    <Window.Resources>
        <local:PriceForegroundConverter x:Key="ForegroudPriceConverter" />
        <local:PriceDataTemplateSelector x:Key="PriceDataTemplateSelector" />
        <DataTemplate x:Key="BookTemplate">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Title}" />
                    <TextBlock Text=" - " />
                    <TextBlock Text="{Binding Author}" />
                </StackPanel>
            </Grid>
        </DataTemplate>
        <DataTemplate x:Key="BookTemplateAlternative">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Title}" Foreground="Red" />
                    <TextBlock Text=" - " />
                    <TextBlock Text="{Binding Author}" />
                    <TextBlock Text=" - "></TextBlock>
                    <TextBlock Text="{Binding Price}" Foreground="Red" />
                </StackPanel>
            </Grid>
        </DataTemplate>
    </Window.Resources>

Następnym krokiem jest implementacja klasy PriceDataTemplateSelector, która będzie dziedziczyć po klasie DataTemplateSelector. Klasa DataTemplateSelector zawiera jedną metodę, która możemy nadpisać - SelectTemplate. Metoda ta ma na celu zwrócić odpowiedni szablon. Przyjmuje ona dwa parametry:
  • item - czyli bindowany obiekt
  • container - kontener, do którego jest bindowany obiekt
Przykładowa implementacja:

Code:
    public class PriceDataTemplateSelector : DataTemplateSelector
    {
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            var element = container as FrameworkElement;
            var book = item as Book;
            if (element != null && item != null && book != null)
            {
                if (book.Price > 5)
                {
                    return (DataTemplate)element.FindResource("BookTemplateAlternative");
                }
                else
                {
                    return (DataTemplate)element.FindResource("BookTemplate");
                }
            }

            return null;
        }
    }


Ostatnim elementem do wykonania jest przypisanie do kontrolki naszego selektora:
Tworzymy zasób związany z selektorem:

Code:
        <local:PriceDataTemplateSelector x:Key="PriceDataTemplateSelector" />

oraz przypisujemy go do kontrolki:

Code:
        <ListBox ItemsSource="{Binding}" ItemTemplateSelector="{StaticResource PriceDataTemplateSelector}">


Efekt:



Źródła przykładów realizowanych w tym poście (wraz z przykładowymi danymi) - do pobrania z linku: http://sdrv.ms/12js6rd

piątek, 21 grudnia 2012

ObjectDataProvider - filtrowanie danych

W poście omawiającym bindowanie do różnych źródeł danych omawiałem ObjectDataProvider.
Tym razem bardziej praktyczny przykład. Scenariusz jest taki:
Na formatce jest ListBox oraz dwa TextBox-y do wprowadzania imienia i nazwiska. Baza danych przechowuje klientów. Formatka ma za zadanie wyświetlać wszystkich klientów jeżeli nie wprowadzono wartości filtrów do TextBox-ów, bądź też przefiltrować dane jeżeli filtry zostały wprowadzone przez użytkownika.

Rozwiązanie:
Tworzymy ObjectDataProvider wraz z definicją dwóch parametrów.
TextBox-y bindujemy z parametrami przekazywanymi do ObjectDataProvide.

Kod:

Code:
<Window x:Class="Binidng_ObjectDataProvider.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:binidngObjectDataProvider="clr-namespace:Binidng_ObjectDataProvider"
        xmlns:system="clr-namespace:System;assembly=mscorlib" Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <ObjectDataProvider x:Key="odpCustomers" ObjectType="{x:Type binidngObjectDataProvider:DataService}" MethodName="GetCustomersByFirstName" >
            <ObjectDataProvider.MethodParameters>
                <x:Static Member="system:String.Empty" />
                <x:Static Member="system:String.Empty" />
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <ListBox ItemsSource="{Binding Source={StaticResource odpCustomers}}" 
                 DisplayMemberPath="FirstName"></ListBox>
        
        <Grid Grid.Column="1" Margin="5">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="70" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <TextBlock VerticalAlignment="Center">Imię:</TextBlock>
            <TextBox Width="100" Grid.Column="1" HorizontalAlignment="Left">
                <TextBox.Text>
                    <Binding Source="{StaticResource odpCustomers}"
                             Path="MethodParameters[0]"
                             BindsDirectlyToSource="true"
                             UpdateSourceTrigger="LostFocus" />
                </TextBox.Text>
            </TextBox>

            <TextBlock VerticalAlignment="Center" Grid.Row="1">Nazwisko:</TextBlock>
            <TextBox Width="100" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left">
                <TextBox.Text>
                    <Binding Source="{StaticResource odpCustomers}"
                             Path="MethodParameters[1]"
                             BindsDirectlyToSource="true"
                             UpdateSourceTrigger="LostFocus" />
                </TextBox.Text>
            </TextBox>
        </Grid>
    </Grid>
</Window>

Najważniejsze w całym przedsięwzięciu są definicje bindowania dla TextBox. W nich specyfikujemy, który parametr ObjectDataProvider będzie związany zawartością w polu tekstowym.

Nasz serwis z metodą odpowiedzialną za zwracanie rekordów z bazy danych wygląda następująco:

Code:
using System.Collections.Generic;
using System.Linq;

namespace Binidng_ObjectDataProvider
{
    public class DataService
    {
        private readonly AdventureWorksLTEntities context = new AdventureWorksLTEntities();

        public DataService()
        {
            
        }

    public IList<Customer> GetCustomersByFirstName(string startLetter, string lastName)
        {
            var result = context.Customer.AsQueryable();
            if (!string.IsNullOrWhiteSpace(startLetter))
            {
                result = result.Where(x => x.FirstName.StartsWith(startLetter));
            }
            if (!string.IsNullOrWhiteSpace(lastName))
            {
                result = result.Where(x => x.LastName.StartsWith(startLetter));
            }

            return result.ToList();
        }
    }
}

Efekt:



Cały kod do pobrania z linku: http://sdrv.ms/UY8jdJ