Programownie w technologii .NET
wykład 6 – Element Binding i Data Binding
Element Binding
• Mechanizm, który pozwala wydobyć pewne informacje z obiektu źródłowego i zapisać je w pewnym obiekcie docelowym.
• Obiektem docelowym zawsze jest jakaś własność zależnościowa, przeważnie w elemencie WPF (wiązanie danych służy głównie obsłudze interfejsu użytkownika).
• Obiektem źródłowym może być cokolwiek: inne kontrolki WPF, własne obiekty dowolnych klas, ich własności, kolekcje obiektów, dane XML.
• Klasyczne podejście polegałoby na obsłudze odpowiednich zdarzeń i ręcznym ustawianiu własności elementów (w kodzie).
• Wiązanie danych tworzy łącznik między interfejsem graficznym a źródłem danych, odpowiedzialny za ich pobieranie i wyświetlanie.
• Wiązanie danych w większości dotyczyć będzie wiązania elementów interfejsu użytkownika z danymi zaczerpniętymi z zewnętrznych źródeł.
Wiązanie elementów
• Najprostszy scenariusz mechanizmu wiązania danych.
• Elementem źródłowym jest własność zależnościowa pewnej kontrolki.
• W momencie zmiany własności obiektu źródłowego, następuje automatyczne powiadomienie i aktualizacja obiektu docelowego.
Nie wymaga szczególnych zabiegów u źródła:
<Window ...>
<StackPanel>
<Slider Name="fontSize" Minimum="12" Value="18"
Maximum="50" TickFrequency="2"
TickPlacement="TopLeft"/>
<TextBlock>
Tak działa Data Binding </TextBlock>
</StackPanel>
</Window>
Wiązanie definiowane jest w elemencie docelowym (zamiast podawania wartości pewnej własności) przy użyciu binding expression:
<TextBlock Margin="5"
FontSize="{Binding Path=Value, ElementName=fontSize}">
Tak działa Data Binding
</TextBlock>
Równoznaczny zapis:
<TextBlock Margin="5">
<TextBlock.FontSize>
<Binding Path="Value" ElementName="fontSize"/>
</TextBlock.FontSize>
Tak działa Data Binding
</TextBlock>
ElementName – wskazujemy na źródło wiązania (skąd pobierzemy wartość)
Path – wskazuje, którą własność odczytamy z obiektu źródłowego i użyjemy jako wartość naszej własności.
Path może być złożoną ścieżką (np. własność własności: FontFamily.Source lub indekser własności: Children[0]). Własności dołączone umieszczane są w nawiasach:
„(Grid.Row)”.
Błędy wiązania:
• Nie powodują podnoszenia wyjątków.
• Pojawia się jedynie informacja w Output window (w trybie debuggowania).
Tworzenie dowiązań w kodzie
Binding binding = new Binding();
binding.Source = fontSize;
binding.Path = new PropertyPath("Value");
binding.Mode = BindingMode.TwoWay;
tekst.SetBinding(TextBlock.FontSizeProperty, binding);
Usuwanie dowiązań:
BindingOperations.ClearBinding(tekst,
TextBlock.FontSizeProperty);
BindingOperations.ClearAllBindings(tekst);
Kiedy tego potrzebujemy?
• Dynamiczne tworzenie wiązań – same wiązanie warto jednak zdefiniować jako zasób, a w kodzie tylko wywoływać SetBinding().
• Usuwanie dowiązań.
• Tworzenie własnych kontrolek.
Kierunek dowiązania
<Slider Name="fontSize" .../>
<TextBlock Name="tekst"
FontSize="{Binding Path=Value, ElementName=fontSize}">
Tak działa Data Binding
</TextBlock>
<Button Click="myClick" ...>Big</Button>
private void Button_Click(object sender, RoutedEventArgs e) {
tekst.FontSize = 30;
}
Kierunek dowiązania
<Slider Name="fontSize" .../>
<TextBlock Name="tekst"
FontSize="{Binding Path=Value, ElementName=fontSize}">
Tak działa Data Binding
</TextBlock>
<Button Click="myClick" ...>Big</Button>
private void Button_Click(object sender, RoutedEventArgs e) {
tekst.FontSize = 30;
}
Kierunek dowiązania
<Slider Name="fontSize" .../>
<TextBlock Name="tekst"
FontSize="{Binding Path=Value, ElementName=fontSize}">
Tak działa Data Binding
</TextBlock>
<Button Click="myClick" ...>Big</Button>
private void Button_Click(object sender, RoutedEventArgs e) {
tekst.FontSize = 30;
}
Kierunek dowiązania
• Element docelowy zawsze aktualizowany jest automatyczne, niezależnie od tego, jak zmieni się źródło.
• Aktualizacja źródła w wyniku zmiany elementu docelowego jest zależna od wybranego trybu wiązania (własność Binding.Mode).
System.Windows.Data.BindingMode
• OneWay – aktualizowany jest wyłącznie cel, w wyniku zmiany źródła.
• TwoWay – aktualizacja obustronna – cel, gdy zmienia się źródło i źródło, gdy zmienia się cel.
• OneTime – własność docelowa zostaje jednorazowo ustawiona na wartość z własności źródłowej, potem zmiany są ignorowane.
• OneWayToSource – jak OneWay, ale w przeciwnym kierunku: źródło jest aktualizowane w wyniku zmian celu.
• Default – domyślny tryb zależy od rodzaju kontrolki i własności, własności
modyfikowane przez użytkownika w kontrolkach edycyjnych (np. Text w TextBox) mają domyślnie TwoWay, pozostałe – OneWay. Zależy od ustawienia flagi
FrameworkPropertyMetadata.BindsTwoWayByDefault.
Kierunek dowiązania
<Slider Name="fontSize" .../>
<TextBlock Name="tekst"
FontSize="{Binding Path=Value, ElementName=fontSize, Mode =TwoWay }">
Tak działa Data Binding
</TextBlock>
<Button Click="myClick" ...>Big</Button>
private void Button_Click(object sender, RoutedEventArgs e) {
tekst.FontSize = 30;
}
Wiązanie OneWayToSource
• Działa tak samo, jak OneWay.
• Jedyna różnica polega na tym, gdzie umieszczone jest wiązanie: jest to zamiana źródła z celem.
<Slider Name="fontSize" Minimum="12" ...
Value="{Binding Path=FontSize, ElementName=tekst, Mode=OneWayToSource}" />
<TextBlock Name="tekst" FontSize="18">
Tak działa Data Binding
</TextBlock>
• Zastosowanie: gdy chcemy ustawić własność nie będącą własnością zależnościową.
Wielokrotne dowiązania
<Slider Name="fontSize" Minimum="12" Value="18" Maximum="50"
TickFrequency="2" TickPlacement="TopLeft"/>
<TextBlock Name="tekst"
FontSize="{Binding Path=Value, ElementName=fontSize}">
Tak działa Data Binding
</TextBlock>
<TextBox Text="{Binding Path=Value, ElementName=fontSize}"/>
<Button Click="Button_Click">Big</Button>
warto ustawić dla Slidera:
IsSnapToTickEnabled="True"
aktualizacja wartości następuje dopiero, gdy pole tekstowe utraci fokus
Inne rozwiązanie (z identycznym efektem):
<Slider Name="fontSize" Minimum="12" Value="18" Maximum="50"
TickFrequency="2" TickPlacement="TopLeft"/>
<TextBlock Name="tekst"
FontSize="{Binding Path=Value, ElementName=fontSize}">
Tak działa Data Binding
</TextBlock>
<TextBox Text="{Binding Path=FontSize, ElementName=tekst}"/>
Inne rozwiązanie (tym razem zmiana czcionki jest wprowadzana natychmiastowo):
<Slider Name="fontSize" Minimum="12"
Value="{Binding Path=Text, ElementName=pole}" Maximum="50"
TickFrequency="2" TickPlacement="TopLeft"/>
<TextBlock Name="tekst"
FontSize="{Binding Path=Value, ElementName=fontSize}">
Tak działa Data Binding
</TextBlock>
<TextBox Name="pole" Text="18"/>
Oba dowiązania (w TextBoksie i Sliderze) mogą istnieć jednocześnie.
Wielokrotne dowiązania
<Slider Name="fontSize" .../>
<TextBox Name="pole" ... />
<ComboBox Name="kolory" ...>
<ComboBoxItem Content="Red" />
<ComboBoxItem Content="Green" />
<ComboBoxItem Content="Blue" />
</ComboBox>
<TextBlock Text="{Binding Path=Text, ElementName=pole}"
FontSize="{Binding Path=Value, ElementName=fontSize}"
Foreground="{Binding Path=SelectedItem.Content, ElementName=kolory}">
</TextBlock>
Aktualizacja dowiązań
• Wartości ze źródła do celu są przesyłane natychmiast.
• Dla odwrotnego kierunku (przy TwoWay lub OneWayToSource) zależy to od ustawienia Binding.UpdateSourceTrigger.
◦ PropertyChanged – źródło jest aktualizowane natychmiast, gdy własność docelowa się zmieni
◦ LostFocus – źródło jest aktualizowane, gdy docelowa własność się zmieni, a cel straci focusa (używane np. w kontrolce tekstowej, gdzie zawartość zmienia się często i ciągła aktualizacja mogłaby powodować problemy)
◦ Explicit – źródło nie jest uaktualniane dopóki nie wymusimy aktualizacji wywołując metodę BindingExpression.UpdateSource()
BindingExpression binding =
poleTxt.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();
◦ Default – dla większości własności jest to PropertyChanged, ale np.
TextBox.Text ma LostFocus; zależy to od ustawienia FrameworkPropertyMetadata.DefaultUpdateSourceTrigger
• Powyższe ustawienia nie mają wpływu na sposób aktualizacji własności docelowej.
Aktualizacja dowiązań
<TextBox Text="{Binding Path=Text, ElementName=poleB}" />
<TextBox Name="poleB" />
• Aktualizacja drugiego pola gdy wpiszemy coś w pierwszym – dopiero po utracie focusa przez pierwsze
• Aktualizacja pierwszego pola gdy wpiszemy coś w drugim – natychmiastowa Aktualizacja natychmiastowa:
<TextBox Text="{Binding Path=Text, ElementName=poleB, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Name="poleB" />
Wiązanie do obiektów, które nie są elementami
• WPF umożliwia dowiązywanie własności obiektów, które nie są elementami wizualnymi.
• Jedyny wymóg, to że muszą być to publiczne własności
• Zamiast Binding.ElementName należy użyć:
◦ Source – referencja wskazująca na obiekt dostarczający dane
◦ RelativeSource – wskazuje na obiekt źródłowy w odniesieniu do aktualnego elementu; użyteczne przy pisaniu szablonów kontrolek i szablonów danych
◦ DataContext – jeśli nie wybierzemy żadnego z powyższych, WPF będzie tu poszukiwał obiektu danych, idąc w górę drzewa elementów; DataContext pozwala wiązać wiele własności tego samego obiektu do różnych elementów
Source
• Należy dostarczyć obiekt, z którego chcemy zaczerpnąć dane.
• Może być to obiekt dowolnej klasy.
public class Osoba {
public string Imie { get; set; } public string Nazwisko { get; set; } public Osoba() { }
public Osoba(string imie, string nazwisko) {
Imie = imie;
Nazwisko = nazwisko;
} }
Source
• Z zasobów:
<Window ...
xmlns:app="clr-namespace:WpfApp6">
<Window.Resources>
<app:Osoba x:Key="osoba" Imie="Adam"/>
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding Path=Imie,
Source={StaticResource osoba}}"/>
</StackPanel>
</Window>
Source
• Jako składowa statyczna:
public partial class Window1 : Window {
public static Osoba jeden = new Osoba("Mikołaj", "Rej");
}
<Window ...
xmlns:app="clr-namespace:WpfApp6">
<StackPanel>
<TextBlock Text="{Binding Path=Imie,
Source={x:Static app:Window1.jeden}}"/>
</StackPanel>
</Window>
RelativeSource
• Wskazuje na obiekt źródłowy, bazując na jego relacji z obiektem docelowym.
◦ Self – dowiązanie do innej własności tego samego elementu
<TextBlock Text="{Binding Path=FontFamily,
RelativeSource={RelativeSource Self}}"/>
◦ FindAncestor – dowiązanie do elementu nadrzędnego; poszukiwany jest element typu podanego jako AncestorType
<TextBlock Text="{Binding Path=Title,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
◦ PreviousData – dowiązanie do poprzedniego elementu listy
◦ TemplatedParent – dowiązanie do elementu, dla którego zastosowano szablon
• RelativeSource jest zwłaszcza przydatne w szablonach danych i szablonach kontrolek.
RelativeSource
• Można wykorzystać, aby sięgnąć do obiektów przechowywanych w oknie:
public partial class Window1 : Window {
private Osoba user = new Osoba("Jan", "Kowalski");
public Osoba User { get { return user; } } }
<TextBlock Text="{Binding Path=User.Imie,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
DataContext
• Często konieczne jest wiązanie wielu elementów do tego samego źródła danych.
<Window ...>
<Window.Resources>
<app:Osoba x:Key="person" Imie="Jan"
Nazwisko="Kowalski"/>
</Window.Resources>
<Grid>
...
<TextBox Text="{Binding Path=Imie,
Source={StaticResource person}}" />
<TextBox Text="{Binding Path=Nazwisko,
Source={StaticResource person}}"/>
</Grid>
</Window>
DataContext
• Dobre rozwiązanie to: obiekt źródłowy zdefiniować raz, w elemencie nadrzędnym
<Window ...>
<Window.Resources>
<app:Osoba x:Key="person" Imie="Jan"
Nazwisko="Kowalski"/>
</Window.Resources>
<Grid DataContext="{StaticResource person}">
...
<TextBox Text="{Binding Path=Imie}" />
<TextBox Text="{Binding Path=Nazwisko}"/>
</Grid>
</Window>
Data Binding
• Klasa produktu służy jedynie reprezentowaniu danych wydobytych z bazy.
• Dostęp jedynie przy pomocy publicznych własności.
• Jeśli planujemy wiązanie dwukierunkowe, własności nie mogą być read-only.
public class Product {
public string Name { get; set; } public decimal Price { get; set; } public string Opis { get; set; } public Product() { }
public Product(string name, decimal price) {
Name = name;
Price = price;
} }
Data Binding
• Kod „komunikujący się z bazą” powinien być zawarty w osobnej klasie.
public class MyDB {
private static List<Product> list = new List<Product>();
static MyDB() {
list.Add(new Product("book", 20));
list.Add(new Product("cd", 10));
list.Add(new Product("dvd", 40));
list.Add(new Product("pen", 2));
}
public Product GetProduct(int ID) {
return list[ID % list.Count];
} }
Data Binding
• Dostęp do obiektu bazy możemy uzyskiwać:
◦ tworząc go za każdym razem, gdy tego potrzebujemy
◦ dostęp przez statyczne składowe
◦ (optymalny) przez statyczną składową innej klasy
public partial class App : Application {
private static MyDB myDB = new MyDB();
public static MyDB MyDB {
get { return myDB; } }
}
Data Binding
<Window ...>
<Grid>
...
<TextBox ... Name="ID"/>
<Button ... Click="GetDataClick">Get Data</Button>
<Grid ... Name="details">
...
<TextBox Text="{Binding Path=Name}"/>
<TextBox Text="{Binding Path=Price}"/>
<TextBox Text="{Binding Path=Opis}"/>
</Grid>
</Grid>
</Window>
Data Binding
private void GetDataClick(object sender, RoutedEventArgs e) {
int id;
if (Int32.TryParse(ID.Text, out id)) {
try {
details.DataContext = App.MyDB.GetProduct(id);
} catch {
MessageBox.Show("Error contacting database.");
} } else {
MessageBox.Show("Invalid ID.");
} }
Data Binding
Data Binding
• Pola o zawartości null:
◦ typy referencyjne (stringi, obiekty) obsługują null automatycznie
◦ typy proste można deklarować w wersji int? zamiast int public class Values
{
public int? X { get; set; } public int Y { get; set; } }
• Domyślna reakcja to brak zawartości w polu. (podczas gdy dla pola int byłoby to 0)
• Można to nadpisać (nawias kwadratowy to tylko ozdobnik):
<TextBox Text="{Binding Path=Opis,
TargetNullValue=[brak danych]}"/>
Data Binding
• Aktualizacja danych:
◦ obiekty z listy są modyfikowane automatyczne w momencie utraty focusa przez odpowiednie pola tekstowe,
◦ samodzielnie powinniśmy jedynie zadbać o zapisanie zmian do bazy,
◦ dobrze jest dodać w tym celu przycisk potwierdzający zmianę:
private void UpdateButtonClick(object sender, ...) {
Product product = (Product)details.DataContext;
try {
App.MyDB.UpdateProduct(product);
} catch {
MessageBox.Show("Error contacting database.");
} }
• Uwaga: jeśli zatwierdzono Enterem (przycisk typu IsDefault), nie nastąpiła zmiana focusa – musimy albo wymusić ją sami, albo ręcznie wywołać UpdateSource.
Data Binding
• Powiadomienia o zmianach: jeśli dowiązany obiekt zmieni się (np. jakiś
zewnętrzny czynnik lub inny fragment kodu zmieni wartości składowych), należy powiadomić interfejs, aby miał okazję pobrać i wyświetlić nowe wartości.
◦ własności zależnościowe same informują o swoich zmianach,
◦ w wypadku data objects najlepiej jest zaimplementować interfejs System.ComponentModel.INotifyPropertyChanged i podnosić zdarzenie PropertyChanged, gdy któraś własność się zmieni.
public class Wybór : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string property) {
if (PropertyChanged != null) PropertyChanged(this,
new PropertyChangedEventArgs(property));
} ...
}
• Zmiana jednej własności pociąga za sobą drugą i od razu jest to odzwierciedlane w interfejsie użytkownika.
public class Wybór : INotifyPropertyChanged {
...
private int x;
private int y;
public int X {
get { return x; }
set { x = value; y = 100 - x; OnPropertyChanged("Y"); } }
public int Y {
get { return y; }
set { y = value; x = 100 - y; OnPropertyChanged("X"); } }
}
• Możemy też przekazać pusty string, jeśli zmieniło się kilka własności obiektu (odświeża wszystkie).
Wiązanie z kolekcją obiektów
• Kolekcja wymaga kontrolki typu ListBox, ComboBox, ListView, Data Grid (a także Menu lub TreeView dla danych hierarchicznych).
• Dzięki szablonom danych, kontrolki list dają dużą kontrolę nad sposobem prezentacji danych.
• Własności ItemsControl odpowiedzialne za wiązanie z danymi:
◦ ItemsSource – wskazanie na kolekcję obiektów do wyświetlenia
◦ DisplayMemberPath – własność, która będzie użyta do stworzenia tekstu wyświetlanego na liście
◦ ItemTemplate – określa szablon użyty do stworzenia wyglądu elementu
• Kolekcja wyświetlana w liście powinna implementować IEnumerable
Wiązanie z kolekcją obiektów public class MyDB {
public List<Product> GetProducts() {
...
List<Product> products = new List<Product>();
try {
...
while (...) {
Product product = new Product(...);
products.Add(product);
} }
finally {
...
}
return products;
} }
Wiązanie z kolekcją obiektów
• Po naciśnięciu przycisku „Wczytaj dane” metoda GetProducts() pobiera z bazy i zwraca całą listę obiektów.
<Button Click="GetProductsClick">Wczytaj dane</Button>
<ListBox Margin="5" Name="lista" DisplayMemberPath="Name"/>
Wiązanie z kolekcją obiektów
• Po naciśnięciu przycisku „Wczytaj dane” metoda GetProducts() pobiera z bazy i zwraca całą listę obiektów.
private void GetProductsClick(object sender, ...) {
products = App.MyDB.GetProducts();
lista.ItemsSource = products;
}
• Zamiast ustawiać DisplayMemberPath, możemy napisać ToString() w klasie produktu lub dostarczyć szablon danych.
public class Product {
public override string ToString() {
return Name + " (" + Price + ")";
} }
Wiązanie z kolekcją obiektów
• Ostatnie zadanie, to wyświetlać szczegóły produktu wybranego na liście. Zamiast reagować na zdarzenie SelectionChanged, lepiej zdefiniować odpowiednie wiązanie elementów:
<Grid Name="details"
DataContext="{Binding ElementName=lista, Path=SelectedItem}">
...
</Grid>
• W podobny sposób można tworzyć złożone listy kategorii:
◦ jedna lista kategorii
◦ druga lista produktów należących do tej kategorii
◦ następnie szczegóły danego produktu
• Pozostaje jeszcze tylko zapisanie danych
◦ dobrym pomysłem jest zmusić użytkownika do użycia przycisku „Zapisz” - np.
wyszarzyć listę, gdy wprowadzi on zmiany w polach tekstowych i dać do dyspozycji przyciski „OK” i „Anuluj”.
Usuwanie obiektów z kolekcji
• Wykorzystanie standardowej listy jako kolekcji elementów nie pozwoli na odzwierciedlenie usuwania i dodawania obiektów:
private void DeleteProductClick(object sender,...) {
products.Remove((Product)lista.SelectedItem);
}
• Obiekt zostaje usunięty z kolekcji, ale nadal jest widoczny na liście.
• Wymagane jest skorzystanie z kolekcji implementującej interfejs
INotifyCollectionChanged. Implementuje go klasa ObservableCollection.
public class MyDB {
public List<Product> GetProducts() {
products = new ObservableCollection<Product>();
...
return products;
} }