PwT.N W6

Transkrypt

PwT.N W6
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ł.
1/41
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.
2/41
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>
3/41
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)”.
4/41
Błędy wiązania:
• Nie powodują podnoszenia wyjątków.
• Pojawia się jedynie informacja w Output window (w trybie debuggowania).
5/41
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.
6/41
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;
}
7/41
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;
}
8/41
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;
}
9/41
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.
10/41
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;
}
11/41
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ą.
12/41
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
13/41
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.
14/41
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>
15/41
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.
16/41
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" />
17/41
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
18/41
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;
}
}
19/41
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>
20/41
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>
21/41
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.
22/41
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}}}"/>
23/41
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>
24/41
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>
25/41
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;
}
}
26/41
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];
}
}
27/41
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; }
}
}
28/41
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>
29/41
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.");
}
}
30/41
Data Binding
31/41
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]}"/>
32/41
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.
33/41
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));
}
...
}
34/41
•
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).
35/41
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
36/41
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;
}
}
37/41
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"/>
38/41
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 + ")";
}
}
39/41
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”.
40/41
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;
}
}
41/41

Podobne dokumenty