BerndGit
BerndGit

Reputation: 1660

ReactiveUI: IObservable.Transform() does not forward Notifications

In the sniplet below, it seems like transform(x => x.Bar.Baz) only forwards changes if items in sl2 are added or removed, but not if th value of Baz is changed. Is this expected behaviour? How to change it, that all modifications are found in transformed?

SourceList<FooClass> sl2 = new SourceList<FooClass>();    
sl2.Connect()
        .Transform(x => x.Bar.Baz)
        .Bind(out ReadOnlyObservableCollection<String> transformed)
        .Subscribe( x=> { Console.WriteLine("CHANGE from Subscribe"); } );

The full code reads as. Running example also available on GitHub

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;

namespace ReactivUI_Test
{
    class BarClass : ReactiveObject
    {
        [Reactive] public String Baz { get; set; } = "";

        public BarClass(String b) { Baz = b; }
        public BarClass() { Baz = "!!!"; }

        public override String ToString() { return Baz.ToString(); }
    }



    class FooClass : ReactiveObject
    {
        [Reactive] public BarClass Bar { get; set; } = new BarClass();

        public override String ToString() { return Bar.ToString(); }
    }


    class ViewModel: ReactiveObject
    {
        [Reactive] FooClass Foo { get; set; } = new FooClass();

        void PrintList<T> (IEnumerable<T> items)
        {
            foreach (T item in items)
            {
                Console.WriteLine(item.ToString());
            }
        }

        public ViewModel()
        {


            Console.WriteLine("===  ===");

            SourceList<FooClass> sl2 = new SourceList<FooClass>();

            FooClass fo1 = new FooClass() { Bar = new BarClass("Hello ") };
            FooClass fo2 = new FooClass() { Bar = new BarClass("World ") };
            FooClass fo3 = new FooClass() { Bar = new BarClass("Out ") };
            FooClass fo4 = new FooClass() { Bar = new BarClass("There ") };
            FooClass fo5 = new FooClass() { Bar = new BarClass("!!!!!!") };

            sl2.Add(fo1);
            sl2.Add(fo2);
            sl2.Add(fo3);
            sl2.Add(fo4);


            sl2.Connect()
                .Transform(x => x.Bar.Baz)
                .Bind(out ReadOnlyObservableCollection<String> transformed)
                .Subscribe( x=> { Console.WriteLine("CHANGE from Subscribe"); } );

            Console.WriteLine("=== Start ===");

            ((INotifyCollectionChanged)transformed).CollectionChanged += 
                new NotifyCollectionChangedEventHandler(( s,e) => Console.WriteLine("CHANGE from Event Handler"));


            Console.WriteLine("sl2: ");
            PrintList<FooClass>(sl2.Items);

            Console.WriteLine("transformed: ");
            PrintList<String>(transformed);


            Console.WriteLine("=== Send to Space ===");
            fo2.Bar.Baz = "Space";

            Console.WriteLine("sl2: ");
            PrintList<FooClass>(sl2.Items);

            Console.WriteLine("transformed: ");
            PrintList<String>(transformed);      

            Console.WriteLine("=== Add !!!! ===" );

            sl2.Add(fo5);

            Console.WriteLine("sl2: ");
            PrintList<FooClass>(sl2.Items);

            Console.WriteLine("transformed: ");
            PrintList<String>(transformed);

            Console.WriteLine("===  ===");

            Console.WriteLine("Finish");
            Console.ReadLine();

        }
    }


    class Program 
    {
        static void Main(string[] args)
        {
            ViewModel vm = new ViewModel();
        }
    }
}

The output is not what I have hoped for. It seems that .Transform(x => x.Bar.Baz) only forwards updates items are added or removed from the list 'sl2'. If there are value changes of Bar.Baz, these changes are not forwaded to transformed.

My Output is:

===  ===
CHANGE from Subscribe
=== Start ===
sl2:
Hello
World
Out
There
transformed:
Hello
World
Out
There
=== Send to Space ===
sl2:
Hello
Space
Out
There
transformed:
Hello
World      <--- I would have hoped to have 'Space' here
Out
There
=== Add !!!! ===
CHANGE from Event Handler   <-- I would have hoped that event handlers are triggered at World->Space
CHANGE from Subscribe       <-- I would have hoped that event handlers are triggered at World->Space
sl2:
Hello
Space
Out
There
!!!!!!
transformed:
Hello
World    <--- I would have hoped to have 'Space' here
Out
There
!!!!!!
===  ===
Finish

Note that: (1) in transformed the change of World->Space is never done. In sl2 this change is done.

(2) The CollectionChangedEvent is only triggered after adding of in Item added.

My question:

How to ensure that transform() sets the correct notifications?

Update

Based on the of Funk answer below, this should be fixed by:

sl2.Connect()
   .AutoRefresh(x => x.Bar.Baz)
   .Transform(x => x.Bar.Baz)
   ...

However this does not change the output.

Upvotes: 3

Views: 413

Answers (1)

Funk
Funk

Reputation: 11201

Is this expected behaviour?

It is. In line with ObservableCollection<T> only changes to the collection (or stream) are monitored.

Of course, what you want is quite common and, luckily, easy to achieve. From the ReactiveUI documentation:

DynamicData supports change tracking for classes that implement the INotifyPropertyChanged interface — ReactiveObjects. For example, if you'd like to do a WhenAnyValue on each element in a collection of changing objects, use the AutoRefresh() DynamicData operator

Simply add the AutoRefresh operator to the pipeline and use the Transform operator overload that accepts a flag to set transformOnRefresh to true.

sl2.Connect()
   .AutoRefresh(x => x.Bar.Baz)
   .Transform(x => x.Bar.Baz, true)
   ...

Note to aid in debugging, you can decompose the IChangeSet<string> and its Change<string>s.

sl2.Connect()
   .AutoRefresh(x => x.Bar.Baz)
   .Transform(x => x.Bar.Baz, true)
   .Do(x =>
   {
       foreach (var c in x) Console.WriteLine($"sl2 ticks {c.Item.Current}");
   })
   ...

Upvotes: 2

Related Questions