6. Obsługa zdarzeń
W poprzednim rozdziale omówiliśmy pojęcie zdarzeń związanych z komponentami. Teraz przyjrzymy się, jak tworzyć zdarzenia w naszych własnych klasach. Jest to trudny temat, który nie zainteresuje początkujących programistów; im zaleca się przejście bezpośrednio do następnego rozdziału.
6.1. Obiekty delegujące
Instrukcja
definiuje typ o nazwie opération, który w rzeczywistości jest prototypem funkcji przyjmującej dwie liczby całkowite i zwracającej liczbę całkowitą. To właśnie słowo kluczowe delegate sprawia, że opération jest definicją prototypu funkcji. Typ [opération] można przypisać do zmiennej w następujący sposób:
Dim op As New opération(AddressOf [fonction])
Zmienna typu [délégué] jest tworzona jako obiekt. Parametrem konstruktora jest funkcja powiązana z instancją delegata. W przypadku typu VB funkcja ta jest podana w postaci AddressOf funkcja, gdzie [fonction] to nazwa funkcji. Po utworzeniu instancji delegata można z niej korzystać jak z funkcji:
' wykonuje się procedurę delegowaną
Dim n As Integer = op(4, 7)
Delegat [opération] został zdefiniowany jako przyjmujący dwa parametry typu [integer] i zwracający wynik tego samego typu. Dlatego jego instancja [op], przedstawiona powyżej, jest wykorzystywana jako taka funkcja. Rozważmy następujący przykład:
' funkcje delegowane
Imports System
Public Class delegate1
' definicja prototypu funkcji
' przyjmuje 2 liczby całkowite jako parametry i zwraca liczbę całkowitą
Delegate Function opération(ByVal n1 As Integer, ByVal n2 As Integer) As Integer
' dwie metody instancji odpowiadające prototypowi
' dodaj
Public Function ajouter(ByVal n1 As Integer, ByVal n2 As Integer) As Integer
Console.Out.WriteLine(("ajouter(" & n1 & "," & n2 & ")"))
Return n1 + n2
End Function
' odejmowanie
Public Function soustraire(ByVal n1 As Integer, ByVal n2 As Integer) As Integer
Console.Out.WriteLine(("soustraire(" & n1 & "," & n2 & ")"))
Return n1 - n2
End Function
' jedna metoda statyczna odpowiadająca prototypowi
Public Shared Function augmenter(ByVal n1 As Integer, ByVal n2 As Integer) As Integer
Console.Out.WriteLine(("augmenter(" & n1 & "," & n2 & ")"))
Return n1 + 2 * n2
End Function
' program testowy
Public Shared Sub Main()
' definiujemy obiekt typu operacja w celu zapisania w nim funkcji
' zapisujemy funkcję statyczną „zwiększ”
Dim op As New opération(AddressOf delegate1.augmenter)
' wywołujemy delegat
Dim n As Integer = op(4, 7)
Console.Out.WriteLine(("n=" & n))
' tworzenie obiektu c1 typu class1
Dim c1 As New delegate1
' w delegacie rejestruje się metodę „dodaj” obiektu c1
op = New delegate1.opération(AddressOf c1.ajouter)
' wykonanie obiektu delegata
n = op(2, 3)
Console.Out.WriteLine(("n=" & n))
' zapisywanie metody odejmowania obiektu c1 w delegacie
op = New delegate1.opération(AddressOf c1.soustraire)
n = op(2, 3)
Console.Out.WriteLine(("n=" & n))
End Sub
End Class
Wyniki wykonania są następujące:
6.2. Zarządzanie zdarzeniami
Do czego może służyć obiekt typu delegate?
Jak zobaczymy w poniższym przykładzie, służy on przede wszystkim do zarządzania zdarzeniami. Klasa C1 może generować zdarzenia typu evti. Podczas zdarzenia typu evti obiekt typu C1 uruchomi wykonanie obiektu evtiDéclenché typu delegate. Wówczas zostaną wykonane wszystkie funkcje zarejestrowane w obiekcie delegującym typu evtiDéclenché. Jeśli obiekt typu C2, korzystający z obiektu typu C1, chce zostać powiadomiony o wystąpieniu zdarzenia evti na obiekcie typu C1, zarejestruje on jedną ze swoich metod C2.f w obiekcie delegowanym C1.evtiDéclenché obiektu C1, tak aby metoda C2.f była wykonywana za każdym razem, gdyzdarzenie evti wystąpi na obiekcie C1. Ponieważ obiekt delegowany C1.evtiDéclenché może zarejestrować wiele funkcji, różne obiekty Ci będą mogły zarejestrować się u delegata C1.evtiDéclenché, aby otrzymywać powiadomienia o zdarzeniu evti na obiekcie C1.
6.2.1. Zgłoszenie zdarzenia
Zdarzenie zgłasza się w następujący sposób (1):
Delegate Sub proc(liste d'arguments)
Public Event événement as proc
lub (2)
Public Event événement(liste d'arguments)
To słowo kluczowe [Event] definiuje dane jako zdarzenie. Do zdarzenia przypisuje się sygnaturę, jaką powinny posiadać procedury obsługujące to zdarzenie. Sygnaturę tę można zdefiniować za pomocą delegata (metoda 1) lub bezpośrednio (metoda 2).
6.2.2. Definiowanie procedur obsługi zdarzenia
Gdy zdarzenie jest wyzwalane przez instrukcję [RaiseEvent], należy je przetworzyć. Procedury odpowiedzialne za przetwarzanie zdarzeń nazywane są procedurami obsługi zdarzeń. Podczas tworzenia obiektu [Event] deklarowana jest jedynie sygnatura procedur obsługi zdarzenia. Jest to sygnatura obiektu [delegate] powiązanego ze zdarzeniem. Po wykonaniu tej czynności będzie można przypisać do zdarzenia procedury obsługi. Są to procedury, które muszą mieć taką samą sygnaturę jak obiekt [delegate] powiązany ze zdarzeniem. Aby przypisać procedurę obsługi zdarzenia do zdarzenia, należy użyć instrukcji [AddHandler] o następującej składni:
- event: zmienna zdarzenia, z którą powiązany jest nowy program obsługi zdarzeń
- eventhandler: nazwa procedury obsługi zdarzenia – musi mieć sygnaturę [délégué] powiązaną ze zdarzeniem
Do jednego zdarzenia można przypisać dowolną liczbę procedur obsługi. Wszystkie zostaną wykonane, gdy zdarzenie, z którym są powiązane, zostanie wywołane.
6.2.3. Wywołanie zdarzenia
Aby wywołać zdarzenie programowo, używa się instrukcji [RaiseEvent]:
- eventname: zmienna zdarzenia (zadeklarowana za pomocą słowa kluczowego Event)
- argumentlist: argumenty procedury powiązanej z wydarzeniem.
Zostaną wywołane wszystkie procedury, które zostały powiązane z tym zdarzeniem za pomocą instrukcji AddHandler. Otrzymają one jako parametry wartości zdefiniowane w sygnaturze zdarzenia.
6.2.4. Przykład
Rozważmy następujący przykład:
'obsługa zdarzeń
Imports System
Public Class myEventArgs
Inherits EventArgs
' klasa zdarzenia
' atrybut
Private _saisie As String
' konstruktor
Public Sub New(ByVal saisie As String)
_saisie = saisie
End Sub
' właściwość wprowadzana w trybie tylko do odczytu
Public Overrides Function ToString() As String
Return _saisie
End Function
End Class
Public Class émetteur
' klasa generująca zdarzenie
' atrybut
Private _nom As String ' nom de l'émetteur
' konstruktor
Public Sub New(ByVal nom As String)
_nom = nom
End Sub
' ToString
Public Overrides Function ToString() As String
Return _nom
End Function
' prototyp funkcji odpowiedzialnych za obsługę zdarzenia
Delegate Sub _evtHandler(ByVal sender As Object, ByVal evt As myEventArgs)
' pula menedżerów zdarzeń
Public Event evtHandler As _evtHandler
' metoda żądania wygenerowania zdarzenia
Public Sub envoyerEvt(ByVal evt As myEventArgs)
' powiadamia się wszystkich subskrybentów
' traktuje się to tak, jakby zdarzenie pochodziło stąd
RaiseEvent evtHandler(Me, evt)
End Sub
End Class
' klasa przetwarzania zdarzenia
Public Class souscripteur
' atrybut
Private nom As String ' nom du souscripteur
Private sender As émetteur ' l'émetteur des évts
' konstruktor
Public Sub New(ByVal nom As String, ByVal e As émetteur)
' zapisujemy nazwę subskrybenta
Me.nom = nom
' oraz nadawcę zdarzeń
Me.sender = e
' rejestruje się w celu otrzymywania zdarzeń od nadawcy e
AddHandler e.evtHandler, AddressOf traitementEvt
End Sub
' menedżer zdarzeń
Public Sub traitementEvt(ByVal sender As Object, ByVal evt As myEventArgs)
' wyświetlanie zdarzeń
Console.Out.WriteLine(("L'objet [" + sender.ToString + "] a signalé la saisie erronée [" + evt.ToString + "] au souscripteur [" + nom + "]"))
End Sub
End Class
' program testowy
Public Class test
Public Shared Sub Main()
' utworzenie nadajnika zdarzeń
Dim émetteur1 As New émetteur("émetteur1")
' utworzenie tablicy subskrybentów
' dla zdarzeń wysyłanych przez nadawcę1
Dim souscripteurs() As souscripteur = {New souscripteur("s1", émetteur1), New souscripteur("s2", émetteur1)}
' odczytuje się sekwencję liczb całkowitych z klawiatury
' gdy tylko jedna z nich jest błędna, wysyłane jest zdarzenie
Console.Out.Write("Nombre entier (rien pour arrêter) : ")
Dim saisie As String = Console.In.ReadLine().Trim()
' dopóki wprowadzony wiersz nie jest pusty
While saisie <> ""
' czy wprowadzona wartość jest liczbą całkowitą?
Try
Dim n As Integer = Integer.Parse(saisie)
Catch
' nie jest to liczba całkowita
' powiadamia się wszystkich
émetteur1.envoyerEvt(New myEventArgs(saisie))
End Try
' powiadamia się wszystkich
' nowe wprowadzenie danych
Console.Out.Write("Nombre entier (rien pour arrêter) : ")
saisie = Console.In.ReadLine().Trim()
End While
' koniec
Environment.Exit(0)
End Sub
End Class
Powyższy kod jest dość złożony. Przeanalizujmy go szczegółowo. W ramach zarządzania zdarzeniami istnieje nadawca zdarzeń (sender), który wysyła szczegóły zdarzeń (EventArgs) do subskrybentów, którzy zgłosili zainteresowanie danymi zdarzeniami. Klasa nadawcza zdarzeń wygląda w tym przypadku następująco:
Public Class émetteur
' klasa generująca zdarzenie
' atrybut
Private _nom As String ' nom de l'émetteur
' konstruktor
Public Sub New(ByVal nom As String)
_nom = nom
End Sub
' ToString
Public Overrides Function ToString() As String
Return _nom
End Function
' prototyp funkcji odpowiedzialnych za obsługę zdarzenia
Delegate Sub _evtHandler(ByVal sender As Object, ByVal evt As myEventArgs)
' pula menedżerów zdarzeń
Public Event evtHandler As _evtHandler
' metoda żądania wygenerowania zdarzenia
Public Sub envoyerEvt(ByVal evt As myEventArgs)
' powiadamia się wszystkich subskrybentów
' traktuje się to tak, jakby zdarzenie pochodziło stąd
RaiseEvent evtHandler(Me, evt)
End Sub
End Class
Każdy obiekt émetteur ma obiekt nom przypisany domyślnie. Metoda ToString została przedefiniowana w taki sposób, że podczas przekształcania obiektu émetteur w string, pobierana jest właśnie jego nazwa. Klasa definiuje zdarzenie:
' prototyp funkcji odpowiedzialnych za obsługę zdarzenia
Delegate Sub _evtHandler(ByVal sender As Object, ByVal evt As myEventArgs)
' zdarzenie
Public Event evtHandler As _evtHandler
Pierwszym argumentem funkcji obsługującej zdarzenie będzie obiekt, który wywołuje to zdarzenie. Drugim argumentem będzie obiekt typu myEventArgs, zawierający szczegóły dotyczące zdarzenia, do którego jeszcze wrócimy. Aby móc wywołać zdarzenie spoza obiektu émetteur, dodajemy do klasy metodę envoyerEvt:
' metoda żądania wygenerowania zdarzenia
Public Sub envoyerEvt(ByVal evt As myEventArgs)
' powiadamia się wszystkich subskrybentów
' traktuje się to tak, jakby zdarzenie pochodziło stąd
RaiseEvent evtHandler(Me, evt)
End Sub
Musimy określić, jakie rodzaje zdarzeń będą wyzwalane przez klasę émetteur. W tym celu zdefiniowaliśmy klasę myEventArgs:
Public Class myEventArgs
' klasa zdarzenia
' atrybut
Private _saisie As String
' konstruktor
Public Sub New(ByVal saisie As String)
_saisie = saisie
End Sub
' właściwość wprowadzona jako tylko do odczytu
Public Overrides Function ToString() As String
Return _saisie
End Function
End Class
Zdarzenia, które nas interesują, to błędne wpisy z klawiatury. Poprosimy użytkownika o wpisanie liczb całkowitych z klawiatury, a gdy tylko wpisze ciąg znaków, który nie jest liczbą całkowitą, wywołamy zdarzenie. Jako szczegół zdarzenia wystarczy podać błędne wprowadzenie. Takie jest przeznaczenie atrybutu _saisie tej klasy. Obiekt myEventArgs jest zatem tworzony z błędnym wprowadzeniem jako parametrem. Ponadto redefiniujemy metodę ToString, aby podczas przekształcania obiektu myEventArgs w ciąg znaków uzyskano atrybut _saisie.
Zdefiniowaliśmy nadawcę zdarzeń oraz typ zdarzeń, które wysyła. Następnie definiujemy typ subskrybentów zainteresowanych tymi zdarzeniami.
' klasa obsługi zdarzenia
Public Class souscripteur
' atrybut
Private nom As String ' nom du souscripteur
Private sender As émetteur ' l'émetteur des évts
' konstruktor
Public Sub New(ByVal nom As String, ByVal e As émetteur)
' zapisywana jest nazwa subskrybenta
Me.nom = nom
' oraz nadawcę zdarzeń
Me.sender = e
' rejestruje się w celu otrzymywania zdarzeń od nadawcy e
AddHandler e.evtHandler, AddressOf traitementEvt
End Sub
' menedżer zdarzeń
Public Sub traitementEvt(ByVal sender As Object, ByVal evt As myEventArgs)
' wyświetlanie zdarzeń
Console.Out.WriteLine(("L'objet [" + sender.ToString + "] a signalé la saisie erronée [" + evt.ToString + "] au souscripteur [" + nom + "]"))
End Sub
End Class
Subskrybent będzie definiowany przez dwa parametry: swoją nazwę (atrybut nom) oraz obiekt émetteur, którego zdarzenia chce przetwarzać (atrybut sender). Te dwa parametry zostaną przekazane do konstruktora obiektu. W trakcie tego samego procesu tworzenia subskrybent subskrybuje zdarzenia nadawcy:
' rejestruje się, aby otrzymywać zdarzenia od nadawcy e
AddHandler e.evtHandler, AddressOf traitementEvt
Funkcja zarejestrowana u nadawcy to traitementEvt. Ta metoda klasy souscripteur wyświetla dwa otrzymane argumenty (sender, evt) oraz nazwę odbiorcy (nom). Zdefiniowano typ generowanych zdarzeń, typ nadawcy tych zdarzeń oraz typ subskrybentów. Pozostaje nam tylko je zaimplementować:
' program testowy
Public Class test
Public Shared Sub Main()
' tworzenie źródła zdarzeń
Dim émetteur1 As New émetteur("émetteur1")
' utworzenie tabeli subskrybentów
' dla zdarzeń wysyłanych przez nadawcę1
Dim souscripteurs() As souscripteur = {New souscripteur("s1", émetteur1), New souscripteur("s2", émetteur1)}
' odczytuje się sekwencję liczb całkowitych z klawiatury
' gdy tylko jedna z nich jest błędna, wysyłane jest zdarzenie
Console.Out.Write("Nombre entier (rien pour arrêter) : ")
Dim saisie As String = Console.In.ReadLine().Trim()
' dopóki wprowadzony wiersz nie jest pusty
While saisie <> ""
' czy wprowadzona wartość jest liczbą całkowitą?
Try
Dim n As Integer = Integer.Parse(saisie)
Catch
' nie jest to liczba całkowita
' powiadamia się wszystkich
émetteur1.envoyerEvt(New myEventArgs(saisie))
End Try
' powiadamia się wszystkich
' nowe wprowadzenie danych
Console.Out.Write("Nombre entier (rien pour arrêter) : ")
saisie = Console.In.ReadLine().Trim()
End While
' koniec
Environment.Exit(0)
End Sub
Tworzymy obiekt émetteur:
' utworzenie emitenta zdarzeń
Dim émetteur1 As New émetteur("émetteur1")
Tworzymy tablicę zawierającą dwóch subskrybentów dla zdarzeń generowanych przez obiekt émetteur1:
' utworzenie tablicy subskrybentów
' dla zdarzeń wysyłanych przez nadawcę1
Dim souscripteurs() As souscripteur = {New souscripteur("s1", émetteur1), New souscripteur("s2", émetteur1)}
Prosimy użytkownika o wpisanie liczb całkowitych za pomocą klawiatury. Gdy tylko wprowadzona wartość jest błędna, prosimy obiekt émetteur1 o wysłanie zdarzenia do swoich subskrybentów:
' powiadamia się wszystkich
émetteur1.envoyerEvt(New myEventArgs(saisie))
Wysłane zdarzenie ma typ myEventArgs i zawiera błędne dane. Obaj subskrybenci powinni otrzymać to zdarzenie i zgłosić je. Pokazuje to poniższe wykonanie.
dos>evt1
Nombre entier (rien pour arrêter) : 4
Nombre entier (rien pour arrêter) : a
L'objet [émetteur1] a signalé la saisie erronée [a] au souscripteur [s1]
L'objet [émetteur1] a signalé la saisie erronée [a] au souscripteur [s2]
Nombre entier (rien pour arrêter) : 1.6
L'objet [émetteur1] a signalé la saisie erronée [1.6] au souscripteur [s1]
L'objet [émetteur1] a signalé la saisie erronée [1.6] au souscripteur [s2]
Nombre entier (rien pour arrêter) :