Notifications for simple queries are not fired

Sep 14, 2009 at 4:09 PM
Edited Sep 14, 2009 at 4:11 PM

Hi!

We've been trying to use the Obtics framework but got stuck with some problems. The problem seems to be quite strange.

I can email the complete 'dummy' project which is buildable and shows the problem very clearly.

It's a simple WPF project, with one window, which does nothing and holds only one listbox. ListBox is only there to check how Obtics works with WPF - and just as declared, it works fine.

ListBox is supposed to show all Orders (exposed by a property, which getter returns an Obtics query) of one Customer created on Window_Loaded. The p roblem is when I try to catch notifications in code myself. Somehow notifications are fired only when I enumerate the property after the subscription.

Here's the code:

 

public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            Loaded += new RoutedEventHandler(Window1_Loaded);
        }

        void Window1_Loaded(object sender, RoutedEventArgs e)
        {
            Customer c = new Customer { CustomerId = 1 };
            //lstOrders.DataContext = c;                    // When uncommented, binding works fine

            ((INotifyCollectionChanged)c.Orders).CollectionChanged += CustomerOrders_CollectionChanged;
            c.Orders.GetEnumerator().MoveNext();            // If you comment this, or move it before the subscription - notifications are not received

            Order.AllOrders.Add(new Order { OrderId = 1, DueDate = DateTime.Now, CustomerId = 1 });
            Order.AllOrders.Add(new Order { OrderId = 2, DueDate = DateTime.Now, CustomerId = 1 });
            Order.AllOrders.Add(new Order { OrderId = 3, DueDate = DateTime.Now, CustomerId = 2 });
            Order.AllOrders.Add(new Order { OrderId = 4, DueDate = DateTime.Now, CustomerId = 1 });
        }

        void CustomerOrders_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            MessageBox.Show("Orders collection modified");
        }
    }
public class Order : INotifyPropertyChanged
    {
        protected int _OrderId;
        public int OrderId
        {
            get { return _OrderId; }
            set
            {
                if (_OrderId != value)
                {
                    _OrderId = value;
                    RaisePropertyChanged("OrderId");
                }
            }
        }

        protected DateTime _DueDate;
        public DateTime DueDate
        {
            get { return _DueDate; }
            set
            {
                if (_DueDate != value)
                {
                    _DueDate = value;
                    RaisePropertyChanged("DueDate");
                }
            }
        }

        protected int _CustomerId;
        public int CustomerId
        {
            get { return _CustomerId; }
            set
            {
                if (_CustomerId != value)
                {
                    _CustomerId = value;
                    RaisePropertyChanged("CustomerId");
                }
            }
        }


        protected static ObservableCollection<Order> _AllOrders = new ObservableCollection<Order>();
        public static ObservableCollection<Order> AllOrders
        {
            get
            {
                return _AllOrders;
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

    }

public class Customer : INotifyPropertyChanged
    {
        protected int _CustomerId;
        public int CustomerId
        {
            get { return _CustomerId; }
            set
            {
                if (_CustomerId != value)
                {
                    _CustomerId = value;
                    RaisePropertyChanged("CustomerId");
                }
            }
        }

        public IEnumerable<Order> Orders
        {
            get 
            {
                return from o in Order.AllOrders
                       where o.CustomerId == this.CustomerId
                       select o;
            }
        }


        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }


    }
So, as a workaround we can create an empty subscription and then enumerate each such property in the constructor. But it definitely seems not pretty.
Any comments?

 

 

Coordinator
Sep 14, 2009 at 10:09 PM

Hi,

This may surprise you but it is actually by design.

The idea is that most of those CollectionChanged events (except the Reset event) are incremental changes on a given base collection. This base collection can be aquired using the enumerator. Without first getting this base collection the incremental change events wouldn't make sense. The Observable linq queries will hold back change events untill at least some client has read (aquired the enumerator) the base collection.

Observable sequences that are (temporarily) not used do not send change events. This may seem silly but leads to some very nice optimizations.

Naturaly this has NOT been documented (I sooo badly need a user guide; just too much to do).

  • Observable Linq queries will not send change events untill some client aquires its enumerator
  • Observable Linq queries will STOP sending change events after sending a RESET event, untill some client aquires its enumerator again
  • Observable Values will not send change events untill some client reads its value and then only one until some client reads it again.

When manually registering for CollectionChanged events make sure that:

  1. you read the contents from the same object instance as the one where your registered for change events.
  2. make sure you unregister from the same object instance when your are done listening and DO unregister.

Obtics will try it's hardest to return the same object instance at each call to 'c.Orders' but it is not a quarantee. Better store an instance in a field and use that.

For those, I suspect rare cases where you want to know when the results of an observable linq query changes without actually being interested in the query contents itself; I'd suggest you create a nice extension method that would return an object with a 'Changed' event and that does all the 'not so pretty' stuff under the hood. That shouldn't be to hard and when you have it, with your permission, I would like to refer to it in the wiki.

Hope this clarifies it a bit and any sugestions are very welcome.

Regs,

Thomas

Sep 15, 2009 at 7:24 AM

Thanks Thomas,

It's clear now why enumerator has to be called, but I still don't understand why I have to subscribe at least once before calling the enumerator. 

Maybe the answer is in your explanation, but apparently i didn't find it :)

Coordinator
Sep 15, 2009 at 12:50 PM

Hi,

That can be explained as follows:

Obtics is a concurrent system and it assumes that when you get the enumerator before you register for change events that you "missed all the events in between" and therefore do not have a valid base collection to apply all subsequent incremental changes to. It can also be interpreted as "You were temporarily not interested in changes to this result". So kicking the observable linq query to send change events is only valid when you have at least one listener attached.

Btw. in the list in my previous posting containing all the 'Will not send' points; It should actually be read as 'May not send'.

Regs,

Thomas.