'Windows App(Universal App)/Reactive extensions'에 해당되는 글 7건

ReactiveUI part2

 

part1에서 Dependency Resolution, MessageBus에 대해서 살펴 보았고, 나머지 내용들도 하나씩 간단하게 살펴보고, 셈플 프로젝트를 진행 하려고 했으나, 시간을 절약하는 의미에서 무조건 셈플 프로젝트를 만들면서, ReactiveUI와 Rx의 기능 중 필요한 것을 찾아서 가져다가 사용하기로 했다.

 

ReactiveUI의 document가 4.x버전 5.x버전이 혼용되어있어서, 하나 하나 찾아서 확인하고, 셈플 만들고 정리하는 것이 간단하지 않다!!

 

 

1. New Universal project!!!!!

 

File -> New -> Project ->

 

Blank App (Universal Apps) 선택 -> UniversalSample.ReactiveUI -> OK

 

 

간단한 셈플을 만드는 것이니 Blank app으로 선택해서 하나 만들자.

 

 

2. 무엇을 만들 것인가??

 

PersonModel을 이용해서, 리스트 뷰에 목록 출력하고, 추가/삭제/수정이 가능한 앱을 만든다. 아주 기본적인 CRUD를 하는 앱이 되는데, 바로 ReactiveUI를 적용하지 않고, MVVM pattern만을 적용해서 만들어 보도록 하자.

 

Windows 8.1 프로젝트에만 먼저 작업을 한다. 그 이유는 몇번 이야기 했지만.. 집에 컴퓨터가 Phone 에뮬레이터가 실행되는 환경을 지원하지 않기 때문이다. 음..아무도 궁금해 하지 않는..

 

뚝딱 뚝딱..약 30분 후에 MVVM pattern을 사용하는 셈플이 만들어졌다. 소스를 일단 대충 보자!

 

 

3. MainPage.xaml

 

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UniversalSample.ReactiveUI"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:ViewModels="using:UniversalSample.ReactiveUI.ViewModels"
    x:Class="UniversalSample.ReactiveUI.MainPage"
    mc:Ignorable="d">
    <Page.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="20"/>
        </Style>
        <Style TargetType="TextBox">
            <Setter Property="FontSize" Value="20"/>
        </Style>
    </Page.Resources>

    <d:DesignInstance.DataContext>
        <ViewModels:MainPageVM/>
    </d:DesignInstance.DataContext>

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0.5*"/>
            <ColumnDefinition Width="0.5*"/>
        </Grid.ColumnDefinitions>

        <Grid Grid.Row="1" Grid.Column="0" Margin="120,0,10,80">
            <Grid.RowDefinitions>
                <RowDefinition Height="50"/>
                <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>
            <StackPanel Orientation="Horizontal">
                <Button Content="Add" Command="{Binding AddCommand, Mode=OneWay}"/>
                <Button Content="Remove" Margin="10,0,0,0" Command="{Binding RemoveCommand, Mode=OneWay}"/>
            </StackPanel>
            <ListView x:Name="PeopleListView" Grid.Row="1" ItemsSource="{Binding People}" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}">
                <ListView.HeaderTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal" Background="Gray">
                            <TextBlock Text="Name" FontSize="20"/>
                            <TextBlock Text="Age" Margin="10,0" FontSize="20"/>
                            <TextBlock Text="Sex" Margin="10,0" FontSize="20"/>
                        </StackPanel>
                    </DataTemplate>
                </ListView.HeaderTemplate>
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal" Margin="10,5,0,0">
                            <TextBlock Text="{Binding Name}" FontSize="20"/>
                            <TextBlock Text="{Binding Age}" Margin="10,0" FontSize="20"/>
                            <TextBlock Text="{Binding Sex}" Margin="10,0" FontSize="20"/>
                        </StackPanel>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </Grid>

        <Grid Grid.Row="1" Grid.Column="1" Margin="10,0,120,80">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="175*"/>
                <ColumnDefinition Width="508*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
                <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>

            <TextBlock Text="Person Detail"/>

            <TextBlock Text="Name" Grid.Row="1" Grid.Column="0" />
            <TextBlock Text="Age" Grid.Row="2" Grid.Column="0" />
            <TextBlock Text="Sex" Grid.Row="3" Grid.Column="0" />

            <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding SelectedPerson.Name, Mode=TwoWay}" Margin="0,5"/>
            <TextBox Grid.Column="1" Grid.Row="2" Text="{Binding SelectedPerson.Age, Mode=TwoWay}" Margin="0,5"/>
            <CheckBox Grid.Column="1" Grid.Row="3" IsChecked="{Binding SelectedPerson.Sex, Mode=TwoWay}" Margin="0,5"/>

        </Grid>

        <TextBlock Text="ReactiveUI Sample - MVVM pattern" Style="{StaticResource HeaderTextBlockStyle}" Grid.Row="0"
   Grid.ColumnSpan="2" Grid.Column="0" VerticalAlignment="Center" Margin="120,0"/>
    </Grid>
</Page>


 

4. MainPage.xaml.cs

 

    public sealed partial class MainPage : Page
    {
        public MainPageVM ViewModel
        {
            get { return DataContext as MainPageVM; }
            set { DataContext = value; }
        }

        public MainPage()
        {
            this.InitializeComponent();

            ViewModel = new MainPageVM();
        }
    }

 

 

5. MainPageVM.cs

 

    public class MainPageVM : BindableBase
    {
        private PersonModel _selectedPerson;
        private ICollection<PersonModel> _people;
        private ICommand _addCommand;
        private ICommand _removeCommand;

        public ICollection<PersonModel> People
        {
            get { return _people; }
            set { SetProperty(ref _people, value); }
        }

        public PersonModel SelectedPerson
        {
            get { return _selectedPerson; }
            set { SetProperty(ref _selectedPerson, value); }
        }

        public MainPageVM()
        {
            People = new ObservableCollection<PersonModel>
            {
                new PersonModel{ Name = "kaki104", Age = 11, Sex = true },
                new PersonModel{ Name = "kaki105", Age = 22, Sex = false },
                new PersonModel{ Name = "kaki106", Age = 33, Sex = true },
                new PersonModel{ Name = "kaki107", Age = 44, Sex = false },
                new PersonModel{ Name = "kaki108", Age = 55, Sex = true },
            };

            this.PropertyChanged += (s, e) =>
            {
                switch (e.PropertyName)
                {
                    case "SelectedPerson":
                        ((RelayCommand)RemoveCommand).RaiseCanExecuteChanged();
                        break;
                }
            };
        }

        public ICommand AddCommand
        {
            get
            {
                return _addCommand = _addCommand ?? new RelayCommand(obj =>
                {
                    var newPerson = new PersonModel();
                    People.Add(newPerson);
                    SelectedPerson = newPerson;
                });
            }
        }

        public ICommand RemoveCommand
        {
            get
            {
                return _removeCommand = _removeCommand ?? new RelayCommand(obj =>
                {
                    if (SelectedPerson == null) return;

                    People.Remove(SelectedPerson);
                    SelectedPerson = null;
                },
                () => SelectedPerson != null);
            }
        }
    }

 

 

 

실행 결과는 아주 심플하다. 뭐 이 내용으로도 크게 문제가 될만한 것은 없지만, 사소한 몇가지 문제가 있다. 그 문제들을 하나씩 해결해 나가 보자.

 

 

6. ReactiveUI를 프로젝트에 추가한다.

 

현재 버전은 6.1.0이다. 앞으로 버전이 올라가면서, 어떻게 바뀔지는 아무도 모른다. 4.x, 5.x 버전이랑 여러가지 부분에서 개선이 되었기 때문이다.

 

 

 

 

설치를 하면 Rx가 모두 설치되고, reactiveui, reactiveui-core, Splat 등이 설치 된다. 즉, 정말 간단한 프로젝트라면 Rx를 사용하지 않는 것을 권장한다.

 

솔루션을 선택하고, 이 화면으로 들어온다면, Manage라는 버튼이 나오는데, 여러개의 프로젝트에 대해서 동일한 작업을 할 수 있도록 지원한다. Windows Phone 프로젝트에도 ReactiveUI를 추가해 준다.

 

 

7. Behaviors SDK (XAML) 추가

 

프로젝트에서 마우스 오른쪽 클릭해서 나오는 Add -> Refrence에서 추가할 수 있다. 이것도 Windows Phone 프로젝트에도 추가한다. 

 

혹시 Behaviors SDK가 설치되어 있지 않다면,

 

TOOLS -> Extensions and Updates...를 선택 후

 

아래 내용을 검색해서 찾은 후 설치하면 된다.

 

대충 설치는 완료 한 것 같다.

 

 

여기까지 작업한 소스 파일

UniversalSample.ReactiveUI_MVVM_Completed.zip

 

 

 

8. 종속성 해결

 

part1에서 종속성 해결을 위한 방법을 배웠다. 이 소스에서 종속성 문제가 발생되는 곳은 MainPage.xaml.cs에서 MainPageVM을 직접 생성해서 DataContext에 연결하는 부분으로 이 문제를 해결해 보도록 하겠다.

 

 

App.xaml.cs

 

        public App()
        {
            this.InitializeComponent();

            AppBootstrapper();

            this.Suspending += this.OnSuspending;
        }

 

        private void AppBootstrapper()
        {
            Locator.CurrentMutable.Register(() => new MainPageVM(), typeof(MainPageVM));
        }

 

 

위의 굵은 글씨 부분을 보면, App이 시작하면서, AppBootstrapper()를 실행 시키고, 그 내부에서 등록을 하게 된다. 여기서, Bootstrapper라는 명칭은 Prism pattern & practices에서 사용되던 명칭으로, Prism에서는 응용 프로그램이 시작이되면, 제일 먼저 실행되어, 기본 설정 작업들을 담당하는 곳을 의미한다. DI 솔루션에서는 자주 접하게 되는 명칭이지 않을까 싶다.

 

part1에서는 위와 같이 MainPageVM type을 등록해서 사용했었는데..이렇게 사용하면, 한가지 문제가 있는데.. 예를 들어 메인페이지 뷰모델은 앱에서 한개만 존재해야한다. 그래야지만, 다른 페이지로 네비게이션되 되었다가, 다시 돌아 오더라도, 넘어가기 전 상태를 기억하고 유지할 수 있기 때문이다. 하지만, 팝업뷰모델의 경우에 팝업이 출력될 때 마다, 처음 상태로 보여야 하지, 이전 상태를 유지하면 사용자에게 혼란을 줄 수 있기 때문이다.

 

 

Locator.CurrentMutable.Register(() => new MainPageVM(), typeof(MainPageVM));

 

 

위의 내용은 MainPageVM을 type을 요청할 때마다, 새로운 뷰모델을 생성해서 반환한다. 즉, 팝업뷰모델용이라는 뜻이다. 그렇다면, 하나의 인스턴스만 유지하려면 어떻게 해야하나?

 

 

Locator.CurrentMutable.RegisterConstant(new MainPageVM(), typeof(MainPageVM));

 

 

위의 문장으로 변경하면 하나의 인스턴스(Singleton)만을 유지하며, MainPageVM이란 type에 대한 인스턴스를 요청 할 때 마다, 이미 생성되어 있는 것을 반환한다. 

 

그렇다면, 생성자에 파라메터가 필요한 경우에는 어떻게 처리할까?

 

 

Locator.CurrentMutable.RegisterLazySingleton(GetTestVM, typeof(TestVM));

 

메소드

        private TestVM GetTestVM()
        {
            return new TestVM("Test Parameter");
        }

 

 

위와 같이 Lazy를 이용한 Singleton 등록 메서드를 이용해서 type을 등록 후에 사용하면된다.

 

Lazy는 객체 인스턴스를 사용하는 시점에서 하도록 고안되어 있기 때문에, 사용하기 전에 여러가지 파라메터를 미리 설정하고, type을 호출하면 파라메터를 이용한 생성이 가능하다.

 

 

this.DataContext = Locator.CurrentMutable.GetService(typeof(MainPageViewModel));

 

 

위의 내용은 part1에서 보았던 인스턴스를 사용하는 방법이다. 그런데, 이렇게 가지고 오면 return type이 object이기 때문에 DataContext가 아니라 ViewModel 프로퍼티에 직접 입력을 하려고 하면 다음과 같이 사용한다.

 

 

        public MainPageVM ViewModel
        {
            get { return DataContext as MainPageVM; }
            private set { DataContext = value; }
        }

        public MainPage()
        {
            this.InitializeComponent();

 

            ViewModel = Locator.Current.GetService<MainPageVM>();

        }

 

 

위와 같이 GetService<MainPageVM>()을 이용해서 뷰모델을 가지고 온다면, 지정한 MainPageVM type으로 반환이 되기 때문에 사용하기가 편하며, ViewModel을 View와 연결할 때 ViewModel이라는 프로퍼티를 만들어서 처리하면, 뷰에서 뷰모델의 기능을 이용하기가 수월하다.

 

 

 

9. ICommand의 문제점 해결

 

9.1 CanExecute() 갱신

 

커맨드는 사용자의 인터렉션을 처리하는 아주 유용한 도구이다. 그러나, 문제점이 없는 것은 아니다. 그 중 하나가 사용 가능 조건을 입력해 놓은 후 사용 가능 여부를 업데이트 시켜 주어야 한다는 것이다. 소스에서는 아래와 같다.

 

 

        public ICommand RemoveCommand
        {
            get
            {
                return _removeCommand = _removeCommand ?? new RelayCommand(obj =>
                {
                    if (SelectedPerson == null) return;

                    People.Remove(SelectedPerson);
                    SelectedPerson = null;
                },
                () => SelectedPerson != null);
            }
        }

 

 

위의 굵은 글씨 부분을 보면 이 RemoveCommand를 사용하기 위해서는 SelectedPerson 프로퍼티가 null이 아닌 경우에만 가능해야한다. 선택한 사람이 없는데, 삭제를 할 수는 없지 않은가? 하지만, 애석하게도 SelectedPerson이 아무리 변경이 되어도 자동으로 RemoveCommand의 사용 가능 여부가 자동으로 변하지는 않는다.

 

 

            this.PropertyChanged += (s, e) =>
            {
                switch (e.PropertyName)
                {
                    case "SelectedPerson":
                        ((RelayCommand)RemoveCommand).RaiseCanExecuteChanged();
                        break;
                }
            };

 

 

뷰모델의 PropertyChanged 이벤트를 통해서, SelectedPerson 프로퍼티체인지 이벤트가 발생하면, RaiseCanExecuteChanged() 메소드를 호출(혹은 다른 이름의 메소드..)해서 조건을 다시 확인시켜 주어야지만, 사용 가능 여부가 변경된다. 물론, 이런 단점을 극복하기 위해서, telerik에서 만든 RelayCommand는 어떤 프로퍼티가 변경이되든지 계속 상태를 확인 하도록 하는 경우도 있다. 하지만, 그것 또한 리소스 낭비가 아닐 수 없다. 커맨드가 한두개가 아닌데, 모든 커맨드들을 그런식으로 체크한다는 것도 그렇고, 모델 내부에 프로퍼티가 변경되었을 때는 체크가 되지 않는 경우가 있기 때문이다.

 

ReactiveUI에서 제공하는 ReactiveCommand를 이용하면 어떻게 변경되는지 확인해 보자.

 

 

프로퍼티

        public ICommand RemoveCommand { get; private set; }

생성자

            RemoveCommand = ReactiveCommand.Create(this.WhenAny(vm => vm.SelectedPerson, p => p.Value != null));
            ((IObservable<object>) RemoveCommand).Subscribe(obj =>
            {
                if (SelectedPerson == null) return;
                People.Remove(SelectedPerson);
                SelectedPerson = null;
            });

 

처음에는 외계어 같은 느낌이 들기도 하지만, 사용을 자주 하다보면 정이 붙어서, 나중에는 떨어지지 않을 것이다. 자 이렇게 만들어 놓고 실행을 해보면, 아까와 동일한 동작을 하는 것을 알 수 있다. 즉, SelectedPerson이 변경이 될 때마다, 자동으로 커맨드의 사용 가능 여부를 업데이트 해주게 된다.

 

 

RemoveCommand = ReactiveCommand.Create(this.WhenAny(vm => vm.SelectedPerson, p => p.Value != null));

 

ReactiveCommand는 new 키워드로 생성할 수 없고, ReactiveCommand의 생성 메소드를 실행해야지만 가능하다.

 

 

public static ReactiveCommand<object> Create(IObservable<bool> canExecute = null, IScheduler scheduler = null);

 

 

그중에 가장 일반적인 생성용 메서드가 Create이며, 기본 인터페이스는 위의 내용을 보면 알 수 있는데, 처음 내용이 canExecute, 두번째가 scheduler이다.

 

 

this.WhenAny(vm => vm.SelectedPerson, p => p.Value != null)

 

public static IObservable<TRet> WhenAny<TSender, TRet, T1>(this TSender This, Expression<Func<TSender, T1>> property1, Func<IObservedChange<TSender, T1>, TRet> selector);

 

 

이 부분이 ReactiveObject를 상속한 ViewModel에서 사용 가능한 메소드로, 뷰모델의 특정 프로퍼티를 이용해서 조건 연산을 한 후에 결과를 IObservable<bool>형태로 반환하는 것이다. 즉, 전체 해석을 하자면, ReactiveCommand를 하나 생성하는데, 뷰모델의 SelectedPerson이란 프로퍼티가 변경되었을 때 null인지 확인해서 null이 아니라면, true를 반환해서 커맨드가 사용 가능하게 되는 것이다.

 

 

            ((IObservable<object>) RemoveCommand).Subscribe(obj =>
            {
                if (SelectedPerson == null) return;
                People.Remove(SelectedPerson);
                SelectedPerson = null;
            });

 

 

커맨드의 사용가능 여부는 위와 같이 처리하고나니, 몇가지 의문 사항이 생기는데 앞에 IObservable..을 꼭 붙여야 하나?

 

우선 앞에 IObservable을 꼭 붙일 필요는 없다. 대신, 그렇게 하려면, RemoveCommand의 type을 ICommand에서 ReactiveCommand<object>로 변경해 주어야 하며, ReactiveCommand는 아래와 같은 인터페이스들을 상속 받는다.

 

 

public class ReactiveCommand<T> : IReactiveCommand<T>, IObservable<T>, IReactiveCommand, IHandleObservableErrors, ICommand, IDisposable, IEnableLogger

{ ... }

 

 

두번째 의문은 실제 실행 코드는 왜 따로 떨어져서 아랫줄에 작성했나?

 

생성후에 바로 Subscribe를 하게되면, return type이 IDisposable이기 때문에, ICommand에 넣을 수 없다. 그래서, 다음 줄에서 Subscribe를 호출한 것으로, 이 부분은 꼭 기억하기 바란다.

 

ReactiveCommand의 생성 메소드는 Create, CreateAsyncObservable, CreateAsyncTask, CreateCombined 이며, 자세한 기능에 대해서는 셈플 앱을 만들면서 하나하나 자세히 알아보도록 하겠다.

 

 

9.2 커맨드 실행 중 사용 가능 여부 변경

 

자주 접하게되는 내용으로, 커맨드가 실행 중에는 다시 커맨드가 눌러지지 않도록, 사용 가능 여부를 false로 바꾸어 줄 필요가 있으며, 특히, 조회 요청하는 커맨드를 연속 두번을 눌러서 오류를 발생하거나, 리소스 낭비를 초래하기도 한다.

 

ReactiveCommand는 자동으로 커맨드가 실행 되면, 사용 가능 여부를 false로, 실행 종료가 되면, 사용 가능 여부를 true로 변경해 준다.

 

 

MainPage.xaml

<Button Content="Longtime process" Margin="10,0,0,0" Command="{Binding LongtimeCommand, Mode=OneWay}"/>

 

 

MainPageVM.cs

프로퍼티

public ICommand LongtimeCommand { get; private set; }

 

 

생성자

            LongtimeCommand = ReactiveCommand.CreateAsyncTask(async obj =>
            {
                await Task.Delay(3000);
                var msgbox = new MessageDialog("Longtime process complete!");
                msgbox.ShowAsync();
            });

 

 

위의 내용은 LongtimeCommand를 하나 만들어서 버튼에 연결한 것이다. LongtimeCommand는 ICommand이며, ReactiveCommand의 CreateAsyncTask 생성 메소드를 이용해서 만들었다. CreateAsyncTask 메소드는 커맨드가 처리할 Func를 입력 할 수 있는 구조로 되어있다.

 

커맨드의 실행 내용은 커맨드가 실행되면, 3초 후에 MessageDialog가 출력되도록 되어 있다.

 

 

커맨드 실행 중

커맨드 실행 완료 후

 

 

위의 실행 결과를 보면, 커맨드가 실행되는 중에는 Longtime process라는 버튼이 비활성화 되어 있는 것을 볼 수 있고, 3초 후 MessageDialog를 출력하고, 다시 활성화가 된 것을 알 수 있다.

 

 

10. End

 

우선 part2는 여기까지만 진행 하도록 하겠다.

 

 

11. Source

 

UniversalSample.ReactiveUI_part2.zip

 

 

퍽퍽퍽!!!

 

"쿨럭.."

 

"일단 맞고 시작하자"

 

"이번에도 시간이 없어서 그냥 넘어가는 거냐?"

 

"아무도 관심을 가지지 않는 것 같아서다."

 

"내가 실력을 발휘하면, 지구가 남아있지 안을텐데?"

 

"그때를 기다리지"

 

"두고보자 인간!"

 

 

'Windows App(Universal App) > Reactive extensions' 카테고리의 다른 글

ReactiveUI part2  (0) 2014.09.30
ReactiveUI part1  (0) 2014.09.29
Reactive Extension part5  (2) 2014.09.19
Reactive Extension part4  (0) 2014.09.14
Reactive Extension part 3  (2) 2014.09.13
Reactive Extension part2  (0) 2014.08.25
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

ReactiveUI part1

 

Reactive Extension에 대해서는 총 5개의 포스트를 통해서 많은 내용을 전달한 것 같다. 하지만, Rx만으로 앱을 개발하기에는 아직 몇가지 산이 있는데, 그런 산을 단숨에 뛰어 넘을 수 있도록 도와주는 ReactiveUI(nuget package)에 대해서 몇 번의 포스트를 통해서 이야기 하려고 한다.

 

ReactiveUI 란?

MVVM pattern과 Reactive Extension(RX)와의 콤비라고 할 수 있겠다. MVVM pattern을 이용해서 decouple한 object를 만들고 Rx를 이용해 object간 복잡한 interactions을 선언적으로 등록해서 사용함으로, 기존 event 방식 프로그램의 단점을 극복할 수 있도록 지원하는 라이브러리로, Microsoft에서 정식으로 만든 것은 아니지만, 활용할 가치가 충분한 것으로 판단된다.

 

 

0. 기본 사항

Visual Studio 2013

Reactive Extension part1~5 읽고 Rx에 대한 기본 지식 필요

MVVM 패턴에 대한 기본 지식 필요

 

ReactiveUI

Reactive Programming for WPF, Silverlight, WinRT, and Windows Phone 7

http://www.reactiveui.net/

 

상단에 DOCS (PDF) 메뉴를 클릭해서, rxuiblog.pdf 파일을 받는다.

 

reactiveui/ReactiveUI.Samples

https://github.com/reactiveui/ReactiveUI.Samples

 

오른쪽 하단에 Download ZIP 버튼을 클릭해서 소스를 다운로드 받는다.

 

ReactiveUI/Doc

https://github.com/reactiveui/ReactiveUI/tree/docs/docs

 

ReactiveUI에서 제공하는 기능들을 하나하나 살펴 보도록하고, 이후에 유니버셜앱을 만들어 보자.

 

 

1. Basics

 

Basic Property Binding

 

맨 위에 있는 것이 Binding이라, 무슨 내용인지 확인을 해봤는데, 코드로 바인딩을 지원하는 것이다. 그런데, 이 기능은 아마도, iOS나 Android를 지원하기 위한 기능인 것 같다. xaml에서닌 기본 바인딩을 사용하는 것이 좋을 것 같다.

다른 플랫폼에서 사용하기를 원하는 개발자는 참고하면 되겠다.

 

 

Dependency Resolution

 

뷰모델안에서 new 키워드로 객체를 인스턴스 시키는 경우 해당 객체가 뷰모델에 종속이 된다고 표현하며, 그 종속된 객체는 별도로 dispose를 하지 않으면, 뷰모델과 동일한 lifetime을 가지게 되거나, 혹은 뷰모델이 dispose되지 못하도록 막을 수도 있다. 특히 팝업을 출력하는 경우 그 팝업 객체를 여러번 생성을 하게 되는데, 팝업이 닫히고 dispose를 하지 않는 경우 메모리에 계속 남아있어서, memory leak이 발생한다.

 

종속성 해결은 객체와 객체사이의 연결관계를 제거하여, memory leak을 줄이는 방법을 이야기하며, 여러가지 DI(Dependency Injection) 솔루션들이 존재하는데, ReactiveUI에서는 IDependencyResolver와 IMutableDependencyResolver 두개의 인터페이스를 이용해서 이 문제를 해결한다.

 

https://github.com/reactiveui/ReactiveUI/blob/docs/docs/basics/dependency-resolution.md

 

위의 페이지를 보면 종속성 해결하는 방법이 나오는데, ReactiveUI 6.x 버전에서는 아래의 방법으로 작업해야 한다.

 

    //인터페이스

    public interface IDependencyResolver : IDisposable
    {
        object GetService(Type serviceType, string contract = null);
        IEnumerable<object> GetServices(Type serviceType, string contract = null);
    }

    public interface IMutableDependencyResolver : IDependencyResolver, IDisposable
    {
        void Register(Func<object> factory, Type serviceType, string contract = null);
        IDisposable ServiceRegistrationCallback(Type serviceType, string contract, Action<IDisposable> callback);
    }

 

//인스턴스를 Type에 등록

Locator.CurrentMutable.Register(() => new MainPageViewModel(), typeof(MainPageViewModel));

 

//Type에 연결된 인스턴스 사용

this.DataContext = Locator.CurrentMutable.GetService(typeof(MainPageViewModel));

 

인스턴스를 Type에 등록할 때 contract을 지정하게되면, Type만으로는 사용할 수 없으니, 사용시에 유의하기 바란다.

 

 

MessageBus

 

다른 MVVM pattern 프레임웩에서 많이 사용되는 message bus pattern이 포함되어 있는데, message bus pattern이란 서로 다른 뷰, 뷰모델, 프로젝트간에 특정 message를 보내고, 받는 용도로 사용되는 것을 말하며, Publish–subscribe pattern(http://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)이라고도 한다.

 

message bus pattern이 Event-driven programming(http://en.wikipedia.org/wiki/Event-driven_programming)과 큰 차이점은 서로 직접적인 연결 관계가 없다는 것이다. 하지만, 언제 어디서든 원하는 메시지를 주고, 받을 수 있으며, 특히, ReactiveUI에서 구현된 MessageBus는 worker thread에서 보낸 메시지가 main thread에서 도착하기 때문에, cross-thread error를 발생시키지 않는다.

 

    //인터페이스

    public interface IMessageBus : IEnableLogger
    {
        bool IsRegistered(Type type, string contract = null);
        IObservable<T> Listen<T>(string contract = null);
        IObservable<T> ListenIncludeLatest<T>(string contract = null);
        IDisposable RegisterMessageSource<T>(IObservable<T> source, string contract = null);
        void RegisterScheduler<T>(IScheduler scheduler, string contract = null);
        void SendMessage<T>(T message, string contract = null);
    }

 

        //메시지 송신

        //IObservable sequence를 message source로 등록

        var click = Observable.FromEventPattern<RoutedEventArgs>(ClickMessageButton, "Click")
                    .Do(x => _count++)
                    .Do(x => ((Button)x.Sender).Content = "Click Message " + _count);
        ReactiveUI.MessageBus.Current.RegisterMessageSource(click);

 

        //object를 send message로 전송

        private void SendMessageButton_Click(object sender, RoutedEventArgs e)
        {
            var person = new PersonModel()
            {
                Name = "kaki104",
                Age = _count,
                Sex = _count % 2 == 0
            };
            ReactiveUI.MessageBus.Current.SendMessage(person);
        }

 

        //메시지 수신

        //IObservable EventPattern 수신

        ReactiveUI.MessageBus.Current.Listen<EventPattern<RoutedEventArgs>>()
                .Subscribe(evtPattern => {
                    Listener1.Items.Add(((Button)evtPattern.Sender).Content);
            });

 

        //object 수신

        ReactiveUI.MessageBus.Current.Listen<PersonModel>()
                .Subscribe(person => {
                    Listener2.Items.Add(person);
            });

 

 

 

 

2. End

오랫동안 테스트 및 문서를 찾으면서 포스트를 작성하다보니 내용은 얼마 없는데, 시간이 벌써 일주일 정도 소요된 것 같다. 이번에는 간단하게 여기서 마무리하고, 다음에 ReactiveCommand라는 엄청난 놈에 대해 설명하도록 하겠다.

 

다음에도 아래의 소스에서 계속 이어서 진행한다.

 

 

3. Source

ReactiveUI.Sample.zip

 

 

"난 출현 안하는 거냐?"

 

"바쁘다"

 

"바쁜 녀석이 어제는 낮에는 잠자고, TV보고, 저녁에는 미드보고 자냐?"

 

"내일을 위한 에너지 충전이다"

 

"후후 그 충전 제대로 해주지!!!!!!!!"

 

지지지이징지잊이이징징지~~~~~~~파파파팟~~퍽퍽~~~푸더덕~

 

 

'Windows App(Universal App) > Reactive extensions' 카테고리의 다른 글

ReactiveUI part2  (0) 2014.09.30
ReactiveUI part1  (0) 2014.09.29
Reactive Extension part5  (2) 2014.09.19
Reactive Extension part4  (0) 2014.09.14
Reactive Extension part 3  (2) 2014.09.13
Reactive Extension part2  (0) 2014.08.25
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Reactive Extension part5

 

페이스북 그룹에 등록한 part4 포스트에 반응이 뜨거워서, 월요일 저녁임에도 불구하고, part5를 포스트 작성을 바로 시작했다....로 시작한 포스트 작성이 금요일까지 이어지고 있다..쿨럭..오늘은 마무리를 해야징

 

페이스북 그룹 이야기를 나왔으니, 광고를 한번 하도록 하겠다.

 

윈도우 & 윈도우폰 앱 개발 모임

https://www.facebook.com/groups/vapps/

 

윈도우와 윈도우폰 앱 개발을 하는 개발자(다수), 기획(0), 디자이너(1-2)가 활동 중인 그룹으로 한달에 2번 오프라인 모임을 통해서 서로 개발에 관한 의견을 나누고, 저녁도 먹으며, 친목을 도모하고 있다.

자세한 사항은 http://vapps.github.io/ 이 곳을 참고한다.

 

 

"포스트에 광고 막 넣는 거야?"

 

"문제 없다. 내 포스트니"

 

"그래서 사기 광고 잖아?"

 

"ㅇㅇ?"

 

"모임에 언제 디자이너(1-2)가 참석 했지?"

 

"전에는 매주 왔다! 사진찍기 신공이 무서운 아이였지!! 하지만, 선택과 집중에서 앱 개발 모임이 밀렸을 뿐이다. 마음은 항상 참가하고 있다."

 

"그걸 어떻게 알아?"

 

"알고 있지..지구인이 아닌 것을.. 내 사진들은 다른 행성에 팔려 갔을지도.."

 

퍼퍼퍽!@#@#@

 

 

1. Bridging the asynchronous method pattern with Rx

 

Rx에서 웹 서비스를 어떻게 사용하는지 알아 보자. 먼저, part4에서 이야기 했듯이 웹 서비스를 이용한 간단한 사전 단어 제안 어플리케이션을 만들어 보도록 할 것인데, 여기서 사용하려는 서비스와 중요 정보 몇가지를 먼저 살펴 보자.

 

http://www.dict.org

 

이 사이트는 사전 검색을 하는 곳인데, 개발자가 이 사이트에 코드로 조회를 요청하거나, 사용하기가 어려운 어렵다. 그 이유는 http 프로토콜이 아닌 Dictionary Server Protocol (RFC 2229)을 사용하기 때문이다. 그래서 개발자가 쉽게 이 사이트를 이용하기 위해 아래의 http://services.aonaware.com 사이트에서 사전 서비스의 asmx 정보를 제공하고 있다.

 

 

http://services.aonaware.com/DictService/DictService.asmx

 

 

위의 이미지에 DictService에서 제공중인 웹 메소드 목록이 표시되는데, 그 중에서 MatchInDict를 사용할 것이기 때문에 어떤 파라메터를 입력해야하는지 확인하기 위해 클릭해본다.

 

 

dictId, word, strategy 3개의 파라메터가 존재하는데, dictId는 wn(WordNet 2.0), word는 검색할 단어 일부, strategy는 prefix를 사용하도록 한다.

 

내용 확인이 끝났으니, 사전 서비스를 프로젝트에 추가하도록 한다.

 

 

Add Service Reference...을 선택한다.

 

 

위와 같이 입력하고, Advanced 버튼을 눌러서 내용을 확인해 본다. Visual Studio 2013에서는 task-base가 기본으로 선택되어 있음으로 pdf에서 async를 선택하라는 내용은 무시해도 된다.

 

몇 가지 웹 메소드에 대해서 간단히 살펴 보면 MatchInDict는 사용자가 지정한 사전에서 입력한 단어로 찾아주는 서비스이고, DictionaryList는 사전 목록 조회, StrategyList는 검색어 찾는 방법에 대한 설정 값 목록을 조회할 수 있다.

 

정상적으로 서비스가 추가가 되었다면, Service References에 DictionarySuggestService가 추가되어 있을 것이다. 이 서비스를 사용하기 위해 program.cs 파일에 static 프로퍼티로 아래 내용을 추가해 준다.

 

        static readonly DictServiceSoapClient Service = new DictServiceSoapClient("DictServiceSoap");

 

이제 사용할 준비가 되었으니, 서비스가 정상적으로 동작하는지 확인을 위한 테스트 코드를 아래와 같이 추가하고 실행해보자

 

            var inputSubscription = input
                                    .ObserveOn(SynchronizationContext.Current)
                                    .Subscribe(async data =>
                {
                    Console.WriteLine(data);
                    lbl2.Text = data;

                    var result = await Service.MatchInDictAsync("wn", data, "prefix");
                    if (result != null)
                    {
                        Console.WriteLine("Results of '{0}'", data);
                        foreach (var dictionaryWord in result)
                        {
                            Console.WriteLine(dictionaryWord.Word);
                        }
                    }
                });

 

Service.MatchInDictAsync라는 메소드가 자동으로 생성된 비동기 메소드이다.

 

 

 

2. SelectMany : the Zen of composition

 

위에서 사용자가 입력한 단어로 시작하는 여러 단어를 반환하는 사전 서비스를 테스트 했다.

 

웹 서비스에는 한가지 이슈가 있는데, 내가 호출한 순서대로 결과를 반환해 주지 않는다는 것이다. 즉, 내가 rea -> reac -> react -> reacti -> reactiv -> reactive ...순서로 웹 서비스를 여러번 비동기 호출했을 때, 결과가 동일한 순서로 도착하는 것이 아니라, 뒤죽 박죽 순서로 돌아온다는 것이다. 그렇기 때문에 내가 rea에 대한 호출을 해서 처음으로 반환되는 결과가 rea에 대한 결과라는 것을 확신할 수도, 마지막에 호출한 reactive의 결과가 제일 마지막에 들어온다는 보장이 없다. (pdf 31page 참조)

 

물론 async, awiat 환경에서는 하나의 비동기 메소드에서 호출하고, 결과가 올때까지 대기를 하고 있다가 처리할 수 있다. 그렇다면, Rx에서 이 이슈를 어떻게 해결하는 알아 보도록 하자.

 

Rx에 SelectMany라는 오퍼레이터는 2개의 서로다른 observable sequence를 연결하는 기능을 가지고 있다. 아래 인텔리 센스에 나오는 설명을 확인하도록 하자.

 

 

* 위의 화면은 Resharper(3rd party tool)에서 제공해 주는 화면으로 기본 화면과는 약간 다름

 

Projects each element of the source observable sequence to the other observable sequence and merges the resulting observable sequences into one observable sequence.

 

원본 observable sequence와 다른 observable sequence를 merge해서 하나의 결과 observable sequence로 만들어 준다는 내용이다. 다시 설명하면, 내가 입력한 문자열과 그 문자열을 이용해서 호출한 웹 서비스를 하나로 연결하는 새로운 observable sequence를 만들 수 있다는 것이다.

 

먼저, 화면에 리스트 박스를 추가한다.

 

            var lbl = new Label();
            var lbl1 = new Label{ Left = 100};
            var txt = new TextBox{ Top = 30, Width = 100, Height = 30 };
            var lst = new ListBox { Top = txt.Top + txt.Height + 10, Height = 200};
            var frm = new Form
            {
                Controls = { lbl, lbl1, txt, lst }
            };

 

* lbl2를 삭제했다. 소스코드 아래에 있는 lbl2.Text = data 부분은 주석/삭제를 한다.

 

            var input = (from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
                        select ((TextBox) evtPattern.Sender).Text)
                        .Throttle(TimeSpan.FromSeconds(0.2))
                        .DistinctUntilChanged()
                        .Do(x => Debug.WriteLine("Start web service of " + x));

 

위에 Do를 추가해서 push가 발생하는 문자열이 어떤 것인지 확인 할 수 있도록 한다.

 

* pdf에 나오는 FromAsyncPattern 오퍼레이터는 더이상 지원하지 않는다.

 

이제 사용자가 입력한 문자열과 그 문자열을 이용해 조회한 웹 서비스를 서로 연결하는 작업을 한다.

 

            var res = (from inp in input
                from words in Service.MatchInDictAsync("wn", inp, "prefix")
                select new {KeyWord = inp, Result = words});

 

            res.Subscribe(results =>
            {

                Debug.WriteLine("Start results of " + results.KeyWord);
                foreach (var result in results)
                {
                    Debug.WriteLine(result.Word);
                }
                Debug.WriteLine("End results of " + results.KeyWord);
            });

 

이 코드의 결과가 정말 입력값과 웹 서비스의 결과가 연결되어서 제대로 나올까?? 궁금하면 실행해서 확인해본다.

 

Start ... rea
Start ... react
Start ... reacti
Start results of rea
Rea Silvia
…..
readapt
End results of rea
Start results of react
react
…..
reactor
End results of react
Start ... reactiv
Start results of reacti
reaction
…..
reactive schizophrenia
Start ... reactive
reactivity
End results of reacti
Start results of reactive
reactive
reactive depression
reactive schizophrenia
End results of reactive
Start results of reactiv
reactivate
reactive
reactive depression
reactive schizophrenia
reactivity
End results of reactiv

 

 

출력된 결과를 보기 쉽게 입력한 문자와 출력된 결과를 같은 컬럼에 배치했고, 아래의 도식화된 내용 처럼 서로 SelectMany로 연결되어 있다는 것을 확인 할 수 있다.

 

 

...마음에 드는 가?

 

 

"별로"

 

"뭐가 문젠가?"

 

"그냥 비동기 메소드에서 웹 서비스 호출하고, await로 기다리고, 도착하면 처리하는 것이랑 차이가 없는 거잖아?"

 

"결과적으로는 그렇치만,..음...음..."

 

"뭐? 왜그래?"

 

"너무 좋은데 딱히 표현할 방법이 없다!"

 

"컹...." 퍽퍽 퍼퍼벅

 

 

잠시 쉬어가는 의미로 다루지 않고 넘어갔던 부분에 대해 알아보고 처리하는 코드를 추가하자.

 

바로 웹 서비스의 오류 발생 상황이다. 웹 서비스의 특성상 네트워크의 문제가 발생하거나, 서비스 자체의 오류가 발생하면, 프로그램이 중지 되기 때문에, 반드시 오류 처리를 하고 넘어가야 한다. 또한, 지금 사용중인 서비스는 한 글자를 조회 요청하면, 서비스 오류를 발생 시키는데, 이 오류를 방지하기 위해 최소한 2글자 이상이 되도록 수정한다.

 

            var input = (from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
                        select ((TextBox) evtPattern.Sender).Text)
                        .Throttle(TimeSpan.FromSeconds(0.5))
                        .DistinctUntilChanged()
                        .Where(x => x.Length >= 3)
                        .Do(x => Debug.WriteLine("Start web service of " + x));

 

* Where 문이 저 곳이 아닌 다른 곳에 위치하면 약간 다른 결과가 나올 수 있다. 테스트 해보자

 

            res.Subscribe(results =>
            {
                Debug.WriteLine("Start results of " + results.KeyWord);
                foreach (var result in results.Result)
                {
                    Debug.WriteLine(result.Word);
                }
                Debug.WriteLine("End results of " + results.KeyWord);
            }, ex => Debug.WriteLine("Exception of web service : " + ex.Message));

 

출력결과가 정상적일 것이라고 생각한다. 다시 실행해서 결과 확인을 하는 것은 각자 하도록 하고, 이제 리스트박스에 결과를 출력 하도록 수정해 보자.

 

            res.ObserveOn(SynchronizationContext.Current)
                .Subscribe(results =>
            {
                lst.Items.Clear();
                lst.Items.AddRange(results.Result.Select(p => p.Word).ToArray());
            }, ex => Debug.WriteLine("Exception of web service : " + ex.Message));

 

실행 결과 아래와 같이 입력한 글자에 대한 유사 단어들의 목록을 리스트 박스에서 볼 수 있다.

 

 

 

휴~ 숨한번 돌리고, 이제 마지막 끝 판왕만 남았다.

 

방금 SelectMany를 이용해서 입력 값과 출력 값을 merge 시켜서 하나의 observable sequence를 만드는데 성공을 했다. 하지만, 여기에도 문제가 존재하는데....

 

rea -> react -> reacti -> reactiv -> reactive 순으로 입력을 진행하고 서비스를 호출했는데, 결과가 도착한 순서로 보자면 rea -> react -> reacti -> reactive -> reactiv 이다. 이렇게 도착 순서가 뒤바뀐 경우 리스트에 출력되는 내용은 어떻게 되는 것일까?

 

 

위의 일러스트에서 보듯이 reactive의 결과가 리스트에 출력이 된 후에 react가 도착이 되면, react의 결과가 리스트에 출력이 되기 때문에 오류 상황이 되는 것이다.

 

혹시 이러한 이슈를 해결하는 방법에 대해서 알고 있는 것이 있나? 아마도 일러스트에 나온 내용일 것이라고 생각한다.

 

 

일러스트의 내용으로 보면 react에 대한 웹 서비스 조회를 하는 중에 reactive를 다시 조회 할 때, 조회 중인 웹 서비스를 취소해서, 결과가 반환되지 못하도록 하는 것이다. 이렇게 처리하는 것을 "crossing out" 혹은 "muting"이라고 한다.

 

위에서 이야기한, 호출 중인 서비스를 취소하는 작업은 SelectMany의 특징 중 하나인 cancellation을 이용하면 쉽게 처리할 수 있다. 이 기능은 source가 웹 서비스를 호출 하고, 결과를 수신하기 전에, 새로운 키워드가 입력이 되어서 웹 서비스를 호출해야 한다면, 그 전에 존재하던 수신 대기 observable을 unsubscribe을 하게 된다. 마치 아래 일러스트 처럼 송유관에 벨브에 비유하기도 하는데, source는 웹 서비스 호출하는 부분이고, 그 뒤에 TakeUntil(value)가 붙어서 value에 값이 채워지기 전까지..라는 의미가 된다.

 

코드를 수정해 보자.

 

            var res = (from inp in input
                from words in Service.MatchInDictAsync("wn", inp, "prefix")
                            .ToObservable()
                            .Finally(() => Debug.WriteLine("Disposed request for " + inp))
                            .TakeUntil(input)
                select new {KeyWord = inp, Result = words});

 

Service.MatchInDictAsync 메소드는 observable sequence가 아니기 때문에 ToObservable() 메소드를 이용해서 observable로 변경해 주고, unsubscirbe가되는지 확인을 하기 위해 Finally를 추가하고, 마지막으로 TakeUntil을 사용한다. 실행 후 결과를 확인해 보자

 

Start web service of reacti
Start web service of reacti
Start web service of reactiv
Start web service of reactiv
Disposed request for reacti
Start web service of reactive
Disposed request for reactiv
Start web service of reactive
Disposed request for reactive
Start results of reactive
reactive
reactive depression
reactive schizophrenia
End results of reactive

 

Debug창에 출력된 log로, 새로운 단어로 웹 서비스를 호출하기 전에 호출 중인 웹 서비스를 unsubscribe하는 것을 확인 할 수 있다. 그런데, 약간 눈에 거슬리는 것이 있는데, 동일한 문장이 보이는 것이다. 중복 문장은 Do로 출력한 Start web service of xxx라는 내용인데, 그 이유는, SelectMany와 TakeUntil 2개의 Subscribe이 존재하기 때문이다.

 

* pdf에 Switch에 대한 사항은 소스에서 Switch를 사용할 수 없어서 일단 제외 한다. Switch가 제거 된 것은 아닌데, 사용하지 못하는 이유는 찾게되면 다시 포스트를 작성하도록 하겠다.

 

 

지금까지 part2~part5까지 4회에 걸쳐서 Rx HOL .NET.pdf의 내용을 Visua Studio 2013와 Rx 2.2.x 버전으로 구현하고, 설명하였다. 이제 Rx에 대한 사용법에 대해서 약간은 느낌이 왔을 것이라고 생각하고, 다음에도 더 좋은 자료가 있으면 포스트를 하도록 하겠다.

 

 

이제 part5 포스트가 일주일이나 걸린 이유를 공개한다. 짜잔~

 

*********************************************************************************

 

pdf에 나온 것과 같은 서비스를 이용해서 작업을 진행할려고 했는데, 갑자기 서비스에 문제가 발생한 것을 알게되었다. 그래서, 부득이하게 다른 예를 이용하는 포스트를 작성 한다. 순식간에 내용이 끊어지고 다른 형태로 한다고 만이 놀랐을 것 같다. 나도 셈플 프로젝트를 만드는데, 서비스 오류가 발생해서, 내가 뭘 잘 못했는 줄 알고, 이것 저것 찾아본다고 고생했다. 혹시라도, 포스트 작성 중 서비스가 다시 정상으로 돌아오면 그 때 계속하도록 하겠다.

 

9월 18일 오후 정말 파란 만장하다고 해야하나;; 새로운 프로젝트 만들어서 열심히 이것 저것 해보고 있었는데 지금 서비스가 정상화 되었다. 하하하..그렇다면 다시 pdf내용으로 작성한다.

 

*********************************************************************************

 

 

2. 새로운 프로젝트로..

 

pdf를 따라하지 못하니 새로운 유니버셜 프로젝트를 만들어서 작업을 진행하도록 하자.

 

포스트 작성 중에 한통의 메일을 확인 했다. 두달전에 물에 빠진 루미아 925를 홍콩으로 a/s를 보냈었는데, 회생 불능 판정을 받았다는 내용이였다. 모든 기능은 정상적으로 동작하는 듯 했는데..(컴퓨터와 연결하면 동기화까지도 했었는데..) lcd에 아무것도 표시가 되지 않는 것으로 보아서, lcd만 갈아 끼우면 될 것 같았는데... 앞으로 윈폰8을 언제 다시 만질 수 있을지..그래서, 이번에는 루미아 925를 추모(?)하는 마음에서 윈폰에서 작업을 하도록 하겠다.

 

새로운 Blank 유니버셜앱 프로젝트를 생성 후 Nuget package에서 rx를 선택해서 Windows store app, Windows Phone app 2개의 프로젝트에 모두 추가하도록 한다.

 

윈도우 폰 프로젝트를 선택 후 Set as Start up Project를 선택해서 시작 프로젝트로 만들고, MainPage.xaml에 다음과 같이 코딩한다.

 

2.1 MainPage.xaml

 

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="70"/>
            <RowDefinition Height="60"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>

 

        <TextBlock Text="Rx part5" FontSize="80"/>

        <StackPanel Grid.Row="1" Orientation="Horizontal">
            <TextBlock Text="Search text :" FontSize="30" VerticalAlignment="Center"/>
            <TextBox x:Name="tb" MinWidth="100"/>
        </StackPanel>
       
        <WebView x:Name="wv" Grid.Row="2" Source="
http://kaki104.tistory.com/m"/>
    </Grid>

 

실행 결과

 

 

 

2.2 MainPage.xaml.cs

 

        public MainPage()
        {
            this.InitializeComponent();

            this.NavigationCacheMode = NavigationCacheMode.Required;

 

      tb.Text = string.Empty;

            var input = (from evtPattern in Observable.FromEventPattern<TextChangedEventArgs>(tb, "TextChanged")
                         select ((TextBox)evtPattern.Sender).Text)
                        .Throttle(TimeSpan.FromSeconds(1))
                        .DistinctUntilChanged();

 

            input.Subscribe(
                txt => Debug.WriteLine("Input text is {0}", txt));

        }

 

part4에서 사용했던 TextChanged 이벤트를 이용하는 부분을 복사해서 붙여 넣은 후 입력 문자를 debug 창에 출력되도록 수정하였다. 추가로 EventArgs도 TextChangedEventArgs로 수정 했다.

 

Input text is react
Input text is reactive

 

정상적으로 결과가 되는 것을 확인했다.

이제 검색어를 입력하면 wikipedia에서 검색을 하여 결과를 화면에 출력하도록 소스를 수정해보자. 

 

            input.ObserveOn(SynchronizationContext.Current)
                .Subscribe(
                inputText =>
                {
                    Debug.WriteLine("Search text : {0}", inputText);

                    wv.Navigate(new Uri("http://en.wikipedia.org/wiki/" + inputText));
                });

 

실행 후 reactive를 입력하면 아래와 같은 결과가 표시 된다.

 

 

 

.ObserveOn(SynchronizationContext.Current)

part4에서 ObserveOn에 사용되는 IScheduler가 조금씩 다르다고 했는데, 이전에 사용하던 스케줄러 중에 몇가지가 사용을 할 수 없도록 삭제되어 있었다. 그래서 정확하게 어떤 스케줄러를 사용해야하는지 찾아보았지만, 검색에 실패! 결국 part4에서 사용한 것을 그대로 사용하기로 했다. 추후에 더 정확한 스케줄러를 찾게 되면 수정 하도록 하겠다.

 

어떻게 생각하면 각 플랫폼마다 여러가지 스케줄러를 혼용함으로 인해 발생하는 복잡함을 감소하기 위해서 SynchronizationContext.Current로 대동 단결 하도록 만들었는지도 모르겠다. 과거 버전에 있던 여러가지 스케줄러를 왜 삭제했는지는 히스토리를 찾아보거나, Rx 팀에 문의를 해보아야 정확하게 알 수 있을 것 같다.

 

...

 

 

"음? 왜 갑자기 끝난거야?"

 

"너무 길면, 읽기 싫어지고, 손가락이 아프다."

 

"힘들어서가 아니고?"

 

"험험..그럼 이만.." =3 =3 =3

 

 

 

9. End

 

여기까지가 웹 서비스가 고장이난 기간 동안 작성한 내용이다. 정신 없기는 하지만, 2개의 내용 모두를 그대로 포함하기로 하고, 소스도 모두 포함한다. 페이스북에 좋아요 눌러주신 한별님과 영재님 큐~

 

콘솔작업 프로젝 소스

 

RxSample.Part5.Console.zip

 

 

유니버셜 프로젝 소스

 

RxSample.Part5.Universal.zip

 

 

 

 

'Windows App(Universal App) > Reactive extensions' 카테고리의 다른 글

ReactiveUI part2  (0) 2014.09.30
ReactiveUI part1  (0) 2014.09.29
Reactive Extension part5  (2) 2014.09.19
Reactive Extension part4  (0) 2014.09.14
Reactive Extension part 3  (2) 2014.09.13
Reactive Extension part2  (0) 2014.08.25
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Reactive Extension part4

 

토요일 오후 커피숍에서 아이스 아메리카노를 빨며 한가롭게..딩굴딩굴..하고 싶었지만, 역시나 마음은 Rx 포스트를 언능 완료하고, ReactiveUI 포스트까지 쓰고, 실무에 적용을 해야하니..쿨럭..배보다 배꼽이 더 커진듯한 느낌은 이전이나 지금이나 변함이 없다.

 

part3에서 사용했던 소스에 추가로 코딩을 하기 때문에 이전 포스트에있는 소스를 받아서 사용하도록 한다.

 

"벌써 part4네 이제 포스트 쓰는 속도가 좀 빨라진 것 같네?"

 

"포스트 쓰는 페이스가 정상으로 회복됐다."

 

"오홍..그런데 페이스는 정상인데 페이스북 반응은 냉냉하던데?"

 

"좋아요랑 코맨드도 달려있다."

 

"누가 한건데?"

 

"나"

 

 

1. More query operators to tame the user’s input

 

            var inputSubscription = input.Subscribe(
                data =>
                {
                    Console.WriteLine(data);
                    lbl2.Text = data;
                });

 

input은 TextBox의 TextChanged 이벤트를 옵저버블(observable) 한 것으로 Text가 변경될 때 마다 Text를 push하고, 위의 Subscribe은 lbl2.Text에 변경된 텍스트를 출력하는 곳이다. 굵은 글씨 부분을 추가해서 각 단계마다 입력되는 Text를 확인해보자

 

 

Console에 출력된 내용은 위와 같다. 여기서 맨 아래 2줄은 동일한 결과를 표시하고 있는데, 그 이유는 reactive라는 글씨를 입력 한 후 커서를 t의 위치로 이동한 후 t를 선택하고, 다시 t를 입력했기 때문에 동일한 내용이지만 TextChanged 이벤트가 발생하여, 동일한 글씨가 출력된 것이다.

 

TextChanged 이벤트는 Text에 기초해서 발생하는 것이 아니라, 키보드 입력 상태를 체크해서 발생하는 이벤트이므로 동일한 Text라 하더라도 다시 발생하게 된다.

 

만약 Text를 web service의 검색어 조건으로 사용 한다면, 아마 동일한 Text를 2번 호출하는 경우가 발생할 것이다. 물론, 이전 입력값과 동일한지 아닌지를 이벤트 핸들러에서 비교해서 같지 않을 경우에만 호출하는 로직을 추가해 넣을 수 도 있지만, 이렇게 처리를 하더라도 비동기 이벤트 핸들러의 타이밍이 어긋나서 원하는 기능을 수행하지 못하는 경우가 발생할 수 있다.

 

 

"정말 그런 경우가 발생한다고?"

 

"가능하다고 생각한다."

 

"어떻게?"

 

"컴퓨터의 반응속도보다 빠르게 지우고, 쓰고를 반복하다 보면.."

 

(ㅡㅡ)a

 

 

Rx에서는 값의 변화를 추적하여, 이전 값과 서로 다른 경우에만 처리를 하도록 하는 강력한 기능이 존재한다.

 

            var input = (from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
                        select ((TextBox) evtPattern.Sender).Text)
                        .DistinctUntilChanged();

 

위에서 사용한 DistinctUntilChanged()가 바로 그 주인공으로, 동작 원리를 이해하기 위해서는 데이터의 흐름을 파악하는 것이 중요한데, 아래 그림과 설명을 참고한다.

 

* pdf에서는 FromEvent를 사용했지만 우리는 FromEventPattern을 사용하고 있다.

 

 

 

TextBox에 사용자가 내용을 입력하면, TextChanged 이벤트가 발생하며, 그 이벤트를 받은 Observer인 LINQ Select에서 TextBox.Text를 string으로 투영화(projection)해서 다음으로 전달하고, DistinctUntilChanged에서 이전 데이터와 같은지 다른지를 판단하여, 다른 경우에만 Subscribe에서 데이터를 전달하게 되는 것이다.

 

그리고, 아마 개발자라면 이런 일련의 과정들이 정말 일어나는지 아닌지 확인 하기를 원할 것이다. 그래서, Rx에서는 Do라는 오퍼레이터를 제공하고 있으며, 이 것을 사용해서 Log를 출력할 수 있다.

 

 

 

            var input = (from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
                        select ((TextBox) evtPattern.Sender).Text)
                        .Do(projectionText => Console.WriteLine("Before DistinctUntilChanged : {0}", projectionText))
                        .DistinctUntilChanged();

 

위와 같이 소스를 수정 후 실행한 결과이다.

 

 

아래에서 3번째 줄을 보면 reactive라는 글씨가 찍힌 것을 볼 수 있다. 하지만 그 내용이 바로 아랫줄에 출력이 되지 않고, 다음에 발생한 reactiv라는 글씨만 출력된 것을 볼 수 있다. 만약, Debug.WriteLine 같은 코드를 찍기 위해서는 이 Do 오퍼레이터를 활용하면 된다.

 

 

이제 다시 원 위치로 돌아가서 소스를 다시 살펴보자. 우리가 입력한 값을 web service를 사용해서 조회를 한다고 했을 때, 사용자의 모든 입력을 web service로 전송해야 하나? 즉, 조회를 하려는 단어는 reactive인데, 그 중간 단어들도 모두 호출을 한다면, 그 것 또한 문제가 아닐 수 없다. 그래서, 일반적으로는 Search 버튼을 두어 버튼을 누르거나, Enter 키를 누를 때 web service를 호출하도록 코딩을 한다.

 

 

"결과만 나오면 돼는 것 아닌가?"

 

"옳치않다."

 

"난 그냥 쓰는게 편하던데??"

 

"용납할 수 없다!"

 

"오~ 강하게 나오는데? 나한테 그렇게 나오다니... 어디 아프구나?"

 

"오전에 사과즙 먹은것이..쿨럭"

 

 

Rx에서는 observable sequence를 지연시키는 Throttle이라는 오퍼레이터를 가지고 있어서, 이런 문제를 쉽게 해결할 수 있다.

 

            var input = (from evtPattern in Observable.FromEventPattern<EventArgs>(txt, "TextChanged")
                        select ((TextBox) evtPattern.Sender).Text)
                        .Throttle(TimeSpan.FromSeconds(1))
                        .Do(projectionText => Console.WriteLine("Before DistinctUntilChanged : {0}", projectionText))
                        .DistinctUntilChanged();

 

위에 소스로 수정 후 실행을 해보자.

 

            var inputSubscription = input.Subscribe(
                data =>
                {
                    Console.WriteLine(data);
                    lbl2.Text = data;
                });

 

An unhandled exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll

Additional information: Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on.

 

위의 굵은 글씨 위치에서 오류가 발생하는데, 이 Cross-thread operation not valid 오류에 대해서는 나중에 다시 설명하고 처리하도록 하고, 우선은 주석 처리를 한 후에 다시 실행한다.

 

            var inputSubscription = input.Subscribe(
                data =>
                {
                    Console.WriteLine(data);
                    //lbl2.Text = data;
                });

 

실행 후 'react'까지 빠르게 입력 후 Console에 'react'가 출력이 되면, 나머지 'ive'를 입력해 준다. 아마도 아래와 같은 결과가 나올 것이다.

 

 

Throttle()을 추가하기 전과 상당히 다른 결과가 출력되었다는 것을 알 수 있다.

 

Throttle는 사용자가 지정한 시간동안 push-notification이 발생하지 않으면, 그 결과를 다음으로 넘기는데, 중간에 한번이라도 발생하면, 타이머가 초기화 되어 처음부터 다시 카운트를 하게된다. 결과적으로, 사용자가 TextChanged 이벤트를 1초동안 발생시키지 않았을 경우에만 TextBox에 입력된 Text를 DistinctUntilChanged 오퍼레이터에서 전달하게 되는 것이다.

 

이 과정을 좀더 자세히 확인하기 위해서는 pdf에 있는 것 처럼 Timestamp(), RemoveTimestamp()를 이용해서 타이머를 데이터를 출력하면 되는데, 여기서는 다루지 않겠다.

 

 

지금까지 작업에 대해서 정리를 해보자.

 

TextBox의 TextChanged 이벤트를 이용해서, 사용자 입력에서 중복 방지 처리를 하고, 입력 지연 기능 또한 구현해 보았다. 위의 두가지 기능을 추가하기 위해서 이벤트 코딩이였다면 상당한 양의 코딩을 해야 했을 것이고, 정확한 동작이 되는지를 오랫동안 테스트를 해보아야 했을 것이다. 하지만, Rx에서는 단 두개의 오퍼레이터를 사용하는 것으로 만족스러운 결과를 만들 수 있는 것이다.

 

이제 Rx가 약간 좋아지려고 하나?? 하지만, 꼭 기억하고 있어야 하는 것이 있는데. 이 모든 처리가 가능한 근본적인 이유는 이벤트를 IObservable<T> object로 처리를 하면서, 발생 순서대로 push-notification을 해주기 때문에 가능하다는 것이다.

 

 

2. Rx’s concurrency model and synchronization support

 

이 곳에서 이야기할 내용이 Thread와 관련된 내용이기 때문에 잘 모르는 분을 위해 짦게 설명을 하고 넘어 가겠다.

 

어플리케이션에는 크게 2가지의 스레드가 존재하며, 각각을 Main Thread(UI Thread), Worker Thread(Background Thread)라고 이야기 한다. Main Thread는 화면에 데이터를 출력하는 것을 관리하기 때문에 화면에 보이는 내용들은 모두 Main Thread에 소속되어 있다. 반면, 시간이 오래 걸리는 작업이나 화면에 출력할 필요가 없는 작업들은 Worker Thread에서 처리를 하게된다. 이렇게 작업 영역을 서로 나누어 놓은 것은 1개의 Thread만 사용하게 되면, 화면 멈춤 현상이 발생하기 때문이다. 과거에 Visual Basic 6.0이 이와 같이 1개의 Thread만 사용한 프로그래밍 언어였다.

 

이벤트는 기본적으로 Main Thread에서 발생한다. 그렇기 때문에 이벤트 핸들러 내부에서 처리되는 모든 내용도 기본적으로는 Main Thread에서 처리가 되도록 되어있다. (하지만, .NET Framework 4.5 이상이고, 이벤트 핸들러에 async를 사용 했다면, 이 이벤트 핸들러는 Worker Thread에서 실행될 것이다.)

 

* Microsoft 김명신 부장님의 제보로 알게된 사항으로 async, await를 사용하더라도 이벤트 핸들러는 모두 Main Thread에서 동작하는 것을 확인하였다.

 

 

 

 

 

 

하지만, 바로 전 소스에서 Throttle() 오퍼레이터를 사용하자마자, Cross-thread operation not valid 오류를 만나게 되었다. 이 오류가 발생한 이유는 지정한 시간동안 실행을 막아야하는데, 이 작업을 Main Thread에서 실행하면, 화면이 멈추어 버리기 때문에, Worker Thread로 처리 하도록 Rx가 변경을 했기 때문이다.

 

 

위의 위치에 Breakpoint를 걸고, 실행을 시켜본 결과이다. Threads 창으로 보면 제일 아래 노란색 화살표가 있는 부분이 현재 실행 중인 Thread인데, Category가 Worker Thread로 표시 되는 것을 알 수 있다. 이렇게 Worker Thread에서 실행 중에 화면 UI에 어떤 내용을 출력하려고 하면 발생하는 오류가 바로 Cross-thread operation not valid 오류이며, 이 것을 가르켜 동시성(concurrency) 문제라고 한다.

 

Rx에서는 이런 동시성 문제를 Scheduler와 동기화 처리 오퍼레이터를 사용하여 해결 할 수 있는데, 이 부분에 대해서 살펴 보도록 하겠다.

 

여기서 알아볼 동기화 처리 오퍼레이터는 ObserveOn() 이라는 것으로, 이 것을 사용하면 Worker Therad의 context와 Main Thread의 context를 동기화 시킬 수 있으며, IScheduler 인터페이스를 반드시 파라메터로 넘겨 주어야 한다.

 

            var inputSubscription = input
                    .ObserveOn(SynchronizationContext.Current)
                    .Subscribe(
                    data =>
                    {
                        Console.WriteLine(data);
                        lbl2.Text = data;
                    });

 

위의 굵은 글씨 부분을 추가하고, 아래 주석을 푼 후에 다시 실행해 보자.

 

 

위에서 보듯이 이제 노란 화살표는 Main Thread에 위치해 있는 것을 볼 수 있다. ObserveOn은 파라메터로 받은 IScheduler와 동기화를 시켜주는데, WinForm일 때와 WPF, Silverlight, WinRT일때 IScheduler를 사용하는 방법에 차이가 있다는 것을 알고 있어야 할 것이다.

 

* pdf에서 사용하는 control자체를 넘기는 방식은 더이상 지원하지 않고 있으며, 검색으로 찾아본 해결 방법 중에서ControlScheduler도 object browser에서 찾을 수 가 없다. 혹시라도 다른 방법을 발견하면 수정하도록 하겠다.

 

ObserOn과 비슷한 동기화 연산자로 SubscribeOn이 존재하는데, 이는 IScheduler와 Subscribe간의 동기화를 하기 위해 사용된다.

 

이제 최종 결과를 확인해 보자.

 

 

Console의 내용과 WinForm상에 출력되는 내용도 모두 정상적으로 출력이 된다.

 

 

3. End

이번 포스트는 여기서 마무리 하고 다음에는 위의 소스를 이용해서 진짜 web service를 호출하는 포스트를 올리도록 하겠다. 그러니, 버리지 말고 잘 간직하자!

 

일요일 저녁에 조용히 ReactiveUI 코딩을 만지작 만지작 하려고 했는데, 이전 포스트에 진심이 묻어나는 멋진 리플을 발견하여 의욕이 1000% 급상승하여 부랴부랴 마무리를 하게 되었다. 좋은 리플한개는 고래도 춤추게 한다는 이야기는 전설이 아니였었다. 하하하

 

 

"배꼽 큰 놈에서 고래로 변신이냐?"

 

"불만인가?"

 

"의욕과 전투력은 비례하지 않을텐데??"

 

"으흠...그렇군..잠시.."

 

 

4. 소스

 

RxSample.part4.zip

 

'Windows App(Universal App) > Reactive extensions' 카테고리의 다른 글

ReactiveUI part1  (0) 2014.09.29
Reactive Extension part5  (2) 2014.09.19
Reactive Extension part4  (0) 2014.09.14
Reactive Extension part 3  (2) 2014.09.13
Reactive Extension part2  (0) 2014.08.25
Reactive Extension part1  (0) 2014.08.24
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

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에 대한 개념이 정리가 되고 있지 않을까 생각한다. 다음에도 이번 소스에서 이어서 계속 작업 하도록 하겠다.

 

 

RxSample.Universal.part3.zip

 

 

"음..아직 Rx에 대한 개념이 정리 앙되고 있는데??"

 

"응"

 

"앵? 뭐가 응이얌? 어떻게 할 생각인데?"

 

"다음 part4를 기대해 줘"

 

"다음에도 정리 앙대면?"

 

"교통 경찰을 불러야지.. 정리좀 해달라고..하하;;"

 

...........슈우웅~~~~퍼엉~ ~ End

 

 

'Windows App(Universal App) > Reactive extensions' 카테고리의 다른 글

ReactiveUI part1  (0) 2014.09.29
Reactive Extension part5  (2) 2014.09.19
Reactive Extension part4  (0) 2014.09.14
Reactive Extension part 3  (2) 2014.09.13
Reactive Extension part2  (0) 2014.08.25
Reactive Extension part1  (0) 2014.08.24
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Reactive Extension part2

 

시작이 반이라는 말이 있다. 이는 로또를 사면 50%의 확율이 생기는 것과 같은 이치이다! 로또를 사면 '된다'와 '않된다'의 기대값을 가질 수 있지만, 로또를 사지 않으면 무조건 않된다. 그렇치 않은가?

 

금주 앱 개발 모임에 참석 했을 때 여러분들이 Rx에 대한 관심을 보여주었다. 하하하 감사드린다. 그런데, 공통적인 의견은 포스트를 봐도 무슨 소리인지 감을 잡을 수 없다는 내용이... 쿨럭.. 쿨럭..^^;;; 

 

Rx에 관심을 가지시는 모든 분들께 최소한 감은 잡을 수 있도록 포스트를 쓰도록 하겠다. 음..물론 모든 포스트를 읽으셔야 하고, 그 포스트가 100개가 될지 모른 다는 것이 함정이지만...

 

"함정 팔꺼냐?"

 

"설마;;"

 

"함정 파라..않그러면 내가 너 무덤을 파주마 푸하하"

 

"컹.........."

 

 

1. Rx HOL .NET.pdf 다운로드

Reactive Extensions for .NET Resources and Community

http://msdn.microsoft.com/en-US/data/gg577612 

 

페이지로 이동해서 좌측 중간에 있는 Hands-on Lab Reactive Extensions for .NET 링크를 눌러서 Rx HOL .NET.pdf 파일을 다운로드 받도록 한다. 처음 Rx를 접하는 개발자들에게 좋은 내용을 담고 있어서, pdf 내용을 중심으로 설명을 하려고 한다.

 

구글의 도움으로 번역을 하고, 예제로 사용할 소스 작성하고 테스트도 하고, 그 내용을 기준으로 포스트를 작성하느랴 포스트 하나 작성하는데 일주일 정도의 시간이 걸린 것 같다. 완벽한 번역을 하는 포스트가 아닌, 핵심 내용을 설명하는 포스트이니, 혹시 번역이나 의역에 오류가 있으면 리플로 남겨 주기 바란다.

 

* pdf에서는 VisualStudio 2010을 기준으로 설명하고 있는데, 포스트에서는 VisualStudio 2013을 기준으로 설명한다.

 

* 솔루션은 part1에서 사용한 솔루션에 프로젝트를 추가해서 진행하기 바라는 마음으로 일부러 아래에서 사용한 소스를 첨부하지 않았다.

 

 

2. 기본 인터페이스

 

    public interface IObservable<out T>
    {
        IDisposable Subscribe(IObserver<T> observer);
    }

 

    public interface IObserver<in T>
    {
        void OnCompleted();
        void OnError(Exception error);
        void OnNext(T value);
    }

 

Rx의 기본이되는 Observable는 IObervable, IObserver 2개가 기본 인터페이스로 구성이 되며, IObservable은 data source를 담당하고, IOberver는 data 수신을 담당한다. 중요한 부분을 정리해 보면 아래와 같다.

 

2-1. 하나의 IObservable에 여러개의 IObserver가 존재할 수 있다. Subscribe를 하는 만큼 생기기 때문인데, Subscribe은 언제 어디서든 가능하다. Subscribe이 되어있다면, Notification이 발생할 때 마다 수신 할 수 있다.

 

2-2. 구독해제는 Subsribe메소드의 결과값인 IDisposable 객체를 Dispose()하면 된다. 

 

2-3. IObserver가 수신할 수 있는 Notification은 OnNext, OnError, OnCompleted 이렇게 3가지 이며, OnNext는 0~무한대로 수신이 가능하고, OnCompleted는 마지막에 한번, OnError는 오류가 발생했을 때 한번 수신된다.

 

2-4. IObservable는 한번에 하나의 data를 IObserver로 push하는데 이러한 과정을 옵저버블 스퀜스(Observable sequences)라고 한다.

 

 

 

3. Creating observable sequences

 

Rx의 기초를 이해하기 위해 pdf에 있는 기본적인 예제들을 따라 해보도록 하겠다. 우선 솔루션에 Console project를 추가하고, 프로젝트에 Rx를 추가해 준다.

 

3-1. Nuget package에서 Rx로 검색후 선택하기 

 

3-2. 솔루션에 Rx를 이미 설치해 놓았다면, Reactive Extension - Main Library를 선택하고 Manage 버튼을 클릭  

 

3-3. Select Projects창에서 새로 추가한 프로젝트를 선택하고 OK 클릭

 

3-4. 새로 추가한 프로젝트를 시작 프로젝트로 설정

 

 

처음 입력할 코딩은 아래와 같다.

 

        static void Main(string[] args)
        {

            //이 부분을 변경하면서 OnNext, (OnCompleted, OnError)가 어떻게 발생하는지 확인한다.
            IObservable<int> source = Observable.Empty<int>();

 

            IDisposable subscription = source.Subscribe(
                x => Console.WriteLine("OnNext: {0}", x),
                ex => Console.WriteLine("OnError : {0}", ex.Message),
                () => Console.WriteLine("OnCompleted")
                );

            Console.WriteLine("Press ENTER to unsubscribe...");
            Console.ReadLine();

            subscription.Dispose();
        }

 

 

IObservable<int> source = Observable.Empty<int>();

 

source를 구성하는 IObservable을 빈 값으로 채우고 f5키를 눌러 실행해보면 다음과 같은 결과를 볼 수 있다.

 

 

source에 아무런 데이터가 없으니 바로 OnCompleted가 실행되었다. 데이터가 존재 했다면, OnNext xxx라는 글씨가 나온 후에 OnComplete가 출력되었을 것이다.

 

여기서 한가지 의문이 생기는데, 지난 part1에서는 마우스의 이벤트를 기다리기 때문에 완료가 되지 않았는데, 왜 여기서는 완료가 되어 버리는 것일까?

 

위의 예제에서 사용한 것은 데이터가 확정된 IObservable 객체를 사용한 것으로 cool observable이라고 한다. 어떤 데이터의 구성이 이미 정해져있기 때문에 바로 하나하나 끄집어 내서 OnNext를 실행하게 되는데, 여기서는 데이터가 없기 때문에 OnNext도 한번 실행하지 않고, 바로 OnCompleted를 실행하게 된 것이다.

 

part1에서 사용했던 pointer move event를 관찰하는 IObservable객체를 hot observable이라고 하는데, 어떤 데이터가 입력될지 정해진 것이 없으니 계속 관찰만 하고 있다가, 데이터가 입력이 되는데로 OnNext를 계속 실행하게 되는 것이다.

 

 

IObservable<int> source = Observable.Return(42);

 

위의 문장으로 교체를 하면 아래와 같은 결과가 나온다.

 

 

1개의 데이터를 가지고 있기 때문에 실행을 하자마자 OnNext로 42라는 숫자를 보내고, 또 다른 데이터를 찾아보지만 찾지 못해서 OnCompleted를 호출하고 완료되는 것을 알 수 있다. 가장 평범한 모습을 보여준 것이라 할 수 있다.

 

이제 소스를 아래와 같이 수정 한 후 실행해보자

 

            IObservable<int> source = Observable.Range(5, 3);

 

            IDisposable subscription = source.Subscribe(
                x => Console.WriteLine("OnNext : {0}", x),
                ex => Console.WriteLine("OnError : {0}", ex.Message),
                () => Console.WriteLine("OnCompleted")
                );

            Console.WriteLine("Press ENTER to unsubscribe...");
            Console.ReadLine();

            subscription.Dispose();

 

            //추가 테스트
            IDisposable subscription2 = source.Subscribe(
            x => Console.WriteLine("OnNext2 : {0}", x),
            ex => Console.WriteLine("OnError2 : {0}", ex.Message),
            () => Console.WriteLine("OnCompleted2")
            );

            Console.WriteLine("Press ENTER to unsubscribe2...");
            Console.ReadLine();

            subscription2.Dispose();

 

 

Range는 cood observable로서 Range(5,3)은 5부터 3개의 데이터를 생성한 IObservable객체를 생성한다.

 

위의 예제는 첫 Subscribe에 의해 5, 6, 7이 출력이 되고, Enter키를 눌러서 subscription을 Dispose시킨 후 두번째 Subscribe에 어떠한 반응을 보이는 지 확인하기 위해서 만들어졌다. 옵저버블 스퀜스라고 해서, 한번 출력되고 나서, 리셋이 않되는 것 아닌가 하고 테스트를 해보았는데, 잘된다.

 

* pdf 9page를 보면 Run메소드라는 것을 사용하는 예제가 있는데, 지금은 존재하지 않는 메소드이다.

 

마지막은 디버깅에 관한 내용으로 아래의 소스를 입력해보자.

 

        private static void Test2()
        {
            IObservable<int> source = Observable.Generate(0,
                i => i < 5,
                i => i + 1,
                i => i * i,
                i => TimeSpan.FromSeconds(i));

            using (
                source.Subscribe(
                    x => Console.WriteLine("OnNext : {0}", x),
                    ex => Console.WriteLine("OnError : {0}", ex.Message),
                    () => Console.WriteLine("OnCompleted")))
            {
                Console.WriteLine("Press ENTER to unsubscribe...");
                Console.ReadLine();
            }
        } 

 

* pdf에서는 GenerateWithTime이라는 메소드를 사용하는데, 이 메소드는 Generate메소드에 통합되었다.

 

Generate는 조건을 주어서 데이터를 생성시키는 메소드로 여기서 조건은 시작값 0에서 부터 5보다 작을 때까지 1씩 증가하며, 결과는 i * i를 한 데이터를 만들어 낸다. 그런데 마지막에 TimeSpan.FromSeconds에 의해서 각 각의 데이터를 만들어 내는 시간이 서로 다르다. 0초, 1초, 2초, 3초, 4초의 간격으로 생성된다

 

.

그런데, 이전 예제에서는 Subscribe가 되면 바로 데이터를 OnNext를 출력하고 모든 데이터를 출력 후에 OnComplete를 호출하고 종료된 후에 Console.WriteLine 문장이 실행이 되는 동기(synchronous) 예제 였다. 그러나 이번 예제는 약간 다른 결과 메시지가 출력되는데, 이는 Subscribe가 비동기(asynchronous)에서 동작하도록 만든 것이기 때문이다.

 

Console.WriteLine("OnNext : {0}", x)

 

이 위치에 커서를 이동 시키고 f9를 눌러서 breake point를 설정한 후 f5키를 눌러서 실행한다.

 

 

결과를 보면 using으로 감싼 블럭 내부에 있는 Console.WriteLine이 먼저 실행이 되고, 브레이크 포인트에 멈추어 진것을 볼 수 있는데, 좀더 자세히 보기 위해 Thread 창을 열어서 보기로 한다.

 

 

Category에 Main Thread가 보이고, 바로 아래 Worker Thread에 깃발이 보이는데, 메인 스레드는 Console.ReadLine()에 멈추어 있고, 워커 스레드가 Console.WriteLine("OnNext : {0}", x) 부분에 멈추어져 있다. pdf에서는 여기서 한발짝 더 들어가서 Location을 확장해서 보고 있는데, 그렇게 하기 위해서는 Debug -> Option and Settings -> Enable My Code의 체크를 풀어주면, 내 코드가 아닌 부분에 대해서도 확인이 가능하다.

 

 

위와 같이 변경 한 후 OK를 눌러서 창을 닫은 후 Location을 확장해서 보면

 

6012 12 Worker Thread Worker Thread RxSample.Console1.exe!RxSample.Console1.Program.Test2.AnonymousMethod__4 Normal
RxSample.Console1.exe!RxSample.Console1.Program.Test2.AnonymousMethod__4(int x) Line 28 
System.Reactive.Core.dll!System.Reactive.AnonymousSafeObserver<int>.OnNext(int value) 
System.Reactive.Linq.dll!System.Reactive.Linq.ObservableImpl.Generate<int,int>.Delta.InvokeRec(System.Reactive.Concurrency.IScheduler self, int state) 
System.Reactive.Core.dll!System.Reactive.Concurrency.DefaultScheduler.Schedule<int>.AnonymousMethod__0(object _) 
System.Reactive.PlatformServices.dll!System.Reactive.Concurrency.ConcurrencyAbstractionLayerImpl.QueueUserWorkItem.AnonymousMethod__0(object _) 
mscorlib.dll!System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(object state) 
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) 
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) 
mscorlib.dll!System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()  
mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() 
mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() 

 

위의 Location 리스트를 보면, OnNext, Generate, Concurrency Schedule, ExecutionContext.Run, ThreadPoolWorkQueue.Dispatch 등이 차례대로 수행되면서, 워크 스레드와 메인 스레드간에 발생할 수 있는 문제들을 자동으로 해결하면서, 비동기 처리를 하게 된다.

 

기초적인 사용 방법에 대해서 살펴 보았다. 포스트에서 다루지 않은 몇가지는 직접 코딩을해서 테스트를 진행하도록 하고, 너무 쉽다고 대충 보고 넘기지 말고, 하나하나를 음미 하면서 익혀 두기 바란다. 다음에는 이벤트를 이용하는 방법에 대해서 알아보도록 하겠다.

 

"엇 벌써 끝내는 거냐?"

 

"ㅇㅇ"

 

"너무 짧은것 같아!"

 

"짧고 굵게!"

 

"짧고 굵게 맞을래?"

 

"그래도 이거 쓴다고 일주일 고생했단 말야~"

 

"그래서?"

 

"다음에는 더 잘 쓴다고.....ㅜㅜ"

 

 

 

 

 

 

 

 

'Windows App(Universal App) > Reactive extensions' 카테고리의 다른 글

ReactiveUI part1  (0) 2014.09.29
Reactive Extension part5  (2) 2014.09.19
Reactive Extension part4  (0) 2014.09.14
Reactive Extension part 3  (2) 2014.09.13
Reactive Extension part2  (0) 2014.08.25
Reactive Extension part1  (0) 2014.08.24
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

Reactive Extension(Rx)! - 포스트에서는 Rx라고 줄여서 이야기한다.

 

이번 포스트에서는 Rx의 정체가 무엇인지, 뭐가 다른 것인지에 대해서 알아 본다.

 

처음 Rx를 알게된 것은 몇 년전 헬렐레 셈을 통해서 배우게 되었는데, 한번 배운 후로 가끔 맛보기만 해봤지, 이걸 본격적으로 사용해야겠구나 하는 생각은 하지 못했다. 왜냐하면, 큰 개념은 이해가 되는데, 새부적으로 들어가면서, 머리에 부하가 걸리면서, 갑자기 코드가 난해해지기 시작하고, 해독 불가 상태에 빠지기 때문이였다.

 

그러다.. 시간이 흐르고 흘러 어느날 갑자기 이넘을 사용해야할 것 같은 불길한 기운이 뒤통수를 치는 느낌을 받았다.

그 불길한 기운의 사연은 이렇게 시작된 것이다. 

만들려고 하는 PCL이 Windows 8.1과 Windows Phone 8.1만을 지원하는 Unversal용 PCL인데, 기존에 내가 사용하고 있던 Prism.PubSubEvent Nuget package가 새로만든 PCL에서는 사용이 불가능하고, Rx Nuget package는 사용이 가능하기 때문에 기존에 만들어 놓았던 부분을 Rx로 바꾸어야 하는 상황이 발생한 것이다...

 

 

"개인 이기주의의 표본 다운 생각이얌! 그럼 이번에도 초보자용은 아닌거냐?"

 

"나도 초보다."

 

"잘됐군. 그럼 나도 읽고 공부를 해야겠네. 자세하게 쓰는거냠?"

 

"아마도.."

 

"아마도?"

 

"나한테도 도움이 될 수 있도록 좀 자세하게 써놔봐~"

 

"그건 모르겠다"

 

"왜???" @,.@a

 

"..."

 

앱 개발 이해도가 바닥을 기는 악마넘이 이해 할 수 있도록 쓰기에는 내용 자체가 난해한 것이 있어서 풀어서 설명하는데도 한계가 있을 것 같은데..저 녀석을 어떻게 이해 시켜야 할지..흠흠..

 

"흠흠..노력하지"

 

"ㅇㅋ ㅎㅎㅎ"

 

주먹은 법보다 가깝다.. 쿨럭,..나의 영혼없는 대답을 듣고, 혼자 느끼한 웃음을 흘리며 눈알을 굴리고 있는 악마넘! 어쩌다가 저넘이랑 엮여서 내 인생의 평탄길이 울퉁불퉁 자갈길로 변했는지...에휴.. 성격 좋은 내가 참고 살아야 하는데도 쉽지 않은 것 같다.

 

 

1. Rx 란?

 

구글에서 rx로 입력하면 Reactive Extensions (Rx) - MSDN - Microsoft라는 곳이 나온다. 이 곳이 Rx홈이다. 이곳으로 이동한다.

 

The Reactive Extensions (Rx)...

http://msdn.microsoft.com/en-us/data/gg577609.aspx

 

맨 처음에 나오는 문장을 살펴 보면..

 

The Reactive Extensions (Rx) is a library for composing asynchronous and event-based programs using observable sequences and LINQ-style query operators. Using Rx, developers represent asynchronous data streams with  Observables, query asynchronous data streams using  LINQ operators, and parameterize the concurrency in the asynchronous data streams using  Schedulers. Simply put, Rx = Observables + LINQ + Schedulers.

 

Rx 비동기와 이벤트 베이스 프로그램에서 사용하는 옵저버블 스퀜스와 LINQ-스타일 쿼리 오퍼레이터의 구성 라이브러리를 이야기한다. ....중간은 대충 패스.. 마지막에 Simply put, Rx = Observables + LINQ + Schedulers라는 내용이 핵심이다.

 

몇일 전에 이 내용을 다시 읽어 보고서야, 이것이 무엇을 의미하는 것인지를 알게되었다. Rx를 파악하기 위해서는..

 

1) Observer Design Pattern

http://msdn.microsoft.com/en-US/library/ee850490.aspx

 

2) LINQ

 

3) Using Schedulers - Thread

http://msdn.microsoft.com/en-us/library/hh242963(v=VS.103).aspx

 

위의 3가지에 대한 어느 정도의 이해가 필요했다라는 것이 나의 결론이다. 그렇다고, 위의 3가지를 모르면 사용하지 못하는가? 꼭 그런 것은 아니다~ 다만 응용을 하기 쉽지 않을 뿐이니 이미 나와있는 내용은 사용이 가능 하리라 생각한다.

 

 

"뭐 나에게는 해당 사항이 없네..으흐흐"

 

"..."

 

"어떻게 생각해?"

 

"닭이 먼저냐 달걀이 먼저나?"

 

"머냐 인간?? 갑자기 뼈다귀 침바르는 소리를 하는 거냐?"

 

"반응은 해야하고, 답은 않나와서 해본 소리다."

 

"............다크 수피어!"

 

내 주위에 생겨난 어둡고 뾰족한 시커먼 창이 날 죽일듯이 날라온다..내가 싫어하는 공격 중에 하나인데..도망이 상책인 듯..칫!! 푸다다다닥!!!!

 

 

2. 백마디 말보다 주먹 한방이 더 직빵이다.

 

Using Reactive Extensions in Silverlight

http://www.silverlightshow.net/items/Using-Reactive-Extensions-in-Silverlight.aspx

 

말로 주저리 주저리 설명해도 이해하는데는 크게 도움이 않될 것이라 생각해서, 일단 간단한 셈플을 하나 만들어 보겠다. 지금 만들 셈플은 위의 포스트에 있는 내용 중에 하나를 응용한 것이다.

 

 

3. RxSample.Universal project 생성

 

포스트에서는 Visual Studio 2013 Update 3버전을 사용하고 있다. 그 이하 버전 특히 Visual Studio 2010 버전으로 테스트를 해 보고 싶은 개발자는 WPF나 Silverlight로 만들어서 테스트를 하면 가능 할 것이다.

 

 

위와 같이 프로젝트를 생성한다.

 

 

4. Windows 8.1 프로젝트 쪽만 작업 한다.

 

4.1 MainPage.xaml

wallpaper031-1920x1200.jpg이미지는 자신이 가지고 있는 적당한 이미지를 사용한다.

 

    <Grid x:Name="grid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Canvas>
            <Image x:Name="ImgImage" Height="200" Width="300" Source="wallpaper031-1920x1200.jpg" Stretch="UniformToFill"/>
        </Canvas>

    </Grid>

 

4.2 MainPage.xaml.cs

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            grid.PointerMoved += grid_PointerMoved;
        }

        void grid_PointerMoved(object sender, PointerRoutedEventArgs e)
        {
            var pos = e.GetCurrentPoint(this);
            Canvas.SetLeft(ImgImage, pos.Position.X);
            Canvas.SetTop(ImgImage, pos.Position.Y);
        }
    }

 

실행해본다.

 

 

아주 간단한 프로그램으로 그리드에 마우스가 움직이면 그 위치로 이미지 컨트롤을 이동시키는 것이다. 잘 동작하는 것을 볼 수 있을 것이다.

이 간단한 것을 Rx를 이용해서 변경해 보자. 처음부터 너무 어려운 것을 변경하면, 자포자기 상태에 빠질 것 같기 때문에 아주 아주 단순 심플한 녀석을 처음으로 선택하게 되었다.

 

 

5. Rx 적용

 

Nuget package에서 rx를 입력해서 검색 후 설치한다.

 

 

 

6. Windows 8.1 프로젝트만 수정한다.

 

6.1 MainPage.xaml은 변경사항이 없다.

6.2 MainPage.xaml.cs

 

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            //grid.PointerMoved += grid_PointerMoved;

 

            IObservable<EventPattern<PointerRoutedEventArgs>> mouseMoveEvent = Observable.FromEventPattern<PointerRoutedEventArgs>(grid, "PointerMoved");
            mouseMoveEvent.Subscribe(evt =>
            {
                var pos = evt.EventArgs.GetCurrentPoint(this);
                Canvas.SetLeft(ImgImage, pos.Position.X);
                Canvas.SetTop(ImgImage, pos.Position.Y);
            });
        }

        void grid_PointerMoved(object sender, PointerRoutedEventArgs e)
        {
            var pos = e.GetCurrentPoint(this);
            Canvas.SetLeft(ImgImage, pos.Position.X);
            Canvas.SetTop(ImgImage, pos.Position.Y);
        }
    }

 

오잉? 이게 무슨 코딩일까?? 생성자에서 별 이상한 짓을 하고 있는데...이 코딩에 대한 설명은 아래에서 설명하도록 하겠다. 실행해보면 아까와 동일한 결과가 나오는 것을 알 수 있다. 화면에 이미지가 잘 돌아 다닌다. 허무한가?? 하하..;;; 하지만, 반전(?)이 있으니 끝까지 가보도록 하자.

 

6.3 MainPage.xaml.cs

 

        public MainPage()
        {
            this.InitializeComponent();

            //grid.PointerMoved += grid_PointerMoved;

            //FromEvent가 있고 FromEventPattern이 있음
            IObservable<EventPattern<PointerRoutedEventArgs>> mouseMoveEvent = Observable.FromEventPattern<PointerRoutedEventArgs>(grid, "PointerMoved");
            IObservable<EventPattern<PointerRoutedEventArgs>> mousePressedEvent = Observable.FromEventPattern<PointerRoutedEventArgs>(grid, "PointerPressed");
            IObservable<EventPattern<PointerRoutedEventArgs>> mouseReleasedEvent = Observable.FromEventPattern<PointerRoutedEventArgs>(grid, "PointerReleased");
            var dragDropEvent = mouseMoveEvent.SkipUntil(mousePressedEvent).TakeUntil(mouseReleasedEvent).Repeat();
            dragDropEvent.Subscribe(evt =>
            {
                var pos = evt.EventArgs.GetCurrentPoint(this);
                Canvas.SetLeft(ImgImage, pos.Position.X);
                Canvas.SetTop(ImgImage, pos.Position.Y);
            });

        }

 

위와 같이 수정 한 후 실행을 해보자! 드래그앤드롭과 동일한 동작을 하는 것을 볼 수 있다. 위의 코딩을 이벤트 핸들러를 사용하는 방법으로 변경한다면 아래와 같이 할 수 있을 것이다.

 

    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            grid.PointerMoved += grid_PointerMoved;
            grid.PointerPressed += grid_PointerPressed;
            grid.PointerReleased += grid_PointerReleased;

 

            //FromEvent가 있고 FromEventPattern이 있음

            ...
        }

 

        private bool _canMove;

        void grid_PointerReleased(object sender, PointerRoutedEventArgs e)
        {
            _canMove = false;
        }

        void grid_PointerPressed(object sender, PointerRoutedEventArgs e)
        {
            _canMove = true;
        }

        void grid_PointerMoved(object sender, PointerRoutedEventArgs e)
        {
            if (_canMove == false) return;
            var pos = e.GetCurrentPoint(this);
            Canvas.SetLeft(ImgImage, pos.Position.X);
            Canvas.SetTop(ImgImage, pos.Position.Y);
        }
    }

 

위에 작성한 방법보다 더 좋은 방법이 있다면, 가차 없이 리플로 남겨주기 바란다.

주석으로 막아 놓은 이벤트 연결부를 풀고, Rx처리부를 주석으로 막은 후 실행 한다. 하지만.. 아직까지도 눈에 보이는 차이점은 발견 할 수 없다.

 

 

7. 기존 코딩으로는 않되고, Rx에는 잘되는 것

아직까지는 눈으로 보이는 부분에서 큰 차이점을 찾기는 어렵다. 그래서, 확실한 차이점을 강제로 만들어 보도록 하겠다.

 

이벤트 처리에 Delay를 넣어 보자

 

        async void grid_PointerMoved(object sender, PointerRoutedEventArgs e)
        {
            if (_canMove == false) return;

            await Task.Delay(1000);
            var pos = e.GetCurrentPoint(this);
            Canvas.SetTop(ImgImage, pos.Position.Y);
            Canvas.SetLeft(ImgImage, pos.Position.X);
        }

 

실행한 후 마우스 클릭 후 마구 마구 돌려보자. 어느 순간 이미지가 마우스를 따라서 움직이지 않는 경우가 발생할 것이다.

 

다음은 Rx 처리 루틴을 수정한다.

 

            dragDropEvent.Subscribe(async evt =>
            {
                await Task.Delay(1000);
                var pos = evt.EventArgs.GetCurrentPoint(this);
                Canvas.SetTop(ImgImage, pos.Position.Y);
                Canvas.SetLeft(ImgImage, pos.Position.X);
            });

 

위와 같이 수정 후 실행해서 마우스를 돌려보자. 물론 이때는 이벤트 연결시키는 코드를 주석으로 막아 놓고 해야한다.

Rx는 마우스의 움직임을 끝까지 추적하는 것을 볼 수 있을 것이다. 하하하 이제서야 설명을 시작 할 수 있을 것 같다.

 

 

"음 이게 반전 코딩?? 내 컴은 슈퍼컴이라 그런지 둘다 잘 되는데??"

 

"내 컴퓨터가 기준이니, 슈퍼컴에서 되는 것은 나의 예상 밖이다."

 

"음..."

 

갑자기 말을 않하고 생각에 빠져있는 저 모습... 웬지 2초 후의 미래가 갑자기 어두워 질 것 같은 불길함이..

 

"인간! 너 컴퓨터도 슈퍼컴으로 바꿔줘??"

 

"정말!?"

 

"뻥인데?? ㅎㅎㅎㅎㅎ"

 

옥상에서 떨어지는 돈 잡을려다가 바닥에 떨어져서 갈비뼈 부러진듯한 배신감이..

 

 

8. Pull-based와 Push-based architectures

 

Curing Your Event Processing Blues with Reactive Extensions (Rx)

http://channel9.msdn.com/Events/TechEd/Europe/2012/DEV413

 

위 예제에서 grid의 PointerMoved, PointerPressed, Pointerreleased 이렇게 3개의 이벤트를 이벤트 핸들러와 연결해서 사용하는데 이러한 방식을 Event-based, 혹은 Event-driven방식이라고 이야기하는데, 이벤트가 발생하고 그 이벤트에 연결된 이벤트 핸들러를 호출해서 사용하는 것을 Pull-based architectures라고 이야기한다.

일반 응용 프로그램의 기본적인 사용방식이며, 웹 프로그램에서도 대부분 많이 사용하고 있는 방식이다.

 

Rx는 Push-based architectures를 사용한다. 평소에 observable 객체가 대기를 하고 있다가, 이벤트나 변경 사항이 발생하면, 자신을 subscriptiion하고 있는 곳으로 push-notification 해준다. 쉽게 스마트 폰에서 카톡 메시지가 도착하면 폰에서 '카톡왔네~'라는 소리가 나는 것을 생각하면 된다.

 

위의 예제에서 grid의 PointerMoved, PointerPressed, Pointerreleased 3개의 이벤트를 감시하는 mouseMoveEvent, mousePressedEvent, mouseReleasedEvent 3개의 observable 객체가 존재하고, 이 객체가 대기를 하고 있다가, grid에서 해당 이벤트가 발생하면, Subscribe으로 지정되에있는 Action을 실행하게 된다. 이러한 일련의 과정을 Event Streams이라 하며 구성은 아래와 같다.

 

8.1 Event Streams구성

 

 

생산자(Producers)는 observable sequences 객체가 되는 것이고, 생산자가 .Net Events, WinRT Events, sensor APIs, APM methods, tasks, etc 등과 연결되어서 변경 사항을 observers에게 push 작업을 실행한다.

소비자(Consumers)는 observers로 Subscribe을 통해 observable과 연결되고, push를 받아서 그에 해당하는 처리를 담당하게 된다.

 

 

9. .Net Event의 한계

 

 

9.1 Can't pass around : 이벤트 자체를 객체로 인식하지 않기 때문에 다른 곳으로 전달할 수 없다.

9.2 Hidden data source : 이벤트에 포함된 데이터를 사용하기 위해서는 이벤트 핸들러를 통해야지만 내용을 알 수 있다.

9.3 Lack of composition : 이벤트 핸들러를 비동기로 실행하면서 랙이 발생한다.

9.4 Hard resource maintenance : 효율적인 메모리 관리를 위해서는 연결한 이벤트 핸들러를 해재해 주어야 하는데, 위의 경우에는 이벤트 핸들러를 해제할 수 없다.

 

 

10. Observable Sequences가 구세주다!

 

 

10.1 Source of Quotes : 이벤트 핸들러를 사용하지 않고도, 데이터의 타입이 Quote라는 것을 알 수 있다.

10.2 Objects can be passed : 이벤트를 IObservable<T> object로 처리하기 때문에 다른 곳으로 보내거나, 저장도 가능하다.

10.3 Can define query operators : 이벤트를 IObservable<T> object로 관리 하기 때문에 LINQ query로 필더링, 데이터 형(type)변환 등의 많은 기능을 추가할 수 있다.

10.4 Easy resource maintenance : Subscribe을 할 때 발생하는 Disposable 객체를 Dispose() 시킴으로서 Unsubscribe을 할 수 있다.

 

 

11. .Net Events와 Observables의 장단점 비교

 

.Net Events 

Observables 

코드 중심 

데이터 중심 

디자인(xaml)에 표현 가능

디자인(xaml)에 표현 불가 

클래스 생성 불가

클래스 생성 가능 

구성방법 변경 불가

다양한 구성방법 변경 가능 

가벼움

약간 무거움 

단단한 실행 모델 

Expression Tree로 번역됨

 

붉은 색 글씨는 불리한 점이고 녹색은 강점임

 

 

12. Rx에 대한 대략적인 사항들만 살펴 보았다.

 

더 자세한 것은 앞으로 part2, part3... 포스트들을 지속적으로 보면 차츰 알 수 있지 않을까 한다. 우선 이 포스트에서는 Rx가 무엇인지 궁금증을 느끼는 정도면 충분하지 않을까 싶다.

 

지금까지의 내용을 한마디로 정의 하면, Rx = Observables + LINQ + Schedulers 이다. Observable를 이용해서 push를 구현하고, 다양한 조건을 주기 위해 LINQ를 사용하며, Scheduler를 이용하여 다이나믹한 실행 환경이 만들어 사용한다. 이러한 내용을 기준으로 위의 소스에서 어떤 차이가 생겨서, 결과가 다르게 나타나는지 생각해보자.

 

 

"생각을 해봤는데.."

 

"ㅇㅇ?"

 

"모르겠는데??"

 

"ㅇㅇ"

 

"뭐가 ㅇㅇ이야! 제대로 설명을 해줘야 할 것 아냐! 너 혹시 너도 모르는 거지??"

 

"..."

 

푸다다닥=3=3=3

 

 

13. 소스 

RxSample.Universal.zip

 

'Windows App(Universal App) > Reactive extensions' 카테고리의 다른 글

ReactiveUI part1  (0) 2014.09.29
Reactive Extension part5  (2) 2014.09.19
Reactive Extension part4  (0) 2014.09.14
Reactive Extension part 3  (2) 2014.09.13
Reactive Extension part2  (0) 2014.08.25
Reactive Extension part1  (0) 2014.08.24
블로그 이미지

kaki104

/// Microsoft MVP - Windows Development - Apr 2014 ~ Mar 2018 /// email : kaki104@daum.net, twitter : @kaki104, facebook : https://www.facebook.com/kaki104 https://www.facebook.com/groups/w10app/

티스토리 툴바