ReactiveUI on reaktiivisia laajennuksia (reactive extensions, Rx) käyttävä kirjasto .NET ympäristöjen UI-kehitykseen (Xamarin.iOS, Xamarin.Android, Xamarin.Mac, Xamarin Forms, WPF, Windows Forms, Windows Phone 8, Windows Store ja Universal Windows Platform). Kirjasto tarjoaa reaktiivisen mallin käyttöliittymien ja käyttöliittymämallien (ViewModel) ohjelmointiin. Lisäksi kirjastossa on aputyökaluja, jotta reaktiivinen lähestymistapa toimisi nykyisten UI-kirjastojen kanssa ja käsin kirjoitettava boilerplate-ohjelmakoodi vähenisi. Tässä tutustutaan ReactiveUI-kirjastoon C#- ja WPF-ympäristössä. Taustatiedoiksi riitävät kevyt WPF- ja RX-tuntemus.

ReactiveUI ei juurikaan luo käsitteitä, joita RX:ssä ei olisi. Kirjaston varsinainen vahvuus onkin reaktiivisten laajennusten käsitteiden tuominen UI-ohjelmointiin. Yksinkertaisista perustuksista rakentuu ilmaisuvoimainen kirjasto, jonka muutamia ominaisuuksia käsitellään tässä.

ReactiveObject

ReactiveUI-kirjaston keskeisin luokka on ReactiveObject, joka on tarkoitettu kantaluokaksi kaikille ViewModeleille. Luokan keskeisin hyöty on ominaisuuksien (property) tarjoaminen tarkkailtavina (IObservable<T>). Kun luokan ominaisuudet ovat tarkkailtavia, mahdollistuu ViewModelin toiminnan määrittely deklaratiivisesti käsin nostettavien notifikaatioiden sijaan. Verrattuna Prismin BindableBase-luokkaan tällä säästetään käsityötä, etenkin monimutkaisempien ViewModeleiden tapauksessa.

Koska WPF ei suoraan ymmärrä mitään tarkkailtavista, eivät ViewModelin ominaisuudetkaan voi olla IObservable<T>-tyyppiä. ReactiveUIn ratkaisu tähän on tarjota WhenAny-, WhenAnyValue– ja WhenAnyObservable-metodit, joilla ReactiveObject-olion ominaisuuksia voidaan käsitellä tarkkailtavina. Ominaisuuksien nosto tähän tapaan tarkkailtaviksi, sen sijaan, että ne ne olisivat IObservable<T>-tyyppiä, on harkittu. Tällä tavoin ReactiveUI ViewModelit pysyvät yhteensopivina perinteisen XAML-merkkauksen kanssa, eikä kuvitteellista {ReactiveBinding}-merkkauslaajennusta tarvita.

ReactiveUIn ReactiveObject-luokasta periytyvän ViewModelin rakentaminen eroaa oleellisessi esimerkiksi Prismin BindableBase-luokasta perivän ViewModelin rakentamisesta. Reaktiivista ViewModelia rakentaessa logiikka kirjoitetetaan rakentajaan, kun taas perinteisesti se on hajallaan erillissä metodeissa ja ominaisuuksien settereissä. Tyypillinen reaktiivinen ViewModel määrittelee rakentajan rungossa ominaisuuksien suhteet, komentojen aktivoinnit ja toimintalogiikan.

Esimerkki Prismille hankalasta logiikasta on monimutkainen komennon aktivoimissääntö. Prismin DelegateCommand-luokan kanssa joutuu ensin määrittelemään komennon aktivoimislogiikan ja tämän jälkeen nostamaan RaiseCanExecuteChanged-notifikaatioita jokaisesta paikasta, mistä aktivointisäännön arvo voi muuttua. ReactiveUI ei luonnollisestikaan vapauta aktivoimislogiikan määrittelystä, mutta säännöstä voi tehdä IObservable<bool>-tarkkailtavan jonka voi kiinnittää ReactiveCommand-olion aktivointiin.

ReactiveList

ReactiveUI tarjoaa myös ReactiveList<T>-luokan. ReactiveList on sitä, mitä ObservableCollectionin olisi pitänyt olla. Se toteuttaa INotifyCollectionChanged– ja IEnumerable<T>-rajapinnat, joista ensimmäinen mahdollistaa sen käytön MVVM-mallin mukaisena datalähteenä käyttöliittymän listoille ja jälkimmäinen mahdollistaa LINQ:n käytön, jonka puute on iso rajoite ObservableCollection-luokan käytössä.

Listan toinen ohjelmointia helpottava ominaisuus on listan IObsevable<T> ItemChanged-ominaisuus. Ominaisuudesta saa kaikkien listan alkioiden muutosnotifikaatiot yhdestä tarkkailtavasta. Tämä helpottaa merkittävästi tapausta, jossa lasketaan jotain aggregoitua arvoa kaikista listalla olevista alkioista. Muutosnotifikaatioiden saaminen edellyttää kuitenkin listan ChangeTrackingEnabled-ominaisuuden asettamista todeksi ja vaikuttaa hieman suorityskykyyn. Lapsien tarkkailua tarvittaessa ilmaisuvoimassa saatava hyöty on kuitenkin niin merkittävä, että menetetty suorituskyky ei merkittävä asia.

ReactiveDerivedList-luokka mahdollistaa näkymien luonnin ReactiveList-listan sisältämään dataan. Näkymälistojen avulla on helppo luoda suodattuja versioita samasta datasta, menettämättä kuitenkaan päivitysten yksinkertaisuutta: riittää, että alkion päivittää alkuperäisellä listalla, näkymälistojen tila päivittyy samalla ilman, että ohjelmoijan pitää sitä erikseen komentaa. Vastaavan toiminnallisuuden toteuttaminen ilman ReactiveUI:ta on verboosia ja vaatii CollectionView-luokan käyttöä, mikä on perinteisesti hyvin virheherkkää. Lisäksi CollectionView ja siihen kuuluvat luokat eivät toteuta IEnumerable<T>-rajapintaa, joka tekee niiden käytöstä LINQ:yn tottuneelle vaivalloista.

ReactiveCommand

Esimerkin kannalta oleellinen luokka on myös ReactiveCommand. Koska ReactiveCommand toteuttaa rajapinnan ICommand sitä voi käyttää komentojen sitomiseen käyttöliittymään MVVM-mallin mukaisesti. IObservable<T>-rajapinnan toteutus taas mahdollistaa sen, että komennon aktivointeja voidaan tarkkailla ja koostaa kuten mitä tahansa tarkkailtavaa reaktiivisen mallin mukaisesti.

ReactiveCommand tukee myös asynkronisten komentojen suorittamista. Välitön hyöty tästä saadaan komennon tilan muutoksista. ReactiveCommand osaa automaattisesti asettaa komennon IsEnabled-ominaisuuden asynkronisen kutsun suorituksen yhteydessä, mikä poistaa tyypillisesti vaadittua boilerplate-koodia asynkronisuuden yhteydessä. Alla oleva esimerkki on tosin niin yksinkertainen, että asynkronisia komentoja ei siinä käytetä.

Esimerkki

ReactiveUI:n edut eivät kovin minimalistisessa tapauksessa tule helposti esiin, joten alla oleva esimerkki on pitkähkö. Esimerkki on yksinkertaisesta ja usein toistuvasta OneToMany-tilanteesta käyttöliittymässä; lista ja sen ominaisuuksia hallinnoiva ViewModel ja yksittäisen alkion ViewModel.

Aloitetaan yksittäisestä listan alkiosta.

public class ItemVm : ReactiveObject // 1
{
    public ItemVm(bool active, int value) // 2
    {
        Active = active;
        Value = value;
        Deactivate = ReactiveCommand.Create(this.WhenAnyValue(x => x.Active)); // 3
        Activate = ReactiveCommand
          .Create(this.WhenAnyValue(x => x.Active).Select(b => !b));
        Deactivate.Subscribe(_ => Active = false); // 4
        Activate.Subscribe(_ => Active = true);
        Remove = ReactiveCommand.Create(); // 5
    }

    private bool _active;
    public bool Active // 6
    {
        get { return _active; }
        set { this.RaiseAndSetIfChanged(ref _active, value); }
    }

    public int Value { get; } // 7

    public ReactiveCommand<object> Deactivate { get; }
    public ReactiveCommand<object> Activate { get; }
    public ReactiveCommand<object> Remove { get; }
}

Luokka on itsessään hyvin pieni. Se sisältää vain immutable int-arvon, Active-lipun ja kolme komentoa sen tilan muuttamiseksi tai koko alkion poistamiseksi. Luokasta kannattaa tässä vaiheessa tehdä seuraavat huomiot:

  1. Kantaluokka ei ole Prismin BindableBase, vaan ReactiveUIn ReactiveObject
  2. Logiikka on täysin rakentajassa gettereiden ja settereiden sijaan.
  3. ReactiveCommand:n luonti ottaa IObservable<bool>:n, jonka mukaan komentoa aktivoidaan.
  4. Activate– ja Deactivate-komentojen toteutus on niiden observablejen kuuntelu, jotka yksinkertaisesti muuttavat Active-lipun tilaa
  5. Remove-komento ei tee tämän luokan kontekstissa mitään. Kuten muistamme, ReactiveCommand on itsessään IObservable, joten sen invokaatioita on mahdollista tarkkailla.
  6. Active-propertyn toteutus on ReactiveUI-idiooman mukainen ja täysin boilerplatea. Ero Prismin BindableBase:n boilerplateen on, mutta ei koodin määrässä.
  7. Value on immutable, joten sen kanssa ei tarvita muutosnotifikaatioita
public class MainViewModel : ReactiveObject
{
    private readonly ReactiveList<ItemVm> _items; // 1

    public MainViewModel()
    {
        var rnd = new Random();
        _items = new ReactiveList<ItemVm> { ChangeTrackingEnabled = true };

        // 7
        var reset = this.WhenAnyValue(x => x.SearchString)
            .Throttle(TimeSpan.FromMilliseconds(500))
            .ObserveOnDispatcher();

        // 2
        Active = _items.CreateDerivedCollection(
            i => i,
            i => { },
            ActiveFilter,
            null,
            reset);

        Old = _items.CreateDerivedCollection(
            i => i,
            i => { },
            i => !i.Active);
        // 3
        AddCommand = ReactiveCommand.Create();
        AddCommand.Subscribe(_ => _items.Add(new ItemVm(true, rnd.Next())));
        // 4
        _items.Changed
            .Select(_ => WhenItemWantsToClose())
            .Switch()
            .Subscribe(x => _items.Remove(x));
        // 5
        _activeEven = Active.Changed.Select(_ => Unit.Default)
            .Select(_ => Active.Count(i => i.Value % 2 == 0))
            .ToProperty(this, x => x.ActiveEven, 0);

        // 6
        _visibleCount = this.WhenAnyValue(
                mainvm => mainvm.Active.Count,
                mainvm => mainvm.Old.Count,
                (a, o) => a + o)
            .ToProperty(this, mainvm => mainvm.VisibleCount);

        SearchString = "";
    }

    public IReactiveDerivedList<ItemVm> Active { get; }
    public IReactiveDerivedList<ItemVm> Old { get; }

    public ReactiveCommand<object> AddCommand { get; }

    // 5
    private readonly ObservableAsPropertyHelper<int> _activeEven;
    public int ActiveEven => _activeEven.Value;

    // 6
    private readonly ObservableAsPropertyHelper<int> _visibleCount;
    public int VisibleCount => _visibleCount.Value;

    // 7
    private string _searchString;
    public string SearchString
    {
        get { return _searchString; }
        set { this.RaiseAndSetIfChanged(ref _searchString, value); }
    }

    // 4
    private IObservable<ItemVm> WhenItemWantsToClose()
    {
        return _items
            .Select(i => i.Remove.Select(_ => i))
            .Merge();
    }

    // 8
    private bool ActiveFilter(ItemVm vm)
    {
        return vm.Active && vm.Value.ToString().Contains(SearchString);
    }
}

MainViewModel on sitten hieman mielenkiintoisempi tapaus sekä ReactiveUI:n että toiminnallisuuden kannalta.

  1. _items-lista on IReactiveList<ItemVm>
  2. Itse _items-listaa ei esitetä käyttäliittymässä suoraan. Siitä luodaan kaksi IReactiveDerivedList-instanssia, joista Active on ne alkiot, joiden Active-lippu on true, ja Old ne, joiden ei.
  3. Listalle lisätään uusia elementtejä kuuntelemalla Add-komennon invokaatioita
  4. Kohta ratkaisee perinteisen UI-ohjelmoinnin ongelman: ”UI-elementti alkion poistamiseen on alkion näkymässä, mutta se pitää poistaa listalta jossa alkio on”. ReactiveUI:n ratkaisu tähän ongelmaan perustuu tarkkailtaviin ja on varsin elegantti.
    • MainViewModel kuuntelee ItemVm-olioiden Remove-komentojen kutsuja. Tämä hoidetaan luomalla luomalla kutsuista uusi tarkkailtava aina kun _items-lista muuttuu (WhenItemWantsToClose-metodi). Näin poistologiikka on suoraan oikeassa paikassa ja vältytään tilanteelta, jossa listan alkion tulisi olla tietoinen siitä, että se on ylipäätään listalla.
    • Prismillä ongelman ratkaisu vaatii joko parent-viittauksen välittämistä, Action-parametria tai viestiväylän käyttöä, joista mikään ei ole erityisen elegantti. (Yksi vaihtoehto olisi myös sitoa komento XAML-merkkauksessa eri paikkaan, missä on myös omat ongelmansa).
  5. MainViewModel tarjoaa käyttöliittymälle myös ActiveEven-ominaisuuden, joka kertoo montako parillista lukua on aktiivisten listalla. Tämä on ReactiveUI:n terminologiassa output property, koska sen arvo lasketaan muiden ominaisuuksien arvoista. ReactiveUI mahdollistaa tällaisen määrittelyn ObservableAsPropertyHelper-luokan avulla. Tällä tavalla toteuttuna ominaisuus reagoi täysin automaattisesti Active-listan tilaan ja listalta laskettu koostava ominaisuus on aina ajantasalla. Ominaisuus on luonnollisesti täysin keinotekoinen, mutta toimii esimerkkinä logiikan kirjoittamiseen output-propertyyn.
  6. Näkymälle tarjotaan myös VisibleCount-ominaisuus, joka kertoo listojen näkyvien alkioiden lukumäärän. Ominaisuus toimii esimerkkinä useamman tarkkailtavan yhdistämisestä output-ominaisuudeksi.
  7. Tässä ViewModelissa on myös aktiivisten listan suodatustoiminnallisuus. Tässä kohdassa kuunnellaan käyttöliittymän sidotun stringin arvoa. Koska ihminen on hidas, suodattimen kuuntelussa on puolen sekunnin jarrutus (Throttle). Jarrutus aiheuttaa sen, että määritellyn aikaikkunan ajan hukataan arvoon tulevia muutoksia ja tuloksena olevaan tarkkailtavaan päästetään vain viimeinen aikaikkunassa saapunut arvo. IReactiveDerivedList ei tietenkään voi tietää, että sen sisältöä suodattavan funktion toiminnallisuus on muuttunut, joten viiveen jälkeen ilmoitetaan listalle, että sen tulee piirtää sisältönsä uudelleen.
  8. Koska SearchString suodattaa Active-listan sisältöä, vaikuttaa suodatuksen muuttaminen luonnollisesti myös ActiveEven-propertyn arvoon. Tämä tapahtuu täysin läpinäkyvästi ja ominaisuus saadaan ilmaiseksi ohjelmointimallin ansiosta.

ReactiveUI-kokemuksia

ReactiveUI:n myyntiargumentti on ehdottomasti sama kuin RX:n: ohjelmakoodin kirjoittaminen deklaratiivisesti. Aluksi reaktiivisen koodin kirjoittaminen ja lukeminen tuntuu hankalalta, mutta kun alkukankeudesta on päässyt yli, käsin tehtävät tilapäivitykset alkavat tuntua työläiltä ja mieluummin asiat määrittelee ReactiveUI:lla.

Henkilökohtaisesti voin sanoa, että Prismin ViewModeleihin ei ReactiveUIn oppimisen jälkeen ole paluuta. Prismin ViewModelit ovat usein aluksi yksinkertaisia, mutta hiljalleen logiikan – etenkin notfikaatioiden – hallinta hajautuu ympäri luokkaa ja ylläpito vaikeutuu. Etenkin refaktoroinnissa ReactiveUI on turvallisempi; kun ominaisuuksien suhteet on määritelty yhteen paikkaan, on ne pakko myös korjata sinne ja riski unohtuneesta tai turhasta notifikaatiosta jossain muualla luokassa pienenee.

ReactiveUI:n käyttöönotto on ollut pääosin miellyttävä kokemus. Dokumentaatio on paikoin tosin kovin vajavaista, mutta senkin laatu on kohentunut hiljalleen. Kirjasto on avointa lähdekoodia, joten mystisimpien ongelmien tapaukset voi tarkistaa GitHubista. Kirjaston pääkehittäjä Paul Betts päivystää myös aktiivisesti StackOverflow’ssa ja hyvin usein omaan ongelmaan on jo löytynyt suora vastaus sieltä. Myös käyttäjämäärä tuntuu viime aikoina kasvaneen mukavasti, joten on todennäköistä, että StackOverflow-vastausten määrä ja dokumentaation laatu jatkaa kasvuaan.

Vaikka tässä postauksessa hieman olikin vastakkainasettelua Prismin ja ReactiveUI:n välillä, ReactiveUI ei kuitenkaan ole intrusiivinen kirjasto. Sen käyttö onnistuu vallan mainiosti Prismin, tai minkä muun tahansa MVVM-kirjaston kanssa.

Pelkkä ViewModel ilman käyttöliittymää on tietenkin hieman ohut esimerkki ja ReactiveUI tarjoaa työkaluja myös näkymän rakentamiseen. ReactiveUI-kirjastoa näkymien kannalta käsitellään tulevassa postauksessa.