티스토리 뷰

UWP & Windows App

ReactiveUI part2

kaki104 2014. 9. 30. 22:43
반응형

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
다운로드

 

 

퍽퍽퍽!!!

 

"쿨럭.."

 

"일단 맞고 시작하자"

 

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

 

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

 

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

 

"그때를 기다리지"

 

"두고보자 인간!"

 

 

반응형

'UWP & Windows App' 카테고리의 다른 글

Privacy & Cookies policy  (0) 2014.12.01
Nuget Package error 해결법  (0) 2014.11.18
ReactiveUI part1  (0) 2014.09.29
Reactive Extension part5  (2) 2014.09.19
Reactive Extension part4  (0) 2014.09.14
댓글