Search code examples
c#wpfreactiveui

ReactiveUI in C# WPF: Bind List of List


I am just starting with ReactiveUI and MVVM in C# - WPF.

I have created a test project whose goal is to represent a chained list of objects. A list of universities each has a list of courses. In the courses, exams are submitted anonymously by students. I started by displaying only the list of universities. This works.

But I can't manage to display the list of courses. I see a ListBox, but the entries are empty. (For the time being, I have omitted the presentation of the exams for the sake of clarity.)

  1. University0
    • Course0
      • Exam0: pending
      • Exam1: finished
    • Course1
      • Exam2: ongoing
      • Exam3: finished
  2. University1
    • Course3
      • Exam4: finished
      • Exam5: finished

I assume that in the UniversityViewModel.cs I have to bind the list of courses somehow, but how?

For starters, I used the example on the ReactiveUI page as a guide: A Compelling Example

AppViewModel.cs

using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;

namespace UniversityViewer
{
    public class AppViewModel : ReactiveObject
    {
        private readonly IDataProviderService _dataProviderService;
        public ReadOnlyObservableCollection<UniversityViewModel> UniversityViewModels { get; }

        private string _searchTerm;
        public string SearchTerm
        {
            get => _searchTerm;
            set => this.RaiseAndSetIfChanged(ref _searchTerm, value);
        }

        public AppViewModel()
        {
            _dataProviderService = new DataProviderService();
            
            Func<University, bool> universityFilter(string text) => university =>
            {
                return
                    string.IsNullOrEmpty(text) ||
                    university.Name.ToLower().Contains(text.ToLower());
            };

            var filterPredicate = this.WhenAnyValue(x => x.SearchTerm)
                                      .Throttle(TimeSpan.FromMilliseconds(250), RxApp.TaskpoolScheduler)
                                      .DistinctUntilChanged()
                                      .Select(universityFilter);

            var dataLoader = _dataProviderService.Universities
                .Connect()
                .Filter(filterPredicate)
                .Transform(university => new UniversityViewModel(university))
                .Sort(SortExpressionComparer<UniversityViewModel>.Ascending(u => u.Name))
                .ObserveOn(RxApp.MainThreadScheduler)
                .Bind(out var bindingData)
                .Subscribe();

            UniversityViewModels = bindingData;
        }
    }
}

University.cs

using System.Collections.Generic;

namespace UniversityViewer
{
    public class University
    {
        public University(string name)
        {
            Name = name;
            Courses = new List<Course>()
            {
                new Course("1234"),
                new Course("2345"),
                new Course("3456")
            };
        }

        public string Name { get; set; }
        public List<Course> Courses { get; set; }
    }
}

UniversityView.xaml

<reactiveui:ReactiveUserControl
  x:Class="UniversityViewer.UniversityView"
  xmlns:universityViewer="clr-namespace:UniversityViewer"
  x:TypeArguments="universityViewer:UniversityViewModel"
  xmlns:reactiveui="http://reactiveui.net"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock TextWrapping="WrapWithOverflow" 
                 Margin="6" VerticalAlignment="Center">
          <Run FontWeight="SemiBold" x:Name="nameRun"/>
        </TextBlock>

        <ListBox x:Name="ListBoxCourses"
                 Grid.Row="1" Margin="5" HorizontalContentAlignment="Stretch"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled" />
    </Grid>
</reactiveui:ReactiveUserControl>

UniversityView.xaml.cs

using ReactiveUI;
using System.Reactive.Disposables;

namespace UniversityViewer
{
    public partial class UniversityView : ReactiveUserControl<UniversityViewModel>
    {
        public UniversityView()
        {
            InitializeComponent();
            this.WhenActivated(disposableRegistration =>
            {
                this.OneWayBind(ViewModel,
                    viewModel => viewModel.Name,
                    view => view.nameRun.Text)
                    .DisposeWith(disposableRegistration);

                this.OneWayBind(ViewModel,
                    viewModel => viewModel.Courses,
                    view => view.ListBoxCourses.ItemsSource)
                    .DisposeWith(disposableRegistration);
            });
        }
    }
}

UniversityViewModel.cs

using ReactiveUI;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace UniversityViewer
{
    public class UniversityViewModel : ReactiveObject
    {
        private University _university;
        public ReadOnlyObservableCollection<CourseViewModel> CourseViewModels { get; }

        public UniversityViewModel(University university)
        {
            _university = university;

            //var dataLoader = _university.Courses
            //    .Connect()
            //    ....

            //CourseViewModels = bindingData;
        }

        public string Name => _university.Name;
        public List<Course> Courses => _university.Courses;
    }
}

Course.cs

namespace UniversityViewer
{
    public class Course
    {
        public Course(string name)
        {
            Name = name;
            //Exams = new List<Exam>();
        }

        public string Name { get; set; }
        //public List<Exam> Exams { get; set; }
    }
}

CourseView.xaml

<reactiveui:ReactiveUserControl
  x:Class="UniversityViewer.CourseView"
  xmlns:universityViewer="clr-namespace:UniversityViewer"
  x:TypeArguments="universityViewer:CourseViewModel"
  xmlns:reactiveui="http://reactiveui.net"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock TextWrapping="WrapWithOverflow" 
                 Margin="6" VerticalAlignment="Center">
          <Run FontWeight="SemiBold" x:Name="nameRun"/>
        </TextBlock>

        <!--<ListBox x:Name="ListBoxExams"
                 Grid.Row="1" Margin="5" HorizontalContentAlignment="Stretch"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled" />-->
    </Grid>
</reactiveui:ReactiveUserControl>

CourseView.xaml.cs

using ReactiveUI;
using System.Reactive.Disposables;

namespace UniversityViewer
{
    public partial class CourseView : ReactiveUserControl<CourseViewModel>
    {
        public CourseView()
        {
            InitializeComponent();
            this.WhenActivated(disposableRegistration =>
            {
                this.OneWayBind(ViewModel,
                    viewModel => viewModel.Name,
                    view => view.nameRun.Text)
                    .DisposeWith(disposableRegistration);
            });
        }
    }
}

CourseViewModel.cs

using ReactiveUI;

namespace UniversityViewer
{
    public class CourseViewModel : ReactiveObject
    {
        private Course _course;

        public CourseViewModel(Course course)
        {
            _course = course;
        }

        public string Name => _course.Name;
    }
}

Solution

  • The UniversityView should bind to CourseViewModels instead of Courses for the CourseView to be resolved:

    this.OneWayBind(ViewModel,
        viewModel => viewModel.CourseViewModels,
        view => view.ListBoxCourses.ItemsSource)
        .DisposeWith(disposableRegistration);
    

    You will then need to populate the CourseViewModels collection.

    The Courses property should be removed from the UniversityViewModel. There is no ReactiveUserControl to be resolved for Course.