Search code examples
c#mvvmreactivereactiveuidynamic-data

How to sum a property from each element in an ObservableCollection Based on a filter property on each element?


I'm working on a system that has a list of Records displayed in a list.

Each item has the following properties:

  • Name
  • Id
  • Size
  • A Boolean indicating if this item should be included or not - ShouldBeIncluded

A user can check a checkbox to change the boolean of the item from false to true. etc.

Based on a user changing various item's checkboxes I need to keep a "TotalSize" property up to date so that it reflects the sum of all item's size property in the list where the item's "ShouldBeIncluded" property is true.

I'm trying to do this within a UI based app running ReactiveUI/Dynamic Data.

I use an MVVM approach in everything else I do. So I'm not looking for tieing this code directly to my view layer.

For the purposes of this question I've minimised this all down to a simple reproduction though.

I think I need to Observe property changes on the ObservableCollection but I cannot figure out how to then bind this back to the ReactiveUI property.

  • I've tried various DynamicData methods, including the ones I've commented out.
  • I've tried ObservableAsAProperty helpers
  • I've tried doing this manually

But I want to understand the most Idiomatic way to do this, but everything I'm trying to do seems to overcomplicate it.

Here's some boilerplate code that shows where I'm at:

using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System.Collections.ObjectModel;
using System.Diagnostics;

namespace ConsoleApp1;
/** Simplified for this question, but let's say I'm building a UI with this layout
 * | Name    | Should be Included| Size |
 * | Record 1| [ ]               |5     |
 * | Record 2| [ ]               |5     |
 * ... etc.
 * 
 * Total: the sum of items' size where their "Should be included" checkbox is true
 */


// For some boilerplate code here we go

// Our "Record" which has the properties from the table above
public class Record : ReactiveObject
{
    public string Id { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;

    [Reactive]
    public int Size { get; set; } = 0;

    [Reactive]
    public bool ShouldBeIncluded { get; set; } = false;

    public Record(string id, string name, int size)
    {
        Id = id;
        Name = name;
        Size = size;
    }
}

// A "View Model" that can be used to display them
public class Test
{
    public ObservableCollection<Record> records;

    // How do I keep Total up to date with the correct value, in this case 15?
    public int Total { get; set; }

    public Test()
    {
        records = new ObservableCollection<Record>();

        //records.ToObservableChangeSet(x => x.Id).Filter(x=> x.ShouldBeIncluded). What's next? Is this right?;
    }

}
public class Program
{
    public static void Main(string[] args)
    {
        // Create a new test object
        var t = new Test();

        // Add 10 records to it, each with a size of 5
        for (var i = 0; i < 10; i++)
        {
            t.records.Add(new Record(i.ToString(), $"Record {i}", 5));
        }

        // Mark 3 records as needing to be "Included"
        t.records[0].ShouldBeIncluded = true;
        t.records[3].ShouldBeIncluded = true;
        t.records[5].ShouldBeIncluded = true;

        Debug.Assert(t.Total == 15);
    }
}

You can also find a minimal reproduction Avalonia app on GH: https://github.com/ProbablePrime/Repro-ReactiveUI-MVVM-Sum-Filter


Solution

  • I asked this same question over on the Avalonia discussions board.

    A user named: alxgenov

    Provided a DynamicData based answer that works perfectly!

    private ObservableAsPropertyHelper<int> total;
    public int Total => total.Value;
    
    // In constructor
    total = Records
        .ToObservableChangeSet(x => x.Id) 
        .AutoRefresh(x => x.ShouldBeIncluded)
        .Filter(x => x.ShouldBeIncluded)
        .Sum(x => x.Size)
        .ToProperty(this, x => x.Total);
    
    

    This will keep Total up to date as an ObservableProperty that Avalonia can bind to.

    Thank you all for your help!

    You can see this implemented in the example app here: https://github.com/ProbablePrime/Repro-ReactiveUI-MVVM-Sum-Filter/commit/55d1042e52a5da6f773e0dc37f099226e27b9829