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ä ObservableCollection
in 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:
- Kantaluokka ei ole Prismin
BindableBase
, vaan ReactiveUInReactiveObject
- Logiikka on täysin rakentajassa gettereiden ja settereiden sijaan.
- ReactiveCommand:n luonti ottaa
IObservable<bool>
:n, jonka mukaan komentoa aktivoidaan. Activate
– jaDeactivate
-komentojen toteutus on niiden observablejen kuuntelu, jotka yksinkertaisesti muuttavat Active-lipun tilaaRemove
-komento ei tee tämän luokan kontekstissa mitään. Kuten muistamme,ReactiveCommand
on itsessäänIObservable
, joten sen invokaatioita on mahdollista tarkkailla.Active
-propertyn toteutus on ReactiveUI-idiooman mukainen ja täysin boilerplatea. Ero PrisminBindableBase
:n boilerplateen on, mutta ei koodin määrässä.- 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.
_items
-lista onIReactiveList<ItemVm>
- Itse
_items
-listaa ei esitetä käyttäliittymässä suoraan. Siitä luodaan kaksiIReactiveDerivedList
-instanssia, joistaActive
on ne alkiot, joiden Active-lippu on true, jaOld
ne, joiden ei. - Listalle lisätään uusia elementtejä kuuntelemalla
Add
-komennon invokaatioita - 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
kuunteleeItemVm
-olioidenRemove
-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).
MainViewModel
tarjoaa käyttöliittymälle myösActiveEven
-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äärittelynObservableAsPropertyHelper
-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.- 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. - 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. - Koska
SearchString
suodattaaActive
-listan sisältöä, vaikuttaa suodatuksen muuttaminen luonnollisesti myösActiveEven
-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.