Zrozumienie zdarzeń i procedur obsługi zdarzeń w C #

329

Rozumiem cel zdarzeń, szczególnie w kontekście tworzenia interfejsów użytkownika. Myślę, że to jest prototyp tworzenia zdarzenia:

public void EventName(object sender, EventArgs e);

Co robią programy obsługi zdarzeń, dlaczego są potrzebne i jak je utworzyć?

Levi Campbell
źródło
9
Jak zauważył @Andy, fragment kodu opisuje tutaj metodę zarejestrowaną dla zdarzenia, a nie samo zdarzenie.
dthrasher

Odpowiedzi:

660

Aby zrozumieć procedury obsługi zdarzeń, musisz zrozumieć delegatów . W języku C # delegata można traktować jako wskaźnik (lub odwołanie) do metody. Jest to przydatne, ponieważ wskaźnik można przekazać jako wartość.

Główną koncepcją delegata jest jego podpis lub kształt. To jest (1) typ zwracany i (2) argumenty wejściowe. Na przykład, jeśli utworzymy delegata void MyDelegate(object sender, EventArgs e), może on wskazywać tylko na metody, które powrócą void, i weź objecti EventArgs. Coś jak kwadratowy otwór i kwadratowy kołek. Mówimy więc, że metody te mają taki sam podpis lub kształt, jak delegat.

Więc wiedząc, jak utworzyć odwołanie do metody, zastanówmy się nad celem zdarzeń: chcemy spowodować, aby jakiś kod został wykonany, gdy coś wydarzy się gdzie indziej w systemie - lub „obsłuży to zdarzenie”. Aby to zrobić, tworzymy określone metody dla kodu, który chcemy wykonać. Klej między zdarzeniem a metodami do wykonania to delegaci. Zdarzenie musi wewnętrznie przechowywać „listę” wskaźników do metod, które należy wywoływać, gdy zdarzenie zostanie wywołane. * Oczywiście, aby móc wywołać metodę, musimy wiedzieć, jakie argumenty przekazać do niej! Używamy delegata jako „umowy” między wydarzeniem a wszystkimi konkretnymi metodami, które zostaną wywołane.

Zatem domyślna EventHandler(i wiele podobnych) reprezentuje określony kształt metody (ponownie, void / object-EventArgs). Kiedy deklarujesz zdarzenie, mówisz, który kształt metody (EventHandler) to zdarzenie wywoła, określając delegata:

//This delegate can be used to point to methods
//which return void and take a string.
public delegate void MyEventHandler(string foo);

//This event can cause any method which conforms
//to MyEventHandler to be called.
public event MyEventHandler SomethingHappened;

//Here is some code I want to be executed
//when SomethingHappened fires.
void HandleSomethingHappened(string foo)
{
    //Do some stuff
}

//I am creating a delegate (pointer) to HandleSomethingHappened
//and adding it to SomethingHappened's list of "Event Handlers".
myObj.SomethingHappened += new MyEventHandler(HandleSomethingHappened);

//To raise the event within a method.
SomethingHappened("bar");

(* To jest klucz do wydarzeń w .NET i odrywa „magię” - wydarzenie to naprawdę, pod przykryciem, tylko lista metod o tym samym „kształcie”. Lista jest przechowywana w miejscu, w którym wydarzenie żyje. Kiedy zdarzenie jest „wywoływane”, to po prostu „przejrzyj tę listę metod i wywołaj każdą z nich, używając tych wartości jako parametrów”. Przypisanie modułu obsługi zdarzeń jest po prostu ładniejszym i łatwiejszym sposobem dodania metody do tej listy metod być nazywanym).

Rex M.
źródło
24
A teraz każdy może wyjaśnić, dlaczego to wydarzenie nazywa się EventHandler? Ze wszystkich mylących konwencji nazewnictwa jest to najgorsze ...
Joel in Gö
37
@Joel in Go wydarzenie nie nazywa się EventHandler - EventHandler to umowa, którą musi mieć wydarzenie z każdym, kto się z nim komunikuje. To jest jak „ciąg MyString” - ciąg deklaruje typ. zdarzenie MyEventHandler TheEvent oświadcza, że ​​każdy, kto wchodzi w interakcję z tym zdarzeniem, musi przestrzegać umowy MyEventHandler. Konwencja dotycząca procedury obsługi jest spowodowana tym, że umowa opisuje przede wszystkim sposób obsługi zdarzenia.
Rex M,
18
Jak odbywa się wydarzenie?
alchemiczny
17
@ Rex M: dziękuję za pierwsze spójne wyjaśnienie „MyEventHandler”, jakie kiedykolwiek widziałem :)
Joel w dniu
10
Dziękuję za fazę: „Klej między wydarzeniem a metodami do wykonania to delegaci.”, To jest naprawdę niesamowite.
zionpi
103

C # zna dwa warunki delegatei event. Zacznijmy od pierwszego.

Delegat

A delegatejest odniesieniem do metody. Tak jak możesz utworzyć odwołanie do instancji:

MyClass instance = myFactory.GetInstance();

Możesz użyć delegata, aby utworzyć odwołanie do metody:

Action myMethod = myFactory.GetInstance;

Teraz, gdy masz już odwołanie do metody, możesz wywołać metodę za pomocą odwołania:

MyClass instance = myMethod();

Ale dlaczego miałbyś Możesz także zadzwonić myFactory.GetInstance()bezpośrednio. W takim przypadku możesz. Istnieje jednak wiele przypadków, w których należy zastanowić się, gdzie nie chcesz, aby reszta aplikacji miała wiedzę myFactorylub do której należy dzwonićmyFactory.GetInstance() bezpośrednie .

Oczywistym jest jeden, jeśli chcesz być w stanie zastąpić myFactory.GetInstance()w myOfflineFakeFactory.GetInstance()jednym centralnym miejscu (aka wzorcem metody fabrycznej ).

Wzór metody fabrycznej

Tak więc, jeśli masz TheOtherClassklasę i trzeba z niej skorzystać myFactory.GetInstance(), tak będzie wyglądał kod bez delegatów (musisz poinformować TheOtherClasso typie twojego myFactory):

TheOtherClass toc;
//...
toc.SetFactory(myFactory);


class TheOtherClass
{
   public void SetFactory(MyFactory factory)
   {
      // set here
   }

}

Jeśli chcesz skorzystać z usług delegatów, nie musisz ujawniać typu mojej fabryki:

TheOtherClass toc;
//...
Action factoryMethod = myFactory.GetInstance;
toc.SetFactoryMethod(factoryMethod);


class TheOtherClass
{
   public void SetFactoryMethod(Action factoryMethod)
   {
      // set here
   }

}

W ten sposób możesz przekazać delegata innej klasie do użycia, bez ujawniania im swojego typu. Jedyne, co ujawniasz, to podpis twojej metody (ile masz parametrów i tym podobne).

„Podpis mojej metody”, gdzie to wcześniej słyszałem? O tak, interfejsy !!! interfejsy opisują podpis całej klasy. Pomyśl o delegatach jako o podpisie tylko jednej metody!

Inną dużą różnicą między interfejsem a delegatem jest to, że kiedy piszesz swoją klasę, nie musisz mówić C # „ta metoda implementuje ten typ delegata”. W przypadku interfejsów musisz powiedzieć „ta klasa implementuje ten typ interfejsu”.

Ponadto odwołanie do pełnomocnika może (z pewnymi ograniczeniami, patrz poniżej) odwoływać się do wielu metod (zwanych MulticastDelegate ). Oznacza to, że po wywołaniu delegata zostanie wykonanych wiele jawnie dołączonych metod. Odwołanie do obiektu może zawsze dotyczyć tylko jednego obiektu.

Ograniczenia dla a MulticastDelegatesą takie, że podpis (metoda / delegowanie) nie powinien mieć żadnej wartości zwracanej ( void) i słów kluczowych outi refnie jest używany w podpisie. Oczywiście nie można wywołać dwóch metod zwracających numer i oczekiwać, że zwrócą ten sam numer. Gdy podpis jest zgodny, delegat jest automatycznie a MulticastDelegate.

Zdarzenie

Zdarzenia są tylko właściwościami (jak get; set; właściwości pól instancji), które udostępniają uczestnikowi subskrypcję z innych obiektów. Te właściwości nie obsługują jednak funkcji get; set ;. Zamiast tego obsługują dodawanie; usunąć;

Możesz mieć:

    Action myField;

    public event Action MyProperty
    {
        add { myField += value; }
        remove { myField -= value; }
    }

Użycie w interfejsie użytkownika (WinForms, WPF, UWP itd.)

Teraz wiemy, że delegat jest odniesieniem do metody i że możemy zorganizować wydarzenie, aby poinformować świat, że może przekazać nam swoje metody, do których można się odwołać od naszego delegata, a my jesteśmy przyciskiem interfejsu użytkownika: może poprosić każdego, kto jest zainteresowany tym, czy zostałem kliknięty, aby zarejestrować swoją metodę w naszej firmie (za pośrednictwem zdarzenia, które ujawniliśmy). Możemy użyć wszystkich tych metod, które zostały nam przekazane i odwołać się do nich przez naszego delegata. A potem poczekamy i zaczekamy .... dopóki użytkownik nie przyjdzie i nie kliknie tego przycisku, będziemy mieli wystarczający powód, aby wywołać delegata. Ponieważ delegat odwołuje się do wszystkich podanych nam metod, wszystkie te metody zostaną wywołane. Nie wiemy, co robią te metody, ani nie wiemy, która klasa implementuje te metody. Dbamy tylko o to, aby ktoś był zainteresowany kliknięciem przez nas,

Jawa

Języki takie jak Java nie mają delegatów. Zamiast tego używają interfejsów. Robią to, prosząc każdego, kto jest zainteresowany „kliknięciem nas”, o wdrożenie określonego interfejsu (za pomocą określonej metody, którą możemy wywołać), a następnie o przekazanie nam całej instancji, która implementuje interfejs. Prowadzimy listę wszystkich obiektów implementujących ten interfejs i możemy wywołać ich „pewną metodę, którą możemy wywołać” za każdym razem, gdy zostaniemy kliknięci.

tofi9
źródło
kibicuje za wyjaśnienie, ale w jaki sposób wydarzenie różni się od wystąpienia delegata, który przyjmuje subskrybentów? oboje wyglądają dokładnie tak samo?
BKSpurgeon
@BKSpurgeon to dlatego, że „delegatami, którzy przyjmują subskrybentów” - eventto tylko cukier składniowy, nic więcej.
Mathieu Guindon
„Ograniczenia dla MulticastDelegate polegają na tym, że podpis (metoda / delegowanie) nie powinien mieć żadnej wartości zwracanej (void)”, nie sądzę, aby było to poprawne. Jeśli mają zwracane wartości, zwróci ostatnią.
Hozikimaru
„W ten sposób możesz przekazać delegata innej klasie do użycia, bez ujawniania im swojego typu. Jedyną rzeczą, którą eksponujesz, jest podpis twojej metody ...” - dla mnie jest to punkt krytyczny. Dziękuję Ci!
Ryan
40

Tak naprawdę jest to deklaracja dla procedury obsługi zdarzeń - metody, która zostanie wywołana, gdy zdarzenie zostanie uruchomione. Aby utworzyć wydarzenie, napiszesz coś takiego:

public class Foo
{
    public event EventHandler MyEvent;
}

Następnie możesz zasubskrybować takie wydarzenie:

Foo foo = new Foo();
foo.MyEvent += new EventHandler(this.OnMyEvent);

Z funkcją OnMyEvent () zdefiniowaną w następujący sposób:

private void OnMyEvent(object sender, EventArgs e)
{
    MessageBox.Show("MyEvent fired!");
}

Ilekroć Foowystrzeli MyEvent, twój przewodnik OnMyEventzostanie wezwany.

Nie zawsze musisz używać wystąpienia EventArgsjako drugiego parametru. Jeśli chcesz dołączyć dodatkowe informacje, możesz użyć klasy pochodnej EventArgs( EventArgsjest to podstawowa konwencja). Na przykład, jeśli spojrzysz na niektóre zdarzenia zdefiniowane Controlw WinForms lub FrameworkElementw WPF, możesz zobaczyć przykłady zdarzeń, które przekazują dodatkowe informacje do procedur obsługi zdarzeń.

Andy
źródło
14
Dziękujemy za odpowiedź na pytanie i nie wchodzenie na delegatów i wydarzenia.
divide_byzero,
3
Polecam przed użyciem OnXXXwzorca nazewnictwa dla obsługi zdarzeń. (Głupio, OnXXX jest rozumiany jako „obsłużyć XXX” w MFC i „podnieść XXX” w .net, więc teraz jego znaczenie jest niejasne i mylące - zobacz ten post, aby poznać szczegóły ). Preferowane nazwy to RaiseXXXwywoływanie zdarzeń HandleXXXlub obsługa Sender_XXXzdarzeń.
Jason Williams
1
Czy możesz pokazać działający przykład z prostą aplikacją WinForms?
MC9000,
40

Oto przykładowy kod, który może pomóc:

using System;
using System.Collections.Generic;
using System.Text;

namespace Event_Example
{
  // First we have to define a delegate that acts as a signature for the
  // function that is ultimately called when the event is triggered.
  // You will notice that the second parameter is of MyEventArgs type.
  // This object will contain information about the triggered event.

  public delegate void MyEventHandler(object source, MyEventArgs e);

  // This is a class which describes the event to the class that receives it.
  // An EventArgs class must always derive from System.EventArgs.

  public class MyEventArgs : EventArgs
  {
    private string EventInfo;

    public MyEventArgs(string Text) {
      EventInfo = Text;
    }

    public string GetInfo() {
      return EventInfo;
    }
  }

  // This next class is the one which contains an event and triggers it
  // once an action is performed. For example, lets trigger this event
  // once a variable is incremented over a particular value. Notice the
  // event uses the MyEventHandler delegate to create a signature
  // for the called function.

  public class MyClass
  {
    public event MyEventHandler OnMaximum;

    private int i;
    private int Maximum = 10;

    public int MyValue
    {
      get { return i; }
      set
      {
        if(value <= Maximum) {
          i = value;
        }
        else 
        {
          // To make sure we only trigger the event if a handler is present
          // we check the event to make sure it's not null.
          if(OnMaximum != null) {
            OnMaximum(this, new MyEventArgs("You've entered " +
              value.ToString() +
              ", but the maximum is " +
              Maximum.ToString()));
          }
        }
      }
    }
  }

  class Program
  {
    // This is the actual method that will be assigned to the event handler
    // within the above class. This is where we perform an action once the
    // event has been triggered.

    static void MaximumReached(object source, MyEventArgs e) {
      Console.WriteLine(e.GetInfo());
    }

    static void Main(string[] args) {
      // Now lets test the event contained in the above class.
      MyClass MyObject = new MyClass();
      MyObject.OnMaximum += new MyEventHandler(MaximumReached);
      for(int x = 0; x <= 15; x++) {
        MyObject.MyValue = x;
      }
      Console.ReadLine();
    }
  }
}
Gary Willoughby
źródło
4
Wywołanie delegata można uprościć w C #:OnMaximum?.Invoke(this,new MyEventArgs("you've entered..."));
Tim Schmelter,
23

Wystarczy dodać do istniejących świetnych odpowiedzi tutaj - bazując na kodzie w zaakceptowanym, który wykorzystuje delegate void MyEventHandler(string foo)...

Ponieważ kompilator zna typ delegowanego zdarzenia SomethingHappened , to:

myObj.SomethingHappened += HandleSomethingHappened;

Jest całkowicie równoważny z:

myObj.SomethingHappened += new MyEventHandler(HandleSomethingHappened);

Można również wyrejestrować programy obsługi w -=ten sposób:

// -= removes the handler from the event's list of "listeners":
myObj.SomethingHappened -= HandleSomethingHappened;

Dla kompletności podniesienie zdarzenia może być wykonane w następujący sposób, tylko w klasie, która jest właścicielem zdarzenia:

//Firing the event is done by simply providing the arguments to the event:
var handler = SomethingHappened; // thread-local copy of the event
if (handler != null) // the event is null if there are no listeners!
{
    handler("Hi there!");
}

Potrzebna jest kopia wątku lokalny obsługi upewnić się, że wezwanie jest bezpieczny wątku - w przeciwnym razie wątek mógł pójść i wyrejestrować ostatniej obsługi zdarzenia natychmiast po tym sprawdzić, czy to było null, i mielibyśmy „zabawy” NullReferenceExceptionnie .


C # 6 wprowadził ładną krótką rękę dla tego wzoru. Używa zerowego operatora propagacji.

SomethingHappened?.Invoke("Hi there!");
Mathieu Guindon
źródło
13

Moje rozumienie wydarzeń jest następujące;

Delegat:

Zmienna przechowująca odniesienie do metody / metod, które mają zostać wykonane. Umożliwia to przekazywanie metod takich jak zmienna.

Kroki tworzenia i wywoływania wydarzenia:

  1. Wydarzenie jest instancją delegata

  2. Ponieważ zdarzenie jest instancją delegata, musimy najpierw zdefiniować delegata.

  3. Przypisz metodę / metody, które mają zostać wykonane po uruchomieniu zdarzenia ( Wywołanie delegata )

  4. Wystrzel wydarzenie ( Zadzwoń do delegata )

Przykład:

using System;

namespace test{
    class MyTestApp{
        //The Event Handler declaration
        public delegate void EventHandler();

        //The Event declaration
        public event EventHandler MyHandler;

        //The method to call
        public void Hello(){
            Console.WriteLine("Hello World of events!");
        }

        public static void Main(){
            MyTestApp TestApp = new MyTestApp();

            //Assign the method to be called when the event is fired
            TestApp.MyHandler = new EventHandler(TestApp.Hello);

            //Firing the event
            if (TestApp.MyHandler != null){
                TestApp.MyHandler();
            }
        }

    }   

}
KE50
źródło
3

wydawca: gdzie zdarzają się wydarzenia. Wydawca powinien określić, z którego delegata korzysta klasa, i wygenerować niezbędne argumenty, przekazać te argumenty i siebie samemu delegatowi.

subskrybent: gdzie następuje odpowiedź. Subskrybent powinien określić metody reagowania na zdarzenia. Te metody powinny przyjmować ten sam typ argumentów co delegat. Subskrybent następnie dodaj tę metodę do delegata wydawcy.

Dlatego, gdy wydarzenie wydarzy się w wydawcy, delegat otrzyma pewne argumenty zdarzenia (dane itp.), Ale wydawca nie ma pojęcia, co się stanie z tymi wszystkimi danymi. Subskrybenci mogą tworzyć metody we własnej klasie, aby reagować na zdarzenia w klasie wydawcy, aby subskrybenci mogli odpowiadać na wydarzenia wydawcy.

Rileyss
źródło
2
//This delegate can be used to point to methods
//which return void and take a string.
public delegate void MyDelegate(string foo);

//This event can cause any method which conforms
//to MyEventHandler to be called.
public event MyDelegate MyEvent;

//Here is some code I want to be executed
//when SomethingHappened fires.
void MyEventHandler(string foo)
{
    //Do some stuff
}

//I am creating a delegate (pointer) to HandleSomethingHappened
//and adding it to SomethingHappened's list of "Event Handlers".
myObj.MyEvent += new MyDelegate (MyEventHandler);
Bilgi Sayar
źródło
0

Zgadzam się z KE50, z tym wyjątkiem, że widzę słowo kluczowe „event” jako alias dla „ActionCollection”, ponieważ wydarzenie zawiera zbiór działań do wykonania (tj. Delegata).

using System;

namespace test{

class MyTestApp{
    //The Event Handler declaration
    public delegate void EventAction();

    //The Event Action Collection 
    //Equivalent to 
    //  public List<EventAction> EventActions=new List<EventAction>();
    //        
    public event EventAction EventActions;

    //An Action
    public void Hello(){
        Console.WriteLine("Hello World of events!");
    }
    //Another Action
    public void Goodbye(){
        Console.WriteLine("Goodbye Cruel World of events!");
    }

    public static void Main(){
        MyTestApp TestApp = new MyTestApp();

        //Add actions to the collection
        TestApp.EventActions += TestApp.Hello;
        TestApp.EventActions += TestApp.Goodbye;

        //Invoke all event actions
        if (TestApp.EventActions!= null){
            //this peculiar syntax hides the invoke 
            TestApp.EventActions();
            //using the 'ActionCollection' idea:
            // foreach(EventAction action in TestApp.EventActions)
            //     action.Invoke();
        }
    }

}   

}
użytkownik3902302
źródło
0

Świetne odpowiedzi techniczne w poście! Nie mam nic technicznego do dodania do tego.

Jednym z głównych powodów pojawiania się nowych funkcji w językach i oprogramowaniu jest marketing lub polityka firmy! :-) To nie może być niedoceniane!

Myślę, że dotyczy to również niektórych delegatów i wydarzeń! uważam je za przydatne i dodają wartość do języka C #, ale z drugiej strony język Java postanowił ich nie używać! zdecydowali, że cokolwiek rozwiązujesz z delegatami, możesz już rozwiązać za pomocą istniejących funkcji języka, tj. interfejsów, np

Około 2001 roku Microsoft wydał platformę .NET i język C # jako konkurencyjne rozwiązanie dla Javy, więc dobrze było mieć NOWE FUNKCJE, których Java nie ma.

Siraf
źródło
0

Niedawno podałem przykład użycia zdarzeń w języku c # i opublikowałem go na moim blogu. Starałem się to wyjaśnić tak, jak to możliwe, na bardzo prostym przykładzie. Jeśli może to komukolwiek pomóc, oto: http://www.konsfik.com/using-events-in-csharp/

Zawiera opis i kod źródłowy (z dużą ilością komentarzy) i koncentruje się głównie na właściwym (podobnym do szablonu) wykorzystaniu zdarzeń i procedur obsługi zdarzeń.

Niektóre kluczowe punkty to:

  • Wydarzenia są jak „podtypy delegatów”, tylko bardziej ograniczone (w dobrym tego słowa znaczeniu). W rzeczywistości deklaracja zdarzenia zawsze zawiera delegata (EventHandlers są rodzajem delegata).

  • Programy obsługi zdarzeń to określone typy delegatów (możesz myśleć o nich jako o szablonie), które zmuszają użytkownika do tworzenia zdarzeń o określonej „sygnaturze”. Podpis ma format: (nadawca obiektu, EventArgs eventarguments).

  • Możesz stworzyć własną podklasę EventArgs, aby uwzględnić dowolny rodzaj informacji, które musi przekazać wydarzenie. Podczas korzystania ze zdarzeń nie jest konieczne używanie EventHandlerów. Możesz je całkowicie pominąć i zamiast tego użyć własnego delegata.

  • Jedną z kluczowych różnic między używaniem zdarzeń i delegatów jest to, że zdarzenia można wywoływać tylko z klasy, w której zostały zadeklarowane, nawet jeśli mogą być zadeklarowane jako publiczne. Jest to bardzo ważne rozróżnienie, ponieważ pozwala na ujawnienie twoich zdarzeń, dzięki czemu są one „połączone” z metodami zewnętrznymi, a jednocześnie są chronione przed „zewnętrznym niewłaściwym wykorzystaniem”.

konsfik
źródło
0

Kolejna rzecz, o której trzeba wiedzieć , w niektórych przypadkach musisz użyć Delegatów / wydarzeń, gdy potrzebujesz niskiego poziomu sprzężenia !

Jeśli chcesz użyć komponentu w kilku miejscach w aplikacji , musisz utworzyć komponent o niskim poziomie sprzężenia, a konkretna nie zainteresowana LOGIKA musi być delegowana NA ZEWNĄTRZ komponentu! Zapewnia to, że masz oddzielony system i czystszy kod.

W zasadzie SOLID jest to „ D ” ( zasada inwersji D ).

Znany również jako „ IoC ”, odwrócenie kontroli .

Możesz zrobić „ IoC ” za pomocą zdarzeń, delegatów i DI (wstrzykiwanie zależności).

Dostęp do metody w klasie podrzędnej jest łatwy. Ale trudniej jest uzyskać dostęp do metody w klasie nadrzędnej od dziecka. Musisz przekazać rodzicowi odniesienie do dziecka! (lub użyj DI z interfejsem)

Delegaci / wydarzenia pozwalają nam komunikować się z dzieckiem do rodzica bez odniesienia!

wprowadź opis zdjęcia tutaj

Na powyższym schemacie nie używam funkcji Deleguj / zdarzenie, a komponent nadrzędny B musi mieć odniesienie do komponentu nadrzędnego A, aby wykonać nieuwzględnioną logikę biznesową w metodzie A. (wysoki poziom sprzężenia)

Przy takim podejściu musiałbym umieścić wszystkie referencje wszystkich komponentów korzystających z komponentu B! :(

wprowadź opis zdjęcia tutaj

Na powyższym schemacie używam Delegata / zdarzenia, a komponent B nie musi znać A. (niski poziom sprzężenia)

Możesz używać komponentu B w dowolnym miejscu aplikacji !

A. Morel
źródło