Data Binding w Windows Phone 7

Udostępnij na: Facebook

Autor: Marcin Kruszyński

Opublikowano: 2010-12-27

Bardzo często w interfejsie użytkownika potrzebujemy zsynchronizować dane z klas biznesowych z kontrolkami. Możemy oczywiście robić to ręcznie, ale nowoczesne technologie, takie jak WPF czy Silverlight, dzięki data bindingowi potrafią ten proces znacznie uprościć i zautomatyzować. Nie inaczej jest w Silverlight na Windows Phone 7, który bazuje na Silverlight 3.

Po przeczytaniu tego artykułu będziesz:

  • wiedział, na czym polega data binding i pomiędzy jakimi obiektami może zachodzić,
  • znał rodzaje bindingu i jego parametry konfiguracyjne.

Wprowadzenie

Binding pełni rolę swoistego „kleju” pomiędzy kontrolkami UI i obiektami z danymi. Kiedy jest ustanowiony i dane ulegają zmianom, elementy UI zbindowane do tych danych automatycznie odzwierciedlają te zmiany. Również zmiany wprowadzone przez użytkownika w komponencie UI (np. edycja napisu w TextBox) mogą być automatycznie odzwierciedlane na obiekcie z danymi.

**Rys.1. Data Binding.

Rysunek 1 przedstawia koncepcję bindingu z podstawowymi informacjami:

  • Target – każda dependency property obiektu typu FrameworkElement wyświetla dane oraz może pozwalać na ich modyfikację.
  • Source – dowolny obiekt CLR, także docelowy lub inne obiekty UI.
  • Kierunek przepływu danych.
  • Value Converter – opcjonalny konwerter dla przekazywanych danych.  

Wyróżniamy następujące rodzaje bindingu:

  • OneWay – od obiektu źródłowego do propercji docelowej (wartość domyślna).
  • TwoWay – od obiektu źródłowego do propercji docelowej i w przeciwnym kierunku.
  • OneTime – tak jak OneWay, ale działa jednorazowo po stworzeniu bindingu i nie informuje o zmianach zachodzących potem na źródle.

Implementacja

Najczęściej binding definiujemy deklaratywnie w języku XAML, zwykle za pomocą markup extension (uproszczona składnia z wyrażeniem w nawiasach {}):

<TextBox Text="{Binding Title, Mode=TwoWay}"/>

Źródłem jest tutaj propercja Title obiektu z danymi, a celem propercja Text kontrolki TextBox. W tym przypadku binding jest dwukierunkowy – możemy edytować dane zmieniając tekst w kontrolce.

Propercję źródła wyrażamy za pomocą tzw. property path, co pozwala na zapis także bardziej złożonych przypadków (np. zagnieżdżona propercja,  attached property, indekser). Na przykład, aby wyświetlić wartość propercji Number obiektu w propercji Header należącej do obiektu w propercji Entity, zapiszemy to tak:

<TextBlock Text="{Binding Entity.Header.Number}"/>

W tym przypadku nie podaliśmy parametru Mode, co oznacza ustawienie domyślnej wartości OneWay.

Propercję źródła możemy także podać jako jawne ustawienie parametru Path lub nie określać jej przy bindingu jednokierunkowym, wtedy cały obiekt z danymi jest źródłem.  

<TextBox Text="{Binding Path=Title, Mode=TwoWay}"/>

<TextBlockText="{Binding}"/>

Oglądając dotychczas przedstawione przykłady, z pewnością zadawaliśmy sobie pytanie, jak określamy sam obiekt z danymi.

Możemy go nie określać, co oznacza przyjęcie domyślnej wartości z DataContext (propercja każdego obiektu typu FrameworkElement). DataContext jest dziedziczony, dzieci domyślnie przyjmują tę wartość od rodzica. Aby ją nadpisać na dziecku, wystarczy ustawić mu nowy DataContext albo jawnie określić źródło na bindingu. DataContext okazuje się szczególnie przydatny, gdy mamy wiele bindingów do tego samego źródła.

Jawnie definiujemy źródło bindingu na kilka sposobów:

  • za pomocą parametru Source (w XAML jako referencja StaticResource)

    <TextBlock Text="{Binding Title, Source= {StaticResource SampleAlbum}}"/>

  • za pomocą parametru ElementName z nazwą elementu UI w drzewie wizualnym (tzw. element binding)

    <TextBlock Text=" {Binding Text, ElementName=txtTitle}"/>

  • za pomocą parametru RelativeSource z wartością Self – obiekt z propercją docelową lub TemplatedParent – kontrolka, w której szablonie zawarta jest definicja bindingu (tzw. binding relatywny)

    <TextBox Text="{Binding Title, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>

W przypadku szablonów kontrolek istnieje również lekka odmiana bindingu, tzw. TemplateBinding (tylko ze ścieżką w postaci propercji kontrolki, bez innych parametrów). I w prostych przypadkach stanowi alternatywę dla bindingu relatywnego z opcją TemplatedParent.

<ContentControl Content="{TemplateBindingContent}"/>

Obiekt źródłowy musi implementować interfejs INotifyPropertyChanged, aby mógł informować o swoich zmianach obiekt docelowy. Przykładowa klasa Document implementująca ten interfejs może wyglądać następująco:

public class Document : INotifyPropertyChanged
{
    private string _title = "";

    public string Title
    {

      get { return _title; }

      set
      {
        _title = value;
        NotifyPropertyChanged("Title");
      }
    }   

    ...

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

    #regionINotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

   #endregion
}

Kolekcje muszą dodatkowo implementować interfejs INotifyCollectionChanged. Kryteria te spełnia standardowa kolekcja ObservableCollection<T>. Kolekcję zazwyczaj bindujemy do propercji ItemsSource kontrolki  ItemsControl, a pojedyncze elementy prezentujemy za pomocą raz zdefiniowanego szablonu DataTemplate w propercji ItemTemplate.

Przykładowo w obiekcie biznesowym możemy mieć propercję Items z kolekcją obiektów klasy Document, do której bindujemy kontrolkę ListBox i określamy dla niej odpowiedni DataTemplate:

<ListBox ItemsSource="{Binding Items}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Margin="0,0,0,17" Width="432">
                    <TextBlock Text="{Binding Title}" TextWrapping="Wrap"
                         Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                    <TextBlock Text="{Binding Author}" TextWrapping="Wrap"
                         Margin="12,-6,12,0" Style="{StaticResource
                         PhoneTextSubtleStyle}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
</ListBox>

W bindingach dwukierunkowych następuje aktualizacja źródła. Możemy decydować, w którym momencie ma to następować, za pomocą parametru UpdateSourceTrigger.

Gdy ten parametr nie jest ustawiony lub przyjmuje wartość Default, to aktualizacja następuje natychmiast po zmianie wartości propercji. Wyjątkiem jest tutaj propercja Text w TextBox, gdzie odbywa się to po utracie focusu przez tę kontrolkę.

Jeśli UpdateSourceTrigger przyjmuje wartość Explicit, to w pełni zarządzamy momentem aktualizacji w kodzie.

Załóżmy, że chcemy aktualizować dane natychmiast po zmianie tekstu w kontrolceTextBox, nie czekając na utratę przez nią focusu. Ustawiamy binding z aktualizacją Explicit oraz obsługujemy zdarzenie TextChanged:

<TextBox x:Name="txtTitle" Text="{Binding Title,Mode=TwoWay, UpdateSourceTrigger=Explicit}" TextChanged="txtTitle_TextChanged" />

private void txtTitle_TextChanged(object sender, TextChangedEventArgs e)
{
   BindingExpression be = txtTitle.GetBindingExpression(TextBox.TextProperty);
   be.UpdateSource();
}

Aktualizacji dokonujemy przez pobranie wyrażenia bindingu i wywołanie na nim metodyUpdateSource.

Czasami potrzebujemy zaprezentować dane w innym formacie, niż są przechowywane. Z pomocą przychodzi wtedy konwerter w postaci klasy implementującej interfejs IValueConverter. Definiujemy go w bindingu za pomocą parametrów: Converter (w XAML referencja do StaticResource), ConverterParameter (opcjonalny parametr konwersji) i ConverterCulture (opcjonalna kultura konwersji).

Na przykład, aby sformatować datę w polu tekstowym, wystarczy zdefiniować prosty konwerter:

public class DateTimeToStringConverter : IValueConverter
{
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter,
                                      System.Globalization.CultureInfo culture)
    {
          DateTime val = DateTime.Parse(value.ToString());
          return val.ToShortDateString();
    }

    public object ConvertBack(object value, Type targetType, object parameter,
                                              System.Globalization.CultureInfo culture)
    {
          return null;
    }

    #endregion
}

Implementacja metody ConvertBack ma znaczenie jedynie przy bindingu dwukierunkowym.

W logicznych zasobach definiujemy instancję klasy DateTimeToStringConverter z kluczem dateTimeToStringConverter. Następnie konfigurujemy binding:

<TextBlock Text="{Binding IssueDate,Converter={StaticResource dateTimeToStringConverter}}"/>

Z bindingiem powiązana jest walidacja. Błędy walidacji powstają, gdy zostanie wyrzucony wyjątek przez konwerter typu w infrastrukturze bindingu lub przez setter obiektu z danymi.

W porównaniu z Silverlight 3 w domyślnych szablonach predefiniowanych kontrolek nie zostały zdefiniowane stany wizualne informujące o błędach walidacji (w razie potrzeby możemy je sami zdefiniować i z nich korzystać – infrastruktura ma zaimplementowane odpowiednie mechanizmy do ich wywołania). Nie ma też znanych z SDK atrybutów walidacji i kontrolki ValidationSummary.

Walidację na bindingu włączamy, ustawiając jego parametr ValidatesOnExceptions na wartość true. Parametr NotifyOnValidationErrors z wartością true oznacza, że infrastruktura bindingu generuje routowane zdarzenie BindingValidationError z informacją o pojawieniu się błędu walidacji lub jego zniknięciu.  Możemy je obsłużyć na poziomie kontrolki z bindingiem, gdzie wystąpił błąd, lub wyżej w drzewie wizualnym.

Aby poinformować użytkownika o błędzie przy wprowadzaniu danych, możemy na przykład zrobić to tak:

<TextBox x:Name="txtAuthor" Text="{Binding Author,Mode=TwoWay, ValidatesOnExceptions=
True, NotifyOnValidationError=True}" BindingValidationError="txtAuthor_
BindingValidationError"/>

private void txtAuthor_BindingValidationError(object sender, ValidationErrorEventArgs e)
{
      e.Handled = true;
      MessageBox.Show(e.Error.ErrorContent.ToString(), "Invalid author value or format",
      MessageBoxButton.OK);
}

Należy wspomnieć, że w Windows Phone 7 zaleca się ograniczenie ilości wprowadzanych danych do niezbędnego minimum. Tam, gdzie jest to możliwe, należy dawać użytkownikowi opcje do wyboru, aby zminimalizować wpisywanie przez niego nieprawidłowych danych.

Warto wspomnieć, że wiele udogodnień w zakresie bindingu posiadają narzędzia Visual Studio 2010 oraz Expression Blend 4, także dla platformy WP7. Więcej na ten temat dowiesz się z artykułu Narzędzia dla Silverlight na Windows Phone 7.

Podsumowanie

W tym artykule poznaliśmy naturę działania data bindingu. Widzimy, gdzie tkwi potęga tego mechanizmu, ile rzeczy potrafi nam uprościć. Wiemy, jakie są rodzaje bindingu. Umiemy go konfigurować.


          

Marcin Kruszyński

Absolwent wydziału EAIE Akademii Górniczo-Hutniczej w Krakowie na kierunku Informatyka. Technologiami Microsoft zajmuje się od sześciu lat. Pracuje w firmie ComArch, gdzie tworzy rozwiązania oparte na platformie .NET i Silverlight. Od dwóch lat jest architektem w centrum badawczo-rozwojowym. Wcześniej zajmował się utrzymaniem oraz rozwojem aplikacji webowych w systemie EDI dla dużych sieci handlowych. Interesuje się wieloma aspektami platformy .NET od pierwszej wersji. Obecnie specjalizuje się w implementacji bogatych interfejsów graficznych aplikacji w oparciu o Silverlight (desktop i mobilne) i WPF, których rozwój śledzi od początku ich istnienia. Sporo uwagi poświęca też platformie WCF RIA Services. Jest pasjonatem nowych trendów i technologii (zwłaszcza w wydaniach CTP i beta), których poznawaniem zajmuje się hobbystycznie i zawodowo. Prelegent grup społecznościowych. Prowadził warsztaty z Silverlight w Krakowie (KGD.NET) i Wrocławiu (PGS). Prezentował tworzenie aplikacji w Silverlight na Windows Phone 7 na Visual Studio 2010 Community Launch we Wrocławiu i Krakowie. Pełnił funkcję eksperta w strefie Ask The Expert (ATE) technologii Windows Phone na konferencji Microsoft Technology Summit (MTS) 2010.