티스토리 뷰
Reactive Extension part 3
Importing .NET events into Rx
추석 연휴때 너무 딩굴딩굴 거려서, 포스트 작성하는 감이 뚝!! 떨어져버렸다. 다시 정상으로 올릴려면 얼마나 걸릴지..흐흐흐;; 이제는 정상적으로 아침에 김밥도 한줄 먹고, 아이스 아메리카노도 한잔 마시고, 웹소설도 보고, 뉴스도 보고...
"포스트는 언제 쓰냐?"
"시간 날때.."
"언제??"
"화장실도 한번 다녀오고, 콧바람도 한번 쎄고..."
ㅡㅡ;;; 퍽퍽퍽퍽퍽!!
"@,.@....등은 생략하고 바로 시작한다구...쿨럭.."
포스트를 기다리는 사람이 있을까?? 하는 생각을 잠시 하면서, 그래도 시작한것 마무리를 짓기 위해 기운냅니다.
1. Events와 Rx
Rx는 events, asynchrony, TPL(Task Parallel Library)를 대체하기 위해 만들어 진것이 아니라는 것이다. 이 3가지는 low-level 컨셉이고, 이 들을 좀더 high-level에서 관리하는 것이 Rx가 되는 것이다.
어떤 부분이 high-level인가? 예를 들어 보면, 이벤트가 발생했을 때 이벤트에 필터 기능을 추가할 수 있고, 서로 다른 이벤트를 통합해서 관리할 수 있고, 이벤트 파라메터를 사용자가 원하는 형으로 변형해서 관리하는 것을 이야기한다. 이번 포스트에서는 이런 내용들을 자세히 다루어 보도록 하겠다.
여기서 작업할 소스도 part1에서 사용했던 소스를 기준으로 part2에서 설명한 대로 Rx를 추가하는 방식으로 사용한다. 이부분에 대한 자세한 설명은 part2를 참고 한다.
2. 기본 events 코딩
class Program
{
static void Main(string[] args)
{
var lbl = new Label();
var frm = new Form
{
Controls = { lbl }
};
frm.MouseMove += (sender, eargs) =>
{
lbl.Text = eargs.Location.ToString();
};
Application.Run(frm);
}
}
기본적인 event를 사용한 코딩을 입력하고, 실행을 시켜 보자.
화면에 Form이 하나 출력되고 그 위에 마우스를 움직이면, 마우스의 좌표가 화면에 표시된다. 일반적으로 사용하고 알고 있는 이벤트를 사용하는 코딩이다.
여기서 part1에서도 잠깐 언급했던 내용이지만, .NET events의 한계점에 대해 다시 알아 보도록 하자.
2.1 Events ar hidden data sources :
이벤트에서 발생하는 데이터들은 EventArgs를 통해서만 전달이 된다. 즉, 이 이야기는 이벤트 핸들러를 만들지 않는다면, 그 데이터에 접근을 할 수 없다는 이야기이다.
EventArgs에 있는 데이터는 이벤트가 발생되는 시점의 데이터이다. 단순 데이터 일 뿐 데이터를 정보로 만들기 위해서는 추가적인 코딩이 필요하다는 것이다.
예제에서 사용한 MouseMove 이벤트를 한번 살펴 보자면, 내가 원하는 데이터는 Location에 있는 x, y인데, 이 데이터를 사용하기 위해서 반드시 이벤트 핸들러를 추가해야 하고, 추가로 위치 데이터를 모아서 처리를 하는 작업을 해야하는 상황이라면, IList 객체나 ICollection 객체를 만들어서 그곳에 넣어주는 작업도 필요할 것이다.
하지만, Rx에서는 이벤트를 IObservable<T> object로 취급하기 때문에 FromEventPattern을 사용해서 이벤트 핸들러를 선언하지 않아도 데이터에 접근이 가능하고, Linq를 이용해서 데이터를 정보로 취급이 가능하도록 형을 변경할 수도 있다.
2.2 Events cannot be handed out :
이벤트는 배포가 불가능 하다. 이벤트는 first-class object가 아니기 때문에, 그 자체를 다른 곳으로 전달하거나, 저장을 할 수 없다.
하지만, Rx에서는 IObservable<T> object로 취급함으로, 다른 곳에서 전송하거나, 저장할 수 있다.
2.3 Events cannot be composed easily :
이벤트는 쉽게 재구성을 할 수 없다. 예를 들어 특정 조건에 맞는 이벤트 만을 처리하기 원한다 하더라도, 이벤트 핸들러를 연결하고, 그 안에서 추가 코딩을 해서 특정 조건인 경우에만 동작하도록 하도록 만들어야 한다.
하지만, Rx에서는 Where과 같은 Linq 구문을 사용해서 쉽게 이벤트를 무시할 수 있는 기능을 제공한다.
2.4 Events require manual handler maintenance which requires you to remember the delegate that was handed to it.
이벤트는 이벤트 핸들러를 수동으로 관리해 주어야 한다.
예제에서 처럼 += (sender, eargs) => {} 이렇게 이벤트 핸들러를 추가한 경우 이 이벤트 핸들러를 해제할 방법이 없다. 이벤트 핸들러를 제거해야하는 상황이라면 메소드를 이용해서 연결하고 해제를 해야한다.
하지만, Rx에서는 Subscription을 할 때 발생하는 IDispose 객체를 Dispose 시키는 것으로 간단하게 Unsubscribe를 시킬 수 있다.
3. Rx 코딩 1
2번에서 했던 코딩을 Rx 스타일로 변경했다.
static void Main(string[] args)
{
var lbl = new Label();
var frm = new Form
{
Controls = { lbl }
};
var moves = Observable.FromEventPattern<MouseEventArgs>(frm, "MouseMove");
using (moves.Subscribe(
evtPattern =>
{
lbl.Text = evtPattern.EventArgs.Location.ToString();
}))
{
Application.Run(frm);
}
}
* pdf에서는 FromEvent를 사용하고 있는데, 과거 버전에서는 FromEvent와 FromEventPattern이 함께 사용되었던 것으로 생각된다. 하지만 지금은 FromEvent와 FromEventPattern으로 나누어져있고, FromEvent는 AddHandler, RemoveHandler가 존재하여 object가 없고 events만 존재하는 경우에 사용하고, FromEventPattern는 object가 존재하는 경우에 이벤트 이름만으로 이벤트를 observable로 만들 수 있다.
실행 결과는 동일하나, 이제는 push-based 스타일로 변경이 되었다.
frm의 MouseMove이벤트를 moves라는 observable 객체로 만드는 부분으로, pdf에서 처럼 moves type을 확인해 보았는데, pdf와는 설명이 좀 다르다.
우선 (awaitable)라는 것이 보이는데, 이는 moves는 await가 가능한 녀석이라는 것을 알 수 있다.
T is System.Reactive.EventPattern<System.Windows.Forms.MouseEventArgs>는 EventPattern이라는 것이 마우스 이동 이벤트를 Wrapping하여 전달한다는 것을 알 수 있다.
using (moves.Subscribe(...)){...}
using 블럭에서 moves를 Subscribe를 하고 있다. 즉, using문을 빠져나가면 자동으로 Unsubscribe가 된다는 것이고, 이벤트 코딩의 한계점 하나를 해결하는 방법을 보여준다.
evtPattern =>{ lbl.Text = evtPattern.EventArgs.Location.ToString(); }
evtPattern은 Sender와 EventArgs를 모두 포함하고 있으며, EventArgs의 형은 MouseEventArgs이다. 이벤트 코딩에서 이벤트 핸들러가 받는 값을 모두 받아서 사용할 수 있다.
이전 part2 포스트에서 observable sequence에서는 OnNext, OnError, OnComplete가 있다고 이야기 했었고, part1 포스트에서는 mouse move 이벤트를 observable 객체로 만들면 hot observable 객체이기 때문에 실시간으로 데이터가 생성되고, 데이터가 생성되면 OnNext만 발생한다고 이야기 했었다. 그래서, OnError, OnComplete의 처리용 delegate를 입력하지 않았다. 입력해도 문제는 없다.
4. Rx 코딩 2
static void Main(string[] args)
{
var lbl = new Label();
var txt = new TextBox{ Top = 30, Width = 100, Height = 30 };
var lbl2 = new Label{ Top = 60 };
var frm = new Form
{
Controls = { lbl, txt, lbl2 }
};
IObservable<EventPattern<MouseEventArgs>> moves = Observable.FromEventPattern<MouseEventArgs>(frm, "MouseMove");
IObservable<EventPattern<EventArgs>> input = Observable.FromEventPattern<EventArgs>(txt, "TextChanged");
var movesSubscription = moves.Subscribe(
evtPattern =>
{
var location = evtPattern.EventArgs.Location;
lbl.Text = location.ToString();
},
ex => Console.WriteLine(ex.Message),
() => Console.WriteLine("Complete move!"));
var inputSubscription = input.Subscribe(
evtPattern =>
{
lbl2.Text = ((TextBox) evtPattern.Sender).Text;
});
using (new CompositeDisposable(movesSubscription, inputSubscription))
{
Application.Run(frm);
}
Console.ReadLine();
}
기존 MouseMove이벤트에 TextBox에서 발생하는 TextChanged 이벤트를 추가로 observable sequence로 만들고, 그 결과를 lbl2.Text에 출력하도록 코드를 추가했다.
실행 결과는 아래와 같이 택스트박스에 글씨를 입력하면, 바로 아래 출력이 된다.
여기서 중요한 것은 이렇게 이벤트들을 observable sequence로 만들면, part1 포스트의 11. .Net Events와 Observables의 장단점 비교에서 보듯이 코드 중심에서 데이터 중심으로 변경이 되고, 그로인해 다양한 연산자들을 이용한 여러가지 기능을 쉽게 추가할 수 있다는 것이다.
그리고, 이벤트를 Rx로 변경하는 것은 위의 예가 기본형으로 이제 쉽게 적용할 수 있을 것이다. 이후 부터 다루는 내용들은 모두 응용 편이라고 생각하면 된다.
new CompositeDisposable(movesSubscription, inputSubscription)
CompositeDisposable는 Disposable 객체 여러개를 하나로 묶어서 관리하는 객체로, using 블럭을 벗어나면 movesSubscription, inputSubscription은 모두 Disposed 된다.
5. A first look at some Standard Query Operators
이제 4. Rx 코딩 2에서 사용한 코드에 LINQ를 이용해서 observable sequnce를 queries하는 것을 추가 하도록 하겠다. 그 전에 .NET 이벤트 코딩을 다시 살펴 보자.
private void from_MouseMove(object sender, MouseEventArgs e)
{
point position = e.Location;
if (position.X == position.Y)
{
//조건에 만족하는 경우에만 실행할 부분
}
}
private void txt_TextChanged(object sender, EventArgs e)
{
string text = ((TextBox)Sender).Text;
//필요한 데이터만 뽑아내고, sender나 e 파라메터에 대해서는 잊어버리게 된다.
}
위의 코드는 pdf에 있는 내용으로 일반적인 .NET 이벤트 코딩을 보여준다.
첫번째는 if문을 이용해서 이벤트 필터 기능을 구현한 것이고, 두번째는 TextBox의 Text 프로퍼티의 값을 로컬변수로 값을 투영(projection) 한 것이다.
if문을 이용한 필터 기능이라고 이야기한 것은 더 자세히 이야기를 하면, 진정한 의미의 필터는 아니다. 일단 무조건 이벤트 핸들러를 실행하고, 그 안에서 데이터를 조회하고, 조건으로 비교하기 때문에 100번 이벤트가 발생된다고 하면, 100번 이벤트 핸들러를 실행하기 때문이다.
두번째 이벤트 핸들러에서는 TextBox의 Text 프로퍼티의 값을 구하기 위해서 이벤트를 사용하는데, text 값을 구하고나면 그 이후에는 sender나 e 파라메터의 데이터들은 버려지게 되며, 다시 이벤트 호출이 일어나지는 않는다는 것이다.
하지만, Rx에서는 이벤트를 IObservable<T> object로 취급을 하기 때문에 LINQ query를 이용해서 원하는 데이터만을 취하는 것이 가능하다.
var moves = from evtPattern in Observable.FromEventPattern<MouseEventArgs>(frm, "MouseMove")
select evtPattern.EventArgs.Location;
var input = from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
select ((TextBox) evtPattern.Sender).Text;
var movesSubscription = moves.Subscribe(
data =>
{
lbl.Text = data.ToString();
},
ex => Console.WriteLine(ex.Message),
() => Console.WriteLine("Complete move!"));
var inputSubscription = input.Subscribe(
data =>
{
lbl2.Text = data;
});
위에서 살펴 보았던 Rx코드를 수정해서, EventPattern 전체를 observable sequence로 변경하는 것이 아니라, 사용자가 필요한 데이터만을 사용하기 쉽게 하기 위해, 형(type)을 변경했다.
이제 moves의 상태를 다시 확인해 보면 이전에는 T is System.Reactive.EventPattern<System.Windows.Forms.MouseEventArgs> 였으나, 지금은 T is System.Drawing.Point로 변경이 되어 있는 것을 알 수 있고, input 또한 string으로 변경되어 있는 것을 알 수 있을 것이다.
이번에는 observable sequence에 조건을 추가한 또 다른 observable sequence를 만들어 보도록 하겠다.
static void Main(string[] args)
{
var lbl = new Label();
var lbl1 = new Label{ Left = 100};
var txt = new TextBox{ Top = 30, Width = 100, Height = 30 };
var lbl2 = new Label{ Top = 60 };
var frm = new Form
{
Controls = { lbl, lbl1, txt, lbl2 }
};
var moves = from evtPattern in Observable.FromEventPattern<MouseEventArgs>(frm, "MouseMove")
select evtPattern.EventArgs.Location;
var input = from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
select ((TextBox) evtPattern.Sender).Text;
var movesSubscription = moves.Subscribe(
data =>
{
lbl.Text = data.ToString();
},
ex => Console.WriteLine(ex.Message),
() => Console.WriteLine("Complete move!"));
var overFirstBisector = from pos in moves
where pos.X == pos.Y
select pos;
var overFirstBisectorSub = overFirstBisector.Subscribe(
pos => lbl1.Text = pos.ToString());
var inputSubscription = input.Subscribe(
data =>
{
lbl2.Text = data;
});
using (new CompositeDisposable(movesSubscription, inputSubscription, overFirstBisectorSub))
{
Application.Run(frm);
}
Console.ReadLine();
}
굵은 글씨 부분을 다시 추가했다. moves에는 마우스의 모든 이동에 대한 포인터 정보가 입력이 되고 있었는데, 그 정보 중에서도 다시 한번 조건을 추가해서 X, Y가 동일한 경우만 반응하는 overFirstBisector라는 observable sequence를 만들고, Subscribe를 했다.
위의 2개의 subscribe은 서로 개별적으로 동작하며, 둘 중 하나를 unsubscribe을 하더라도 다른 하나는 정상적으로 동작하도록 되어있다. 확인을 위해 아래 코드를 추가한 후 실행해 보자.
var overFirstBisectorSub = overFirstBisector.Subscribe(
pos =>
{
movesSubscription.Dispose();
lbl1.Text = pos.ToString();
});
x, y가 동일해서 위의 내용을 한번이라도 수행하게 되면, movesSubscription을 Disposed()해서 unsubscribe하도록 하면, 마우스 이동 전체에 대한 subscribe은 더 이상 실행되지 않고, 오직 x, y가 동일한 경우에만 실행이 되는 것을 확인 할 수 있다.
Rx는 observable sequence 상에 IObservable<T> object를 사용자가 원하는 형으로 재정의 할 수 있는 기능을 지원하고, 또, 다른 조건을 가지는 observable sequence를 추가로 생성할 수 있어서, 각각을 subscribe, unsubscribe를 통해서 자유롭게 연결과 해제를 할 수 있다.
아직까지도 맛보기에 불과하지만, 이제는 Rx에 대한 개념이 정리가 되고 있지 않을까 생각한다. 다음에도 이번 소스에서 이어서 계속 작업 하도록 하겠다.
"음..아직 Rx에 대한 개념이 정리 앙되고 있는데??"
"응"
"앵? 뭐가 응이얌? 어떻게 할 생각인데?"
"다음 part4를 기대해 줘"
"다음에도 정리 앙대면?"
"교통 경찰을 불러야지.. 정리좀 해달라고..하하;;"
'UWP & Windows App' 카테고리의 다른 글
ReactiveUI part1 (0) | 2014.09.29 |
---|---|
Reactive Extension part5 (2) | 2014.09.19 |
Reactive Extension part4 (0) | 2014.09.14 |
Reactive Extension part2 (0) | 2014.08.25 |
Reactive Extension part1 (0) | 2014.08.24 |
- Total
- Today
- Yesterday
- Visual Studio 2022
- uno-platform
- Microsoft
- uno platform
- #Windows Template Studio
- #uwp
- Windows 10
- Bot Framework
- Always Encrypted
- ComboBox
- visual studio 2019
- MVVM
- windows 11
- C#
- Cross-platform
- #prism
- IOT
- .net 5.0
- kiosk
- .net
- WPF
- XAML
- Behavior
- ef core
- PRISM
- LINQ
- #MVVM
- Build 2016
- UWP
- dotNETconf
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |