Search code examples
c#wpfreactiveui

Binding a collection to a WPF ListBox using ReactiveUI


I'm learning ReactiveUI at the moment, and I'm struggling to figure out how to bind a collection to a ListBox in WPF. I already understand the MVVM pattern, and I was able to do this very thing in MVVM Light some years ago, but ReactiveUI is proving more difficult. I haven't been able to find too many simple examples, just convoluted ones that don't do a great job at demonstrating the basics. Also, a lot of the answers I've found on Stack Overflow are very old, so they don't use the new Dynamic Data libraries that ReactiveUI recommends nowadays.

I'm working in a very simple WPF application while I try to learn the ropes. I have a simple view called ListBoxView:

<rxui:ReactiveUserControl x:Class="MvvmSandbox.Views.ListBoxView"
             x:TypeArguments="vm:ListBoxViewModel"
             xmlns:rxui="http://reactiveui.net"
             xmlns:vm="clr-namespace:MvvmSandbox.ViewModels"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:MvvmSandbox.Views"
             mc:Ignorable="d" 
             d:DesignHeight="250" d:DesignWidth="500">
    <ListBox x:Name="Strings" />
</rxui:ReactiveUserControl>

Here's the code-behind:

using MvvmSandbox.ViewModels;
using ReactiveUI;

namespace MvvmSandbox.Views
{
    public partial class ListBoxView : ReactiveUserControl<ListBoxViewModel>
    {
        public ListBoxView()
        {
            InitializeComponent();
            ViewModel = new ListBoxViewModel();

            this.WhenActivated(disposable =>
            {
                this.OneWayBind(ViewModel, vm => vm.Strings, v => v.Strings.ItemsSource);
            });
        }
    }
}

And here's the view model:

using ReactiveUI;
using System.Collections.ObjectModel;

namespace MvvmSandbox.ViewModels
{
    public class ListBoxViewModel : ReactiveObject
    {
        public ObservableCollection<string> Strings => _strings;
        private ObservableCollection<string> _strings;

        public ListBoxViewModel()
        {
            _strings = new ObservableCollection<string>()
            {
                "This is a test of the Emergency Broadcast System.",
                "This is only a test."
            };
        }
    }
}

The view does come out with the items, but they're collapsed to their minimum height and don't display the strings themselves; you can see the blue bounding box for the first item here.


Solution

  • I'm not the best at ReactiveUI (or even WPF), so I simplified a bit by converting the 'control' to a 'mainwindow'. Your problem seeing the output might just be that you didn't set the collection as the itemsource for the ListBox. I also used an ItemTemplate in my xaml code. Finally, I used the ReactiveUI suggested collection types instead of a just a simple ObservableCollection, and did a bit of fancy timed changes to the list so you can better see how that works.

    see: https://www.reactiveui.net/docs/handbook/collections.html

    MainWindow.xaml:

    <rxui:ReactiveWindow x:Class="MvvmSandbox.MainWindow"
                         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                         xmlns:rxui="http://reactiveui.net"
                         xmlns:vm="clr-namespace:MvvmSandbox.ViewModels"
                         Title="MainWindow"
                         Width="200"
                         Height="250"
                         x:TypeArguments="vm:ListBoxViewModel"
                         mc:Ignorable="d">
      <StackPanel>
        <ListBox x:Name="stringLB" HorizontalAlignment="Center">
          <ListBox.ItemTemplate>
            <DataTemplate>
              <TextBlock Text="{Binding Path=.}" />
            </DataTemplate>
          </ListBox.ItemTemplate>
        </ListBox>
      </StackPanel>
    </rxui:ReactiveWindow>
    

    MainWindow.xaml.cs

    using System.Reactive.Disposables;
    using ReactiveUI;
    using MvvmSandbox.ViewModels;
    
    namespace MvvmSandbox
    {
      public partial class MainWindow : ReactiveWindow<ListBoxViewModel>
      {
        public MainWindow() {
          InitializeComponent();
          ViewModel = new ListBoxViewModel();
    
          this.WhenActivated(d => {
            this.OneWayBind(ViewModel, vm => vm.MyStrings,
              view => view.stringLB.ItemsSource).DisposeWith(d);
          });
        }
      }
    }
    

    ListBoxViewModel.cs

    using System.Collections.ObjectModel;
    using System.Reactive.Linq;
    using DynamicData;
    using ReactiveUI;
    
    namespace MvvmSandbox.ViewModels
    {
      public class ListBoxViewModel : ReactiveObject
      {
        private readonly SourceList<string> _myStrings;
        public readonly ReadOnlyObservableCollection<string> MyStrings;
        public ListBoxViewModel() {
          _myStrings = new SourceList<string>();
    
          _myStrings.Connect()
            .ObserveOn(RxApp.MainThreadScheduler)
            .Bind(out MyStrings)
            .Subscribe();
    
          // initial list
          _myStrings.Add("This is a test");
    
          // add items one sec at a time
          Observable.Interval(TimeSpan.FromSeconds(1))
            .Take(3)
            .Select(i => $"This is test {i+1}")
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(x => _myStrings.Add(x));
    
          // remove an item
          Observable.Interval(TimeSpan.FromSeconds(1))
            .Skip(3)
            .Take(1)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(x => _myStrings.RemoveAt(0));
        }
      }
    }