WPF – Caliburn.Micro – Eventy i Event Aggregator

Zgodnie z obietnicą pora na przekazywanie danych pomiędzy oknami(widokami).
Jak możemy zaimplementować taką bądź co bądź podstawową funkcjonalność (na razie nie myślmy w ogóle o WPF a tym bardziej o Caliburn.Micro).

Public Property

Najprostszym sposobem (nie znaczy, że złym) może być zastosowanie jakiejś publicznej property np. w takim stylu:

private void GetAddress_Click(object sender, EventArgs e)
{
    AddressForm addressForm=new AddressForm ();
    addressForm.ShowDialog();
    string address=string.Format("{0] {1}",addressForm.PostCode,addressForm.Town);
    //Some logic with address
}

Jakie są zalety takiego rozwiązania? Z pewnością prostota. Taki kod jest w stanie odczytać każdy, nawet (a w sumie przede wszystkim) “świeżak”. Do najprostszych operacji nadaje się doskonale (simple as possible). A wady? No jest ich kilka (po czasie “świeżakowi” też zaczną doskwierać). Np. silne uzależnienie jednej formatki od drugiej. Innym zagadnieniem jest takie pytanie a dlaczego w ogóle ShowDialog a nie Show? Przecież nie zawsze chcemy (musimy, możemy niepotrzebne skreślić) przekazywać sterowanie do nowo otwartego okna. Czasami nowo otwierane okna służą jako przyborniki (np. tak jak w Paint.NET). Gdybyśmy w przykładzie powyżej zamienili zdarzenie ShowDialog na Show to nie ważne jakie operacje byśmy wykonywali zawsze otrzymamy wartość property taką jaką przyjęła ona w konstruktorze swojego widoku. Te dwie wady powinny już wystarczyć do dyskwalifikacji tego sposobu w “bardziej zaawansowanych” celach.
Co więc nam pozostaje?

Events

Spróbujmy więc zrobić to lepiej.
W AddressForm dodajmy

public delegate void AddressHandler(object sender, AddressEventArgs e);
public event AddressHandler AddressChanged;

a gdzieś później np. w obsłudze zdarzenia Click

private void SendButton_Click(object sender, EventArgs e)
{
    AddressEventArgs args = new AddressEventArgs(TbTown.Text, TbPostCode.Text);
    AddressChanged(this, args);
    Dispose();
}

Oczywiście musi powstać klasa AddressEventArgs

public class AddressEventArgs : EventArgs
{
    public string Town { get; set; }
    public string PostCode { get; set; }
    public AddressEventArgs(string town, string postCode)
    {
        Town = town;
        PostCode = postCode;
    }
}

Na koniec w formatce, która wywołała AddressForm

private void GetAddress_Click(object sender, EventArgs e)
{
    AddressForm addressForm=new AddressForm ();
    addressForm.AddressChanged += new addressForm.AddressHandler(AddressForm_ButtonClicked);
    addressForm.Show();
}

private void AddressForm_ButtonClicked(object sender, AddressEventArgs e)
{
    string address=string.Format("{0] {1}",e.PostCode,e.Town);
    //Some logic with address
}

Proste? Zapewne tak. Tylko trzeba uważać. Eventy są jedną z przyczyn memory leaków. Choć jak pisał Jon Skeet w odpowiedzi na pytanie na StackOverflow jest to problem przewartościowany.

Event Aggregator

Wróciliśmy wreszcie do Caliburn.Micro. Jeśli chcemy przekazywać dane pomiędzy Widokami (ViewModelami) to zalecane jest użycie w tym celu klasy EventAggregator. Ogólną koncepcję Event Aggregatora przedstawił na swoim blogu Martin Fowler. Jak więc będzie wyglądać implementacja Event Aggregatora w Caliburn.Micro? (kod dostępny na githubie pod tagiem EventAggregator).
Po pierwsze potrzebuemy jednej instancji EventAggregator dla całej aplikacji (albo chociaż dla ViewModeli między, którymi ma następować przesyłanie danych). Na tą chwilę rozwiązałem to w ten sposób:

public class EventAggregatorViewModel : Screen
{
    public LeftViewModel LeftPanel { get; private set; }
    public RightViewModel RightPanel { get; private set; }

    public EventAggregatorViewModel()
    {
        IEventAggregator eventAggregator = new EventAggregator();
        RightPanel = new RightViewModel(eventAggregator);
        LeftPanel = new LeftViewModel(eventAggregator);
    }
}

docelowo wszystko odbywa się w kontenerze IoC (ale to już inna historia).

Kod View jest banalny:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <ContentControl x:Name="LeftPanel" Grid.Column="0" />
    <ContentControl x:Name="RightPanel" Grid.Column="1" />
</Grid>

To teraz lewy ViewModel a konkretnie najważniejsza rzecz

_eventAggregator.PublishOnUIThread(Ctx);

Dla określonego zdarzenia używamy metody PublishOnUIThread a jako parametr przekazujemy wybrany obiekt. Nic wielkiego.
Uwaga! W poprzednich wersjach Caliburn.Micro mamy zdarzenie Publish. W najnowszej (2.0.1) metoda Publish ma nieco inną składnię, ale w zamian za to dostaliśmy 5 metod rozszerzających klasę EventAggregator (ich nazwy same tłumaczą o co chodzi):

  • BeginPublishOnUIThread
  • PublishOnBackgroundThread
  • PublishOnCurrentThread
  • PublishOnUIThread
  • PublishOnUIThreadAsync

Teraz czas na prawy ViewModel. Tutaj również sprawa jest prosta.
Po pierwsze klasa musi implementować generyczny interfejs IHandle gdzie T jest typu, który publikujemy w lewym ViewModelu(jeśli będzie innego to po prostu nie zostanie obsłużony).
Interfejs ten ma jedną metodę Handle(T message). W metodzie tej obsługujemy przekazany nam obiekt.
Po drugie należy wcześniej wymusić na eventaggregatorze subskrypcję poprzez taką oto komendę

_eventAggregator.Subscribe(this);

Chcącym poczytać więcej na ten temat polecam dokumentację.

Cykl o MVVM

One thought on “WPF – Caliburn.Micro – Eventy i Event Aggregator

  1. Pingback: dotnetomaniak.pl

Comments are closed.