Programowanie w technologii .NET
wykład 3 – Dependency Properties, Routed Events
Dependency Properties – własności zależnościowe - wydajniejsze pamięciowo
- dziedziczenie wartości (w drzewie elementów) - powiadomienia o zmianie wartości
- potrzebne do stylów, animacji, wiązania danych - używa się ich tak samo, jak zwykłych własności
klasyczne własności:
class FrameworkElement {
Thickness Margin {
set { ... = value; } get { return ...; } }
}
a jak są definiowane Dependency Properties?
najpierw statyczna składowa reprezentująca definiowaną własność:
public class FrameworkElement : UIElement, ...
{
public static readonly DependencyProperty MarginProperty;
// ...
}
rejestrowanie właściwości w statycznym konstruktorze:
static FrameworkElement() {
FrameworkPropertyMetadata metadata =
new FrameworkPropertyMetadata(new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure);
MarginProperty = DependencyProperty.Register("Margin", typeof(Thickness), typeof(FrameworkElement),
metadata, IsMarginValid);
//...
}
walidacja (uwaga: to metoda statyczna, zatem sprawdza tylko podaną wartość):
private static bool IsMarginValid(object value) {
Thickness thickness1 = (Thickness)value;
if(...)
return true;
return false;
}
wrapper (te metody są wołane tylko z kodu C#, a nie XAMLa):
public Thickness Margin {
set { SetValue(MarginProperty, value); }
get { return (Thickness)GetValue(MarginProperty); } }
teraz mamy gotową właściwość:
myElement.Margin = new Thickness(5);
jest też dostępne czyszczenie lokalnie ustawionej wartości:
myElement.ClearValue(FrameworkElement.MarginProperty);
Property Metadata
pozwala na ustawienie kilku dodatkowych cech definiowanej własności najważniejsze:
DefaultValue – domyślna wartość własności
CoerceValueCallback – testowanie zgodności wartości
PropertyChangedCallback – powiadomienie o zmianie wartości
Coercion
1. CoerceValueCallback ma szansę na zmianę dostarczonej wartości albo ją odrzucić 2. ValidateValueCallback sprawdza poprawność wartości (statycznie!)
3. PropertyChangedCallback – gdy zmiana zaszła pomyślnie przykład koercji na scrollu i właściwości Maximum:
private static object CoerceMaximum(DependencyObject d, object value) { RangeBase base1 = (RangeBase)d;
if (((double)value) < base1.Minimum) {
return base1.Minimum;
}
return value;
}
podobnie dla Value:
internal static object ConstrainToRange(DependencyObject d, object value) { double newValue = (double)value;
RangeBase base1 = (RangeBase)d;
double minimum = base1.Minimum;
if (newValue < minimum) {
return minimum;
}
double maximum = base1.Maximum;
if (newValue > maximum) {
return maximum;
}
return newValue;
}
w Minimum nie ma koercji, ale jest odpalenie zmiany pozostałych, gdy trzeba:
private static void OnMinimumChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) { RangeBase base1 = (RangeBase)d;
// ...
base1.CoerceValue(RangeBase.MaximumProperty);
base1.CoerceValue(RangeBase.ValueProperty);
}
podobnie Maximum wymusza koercję Value:
private static void OnMaximumChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) { RangeBase base1 = (RangeBase)d;
//...
base1.CoerceValue(RangeBase.ValueProperty);
}
Ma to zadbać o właściwe dopasowanie wartości:
ScrollBar bar = new ScrollBar(); // Value = 0, Minimum = 0, Maximum = 1 bar.Value = 100; // Value = 1 (koercja)
bar.Minimum = 1; // Value = 1
bar.Maximum = 200; // znow odpalona koercja Value i Value = 100 (*)
(*) - koercja odpalona z oryginalnie podaną wartością właściwości – zatem ustalone jest Value = 100 (!)
Shared Dependency Properties
Niekiedy kilka klas (w osobnych hierarchiach) korzysta z tej samej własności, np.
TextBlock.FontFamily i Control.FontFamily wskazują na tę samą własność zdefiniowaną w klasie TextElement; robi się to wywołując AddOwner:
TextBlock.FontFamilyProperty =
TextElement.FontFamilyProperty.AddOwner(typeof(TextBlock));
Attached Dependency Properties
Dotyczą innego elementu niż są zdefiniowane. Np. Grid.Row zdefiniowane jest w Gridzie, a dotyczy elementu w nim osadzonego.
Rejestruje się je przy użyciu RegisterAttached:
FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(
0, new PropertyChangedCallback(Grid.OnCellAttachedPropertyChanged));
Grid.RowProperty = DependencyProperty.RegisterAttached("Row", typeof(int),
typeof(Grid), metadata, new ValidateValueCallback(Grid.IsIntValueNotNegative));
Nie definiuje się dla nich wrappera, gdyż mogą być ustawione dla dowolnego elementu.
Zamiast tego korzystamy ze statycznych metod:
public static int GetRow(UIElement element) {
if (element == null) {
throw new ArgumentNullException(/*...*/);
}
return (int)element.GetValue(Grid.RowProperty);
}
public static void SetRow(UIElement element, int value) {
if (element == null) {
throw new ArgumentNullException(/*...*/);
}
element.SetValue(Grid.RowProperty, value);
}
A tak z tego korzystamy:
Grid.SetRow(txtElement, 0);
co przekłada się na:
txtElement.SetValue(Grid.RowProperty, 0);
Jak używane są własności zależnościowe?
Gdy zmieni się wartość własności, uruchamiana jest metoda callbackowa
PropertyChangedCallback – nie odpala ona jednak domyślnie eventów. Zamiast tego powiadamiane są data-bindingi i triggery (będzie o nich mowa w następnych rozdziałach).
Jedynie część własności uruchamia jakieś powiązane z ich zmiana zdarzenia (np.
TextBox.TextChanged, ScrollBar.ValueChanged).
Gdy pobieramy wartość własności zależnościowej, WPF poszukuje jej w:
1. domyślnej wartości 2. wartości odziedziczonej 3. wartości podanej w stylu
4. wartości wpisanej lokalnie (w kodzie lub XAMLu)
Tak pobrana wartość, zanim zostanie zwrócona, może być modyfikowana przez wyrażenia, wiązanie danych, dołączone animacje, koercje.
Zdarzenia w WPF
Event Routing
Routed Events – podróżują po drzewie elementów.
rodzaje zdarzeń:
- direct – bezpośrednie (dotyczą tylko jednego elementu)
- bubbling (wędrują w górę hierarchii zagnieżdżenia – najpierw podnoszone przez element którego dotyczą)
- tunneling (wędrują w dół hierarchii zagnieżdżenia – najpierw podnoszone przez element najwyższego poziomu – okno)
przekazany do obsługi argument typu RoutedEventArgs zawiera własność Handled – pozwala przerwać tunelowanie/ bąbelkowanie
private void DoSomething(object sender, RoutedEventArgs e) {
if (...) {
e.Handled = true;
} }
RoutedEventArgs.Source – od kogo pochodzi zdarzenie (przeważnie kontrolka) sender – kto je przysłał (gdzie umieszczono obsługę zdarzenia)
RoutedEventArgs.RoutedEvent – zdarzenie
Attached Events
Wykonawca zdarzenia może być podpięty na poziomie elementu, który podnosi zdarzenie albo do innego elementu powyżej lub poniżej hierarchii zagnieżdżenia:
<StackPanel Button.Click="DoSomething" Margin="5">
<Button Name="btn1" Tag="jeden">Przycisk 1</Button>
<Button Name="btn2" Tag="dwa">Przycisk 2</Button>
<Button Name="btn3" Tag="trzy">Przycisk 3</Button>
...
</StackPanel>
private void DoSomething(object sender, RoutedEventArgs e) {
object tag = ((FrameworkElement)e.Source).Tag;
MessageBox.Show((string)tag);
}
Tunneling Events
Tunneling i Bubbling występują w parach (tunneling ma przeważnie przedrostek Preview) – najpierw wędruje tunneling, a potem bubbling
<Label BorderBrush="Black" BorderThickness="1">
<StackPanel>
<TextBlock Margin="3">Tekst i ikona</TextBlock>
<Image Source="ikona.jpg" Stretch="None" />
<TextBlock Margin="3">Podpis</TextBlock>
</StackPanel>
</Label>
Label PreviewMouseDown StackPanel PreviewMouseDown Image PreviewMouseDown Image MouseDown
StackPanel MouseDown Label MouseDown Label PreviewMouseUp StackPanel PreviewMouseUp Image PreviewMouseUp Image MouseUp
StackPanel MouseUp Label MouseUp
Definiowanie zdarzeń w WFP:
public abstract class ButtonBase : ContentControl, ... { // definicja
public static readonly RoutedEvent ClickEvent;
// rejestracja
static ButtonBase() {
ButtonBase.ClickEvent = EventManager.RegisterRoutedEvent(
"Click", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ButtonBase));
//...
}
// wrapper
public event RoutedEventHandler Click {
add { base.AddHandler(ButtonBase.ClickEvent, value); } remove {
base.RemoveHandler(ButtonBase.ClickEvent, value); } }
//...
}
posługiwanie się zdarzeniami:
dołączanie obsługi zdarzenia:
<Button Name="btn1" Click="klik">OK</Button>
w kodzie:
btn1.Click += klik;
i odłączanie
btn1.Click -= klik;
lub:
btn1.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(klik));
i
btn1.RemoveHandler(ButtonBase.ClickEvent, new RoutedEventHandler(klik));
nie powinniśmy dołączać w ten sposób wysokopoziomowych (logicznych) metod, a tylko
„event handlery” - oddelegowujące polecenia do warstwy logiki
WPF Events
Kategorie zdarzeń:
● czasu życia (gdy element jest ładowany, inicjowany, usuwany)
● zdarzenia myszy (akcje myszy)
● zdarzenia klawiatury (akcje klawiatury)
● zdarzenia stylusa Czasu życia:
Podnoszą je wszystkie elementy, gdy są tworzone bądź zwalniane.
Initialized – gdy utworzono instancję elementu i ustawiono jego właściwości. Inne elementy tego samego okna mogą jeszcze nie istnieć. IsInitialized == true. Jest to zwykłe zdarzenie (nie jest routed).
Loaded – gdy całe okno zostało zainicjowane, dołączono style i wiązanie danych. Tuż przed jego wyświetleniem. IsLoaded == true.
Unloaded – gdy element został zwolniony (usunięto go z okna bądź zamknięto okno).
kolejność działań:
- tworzona instancja obiektu
- przetwarzane i ustawiane właściwości z XAMLa
- Initialized (gdy tworzymy okno elementy są inicjowane „z dołu do góry” – te głębiej w zagnieżdżeniu są pierwsze)
- ułożenie w kontenerze - Loaded („z góry do dołu”)
- renderowanie (wyświetlenie okna, gdy wszystkie elementy załadowane) Zdarzenia czasu życia dla klasy Window:
SourceInitialized – ustawiane powiązania do HWND (Win32 API)
ContentRendered – gdy zawartość okna wyrenderowana po raz pierwszy (okno wyświetlone i gotowe do przyjmowania wejścia)
Activated – gdy nastąpiło przełączenie do okna (albo załadowane po raz pierwszy) – jest to odpowiednik GetFocus kontrolek
Deactivated – użytkownik przełączył się na inne okno (lub zamknął to) – odpowiednik LostFocus
Closing – okno się zamyka (można to anulować – CancelEventArgs.Cancel na true); nie ma Closing, jeśli to system się wyłącza
Closed – okno zostało zamknięte (ale do jego elementów wciąż mamy jeszcze dostęp) Typowe miejsce dla inicjacji kontrolek – Loaded
Zdarzenia wejścia:
Wszystkie dołączają dwie właściwości: Timestamp (czas zdarzenia w milisekundach) i Device (urządzenie, które odpaliło zdarzenie).
Zdarzenia klawiatury:
naciśnięcie klawisza:
PreviewKeyDown (Tunneling) KeyDown (Bubbling)
wpisanie znaku (odpalają je tylko te klawisze, które powodują wpisanie tekstu):
PreviewTextInput (Tunneling) TextInput (Bubbling)
zwolnienie klawisza:
PreviewKeyUp (Tunneling) KeyUp (Bubbling)
gdy trzymamy naciśnięty klawisz powtarzane są zarówno oba KeyDown jak i TextInput
Obsługa klawiszy:
<Window ...
KeyDown="KeyEvent" PreviewKeyDown="KeyEvent"
KeyUp="KeyEvent" PreviewKeyUp="KeyEvent">
<ScrollViewer Name="scroll">
<Label Name="lblInfo"/>
</ScrollViewer>
</Window>
private void KeyEvent(object sender, KeyEventArgs e) {
lblInfo.Content += "Event: " + e.RoutedEvent + " Key: " + e.Key + "\n";
scroll.ScrollToBottom();
}
Uwaga: „x” to „x” niezależnie od shift, alt, etc.
Ale czym innym jest Key.D0, a czym innymi Key.NumPad0.
e.IsRepeat – pozwala sprawdzić, czy ten event to efekt trzymania klawisza:
e.Text – zwraca tekst, jaki ma otrzymać kontrolka (w zdarzeniach typu TextInput) e.KeyStates – informuje o stanie naciśniętego klawisza
o stanie pozostałych można dowiedzieć się z KeyboardDevice:
if ((e.KeyboardDevice.Modifiers & ModifierKeys.Control) ==
ModifierKeys.Control) {
lblInfo.Content = "You held the Control key.";
}
metody KeyboardDevice:
IsKeyDown() - czy dany klawisz był naciśnięty gdy zaszło zdarzenie IsKeyUp()
IsKeyToggled() - tylko dla Caps Lock, Num Lock, Scroll Lock GetKeyStates() - połączenie KeyDown i KeyToggled
Keyboard – aktualny stan klawiszy:
if (Keyboard.IsKeyDown(Key.LeftShift)) {
lblInfo.Content = "The left Shift is held down.";
}
PreviewTextInput – dobre miejsce do walidacji tekstu w kontrolce (np. gdy chcemy tylko numeryczne ustawiamy Handled gdy nie to co chcemy)
private void pnl_PreviewTextInput(object sender,
TextCompositionEventArgs e) { short val;
if (!Int16.TryParse(e.Text, out val)) {
// tylko klawisze numeryczne e.Handled = true;
} }
private void pnl_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Space)
{
// spacja tutaj, bo nie podnosi ona PreviewTextInput e.Handled = true;
} }
Mysz
MouseEnter – kursor wjeżdża nad element MouseLeave – opuszcza element
nie są routed
Poza nimi: PreviewMouseMove (tunneling) i MouseMove (bubbling) – gdy mysz się porusza.
private void MouseMoved(object sender, MouseEventArgs e) {
Point pt = e.GetPosition(this);
lblInfo.Content =
String.Format("Współrzędne: ({0},{1})", pt.X, pt.Y);
}
można sprawdzić też stan przycisków:
if(e.LeftButton == MouseButtonState.Pressed) ...
Kliknięcia
MouseLeftButtonDown MouseLeftButtonUp to samo jest dla Right
dla każdego istnieje odpowiednik Preview* (tunelling) MouseWheel i Preview* - obsługa kółka
Przekazany parametr MouseButtonEventArgs ma dodatkowo właściwość ClickCount.
Niektóre kontrolki przejmują te zdarzenia, a dają dodatkowe (np. Click w Buttonie).
Przechwytywanie myszy
Aby otrzymywać zdarzenia z myszy poza elementem.
Mouse.Capture(element) aby zwolnić:
Mouse.Capture(null)
i jeszcze zdarzenie LostMouseCapture, gdy to stracimy
<Window ...
MouseMove="Canvas_MouseMove"
MouseLeftButtonDown="Canvas_MouseLeftButtonDown"
MouseLeftButtonUp="Canvas_MouseLeftButtonUp">
<Canvas>
<Rectangle Name="box" Width="50" Height="50"
Canvas.Top="50" Canvas.Left="100" Fill="Blue" />
</Canvas>
</Window>
private void Canvas_MouseMove(object sender, MouseEventArgs e) {
if (e.LeftButton == MouseButtonState.Pressed) {
Point pt = e.GetPosition(this);
box.SetValue(Canvas.TopProperty, pt.Y-25);
box.SetValue(Canvas.LeftProperty, pt.X-25);
} }
private void Canvas_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e) {
Mouse.Capture(this);
}
private void Canvas_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e) {
Mouse.Capture(null);
}
przydatne do Drag-and-Drop:
1. klikamy i trzymamy przycisk (pewna informacja zapisana i zaczynamy przeciąganie) 2. ruch myszy na inny element, który może przyjąć drop – sygnalizacja kursorem 3. zwolnienie przycisku – zrzucenie danych
<Label MouseDown="lbl1_MouseDown">Źródło</Label>
private void lbl1_MouseDown(object sender,
MouseButtonEventArgs e) {
Label lbl = (Label)sender;
DragDrop.DoDragDrop(lbl, lbl.Content,
DragDropEffects.Copy);
}
odbiorca wymaga ustawienia:
<Label DragEnter="lbl2_DragEnter"
Drop="label2_Drop" AllowDrop="True">Cel</Label>
przyjęcie zrzutu:
private void lbl2_Drop(object sender, DragEventArgs e) {
((Label)sender).Content =
e.Data.GetData(DataFormats.Text);
}
sprawdzanie czy dane które możemy przyjąć:
private void lbl2_DragEnter(object sender, DragEventArgs e) {
if (e.Data.GetDataPresent(DataFormats.Text)) e.Effects = DragDropEffects.Copy;
else
e.Effects = DragDropEffects.None;
}