Ostatnio miałem niemiłą zagwozdkę. Zostałem poproszony o napisanie własnej implementacji kontrolki, która ma zastąpić PageControl devexowy.
No przecież to proste… 4 przyciski, jakieś 2 text blocki i tyle. Wszystko spinam binduję do odpowiedniego ViewModelu i działa.
No nie 😀 Nie tak do końca… Zaciąłem się na implementacji ICommand z przycisku, który jest w kontrolce i obsłużeniu go moim view modelu.
Wcześniej robiłem już coś takiego, ale było to o tyle proste, że miałem z góry zdefiniowaną klasę view model i było to nie zmieniane.
1 |
d:DataContext="{d:DesignInstance IsDesignTimeCreatable=True,Type=app:MainWindowVM}" |
DataContext kontrolki został spięty bezpośrednio z view modelem.
Definicja przycisku wyglądała następująco:
1 2 3 4 5 |
<Button Name="AmountBtn" Width="290" Height="290" Margin="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Command="{Binding ButtonClickDesc, RelativeSource={RelativeSource AncestorType={x:Type app:MainWindow}}}" CommandParameter="{Binding ElementName=TextBlockDesc, Path=Text}" > |
Po stronie ViewModelu obsługa RelayCommand
1 2 3 4 5 6 7 8 9 10 11 12 |
private RelayCommand _buttonClickDesc; public RelayCommand ButtonClickDesc { get { return _buttonClickDesc ?? (_buttonClickDesc = new RelayCommand(e => { MessageBox.Show("Done!"); })); } } |
I koniec. Myślałem wcześniej, że zrobię to dokładnie w ten sam sposób ale nie przemyślałem tego, że moja kontrolka ma być uniwersalna i być niezależna od view modelu.
Czyli moje wcześniejsze rozwiązanie nie jest tym, o które mi chodzi.
Nie ukrywam, że trochę czasu poświęciłem na to aby w końcu problem rozwiązać w sposób zadowalający i wydaje mi się, że najbardziej optymalny 🙂
Schemat mojego projektu:
Założenia do testowego projektu są następujące:
- stworzyć kontrolkę użytkownika, która będzie zawierała przycisk i pole tekstowe
- każdorazowe naciśnięcie przycisku będzie powodować to, że w polu tekstowym pokaże się ilość kliknięć w przycisk
- projekt ma implementować wzorzec MVVM
Niby banał, ale najbardziej upierdliwe to binding kontrolek takich jak przycisk i text block z user control po stronie klasy view modelu 🙂
Stwórzmy sobie kontrolkę użytkownika w projekcie BindingTestControls
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<UserControl x:Class="BindingTestControls.MyUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:BindingTestControls" mc:Ignorable="d" Width="166.667" Height="61.458"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}, Path=UserControlCommand}" Content="Click Me" /> <TextBlock Grid.Column="1" Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}, Path=UserControlText}"/> </Grid> </UserControl> |
Dodajmy teraz trochę kodu w klasie kontrolki:
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 |
public static readonly DependencyProperty UserControlCommandProperty = DependencyProperty.Register("UserControlCommand", typeof(ICommand), typeof(MyUserControl), new PropertyMetadata(null)); public static readonly DependencyProperty UserControlTextProperty = DependencyProperty.Register("UserControlText", typeof (string), typeof (MyUserControl), new PropertyMetadata("")); public ICommand UserControlCommand { get { return (ICommand)GetValue(UserControlCommandProperty); } set { SetValue(UserControlCommandProperty, value); } } public string UserControlText { get { return (string) GetValue(UserControlTextProperty); } set { SetValue(UserControlTextProperty, value); } } |
I tu właśnie zaczyna (jak ja to mówię) dziać się magia 🙂
Jako, że po dodaniu kontrolki do jakiegokolwiek widoku nie mamy dostępu do jej wewnętrznych kontrolek. Jak w naszym przypadku (przycisk i tekst) trzeba pomóc sobie inaczej 😉
W code behind kontrolki dodałem 2 dependence property. Jeden dla przycisku a drugi dla text block, które to będą widoczne w xaml po dodaniu naszej kontrolki do widoku.
W kodzie xaml kontrolki jest jedna bardzo ważna rzecz, na którą trzeba zwrócić uwagę
1 2 3 4 |
<Button <strong>Command</strong>="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}, Path=UserControlCommand}" Content="Click Me" /> <TextBlock Grid.Column="1" <strong>Text</strong>="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MyUserControl}}, Path=UserControlText}"/> |
Przycisk sam w sobie ma właściwość Command, do której to możemy podpiąć nasz RelayCommand i obsłużyć naciśnięcie przycisku. I dokładnie to robimy. Tak samo dla właściwości Text kontrolki TextBlock. Z tą jedną różnicą, że ustawiamy RelativeSource dokładnie to tych dependence property, które zostały zdefiniowane w code behind. Po co ten cały zabieg? Ano po to, że właściwość Command na przycisku nie będzie widoczna w MainWindow.xaml (głównym widoku naszej aplikacji) ale możemy zrobić tak, że “wystawimy właściwość (w naszym przypadku) UserControlCommand, która to jest bezpośrednio spięta z Command naszego przycisku.
Przejdźmy teraz do głównego widoku naszej aplikacji
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<Window x:Class="BindingTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" xmlns:viewModel="clr-namespace:BindingTestViewModel;assembly=BindingTestViewModel" xmlns:bindingTestControls="clr-namespace:BindingTestControls;assembly=BindingTestControls"> <Window.DataContext> <viewModel:MainViewModel/> </Window.DataContext> <Grid> <bindingTestControls:MyUserControl UserControlCommand="{Binding ViewModelCommand}" UserControlText="{Binding ControlText}"/> </Grid> </Window> |
Jak można zauważyć nasza kontrolka posiada 2 właściwości:
- UserControlCommand – ICommand dla obsługi przycisku
- UserControlText – pole tekstowe kontrolki text block
Teraz wystarczy dodać klika linijek po stronie naszego view model:
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 36 |
public class MainViewModel : ViewModelBase { private string _controlText; private RelayCommand _viewModelCommand; private int _counter; public RelayCommand ViewModelCommand { get { return _viewModelCommand ?? (_viewModelCommand = new RelayCommand(e => { ControlText = (_counter++).ToString(); })); } } public string ControlText { get { return _controlText; } set { if (_controlText == value) return; _controlText = value; OnPropertyChanged("ControlText"); } } public MainViewModel() { _counter = 1; } } |
I wszystko zaczyna działać dokładnie tak jakbyśmy tego chcieli 🙂
Kod oczywiście umieściłem na github dla potomnych.