ZdarzeniaCzęść NR.20 Zobaczyłeś już jak zadeklarować delegat i jak jej użyć.

Widziałeś też co potrafią wyrażenia lambda w C#.
Jednak to jeszcze nie koniec. Co prawda możesz wywołać wiele metod nie bezpośrednio za pomocą delegaty, ale wciąż musisz ją wywołać jawnie.

Z programistycznego punktu widzenia dobrze byłoby , aby delegaty uruchomiały się automatycznie gdy coś ważnego się wydarzy.

Przykładowo, w przypadku przegrzewania się reaktora atomowego wywołać odpowiednią delegate, która wykona wszystkie metody po kolei w celu jego wyłączenia .

W wielu wypadkach klasy znajdujące się w .NET pokazują użycie zdarzeń (event). Zdarzenia są używane do zdefiniowania i przechwytywania określonych akcji .Określają one też użycie odpowiedniej delegaty, która sobie z daną sytuacją X poradzi.

Większość kontrolek z WPF ,Windows Forms, Silverlight , a nawet z ASP.NET używa zdarzeń.
Nawet np. sama klasa okna głównego w WPF przechowuje następujące zdarzenia.

Zdarzenia dla okna głównego w WPF

Mam tutaj zdarzenia: wczytania okna, kliknięcia , poruszanie myszką itp.
Te zdarzenia uruchomią się w zależności od tych akcji.

Ale na razie zostawmy WPF w spokoju ponieważ opowiem jak ty sam możesz napisać zdarzenia.

Deklarowanie zdarzenia

Zdarzenie może być zadeklarowane tylko w klasie i to ona jest zawsze źródłem tego zdarzenia.
Źródłem danego zdarzenia jest zazwyczaj klasa, która monitoruje dane środowisko oraz wywołuje zdarzenie w momencie spełnienia danego warunku.

Niektóre klasy mają np. zdarzenia, które zostaną wywołane w przypadku zajścia błędu. Idąc tym tropem przyszedł mi głowy następujący przykład zastosowania zdarzenia.

Mamy do dyspozycji klasę “PobieraczPlików” , której zadaniem jest pobranie poprzez sieć różnego rodzaju plików.

Klasa ta ma dwa pola

  • liczbeDoPobraniaPlików”, która będzie mówić nam ile plików ma ta klasa pobrać.
  • “liczbePobranychPlikow” , która będzie opisywać ile plików zostało już pobranych.

Zdarzenie “PrzyBledziePobierania” zajdzie jeśli klasa wykryje , że dane pobieranie nie skończyło się prawidłowo. Inne zdarzenie “SkonczonePobieranie” zajdzie gdy liczba pobranych plików będzie równa liczbie plików, którą planowaliśmy pobrać.

Zdarzenia te będą przechowywać listę metod, które mają być wywoływane w czasie ich zajścia.
Te metody są nazywane subscribers, co dosłownie można przetłumaczyć jako wyraz subskryptami. Jednak na pewno nie jest to prawidłowe tłumaczenie (nie ma takiego słowa w języku polskim). Metody te obsłużą nasze zdarzenie i wykonają odpowiednie akcje np. przy “PrzyBłędziePobierania” użytkownik zostanie poinformowany , że dany plik nie mógł zostać pobrany.

Deklaracja zdarzenia jest podobna do deklaracji pola, jednakże ponieważ zdarzenia są przeznaczone dla delegat ich typem musi być delegata. No i oczywiście pojawia się tutaj kolejne słowo kluczowe “event”. Oto schemat deklaracji zdarzenia.

event NazwaTypuDelegaty NazwaZdarzenia


Przykłady delegat do naszych zdarzeń są następujące. Delegata BłądPobierania będzie potrzebować jednego argumentu string z wiadomością, którą chcemy przekazać dla użytkownika. Delegata “SkończonePobieranieDelegata” nie pobiera żadnych argumentów i nie zwraca żadnej wartości.

Wszystkie te delegaty znajdują się w klasie PobieraczPlikow.

class PobieraczPlikow
{
    public delegate void BladPobierniaDelegata(string wiadomosc);
    public delegate void SkonczonePobieranieDelegata();

    private int liczbaPobranychPlikow;
    private int liczbaDoPobraniaPlikow;
}

Posiadając już odpowiednie delegaty możemy zdefiniować potrzebne zdarzenia.

class PobieraczPlikow
{
    public delegate void BladPobierniaDelegata(string wiadomosc);
    public event BladPobierniaDelegata PrzyBlendziePobierania;

    public delegate void SkonczonePobieranieDelegata();
    public event SkonczonePobieranieDelegata SkonczonePobieranie;

    private int liczbaPobranychPlikow;
    private int liczbaDoPobraniaPlikow;
}

W klasie jeszcze brakuje logiki, która wywołałaby to zdarzenia ale o tym później. Najpierw pokażę jak dodać metody do danego zdarzenia. Jest to proces subskrypcji zdarzenia i raczej nie powinien być kojarzony z dodawaniem metod do delegaty, na którym zdarzenie bazuje.

Subskrypcja zdarzenia

Tak jak delegaty zdarzenia mają nadpisany operator +=. Subskrypcja zdarzenia wykonuje się poprzez operator +=.

W zdarzeniu “PrzyBlendziePobierania” powinny zajść metody, które by poinformowały użytkownika o danym problemie.

Zdarzenia

W zdarzeniu “SkonczonePobieranie” powinny zajść metody, które obsłużyłyby pobrane pliki. Właśnie sobie uświadomiłem , że to zdarzenie powinno pobierać jako argument właśnie te strumienie plików , ale to tylko przykład. Powiedzmy , że to zdarzenie ma tylko poinformować o tym, że pobieranie zostało zakończone.

class Program
{
    static void Main(string[] args)
    {
        PobieraczPlikow pobieracz = new PobieraczPlikow();

        pobieracz.SkonczonePobieranie += pobi_Poinformuj;

        //zastosowanie wyrażenia lambda
        pobieracz.PrzyBlendziePobierania +=
            (s) => { Console.WriteLine(s); };

        //kolejna metoda może być dodana do zdarzenia pobieracz.
        SkonczonePobieranie += pobi_ObsluStrum;
    }

    static void pobi_ObsluStrum()
    {
        Console.WriteLine("\t Obsługuje teraz strumienie.");
    }

    static void pobi_Poinformuj()
    {
        Console.WriteLine("\t Skonczyłem pobieranie");
    }
}

Możesz przypisać do zdarzenia wyrażenie lambda. “s” to potrzebny argument dla tego zdarzenia jest to string wiadomość, który zostanie wyświetlony w konsoli.

Kiedy zdarzenia są wywoływane wtedy uruchomią się te metody.

OdSubskryptowanie Zdarzenia

W zdarzeniach możesz też używać operatora –= , aby usunąć daną delegate ze zdarzenia. Wywołanie operatora “-=” usuwa daną metodę z wewnętrznej kolekcji delegat danego zdarzenia. Ta akcja jest referowana jako “unsubscribing” czyli w dosłownym niepolskim tłumaczeniu “Odsubrkyptowanie”.

Wywołanie Zdarzenia

Wywołanie zdarzenia zachodzi w zbliżony sposób do delegat , a delegata jest wywoływana w podobny sposób do metody.

Kiedy wywołujesz dane zdarzenie wszystkie dołączone do niej delegaty uruchamiają się w sekwencji w jakiej je dodałeś.BlandPobierania

Na początek wywołajmy zdarzenie “PrzyBlendziePobierania” w jakiś prosty sposób.

class PobieraczPlikow
{
    public void WystapilBlad()
    {
        PrzyBlendziePobierania("Wystąpił błąd");

Kod ten jednak nie jest prawidłowy w końcu dane zdarzenie może nie mieć przypisanej do siebie delegaty. W klasach .NET na pewno nie ma bezpośrednio wywołania danego zdarzenia bez jego sprawdzenia. Wywołanie zdarzenia, które jest puste grozi wyrzuceniem wyjątku NullReferenceException.

class PobieraczPlikow
{
    public void WystapilBlad()
    {
        if (PrzyBlendziePobierania != null)
            PrzyBlendziePobierania("Wystapił błąd");
    }

Jak widzisz jeśli dana delegata w zdarzeniu definiuje jakieś parametry muszą one być podane w trakcie ich wywołania.

Zdarzenia też mają pewną istotną wbudowaną funkcje mogą one być wywołane wewnątrz klasy, która je definiuje w tym wypadku wewnątrz klasy “PobieraczPlikow”. Próba wywołania zdarzenia poza tą klasą skończy się błędem w czasie kompilacji programu.

zdarzenia_01

Teraz w ramach utrwalenia wiedzy zrobimy dokładnie to samo dla zdarzenia “SkonczonePobieranie”.

Spróbujemy wywołać to zdarzenie gdy liczba pobranych plików będzie równa liczbie plików do pobrania. Zastosuję tutaj też właściwości oraz konstruktor klasy. Ostateczna wersja klasy”PobieraczPlikow” wygląda tak.

class PobieraczPlikow
{
    //konstruktor
    public PobieraczPlikow(int l)
    {
        liczbaDoPobraniaPlikow = l;
    }

    //proste wywołanie zdarzenia 
    public void WystapilBlad()
    {
        if (PrzyBlendziePobierania != null)
            PrzyBlendziePobierania("Wystapił błąd");
    }

    public delegate void BladPobierniaDelegata(string wiadomosc);
    public event BladPobierniaDelegata PrzyBlendziePobierania;

    public delegate void SkonczonePobieranieDelegata();
    public event SkonczonePobieranieDelegata SkonczonePobieranie;

    private int liczbaPobranychPlikow;
    public int LiczbaPobranychPlikow
    {
        get { return liczbaPobranychPlikow; }
        set {
                if (value == liczbaDoPobraniaPlikow)
                {
                    if (SkonczonePobieranie != null)
                        SkonczonePobieranie();
                }
                liczbaPobranychPlikow = value; 
            }
    }

    private int liczbaDoPobraniaPlikow;
    public int LiczbaDoPobraniaPlikow
    {
        get { return liczbaDoPobraniaPlikow; }
    }
}

W konstruktorze podajemy liczbę plików do pobrania. Właściwość powiązana z tym polem prywatnym jest tylko do odczytu. We właściwości opisującej liczbę pobranych plików w bloku kodu SET (który wywołuje się za każdym razem gdy ta właściwość jest zmieniana) sprawdzam za każdym razem czy nowa przypisana wartość jest równa liczbie plików do pobrania, jeśli tak jest, to sprawdzam czy zdarzenie nie jest puste , a potem je wywołuję (jeśli nie jest puste).

Sprawdźmy czy ta klasa działa poprawnie.

PobieraczPlikow pobieracz = new PobieraczPlikow(3);
pobieracz.SkonczonePobieranie += pobi_Poinformuj;
pobieracz.SkonczonePobieranie += pobi_ObsluStrum;
pobieracz.LiczbaPobranychPlikow = 3;//odpalenie zdarzenia

Tak program działa dokładnie tak, jak zaplanowałem zmiana właściwości LiczbaPobranychPlikow na 3 wywołała dane zdarzenie.

Zdarzenia_02

Zobaczmy jak zdarzenia są skonstruowane w WPF , aby zrozumieć jak je dobrze pisać.

Zdarzenia w WPF

Pisałem już wcześniej , że WPF, Silverlight, ASP.NET w swoich klasach i kontrolkach używa zdarzeń do kontrolki nad graficznym interfejsem użytkownika.

Ucząc się którejś z tych technologii czasami spotkasz się ze zdarzeniami. Nawet początkujący programiści z nich korzystają i nie wiedzą dokładnie jak one działają ponieważ wymaga to poznania wielu zagadnień jak np. delegaty.

No cóż , dla uproszczenia nie będę w tym wpisie zagłębiać się jak zdarzenia są skonstruowane w ASP.NET. Zobaczmy jak zdarzenia są zrobione w WPF zwłaszcza , że WPF i Silverlight w tym wypadku powinni być do siebie podobni.

Kontrolka przycisku dziedziczy po klasie bazowej “ButtonBase”. Ma to sens ponieważ w WPF są też inne przyciski od tego standardowego jak np. ToggleButton.

public class Button : ButtonBase

Przycisk dziedziczy po niej zdarzenie “Click” , który jest typem delegaty “RoutedEventHandler”. Delegata ta spodziewa się dwóch parametrów: referencji do obiektu, który spowodował wywołanie tego zdarzenia oraz obiekt klasy “RoutefEventArgs” , który przechowuje różne dodatkowe informacje na temat tego zdarzenia. Jest to pomysłowe ponieważ nie chciałbyś widzieć całej linijki parametrów za każdym razem gdy obsługujesz dane zdarzenie.

public abstract class ButtonBase : ContentControl, ICommandSource
{
    public event RoutedEventHandler Click;

    // Summary: 
    // Raises the System.Windows.Controls.Primitives.ButtonBase.
    // Click routed event.
     protected virtual void OnClick();

}

Klasa ButtonBase posiada też metodę wirtualną dostępną tylko dla klas pochodnych, która może wywołać bezpośrednio to zdarzenie.

Klasa Button uruchamia to zdarzenie gdy klikniesz na przycisk . Jednak co dokładnie się dzieje w momencie jego kliknięcia? Skąd klasa to wie ? To wychodzi poza ramy tego wpisu.

W WPF mamy XAML , a w nim najczęściej mówimy, która kontrolka obsługuje jakie zdarzenie. Jest to wygodne ale warto sobie uświadomić , że kod łączący naszą metodę np. ”Button_Click” z wydarzeniem wciąż istnieje jest on tylko wygenerowany przez kompilator. W sumie to dużo rzeczy w kompilatorze się dzieje aby powiązać kod XAML z kodem w C#.

private void Button_Click(object sender, RoutedEventArgs e)
{

}

Zdarzenia w wielu kontrolkach działają według tego samego wzoru. Sygnatura delegata w zdarzeniach w kontrolkach jest prawie zawsze taka sama. Delegata mówi , że ta metoda nic nie może zwracać “void” oraz pokazuje dwa parametry:

  • sender:Referencja do kontrolki, która wywołała dane zdarzenie.
  • e: Zbiór pomocniczych argumentów, które otrzymało dane zdarzenie.

Mając do dyspozycji argument “seneder” teraz nic nie stoi na przeszkodzie aby dana metoda X obsługiwała zdarzenie Click dla 100 przycisków skoro możesz sprawdzić, który z nich wywołał to zdarzenie.

Co dalej

System.Generic.

Kurs powoli dobiega końca.