Wyobraźmy sobie scenariusz….
Aplikacja spięta z bazą danych, na której pracuje wiele osób, (dzień jak co dzień 😀 kto takiego czegoś nie robił? :D) modyfikować dany rekord, wpis może tylko jedna osoba na raz. Przed rozpoczęciem wprowadzania danych leci do bazy zapytanie czy rekord można zablokować. Jeżeli tak to taki rekord w bazie zostaje zablokowany. Jeżeli dzieje się coś innego, jak na przykład, dostajemy informacje, że rekord został zmodyfikowany ale aktualnie klient nie posiada jego zmian to proszony jest o odświeżenie swojego widoku. A co jeżeli rekord jest zablokowany przez innego użytkownika a ktoś inny próbuje coś w nim zmienić?
Oczywiście baza odpowiada, że rekord jest zablokowany przez innego użytkownika i nic nie możemy w tym momencie z nim zrobić. Super!
Lecz patrząc na WPF, bindingi, zależności, mnogość innych rzeczy… Trzeba wymyślić coś co nie pozwoli w takiej sytuacji zmienić zablokowanego rekordu, czyli należy uniemożliwić użytkownikowi zmiany w formularzu/gridzie czy czymkolwiek innym.
Osobiście miałam takie wyzwanie jakiś czas temu, podejście do tego co zrobiliśmy z zespołem nie do końca mnie zadowalało ponieważ nie miało w sobie tego “czegoś”. Tworzyliśmy swoje własne kontrolki (DevExpress nas do tego zmuszał…) w nich setki własnych dependence property. Po stronie XAML’a wiele eventów odpowiadających za edycję danego elementu wywoływało jeden RelayCommand, który w switch sprawdzał przychodzący rodzaj eventu i w zależności od statusu rekordu pozwalał edytować formularz lub nie.
Szczerze…
Inne rozwiązanie chodziło mi po głowie wiele miesięcy starałem się zrobić coś co będzie w miarę ładne, proste i nie trzeba będzie pamiętać o tym, że przy dodaniu kontrolki X w nowym XAML należy jeszcze (w zależności co ma robić) zaimplementować te eventy, które w danym momencie są potrzebne. Kilka ładnych wieczorów spędziłem nad tym aby znaleźć takie rozwiązanie i dzisiaj w końcu osiągnąłem to co chciałem 🙂
Rozwiązanie okazało się banalnie proste a ja jak zwykle za bardzo kombinowałem 😀
Ale od początku.
Mamy formatkę z kilkoma kontrolkami. Jedną z nich jest ComboBox
|
1 2 3 4 5 6 7 8 9 10 |
<ComboBox Grid.Row="1" Margin="10"> <ComboBoxItem Name="cbi1">Item1</ComboBoxItem> <ComboBoxItem Name="cbi2">Item2</ComboBoxItem> <ComboBoxItem Name="cbi3">Item3</ComboBoxItem> <i:Interaction.Behaviors> <local:EventToCommandBehavior Command="{Binding ChangedControlValueCommand}" Event="PreviewKeyDown" PassArguments="True"/> <local:EventToCommandBehavior Command="{Binding ChangedControlValueCommand}" Event="PreviewTextInput" PassArguments="True"/> <local:EventToCommandBehavior Command="{Binding ChangedControlValueCommand}" Event="PreviewMouseDown" PassArguments="True"/> </i:Interaction.Behaviors> </ComboBox> |
Aby zabezpieczyć się przed zmianą danych w tej kontrolce należałoby sprawdzić chociażby te 3 eventy, które są widoczne (ale zawsze może być ich więcej niestety)
Po stronie ViewModelu trzeba zaimplementować RelayCommand, który rożnego rodzaju eventy sprawdzi i w zależności od tego czy formularz (w tym wypadku kontrolka ComboBox) może być edytowalna czy też nie.
Właściwość CanChanged symuluje pytanie do bazy i jej zwrotną informację. Rekord albo można edytować albo nie.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
ChangedControlValueCommand = new DelegateCommand(v => { if (v is RoutedEventArgs) { var eventArgs = v as RoutedEventArgs; eventArgs.Handled = !CanChanged; } else if (v is MouseButtonEventArgs) { var eventArgs = v as MouseButtonEventArgs; eventArgs.Handled = !CanChanged; } else if (v is TextCompositionEventArgs) { var eventArgs = v as TextCompositionEventArgs; eventArgs.Handled = !CanChanged; } }); |
I teraz wyobraźmy sobie, że przeróżnych eventów będzie 30 razy więcej…. No u mnie było ich co najmniej 6, ale to DevEx….
Prawda, że nie wygląda to zachęcająco? 😉
Tak jak już wcześniej wspomniałem, kontrolki na formatkach były nasze, które dziedziczyły z innych kontrolek i tam właśnie brakowało mi rozwiązania, które pozwoli mi w łatwy sposób zarządzać blokowaniem wprowadzania danych.
Aby programowanie stało się “przyjemniejsze” wymyśliłem coś takiego.
|
1 2 3 4 |
public interface IMyControls { event RoutedEventHandler ValueChanged; } |
Prosty interfejs, który ma implementować każda kontrolka. Mega prosty event do zaimplementowania, czyż nie? 😀
A teraz ta wisienka na torcie, czyli dlaczego tylko jeden…
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class MyComboBox : ComboBox, IMyControls { public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MyComboBox)); public event RoutedEventHandler ValueChanged { add { AddHandler(ValueChangedEvent, value); } remove { RemoveHandler(ValueChangedEvent, value); } } protected virtual bool RaiseCustomEvent() { RoutedEventArgs eventArg = new RoutedEventArgs(MyComboBox.ValueChangedEvent); RaiseEvent(eventArg); return eventArg.Handled; } protected override void OnPreviewTextInput(TextCompositionEventArgs e) { e.Handled = RaiseCustomEvent(); base.OnPreviewTextInput(e); } protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { e.Handled = RaiseCustomEvent(); base.OnPreviewMouseDown(e); } protected override void OnPreviewKeyDown(KeyEventArgs e) { e.Handled = RaiseCustomEvent(); base.OnPreviewKeyDown(e); } } |
Stworzyłem kontrolkę, która dziedziczy bo ComboBox WPF’owym i dodatkowo implementuje wcześniej pokazany interfejs.
Napisanie custom eventu nie graniczy z cudem ponieważ jest to jak budowa cepa 🙂
Najważniejsza w tym wszystkim jest metoda RaiseCustomEvent, która zwraca wartość logiczną true lub false i to ona właśnie robi całą pracę.
Przyglądnijmy się pierwszemu lepszemu eventowi, którego ta klasa nadpisuje
protected override void OnPreviewTextInput(TextCompositionEventArgs e) .
Parametrem, który dostajemy jest klasa TextCompositionEventArgs, która to w sobie ma property Handled, którego ustawienie na true powoduje, że event przestaje się wykonywać. I tutaj właśnie ukazuje się mój pomysł.
Stworzyłem jeden event, który posiada property Handled i to właśnie jego będę zawsze wywoływał w nadpisanych innych eventach, które mnie interesują. Metoda RaiseCustomEvent zwraca mi informacje o tym czy rekord może być edytowany czy nie.
Po zmodyfikowaniu mojego ViewModelu i RelayCommand otrzymuję coś takiego:
|
1 2 3 4 5 6 7 8 |
ChangedControlValueCommand = new DelegateCommand(v => { var eventArgs = v as RoutedEventArgs; if (eventArgs != null) { eventArgs.Handled = !CanChanged; } }); |
Jeden event obsługiwany po stronie ViewModelu, mniej niepotrzebnego kodu i jesteśmy bardziej SOLID 😀
Jak po zmianach wygląda nasza kontrolka w XAML?
|
1 2 3 4 5 6 7 8 |
<local:MyComboBox Grid.Row="1" Margin="10"> <ComboBoxItem Name="cbi1">Item1</ComboBoxItem> <ComboBoxItem Name="cbi2">Item2</ComboBoxItem> <ComboBoxItem Name="cbi3">Item3</ComboBoxItem> <i:Interaction.Behaviors> <local:EventToCommandBehavior Command="{Binding ChangedControlValueCommand}" Event="ValueChanged" PassArguments="True"/> </i:Interaction.Behaviors> </local:MyComboBox> |
Lepiej, prawda?
W tym momencie można jakikolwiek inny event, który chcemy zablokować, nadpisać w klasie kontrolki i będzie to ładniej wyglądać niż w przykładzie, który pokazałem na samym początku.
Kod do mojego rozwiązania jak zawsze na GitHub

