AvaloniaUI x ReactiveUI 사용 후기

Xamarin, MAUI는 아니지만 크로스 플랫폼이라 여기에 공유합니다.

사내 프로그램이라 이미지 및 소스를 공개하지 못하는 점 양해 부탁드립니다.


사내 CCU(Concurrent connected User) 모니터링 프로그램을 AvaloniaUI 0.10.18 버전과 Community Toolkit 8 버전으로 작업을 했었습니다.

AvaloniaUI(이하 Avalonia)를 0.10.18 버전을 선택했던 이유는, 현재 Avalonia 11.0.0-Preview 버전에서 Community Toolkit 8이 빌드 에러를 냈기 때문입니다.

이번에 Avalonia를 11 preview2 버전으로 올리면서 Reactive UI도 함께 같은 버전으로 적용해봤습니다.

Reactive UI를 공부했던 경험을 메모 했었는데 원본을 공유드립니다.

Avalonia UI RxUI 메모

  1. RxUI 문서에 Command를 바인딩할 때 Code-Behind를 사용하도록 되어있지만, XAML을 통해 바인딩할 수 있다.

  2. WhenActivated는 View마다 해줘야하는 것 같다? (확실하지 않음)
    View의 Code-Behind에서 바인딩하지 않고, XAML에서 바인딩할 경우엔 필요 없는 것 같다.

  3. ViewModel의 활성화 시점이 어떤 때인지는 모르겠으나, ViewModel에서 활성화가 될 때 이벤트 캐치하려면 IActivatableViewModel 인터페이스를 구현해줘야한다.
    해당 인터페이스를 ViewModel에 구현하면 WhenActivated를 ViewModel에서도 사용할 수 있는데,
    이 코드 블럭 안의 코드는 View 쪽에서 WhenActivated를 호출할 때 같이 호출된다.

  4. RxUI에선 ReactiveWindow와 Window를, ReactiveUserControl와 UserControl 을 혼용해서 사용할 수 있다. 하지만 그냥 ReactiveWindow, ReactiveUserControl을 사용하는 것으로 통일하자.

  5. 4번 때문에 RxUI에서는 View와 ViewModel을 서로 의존시킬 수 밖에 없는 것 같다.

  6. 의존성 등록을 할 때는 Splat을 사용하는데, 문서보고 잘 따라하면 될 것 같다.
    아래 문서에 적혀있듯, 서비스를 등록할때는 CurrentMutable을 사용하고 객체를 꺼내올 때는 Current를 이용하면 될 것 같다.
    reactiveui - Splat: Locator.Current vs Locator.CurrentMutable - Stack Overflow
    나처럼 Generic Host를 이용할 경우 아래 문서대로 한다.
    splat/README.md at main · reactiveui/splat · GitHub

  7. ViewModel을 Splat에 등록할 때는 IViewFor 인터페이스를 이용해서 해당 뷰모델이 어떤 뷰의 뷰모델인지 간접적으로 지정한다.
    IViewFor는 반드시 할 필요는 없고, ViewModel에 대해서 View가 여러개가 있을 때 Locator로 매칭하지 못하는 경우, Custom하게 View에 대한 ViewModel을 지정하는 방법이다.
    따라서 일반적으로는 필요없고 Naming 규칙을 잘 따른다면 쓰지 않아도 된다.
    특정 View를 확장하여 그 확장된 view의 ViewModel을 지정하는 경우에도 사용된다.

  8. Converter를 구현할 때도 IValueConverter 인터페이스가 아니라 IBindingTypeConverter를 구현해야 한다. Converter 예시
    사실 IValueConverter로 구현해서 XAML의 바인딩이 가능해 보이지만,
    RxUI의 Code-Behind Binding을 하면서 Converter를 적용하려면 IBindingTypeConverter를 이용해야하는 것 같다.
    그리고 Locator.CurrentMutable.RegisterConstant(new MyCoolTypeConverter(), typeof(IBindingTypeConverter)); 이렇게하면 Splat에 등록해서 IoC 방식으로도 이용가능하다.
    하지만 역시, 일반적인 IValueConverter를 이용해서도 프로그램이 동작하는데 오동작은 없다.

  9. 데이터 영속성은 일종의 캐싱기능이다. App을 껐다가 켜도 데이터가 유지되는 것이라고 보면 된다. (원리는 잘 모르겠음)
    DataContractAttribute와 DataMemberAttribute를 이용해서 저장한 값을 지정한다.

  10. WhenActivated 문서를 보면 WhenActivated메서드의 Action 메서드의 파라미터인 disposables을 이용하여 View가 제거될때 View와 연결된 ViewModel의 관찰가능 Property와 관찰가능 Command를 제거할 수 있다.

  11. Interaction 클래스는 input viewmodel과 output viewmodel을 제네릭으로 지정할 수 있다.
    이것은 대화창 같은 UI와 상호작용을 할 때 쓰이는 것으로 다른 프레임워크들에선 DialogService를 구현해서 호출하는데, rxui에서는 ViewModel간 관계를 맺어주고
    Interaction객체에서 Handle 메서드를 호출하면서 input viewmodel을 넣어서 호출 방향으로 하고있다.
    Handle 메서드를 호출하면 Code-Behind에서 구독한 메서드가 호출된다.
    위의 방법은 Avalonia.MusicStore 샘플 앱의 MainWindowViewModel의 BuyMusicCommand에서 ShowDialog.Handle을 호출했을 때,
    다음 줄로 넘어가기 전 Handle 메서드로 호출되는 것이 무엇인지 파악할 수 있다.
    MainWindow.axaml.cs의 DoShowDialogAsync 메서드를 호출하게되는데 이는 MainWindow의 생성자에서 ViewModel의 Interaction 객체인 ShowDialog의 구독메서드로
    DoShowDialogAsync를 등록해놨기때문이다.
    클래스간의 관계가 코드로 명시되어있어서 직관적이다.

  12. 반복된 작업이나 무거운 작업의 경우 스케쥴링을 이용하면 되며 아래 글에 잘 나타나 있다.
    ReactiveUI - Scheduling
    아래 글을 보면 Invoke로 처리하는 방식을 RxUI 방식으로 교체할 수 있다.
    ReactiveUI - Scheduling

  13. 반복된 값이 확인될 경우 중복 제거는 DistinctUntilChanged 메서드를 통해 해결할 수 있다.

  14. Subscribe 메서드를 이용하면 해당 프로퍼티에 어떤 작업이 발생할 때마다 구독에 등록한 Action 메서드가 발생하는 것인데, ToProperty를 사용하면
    데이터를 특정 프로퍼티로 쏠 수도 있다.

  15. WhenAny 메서드는 특정 속성같은 것들이 변경되었을 때 그것이 UI에 노티 되는 것과 별개로 매번 다른 작업을 하고 싶을 때 사용하는 것이다.
    예를 들어 WhenAny가 없는 다른 프레임워크들은 이벤트 어그리게이터 기능을 통해 메세지로 특정 속성을 구독하고 메세지를 던지면
    그 메세지를 처리하는 곳에서 이것저것 작업을 해야하는 부분인데, RxUI에서는 WhenAny에서 메신저 대신에 작업을 구현할 수 있다.
    물론 RxUI에도 메신저 기능이 존재하긴하지만 WhenAny 기능이 더욱 선호되는 기능이다.
    ReactiveUI - When Any
    또한 아래의 방법을 이용하면 메세징을 피할 수 있다.
    ReactiveUI - Message Bus


  • 현재 실시간으로 CCU 데이터를 가져오기 때문에 데이터 영속성이 따로 필요없다. 필요하다면 프로그램 화면을 중앙에 띄우는 옵션정도 나중에 생각해보면 될 것 같다.

  • Avalonia MVVM Template으로 만들어서 최신 버전인 Avalonia UI 11 Preview 2 버전으로 진행할 때 Nuget에서 Avalonia.Themes.Fluent 의 추가 다운로드가 필요하다. 다운로드 하지 않으면 빌드에러가 발생한다. Template에 안 들어 있어서 에러가 발생하며 추후 개선될 것 같다.

  • 현재 ReactiveUI의 ViewLocator 방식이 아닌 AvaloniaUI의 ViewLocator로 View를 불러오고 있는데 나중에 IRoutingViewModel을 사용한 ReactiveUI의 Routing 방식으로 변경하면 좋을 것 같다.
    Advanced Avalonia+ReactiveUI+Routing example · Issue #3605 · AvaloniaUI/Avalonia · GitHub
    물론 AvaloniaUI의 MVVM Template의 기본 ViewLocator로도 잘 동작한다.

  • ReactiveUI의 공식 문서에도 나와있듯, MessageBus는 최후의 수단으로 사용하라고 한다.
    실제로 내가 해보니까 가비지가 엄청나게 나오면서 프로그램이 느려지는 경향이 있다.
    커뮤니티 툴킷으로 했을 때는 그러지는 않았었는데, ReactiveUI가 자동화하고, 추상화를 많이 해놓은 것 때문에 중간 임시객체가 많이 생성되어 Messaging이 방해받는 느낌이다.
    따라서 개발 매커니즘을 RxUI 방식에 맞게 바꿔야한다.

While this class is provided because it is sometimes necessary, the MessageBus should be used only as a last resort . The MessageBus is effectively a global variable , which means it is subject to memory and event leaks, and furthermore, the detached nature of MessageBus means that it’s a goto whose destination is invisible. It also encourages bad design as many people will directly proxy View events to the ViewModel layer, which makes them not particularly ViewModelly.

  • 링크를 보면 Program.cs에 있는 UseReactiveUI() 메서드를 쓰면 RxApp.MainThreadScheduler를 AvaloniaScheduler.Instance로 초기화 한다고 한다.
    실제로 UseReactiveUI 메서드를 디컴파일해보면 그런 코드를 볼 수 있다.
    그런데 이해가 안가는 것은 아래 코드를 디버깅하는 시점에 RxApp.MainThreadScheduler를 확인하면 System.Reactive.Concurrency.DefaultScheduler 가 들어있다…
    따라서 RxApp.MainThreadScheduler를 쓰는 곳에는 아직은 AvaloniaScheduler.Instance로 해야할 것 같다.

  • Quartz도 Splat Generic Host와 함께 사용했는데, 이유를 모르겠지만 전혀 동작하지 않았다.


그리고 RxUI는 아니고 Avalonia 문제인듯 한데, 마지막에 메모한 것이 완전히 드러난 문제는 아닙니다.
여전히 비동기로 데이터를 자주 업데이트 해주면 문제가 발생합니다.
저의 모니터링 프로그램에는 보는 방법의 메뉴가 여러개 있어서 그 버튼들을 막 클릭하면 보는 방식이 마구 바뀌는데,
UI에서 업데이트할 것이 많으면 오류가 종종 납니다. (가만히 켜두면 문제 안남)
하지만 이 부분은 개선될 것이라고 믿고 진행해야할 것 같습니다. (물론 Toolkit을 11버전에도 빌드 에러 없이 지원해준다면 RxUI를 안써도 되기 때문에 문제가 되는 부분은 아닐 것 같습니다.)

좋아요 5