Skip to content
Wouterdek edited this page Apr 1, 2019 · 1 revision

Sometimes it is useful to be able to observe changes before they are applied to the collection. For example, when maintaining an invariant preview events can be used to prevent undefined behavior.

A usecase example

Say you are building a issue tracker app for a company. You have an IssueTracker class which contains a SourceList of Issues. Each Issue has a reference to its parent IssueTracker through the property Parent, which is automatically set when it is added to the IssueTracker (eg. using OnItemAdded/OnItemRemoved). The employees assigned to each issue are decided using the tags on the issue and the members of the IssueTracker. For example, if an issue has the "Critical" tag, all manager users in the parent IssueTracker are assigned to the issue. Note that because the list of assigned users is derived from the list of users in the issue tracker, the list becomes empty when the issue is removed from the issue tracker.

Now imagine you want to notify all assigned users when an issue is deleted. One way to write this would be like this:

Issues.Connect().OnItemRemoved(issue => {
   foreach(var user in issue.AssignedUsers.Items){
      user.NotifyIssueDeleted();
   }
});

This code introduces some undefined behavior. Either the previous snippet is executed first and everything works as expected. But if the list of assigned users is updated first, it will be empty and no-one will be notified.

You can fix this by using a preview observable instead. Simply replace Connect with Preview and the code will work as expected.

Issues.Preview().OnItemRemoved(issue => {
   foreach(var user in issue.AssignedUsers.Items){
      user.NotifyIssueDeleted();
   }
});

What is Preview exactly?

When changes are made to caches/lists, the changes are first pushed to the Preview event before they are applied to the collection. This means that all observers of the preview observable will receive notifications before they are pushed through Connect(). It also means that during the execution of Preview observers, the collection is still in its old state. Only after all Preview observers have been notified, the changes are applied and pushed through Connect.

Can I still use the regular operators?

Yes, Preview changes the timing of the events but not the events themselves. You can still filter, sort, ... on the change events as you would normally.

Are there limits on what I can do with Preview?

Modifying a collection while you are processing a change from this collection is not allowed as this would introduce unexpected behavior.

Is there performance overhead? How does this really work?

The preview is implemented as follows: If the preview observable has no subscribers, the collection editing process works as usual. If it has one or more subscribers, then the list/cache is first copied. Then, the change is applied to the main collection. Next, the set of changes is obtained from this modified list/cache. The modified datastructure is then swapped with the copy so that the old state is visible again. The preview event is then run. Finally, the modified datastructure is swapped with the old copy once more so that the new state becomes visible and the Connect change event is triggered. While using Preview does introduce the extra cost of copying, this may be acceptable for collections that are not frequently modified or contain a limited amount of items. Avoiding the copy is non-trivial because of the way collection editing is implemented: changes need to be visible inside the editing function.

A note on recursive Edit() calls

Some update action functions (eg. TreeBuilder) recursively call update methods on collection objects. Allowing recursing edit calls with previews is non-trivial as it introduces performance concerns and ambiguities over the order of preview events, the state of the datastructure at each point and more. DynamicData handles this as follows. If a collection is updated during the invocation of another update on the same collection, then its changes are merged with the main update. The change is immediately visible inside the update action as is expected, but the preview/change event are not triggered until when the topmost update function exits. This ensures that the collection state is consistent in the update function. It also improves performance by having each nested update operate on the same collection instance (instead of creating an new copy each time) and merging the different changesets into one notification. Finally, it also ensures that both Preview() and Connect() receive the same changesets, in the same order.