ICommandCzęść.2 W poprzednim wpisie stworzyliśmy prostą aplikację WPF, która implementuje Inversion of Control przy pomocy kontenera Castle.Windosor.

Przykład był prosty, ponieważ na końcu powiązaliśmy tylko odpowiednie właściwości do odpowiednich etykiet.

 

Czas jednak zabrać ten przykład na wyższy poziom, gdyż napiszemy kod, który powiąże metody z odpowiednimi przyciskami,

Aby to zrobić będę musiał mieć klasy implementujące interfejs ICommand. Jak to jednak zrobić w miarę dobrze?

Na początku też zaznaczę, że postanowiłem zignorować fakt, że techniki związane z ICommand posiadają pewien specyficzny styl przesyłania parametrów, jak i rezultatu działania metod.

To, co zrobię zapewne będzie złe, ale w następnym wpisie będziemy mogli zrobić sobie porównanie.

CommandParameter

Na razie to zignorowałem. Przyjrzymy się w następnym wpisie. Na razie pomyśl jak przy pomocy kontenera IOC przesłać rezultat i parametry do metody.

Pamiętaj, że instancje klas mogą być singeltonami, a to znaczy, że te same egzystencje mogą istnieć w wielu miejscach.

Przejdźmy jednak do implementacji. Stworzyłem interfejs IMethod. Interfejs będzie przechowywał dwie komendy służące do dodawania.

public interface IMethods
{
    ICommand Add { get; }

    ICommand Multi { get; }
}

Interfejs ICommand nam by zasugerował utworzenie tylu klas, ile mamy mieć metod, ale jest to bardzo zła praktyka.

public class What : ICommand
{
    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        throw new NotImplementedException();
    }

    public void Execute(object parameter)
    {
        throw new NotImplementedException();
    }
}

Potrzebna jest nam pewna uniwersalna implementacja, która by uprościła ten interfejs. Na chwilę obecną metoda CanExecute w ogóle nie jest nam potrzebna.   A zdarzenie CanExecuteChange też nie wiadomo co ma robić.

Na razie więc ułatwmy sobie zadanie w ten sposób.

public class ModelCommand : ICommand
{
    private readonly Action<object> _execute = null;
    private readonly Predicate<object> _canExecute = null;

    public ModelCommand(Action<object> execute)
    : this(execute, null) { }

    public ModelCommand(Action execute)
        : this(execute, null) { }

    public ModelCommand(Action<object> execute, 
        Predicate<object> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public ModelCommand(Action execute, 
        Predicate<object> canExecute)
    {
        _execute = (ob) => { execute(); };
        _canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        return _canExecute != null ? _canExecute(parameter) : true;
    }

    public void Execute(object parameter)
    {
        if (_execute != null)
            _execute(parameter);
    }

    public void OnCanExecuteChanged()
    {
        CanExecuteChanged(this, EventArgs.Empty);
    }

}

Ta klasa pozwoli skoncentrować się w samych metodach, których nie muszę implantować interfejsu ICommand. Do tej klasy muszę tylko przesłać delegaty Action lub Action z parametrem object.

Wszystkie moje metody na razie będą bezparametrowe i nie będą niczego zwracały.

public interface ICalculator
{
    void Add();
    void Multi();
}

Oto interfejs kalkulatora, który zawiera tylko dwie metody.

Zapewne się zastanawiasz, jak metody dodawania i mnożenia mają cokolwiek zmieniać, jeśli nie są do nich przesyłane parametry oraz nic nie zwracają.

Chcę jednak ci pokazać jak można wykorzystać kontenery IOC.

public interface IParameters
{
    int A { get; set; }
    int B { get; set; }

    int Result { get; set; }

}

Oto interfejs IParameters, który będzie przechowywał nasze parametry, jak i wartość zwracaną.

public class Parameter : IParameters, INotifyPropertyChanged
{
    public int A { get; set; }
    public int B { get; set; }

    private int _c = 0;
    public int Result
    {
        get { return _c; }
        set
        {
            //if (PropertyChanged != null)
            //{
            //    PropertyChanged(this, new PropertyChangedEventArgs("Result"));
            //}
            _c = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Result"));


        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Implementacja tego interfejsu wymaga ode mnie, aby rezultat wynikowy wywoływał zdarzenie PropertyChange. Bez tego kontrolki w WPF nie będą wiedziały, kiedy mają odświeżać wartość rezultatu przy powiązaniu.

public class Calcuator : ICalculator
{
    private IParameters _parameters;
    public Calcuator(IParameters parameters)
    {
        _parameters = parameters;
    }

    public void Add()
    {
        _parameters.Result = _parameters.A + _parameters.B;
    }

    public void Multi()
    {
        _parameters.Result = _parameters.A * _parameters.B;
    }
}

Sama implementacja kalkulatora jak widzisz będzie otrzymywać parametry przy pomocy wstrzykiwania zależności. Instancja obiektu parametrów będzie singletonem, a to pozwoli mi uzyskać dostęp do tego samego obiektu w wielu miejscach aplikacji.

W tym przypadku w kalkulatorze.

Czas na implementacje interfejsu IMethods. Muszę tutaj określić, która metoda z kalkulatora trafi do którego zbioru metod ICommand.

Metody kalkulatora są opakowane w obiekt ModelCommand, który dziedziczy po ICommand, więc wszystko tutaj jest w porządku.

public class Methods : IMethods
{
    private ICalculator _calculator;
    public Methods(ICalculator calcaultor)
    {
        _calculator = calcaultor;
        Add = new ModelCommand(_calculator.Add);
        Multi = new ModelCommand(_calculator.Multi);
    }
    public ICommand Add
    {
        get;
    }

    public ICommand Multi
    {
        get;
    }
}

Instancje kalkulatora otrzymam poprzez wstrzykiwanie zależności. Interfejs reprezentujący cały model głównej strony trochę uległ zmianie.

public interface IMainPageModel
{
    ILabel MainLabel { get; set; }
    ILabel SubLabel { get; set; }

    IMethods Methods { get; set; }

    IParameters Parameters { get; set; }
}

Do szczęścia jeszcze potrzebne są rejestracje nowo powstałych komponentów do instalatora. Dla pewności ustaliłem, że każdy ten komponent jest singeltonem.

container.Register(Component.For<IParameters>().
    ImplementedBy<Parameter>()
    .LifestyleSingleton());

container.Register(Component.For<IMethods>().
    ImplementedBy<Methods>()
    .LifestyleSingleton());

container.Register(Component.For<ICalculator>().
    ImplementedBy<Calcuator>()
    .LifestyleSingleton());

W kodzie XAML na koniec dodajemy jeszcze odpowiednie powiązania i tak udało nam się zaimplementować interfejs ICommand.

<Window x:Class="CastleWindsorWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CastleWindsorWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <Label  Content="{Binding MainLabel.Text}" FontSize="{Binding MainLabel.Size}"   />
        <Label  Content="{Binding SubLabel.Text}" FontSize="{Binding SubLabel.Size}"   />

        <TextBox Text="{Binding Parameters.A, Mode=TwoWay}" />
        <TextBox Text="{Binding Parameters.B, Mode=TwoWay}" />

        <Button Command="{Binding Methods.Add}" Content="Dodaj" />
        <Button Command="{Binding Methods.Multi}" Content="Mnóż" />
        <TextBlock  Text="{Binding Parameters.Result, Mode=TwoWay}"   />
    </StackPanel>
</Window>

Pola tekstowe są powiązane w obie strony z parametrami A i B.  Rezultat jest powiązany z blokiem tekstu pod kontrolkami.

Przyciski są powiązane z poleceniami z implementacji interfejsu IMethods.

W praktyce działa to tak.

Jak ta aplikacja działa

Następnym razem ulepszymy kod o przekazywanie parametrów przy pomocy atrybutów XAML CommandParameter.

CommandParameter 2

Edit z 2022 roku :
Kod jest na GitHubie : PanNiebieski/CastleWindsorWithWPF (github.com)