Sven-Michael Stübe
Sven-Michael Stübe

Reputation: 14750

RxUI fill ObservableCollection async

Is there a build in way to add Items to an ObservableCollection within a ReactiveAsyncCommand or do I have to use the Dispather? What's the RxUI way of handling such situations?

EDIT: The items should be loaded into the collection one by one while LoadData is running.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        MaxRows = 100;
        Translations = new ReactiveCollection<string>();

        LoadDataCommand = new ReactiveAsyncCommand();

        LoadDataCommand.RegisterAsyncAction(_ => LoadData());

        InitializeComponent();

        this.DataContext = this;
    }

    private void LoadData()
    {
        for (int i = 1; i < MaxRows; ++i)
        {
            Thread.Sleep(100);
            // What's the RxUI Way for this?
            this.Dispatcher.Invoke(() => Translations.Add(i.ToString()));
        }
    }

    public int MaxRows { get; set; }
    public ReactiveCollection<string> Translations { get; set; }
    public ReactiveAsyncCommand LoadDataCommand { get; set; }
}

Upvotes: 1

Views: 623

Answers (1)

Ana Betts
Ana Betts

Reputation: 74654

Give this a go:

public MainWindow()
{
    InitializeComponent();
    MaxRows = 100;
    Translations = new ReactiveCollection<string>();

    LoadDataCommand = new ReactiveAsyncCommand();

    LoadDataCommand.RegisterAsyncFunction(_ => LoadData())
        .Subscribe(items => 
        {
            foreach(var i in items) Translations.Add(i);
        })

    LoadDataCommand.ThrownExceptions.Subscribe(ex => 
    {
        Console.WriteLine("Oh crap: {0}", ex);
    })

    InitializeComponent();

    this.DataContext = this;
}

private List<int> LoadData()
{
    // TODO: Return the list of stuff you want to add
    return Enumerable.Range(0, maxRows).ToList();
}

The idea is, LoadData is going to be called on the background thread, you can do whatever you want there, block on network call, read files, etc. However, the Subscribe of RegisterAsync* is guaranteed to run on the UI thread. Convenient!

Note, that I also added a ThrownExceptions Subscribe - this is important or else your RegisterAsync* call stops working if LoadData ever fails.

Edit: Ok, now you're getting a bit fancy. While I debate the utility of a UI that streams in results like that (trying to select stuff in a constantly moving ListBox is frustrating), let's do it anyways - first, change LoadData to stream in results:

private IObservable<int> LoadData()
{
    var ret = new Subject<int>();

    // Run something in the background
    Observable.Start(() => 
    {
        try 
        {
            for(int i=0; i < 100; i++) 
            {
                // TODO: Replace with streaming items
                ret.OnNext(10);
                Thread.Sleep(100);
            }

            ret.OnCompleted();
        }
        catch (Exception ex) 
        {
            ret.OnError(ex);
        }
    }, RxApp.TaskpoolScheduler)

    return ret;
}

Then, all you do is change a few lines on the command:

// Now it's RegisterAsyncObservable instead
LoadDataCommand.RegisterAsyncObservable(_ => LoadData())
    .Subscribe(item => 
    {
        Translations.Add(item);
    })

Note that this isn't very Functional™, but I wanted to give you something to start with that was easy to grok.

Upvotes: 2

Related Questions