이벤트(Events)
이벤트(Events)
이벤트는 사용자 동작에 함수 호출을 연결해 주는 장치로, GUI 프로그래밍에서 아주 중요한 역할을 해요. 이벤트는 애플리케이션이나 운영 체제가 직접 발생시킬 수도 있답니다.
출처: Microsoft Learn — Events - F#
본문
이벤트 처리하기 (Handling Events)
Windows Forms이나 Windows Presentation Foundation(WPF) 같은 GUI 라이브러리를 쓸 때, 애플리케이션 코드의 상당 부분은 라이브러리가 미리 정의해 둔 이벤트에 응답하는 방식으로 실행돼요. 이런 이벤트들은 폼(form)이나 컨트롤 같은 GUI 클래스의 멤버로 존재해요.
버튼 클릭처럼 이미 존재하는 이벤트에 원하는 동작을 추가하려면, 관심 있는 이벤트 이름(예: Form 클래스의 Click 이벤트)을 지정한 뒤 Add 메서드를 호출하면 돼요. 아래 코드를 보면 바로 감이 오실 거예요. 참고로 F# Interactive에서 실행한다면 System.Windows.Forms.Application.Run(System.Windows.Forms.Form) 호출은 빼 주세요.
open System.Windows.Forms
let form = new Form(Text="F# Windows Form",
Visible = true,
TopMost = true)
form.Click.Add(fun evArgs -> System.Console.Beep())
Application.Run(form)
Add 메서드의 타입은 ('a -> unit) -> unit이에요. 그래서 이벤트 처리 함수는 보통 이벤트 인자 하나를 받고 unit을 돌려주는 형태가 돼요. 앞의 예시는 이벤트 핸들러를 람다 식으로 썼는데, 그 대신 일반 함수 값으로도 처리할 수 있답니다.
또한 이벤트 처리 함수의 인자에는 이벤트 종류에 따른 정보가 담겨요. 예를 들어 MouseMove 이벤트에서는 시스템이 System.Windows.Forms.MouseEventArgs 객체를 넘겨주는데, 여기에는 마우스 포인터의 X, Y 좌표가 들어 있답니다.
open System.Windows.Forms
let Beep evArgs =
System.Console.Beep( )
let form = new Form(Text = "F# Windows Form",
Visible = true,
TopMost = true)
let MouseMoveEventHandler (evArgs : System.Windows.Forms.MouseEventArgs) =
form.Text <- System.String.Format("{0},{1}", evArgs.X, evArgs.Y)
form.Click.Add(Beep)
form.MouseMove.Add(MouseMoveEventHandler)
Application.Run(form)
사용자 정의 이벤트 만들기 (Creating Custom Events)
F#에서 이벤트는 F#의 Event 타입으로 표현되며, 이 타입은 IEvent 인터페이스를 구현해요. IEvent는 다시 System.IObservable<'T>와 IDelegateEvent라는 두 인터페이스의 기능을 합쳐 놓은 인터페이스예요.
덕분에 F#의 Event는 다른 언어의 델리게이트(delegate)와 같은 기능에 더해 IObservable에서 제공하는 추가 기능까지 갖추고 있어요. 즉, 이벤트 필터링이나 일급 함수·람다 식을 이벤트 핸들러로 쓰는 일이 모두 가능하다는 뜻이죠. 이런 기능은 Event 모듈에서 제공된답니다.
클래스에 다른 .NET Framework 이벤트처럼 동작하는 이벤트를 만들려면, 클래스 안에 Event를 필드로 정의하는 let 바인딩을 추가하면 돼요. 원하는 이벤트 인자 타입을 타입 인자로 지정할 수도 있고, 비워 두면 컴파일러가 알아서 추론해 줘요.
그리고 이벤트를 CLI 이벤트로 노출하는 이벤트 멤버도 정의해야 하는데, 이 멤버에는 CLIEvent 특성을 붙여야 해요. 멤버는 프로퍼티처럼 선언하고, 그 구현은 그냥 이벤트의 Publish 프로퍼티를 호출하는 형태예요.
클래스를 사용하는 쪽에서는 노출된 이벤트의 Add 메서드로 핸들러를 추가할 수 있고, 여기에 람다 식을 넘길 수 있어요. 이벤트를 발생시키려면 Trigger 프로퍼티를 사용해 핸들러 함수에 인자를 넘기면 돼요. 아래 코드가 바로 그 예시인데, 여기서 이벤트의 추론된 타입 인자는 람다 식의 인자를 나타내는 튜플이에요.
open System.Collections.Generic
type MyClassWithCLIEvent() =
let event1 = new Event<string>()
[<CLIEvent>]
member this.Event1 = event1.Publish
member this.TestEvent(arg) =
event1.Trigger(arg)
let classWithEvent = new MyClassWithCLIEvent()
classWithEvent.Event1.Add(fun arg ->
printfn "Event1 occurred! Object data: %s" arg)
classWithEvent.TestEvent("Hello World!")
System.Console.ReadLine() |> ignore
실행 결과는 다음과 같아요.
Event1 occurred! Object data: Hello World!
Event 모듈이 제공하는 추가 기능도 살펴볼게요. 아래 코드는 Event.create를 이용해 이벤트와 트리거 메서드를 만들고, 람다 식 형태의 이벤트 핸들러 두 개를 등록한 다음, 이벤트를 트리거해서 두 람다 식을 모두 실행하는 기본적인 사용법을 보여 줘요.
type MyType() =
let myEvent = new Event<_>()
member this.AddHandlers() =
Event.add (fun string1 -> printfn "%s" string1) myEvent.Publish
Event.add (fun string1 -> printfn "Given a value: %s" string1) myEvent.Publish
member this.Trigger(message) =
myEvent.Trigger(message)
let myMyType = MyType()
myMyType.AddHandlers()
myMyType.Trigger("Event occurred.")
위 코드의 실행 결과는 다음과 같아요.
Event occurred.
Given a value: Event occurred.
이벤트 스트림 처리하기 (Processing Event Streams)
Event.add 함수로 이벤트 핸들러를 그냥 추가하는 대신, Event 모듈의 함수들을 활용하면 이벤트 스트림을 훨씬 유연하게 처리할 수 있어요. 이벤트를 전방 파이프(|>)의 첫 번째 값으로 두고, 그 뒤에 Event 모듈의 함수들을 연달아 적용하는 방식이에요.
아래 코드는 특정 조건을 만족할 때만 핸들러가 호출되도록 이벤트를 설정하는 예시랍니다.
let form = new Form(Text = "F# Windows Form",
Visible = true,
TopMost = true)
form.MouseMove
|> Event.filter ( fun evArgs -> evArgs.X > 100 && evArgs.Y > 100)
|> Event.add ( fun evArgs ->
form.BackColor <- System.Drawing.Color.FromArgb(
evArgs.X, evArgs.Y, evArgs.X ^^^ evArgs.Y) )
Observable 모듈에는 observable 객체를 다루는 비슷한 함수들이 있어요. observable 객체는 이벤트와 비슷하지만, 자기 자신이 구독(subscribe)될 때만 이벤트에 능동적으로 구독한다는 차이가 있답니다.
인터페이스 이벤트 구현하기 (Implementing an Interface Event)
UI 컴포넌트를 개발하다 보면 기존 폼이나 컨트롤에서 상속받아 새 폼을 만들거나 새 컨트롤을 만드는 일이 잦아요. 이때 이벤트가 인터페이스에 정의되어 있다면, 그 인터페이스를 구현해서 이벤트를 구현해 줘야 해요.
System.ComponentModel.INotifyPropertyChanged 인터페이스는 System.ComponentModel.INotifyPropertyChanged.PropertyChanged라는 이벤트 하나만 정의하고 있어요. 아래 코드는 상속받은 인터페이스가 정의한 이벤트를 구현하는 방법을 보여 주답니다.
module CustomForm
open System.Windows.Forms
open System.ComponentModel
type AppForm() as this =
inherit Form()
// Define the propertyChanged event.
let propertyChanged = Event<PropertyChangedEventHandler, PropertyChangedEventArgs>()
let mutable underlyingValue = "text0"
// Set up a click event to change the properties.
do
this.Click |> Event.add(fun evArgs ->
this.Property1 <- "text2"
this.Property2 <- "text3")
// This property does not have the property-changed event set.
member val Property1 : string = "text" with get, set
// This property has the property-changed event set.
member this.Property2
with get() = underlyingValue
and set(newValue) =
underlyingValue <- newValue
propertyChanged.Trigger(this, new PropertyChangedEventArgs("Property2"))
// Expose the PropertyChanged event as a first class .NET event.
[<CLIEvent>]
member this.PropertyChanged = propertyChanged.Publish
// Define the add and remove methods to implement this interface.
interface INotifyPropertyChanged with
member this.add_PropertyChanged(handler) = propertyChanged.Publish.AddHandler(handler)
member this.remove_PropertyChanged(handler) = propertyChanged.Publish.RemoveHandler(handler)
// This is the event-handler method.
member this.OnPropertyChanged(args : PropertyChangedEventArgs) =
let newProperty = this.GetType().GetProperty(args.PropertyName)
let newValue = newProperty.GetValue(this :> obj) :?> string
printfn "Property {args.PropertyName} changed its value to {newValue}"
// Create a form, hook up the event handler, and start the application.
let appForm = new AppForm()
let inpc = appForm :> INotifyPropertyChanged
inpc.PropertyChanged.Add(appForm.OnPropertyChanged)
Application.Run(appForm)
생성자에서 이벤트를 연결하고 싶다면 코드가 조금 더 복잡해져요. 이벤트 연결은 추가 생성자의 then 블록 안에서 이뤄져야 하기 때문이에요. 다음과 같이 하면 돼요.
module CustomForm
open System.Windows.Forms
open System.ComponentModel
// Create a private constructor with a dummy argument so that the public
// constructor can have no arguments.
type AppForm private (dummy) as this =
inherit Form()
// Define the propertyChanged event.
let propertyChanged = Event<PropertyChangedEventHandler, PropertyChangedEventArgs>()
let mutable underlyingValue = "text0"
// Set up a click event to change the properties.
do
this.Click |> Event.add(fun evArgs ->
this.Property1 <- "text2"
this.Property2 <- "text3")
// This property does not have the property changed event set.
member val Property1 : string = "text" with get, set
// This property has the property changed event set.
member this.Property2
with get() = underlyingValue
and set(newValue) =
underlyingValue <- newValue
propertyChanged.Trigger(this, new PropertyChangedEventArgs("Property2"))
[<CLIEvent>]
member this.PropertyChanged = propertyChanged.Publish
// Define the add and remove methods to implement this interface.
interface INotifyPropertyChanged with
member this.add_PropertyChanged(handler) = this.PropertyChanged.AddHandler(handler)
member this.remove_PropertyChanged(handler) = this.PropertyChanged.RemoveHandler(handler)
// This is the event handler method.
member this.OnPropertyChanged(args : PropertyChangedEventArgs) =
let newProperty = this.GetType().GetProperty(args.PropertyName)
let newValue = newProperty.GetValue(this :> obj) :?> string
printfn "Property {args.PropertyName} changed its value to {newValue}"
new() as this =
new AppForm(0)
then
let inpc = this :> INotifyPropertyChanged
inpc.PropertyChanged.Add(this.OnPropertyChanged)
// Create a form, hook up the event handler, and start the application.
let appForm = new AppForm()
Application.Run(appForm)