ReactiveUI - Model object with many related properties
Asked Answered
P

2

7

I have a WPF MVVM application. My model objects are POCOs that I have full control over. Some properties in these objects have a relationship between them.

For example: let's say that I have

public class Model
{
    public ObservableCollection<double> XCoordinates { get; set; } // Cumulative sum of DistancesBetweenXCoordinates.
    public ObservableCollection<double> DistancesBetweenXCoordinates { get; set; } // {x[1]-x[0], x[2]-x[1], ..., x[n]-x[n-1]}
    public double TotalLength { get; set; } // Sum of DistancesBetweenXCoordinates.
}

I'd like the enduser to be able to edit either the list of distances, or the list of x-coordinates, and the other properties should automatically update. So far I've done this using INPC events, but this quickly gets too messy. Some UI updates are done after each PropertyChange, so I want to optimize this.

Since some property updates in my real app doesn't have negligible performance impact, I don't want to use "calculated properties" such as

public double TotalLength => DistancesBetweenXCoordinates.Sum();

From what I've read, the ReactiveUI framework seems to have the capabilities I'm looking for. I have some questions though:

1) I'm currently using another framework (Catel) that I rather not abandon completely. Can I use the .WhenAny() etc. from ReactiveUI without inheriting from ReactiveObject (for instance just by implementing IReactiveObject)?

2) Almost all examples I've seen inherits from ReactiveObject in the ViewModel. Is this the preferred way to achieve what I want? Would it not make more sense to implement this inside my model? If my Model should just be a "dumb" POCO without any mechanism to keep all related properties up-to-date, then is this the responsibility of my Reactive VM?

I would be really grateful for a simple example or some other guidance.

Procto answered 31/8, 2017 at 22:14 Comment(0)
P
14

tl;dr

  • Implement INotifyPropertyChanged and you can use .WhenAny()
  • ReactiveObject can be used for both ViewModel and Model classes. It is literally just a "reactive" object. i.e. like System.Object. (Don't believe me? Check out the source.)
  • Use ObservableAsPropertyHelper for calculated properties that are dependent on other reactive properties or events. (See the docs)

The Answer

There are a couple of ways you could handle this, but in particular it seems like you want to reason out a couple things:

  1. How can I take advantage of reactive capabilities without migrating from another framework that I'm using?
  2. Should ReactiveObject just be limited to ViewModels? Is it useful in normal Model classes?

First as you've already noticed, a lot of the draw of ReactiveUI comes with the powerful capabilities contained in .WhenAny(). Can you use these methods between frameworks, yes! The .WhenAny(), .WhenAnyValue(), .WhenAnyObservable() methods work on any object that implements INotifyPropertyChanged. (Related Docs)

In fact, it's likely that your existing framework already implements INotifyPropertyChanged on many of their own types, so .WhenAny() naturally extends to work on those objects seamlessly. You almost never actually need a ReactiveObject. It just makes your life easier.

Note: This is actually one of the core values of ReactiveUI. That at the core, ReactiveUI is really just a bunch of extension methods designed to make working with observables easier in the existing .Net world. This makes interoperability with existing code one of ReactiveUI's most compelling features.

Now, should ReactiveObject be used for normal "dumb" models? I guess it depends on where you want the responsibilities to lie. If you want your model class to only contain normalized state and no logic at all, then probably not. But if your model is designed to handle both state and domain-related logic, then why not?

Note: There's a larger philosophical debate here about the single responsibility principal, software architecture, and MVVM, but that's probably for Programmers SE.

In this instance, we care about notifying listeners about updates to some calculated properties such as TotalLength. (i.e. our model contains some logic) Does ReactiveObject help us do this? I think so.

In your scenario, we want TotalLength to be computed from DistancesBetweenXCoordinates whenever an element is added or changed or something. We can use a combination of ReactiveObject and ObservableAsPropertyHelper. (Related Docs)

For example:

class Model : ReactiveObject
{
    // Other Properties Here...

    // ObservableAsPropertyHelper makes it easy to map
    // an observable sequence to a normal property.
    public double TotalLength => totalLength.Value;
    readonly ObservableAsPropertyHelper<double> totalLength;
    public Model()
    {
        // Create an observable that resolves whenever a distance changes or
        // gets added. 
        // You would probably need CreateObservable()
        // to use Observable.FromEventPattern to convert the
        // CollectionChanged event to an observable.
        var distanceChanged = CreateObservable();

        // Every time that a distance is changed:
        totalLength = distanceChanged

            // 1. Recalculate the new length.
            .Select(distances => CalculateTotalLength(distances))

            // 2. Save it to the totalLength property helper.
            // 3. Send a PropertyChanged event for the TotalLength property.
            .ToProperty(this, x => x.TotalLength);
    }
}

In the above, TotalLength would be recalculated every time that the distanceChanged observable resolves. This could for example be whenever DistanceBetweenXCoordinates emits a CollectionChanged event. In addition, because this is just a normal observable you could have the calculation occur on a background thread, allowing you to keep the UI responsive during a long operation. Once the calculation is done, the PropertyChanged event is sent for TotalLength.

Pucida answered 1/9, 2017 at 0:57 Comment(0)
L
0

You can also use ReactiveList instead ObservableCollection, for example:

public class Model : ReactiveObject
{
    private ReactiveList<double> distancesBetweenXCoordinates;
    private readonly ObservableAsPropertyHelper<double> totalLength;

    public Model()
    {
        // ChangeTrackingEnabled allow to raise changes notifications when individual values changes
        DistancesBetweenXCoordinates  = new ReactiveList<double> { ChangeTrackingEnabled = true };

        this.WhenAnyValue(x => x.DistancesBetweenXCoordinates, x => x.Sum())
            .ToProperty(this, x => x.TotalLength, out totalLength);
    }

    public ReactiveList<double> DistancesBetweenXCoordinates 
    { 
        get => distancesBetweenXCoordinates; 
        set => this.RaiseAndSetIfChanged(ref distancesBetweenXCoordinates, value); 
    } 
    public double TotalLength { get => totalLength.Value; } 
}

This way, changes to individual elements of DistancesBetweenXCoordinates will change the TotalLengthvalue.

Lemaceon answered 1/11, 2017 at 23:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.