Opis projektu MemoryVisualizera toczy się w kilku wątkach. Jednym z nich jest moja przygoda z F# w kontekście WPF. Po pierwszej części, w której w ogólności opisywałem jak możemy “pożenić” WPF z F#, pora kolejne kroki. Ale wcześniej potrzebne nam będzie krótkie przypomnienie z WPF w C#. Rzecz się tyczy użycia komend, do wykonywania akcji. Oraz nieśmiertelnego OnPropertyChanged do notyfikowania interfejsu o zmianie – ale o tym w kolejnej części.
Zajmiemy się najpierw komendami, przypominając je sobie w C#. Załóżmy, że mamy proste okienko zdefiniowane w XAML. Dla przejrzystości różne nieistotne atrybuty, związane głównie z wyglądem, wykropkowałem:
<Window x:Class="SampleWpfApp.MainWindow"
Title="SampleWPFApp1" ...>
<Grid>
<Button x:Name="btnClickMe" Command="{Binding ExecuteCommand}" Content="ClickMe!" .../>
</Grid>
</Window>
DataContext ustawiamy dla tego okienka w jego konstruktorze:
public partial class MainWindow : Window
{
public MainWindow()
{
this.DataContext = new MainViewModel();
InitializeComponent();
}
}
W czasach Windows Forms przyciski wywoływały metody, jednak cała “fajność” dobrze użytego WPF polega na zmianie podejścia w kierunku pisania deklaratywnego i “bindowania” danych. Dlatego do przycisku btnClickMe przypięliśmy komendę ExecuteCommand, która to technicznie rzecz biorąc jest właściwością ViewModelu:
class MainViewModel
{
public ICommand ExecuteCommand { get; }
}
Oczywiście powyższy kod nic nie wykona, musimy to property ustawić na konkretny obiekt implementujący interfejs ICommand. Ma on prostą definicję, będącą częścią samego WPFa:
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
Komenda przestaje być zatem zwykłą ordynarną metodą z czasów Windows Forms, ponieważ nie tylko mówi co ma zrobić (Execute) ale również, czy w ogóle może być wywołana (CanExecute). Dostarczyć ma również informację, że zmieniła się decyzja co do możliwości wykonania (CanExecuteChanged).
Wracając do przykładu, moglibyśmy stworzyć klasę implementującą ICommand dla tego konkretnego przycisku, ale jest to oczywiście bez sensu. Musielibyśmy tworzyć typy dla każdej akcji (komendy) w naszym systemie. Zamiast tego, używając WPFa, tworzy się jakąś pomocniczą klasę, którą można reużywać. Na przykład taką prostą, w której konstruktorze podaje się akcję wykonującą kod oraz funkcję mającą określić, czy komendę można wykonywać:
public class CommandHandler : ICommand
{
private Action action;
private Func<bool> canExecute;
public CommandHandler(Action action, Func<bool> canExecute)
{
this.action = action;
this.canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return canExecute();
}
public void Execute(object parameter)
{
action();
}
}
Taka implementacja jest w rzeczywistości bardzo zbliżona do klasy RelayCommand z popularnego frameworku MVVM Light. Mając już do dyspozycji klasę pomocniczą, możemy rozszerzyć definicję ViewModelu:
class MainViewModel
{
public ICommand ExecuteCommand { get; }
public MainViewModel()
{
ExecuteCommand = new CommandHandler(Execute, () => true);
}
private void Execute()
{
MessageBox.Show("Hello!");
}
}
Dla prostoty, założyłem, że komendę możemy wykonać zawsze i dlatego jako funkcję podałem funkcję anonimową zawsze zwracającą
true. Co warto podkreślić, implementacja
CanExecute oraz
CanExecuteChanged jest używana automatycznie przez WPF aby “wyszarzyć” lub "odszarzyć" przycisk (albo pozycję menu itp. w zależności od tego, do czego ją zbindujemy). To jest cenne, bo komendę możemy przypiąć do przycisku, pozycji w menu, skrótu klawiszowego itd., a zmiany będą odzwierciedlane w każdym z tych użyć. Zaczynamy zatem bardziej myśleć o akcjach, niż o handlerach reagujących na różne przyciski.
I to by było na tyle z krótkiego przypomnienia czym jest komenda w WPF. Wiedza tam bardzo się przyda, bo już w następnej części będziemy ten kod przepisywać na F#.