Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/csharp/296.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 如何使用反应式扩展来缓存、限制和中继多个事件?_C#_Events_Caching_System.reactive_Reactive Extensions Js - Fatal编程技术网

C# 如何使用反应式扩展来缓存、限制和中继多个事件?

C# 如何使用反应式扩展来缓存、限制和中继多个事件?,c#,events,caching,system.reactive,reactive-extensions-js,C#,Events,Caching,System.reactive,Reactive Extensions Js,我正在尝试学习新的.Net反应式扩展框架,它听起来是我的应用程序的完美解决方案。在研究了一些示例之后(并且对于LINQ仍然相当薄弱),我正在努力找出如何利用RX框架来完成下面的任务 目标是在自定义数据源和GUI之间创建一个可配置的事件“中继”。中继将使用LINQ测试和过滤传入事件,在等待下一个时间间隔时将符合条件的事件缓存在列表中,然后同步到GUI线程,并按照接收到的顺序播放事件 如何使用RX协调缓存、筛选和中继多个事件,例如用于添加、更改和删除自定义数据源中的值的事件 这可能会要求很多,但对于

我正在尝试学习新的.Net反应式扩展框架,它听起来是我的应用程序的完美解决方案。在研究了一些示例之后(并且对于LINQ仍然相当薄弱),我正在努力找出如何利用RX框架来完成下面的任务

目标是在自定义数据源和GUI之间创建一个可配置的事件“中继”。中继将使用LINQ测试和过滤传入事件,在等待下一个时间间隔时将符合条件的事件缓存在列表中,然后同步到GUI线程,并按照接收到的顺序播放事件

如何使用RX协调缓存、筛选和中继多个事件,例如用于添加、更改和删除自定义数据源中的值的事件

这可能会要求很多,但对于如何处理这个问题的任何指导都将非常感激。请参阅下面的示例代码

public delegate void EventDelegateAdd(Thing thing);
public delegate void EventDelegateChange(Thing thing);
public delegate void EventDelegateRemove(Thing thing);

public delegate void EventDelegateBulkChangesStart();
public delegate void EventDelegateBulkChangesEnd();

// The "Things" that are stored in MyCustomDataSource

public class Thing
{
    public string Key { get; set; }
    public string Title { get; set; }
    public object OtherStuff { get; set; }
}

// A custom observable data source with events that indicate when Things are
// added, changed, or removed.

public class MyCustomDataSource
{
    public event EventDelegateAdd AddingThing;
    public event EventDelegateChange ChangingThing;
    public event EventDelegateRemove RemovingThing;

    // The rest of the class that manages the database of Things...
}

// This class forms a configurable event bridge between the MyCustomDataSource and
// the GUI.  The goal is to cache, filter, and throttle the events so that the GUI
// updates only occasionally with bulk changes that are relevant for that control.

public class MyEventCachingBridge
{
    private MyCustomDataSource mSource;

    public event EventDelegateAdd AddingThing;
    public event EventDelegateChange ChangingThing;
    public event EventDelegateRemove RemovingThing;

    public event EventDelegateBulkChangesStart BulkChangesStart;
    public event EventDelegateBulkChangesEnd BulkChangesEnd;


    public MyEventCachingBridge(MyCustomDataSource source, int eventRelayInterval)
    {
        mSource = source;

        // Magical Reactive Extensions code goes here that subscribes to all 3 events...
        //
        //   mSource.AddingThing
        //   mSource.ChangingThing
        //   mSource.RemovingThing
        // 
        //  ...filters and records a list of the events as they are received ( maintaining order of events too ),
        //  then every eventRelayInterval milliseconds, plays back the events in bulk to update the GUI 
        //  ( on the GUIs thread ).  Note that LINQ will be used to filter the Things so that a subset of
        //  Thing changes are relayed to the GUI - i.e. - not all Thing events are observed by the GUI.

    }


    public void PlayBackCachedEvents()
    {
        BulkChangesStart();   // Raise Event to notify GUI to suspend screen updates

        // Play back the list of events to push changes to ListView, TreeView, graphs, etc...
        //
        //  this.AddingThing(Thing);        // Fire events in order received
        //  this.ChangingThing(Thing);      // Fire events in order received
        //  this.RemovingThing(Thing);      // Fire events in order received

        BulkChangesEnd();   // Raise Event to notify GUI to allow control refresh
    }
鉴于所描述的任务,我不确定一般类代码中应该协调什么,以及RX语句中应该隐藏什么

我还很欣赏这样一个事实,即将3个事件组合成一个事件,并使用一个枚举指示事件的目的,这将极大地简化事情,但是在实际应用程序中要缓存的不仅仅是3个事件。我不希望使用常量Switch语句测试来识别每个事件的开销。将有大量事件路由到可能大量的GUI界面


谢谢你的建议。

啊,我喜欢玩Rx……这里有一个方法;首先是部分,然后是全部:

编辑:根据注释进行修改

首先,您需要为事件设置流-在我们进行设置时,让我们用Rx的“新hawth”替换桥接器公开的“旧的.NET事件模式”:

然后,您需要将所有传入事件合并到一个流中,然后根据时间窗口“分块”该流—
Buffer
运算符非常适合此操作:

_buffer = Observable.Merge(scheduler,
        _adds.Select(e => Tuple.Create(e, ThingEventType.Add)), 
        _changes.Select(e => Tuple.Create(e, ThingEventType.Change)), 
        _removes.Select(e => Tuple.Create(e, ThingEventType.Remove)))
    .Buffer(TimeSpan.FromMilliseconds(eventRelayInterval), scheduler);
请注意,我正在将事件类型打包回流中-这样我们可以在回放期间采取适当的操作-枚举为:

private enum ThingEventType
{
    Add,
    Change,
    Remove
}
现在我们需要一些东西来监听和保存批处理事件-这里有很多选项,但是让我们使用一个简单的
列表来同步:

private Queue<IList<Tuple<Thing,ThingEventType>>> _eventQueue;
private static object SyncRoot = new object();

_eventQueue = new Queue<IList<Tuple<Thing,ThingEventType>>>();

// A serial disposable is a sort of "Disposable holder" - when you change it's
// Disposable member, it auto-disposes what you originally had there...no real
// need for it here, but potentially useful later
_watcherDisposable = new SerialDisposable();
_watcherDisposable.Disposable = _buffer
    .ObserveOn(_scheduler)
    .Subscribe(batch => 
    { 
        lock(SyncRoot) { _eventQueue.Enqueue(batch); }
    });
    _disposables.Add(_watcherDisposable);
订阅总是可以IDisposable的,您会想要处理它们,所以让我们添加一些东西:

public class MyEventCachingBridge : IDisposable
{
    CompositeDisposable _disposables;

    public void Dispose()
    {
        _disposables.Dispose();
    }
现在播放:

public void PlayBackCachedEvents()
{
    BulkChangesStart();   // Raise Event to notify GUI to suspend screen updates

    // Play back the list of events to push changes 
    lock(SyncRoot)
    {
        foreach(var batch in _eventQueue)
        {
            // Play back the list of events to push changes to ListView, TreeView, graphs, etc...            
            foreach(var evt in batch)
            {
                switch(evt.Item2)
                {
                    case ThingEventType.Add: BufferedAdds.OnNext(evt.Item1); break;
                    case ThingEventType.Change: BufferedChanges.OnNext(evt.Item1);break;
                    case ThingEventType.Remove: BufferedRemoves.OnNext(evt.Item1);break;
                }
            }
        }
        _eventQueue.Clear();
    }
    BulkChangesEnd();   // Raise Event to notify GUI to allow control refresh
}
现在-我们也希望在消费者方面有更多的想象力,所以让我们模拟一个UI窗口(这是WPF,相应地进行调整):

公共类BridgeConsumer:窗口,IDisposable
{
私人只读可组合可处置设备;
专用IScheduler\u调度器;
专用StackPanel(面板);;
已加载公共void(对象发送方、RoutedEventArgs ea)
{
_面板=新的StackPanel();
this.Content=\u面板;
}
公用桥接器使用者(MyEventCachingBridge、ISScheduler调度器)
{
//用于清除任何订阅
_可处置的=新的可组合的();
_一次性。添加(桥);
_调度程序=调度程序;
已加载+=已加载;
//为网桥上的大容量开始/结束事件设置侦听器
var bulkStart=Observable.FromEvent(
h=>bridge.BulkChangesStart+=new EventDelegateBulkChangesStart(h),
h=>bridge.BulkChangesStart-=neweventdelegatebulkchangesstart(h));
var bulkEnd=可观测的FromEvent(
h=>bridge.BulkChangesEnd+=new EventDelegateBulkChangesEnd(h),
h=>bridge.BulkChangesEnd-=neweventdelegatebulkchangesend(h));
//“肉块”——
//1.创建由批量开始/结束事件定义的“窗口”
//2.在该“窗口”内,捕获某个窗口上的任何事件
//添加/更改/删除的合并视图
//3.对于该窗口中的每个事件,选择该事件
//(即,将窗口内容作为排序流提供给我们)
桥梁观察者=
从伦敦的thingEventWindow
可观察,合并(
bridge.BufferedAdds.Select(t=>Tuple.Create(“add”,t)),
bridge.BufferedChanges.Select(t=>Tuple.Create(“change”,t)),
bridge.BufferedRemoves.Select(t=>Tuple.Create(“remove”,t))
)
.Window(bulkStart,start=>bulkEnd)
来自thingEvent中的thingEvent
选择thingEvent;
//这很容易成为一个方法,一个对viewmodel的绑定调用,等等
Action addToList=(thing、msg、ts)=>
{
var text=new TextBlock()
{ 
Text=string.Format(
“At:{0}键:{2}Msg:{3}-nowTime={1}”,
时间戳,
ts,
东西,钥匙,
msg)};
_panel.Children.Add(文本);
};
_一次性。添加(bridgeWatcher)
//小心!ObserveOn的意思和你认为的“订阅”一样
.ObserveOnDispatcher()
.Subscribe(tup=>
{
addToList(tup.Item2、tup.Item1、_scheduler.Now);
}));
}
公共空间处置()
{
//清理
如果(_disposables!=null)_disposables.Dispose();
}
}
整个谢邦:

void Main()
{
    var scheduler = Scheduler.Default;
    var rnd = new Random();
    var canceller = new CancellationTokenSource();

    var source = new MyCustomDataSource();    
    var eventRelayInterval = 2000;
    var bridge = new MyEventCachingBridge(source, eventRelayInterval, scheduler);

    var window = new BridgeConsumer(bridge);
    window.Closed += (o,e) => { canceller.Cancel(); window.Dispose(); };
    window.Show();

    Task.Factory.StartNew(
        () => 
        {
            while(true)
            {
                var thing = new Thing() 
                { 
                    Key = "added thing " + rnd.Next(0, 100), 
                    Title = "title for added thing", 
                    TimeStamp = scheduler.Now.DateTime 
                };
                source.FireAdd(thing);
                Thread.Sleep(rnd.Next(1,10) * 100);
            }
        }, canceller.Token);            
}

public class BridgeConsumer : Window, IDisposable
{
    private readonly CompositeDisposable _disposables;
    private StackPanel _panel;

    public void OnLoaded(object sender, RoutedEventArgs ea)
    {
        _panel = new StackPanel();
        this.Content = _panel;
    }

    public BridgeConsumer(MyEventCachingBridge bridge)
    {
        _disposables = new CompositeDisposable();
        _disposables.Add(bridge);

        Loaded += OnLoaded;

        var bulkStart = Observable.FromEvent(
                h => bridge.BulkChangesStart += new EventDelegateBulkChangesStart(h),
                h => bridge.BulkChangesStart -= new EventDelegateBulkChangesStart(h));
        var bulkEnd = Observable.FromEvent(
                h => bridge.BulkChangesEnd += new EventDelegateBulkChangesEnd(h),
                h => bridge.BulkChangesEnd -= new EventDelegateBulkChangesEnd(h));
        var bridgeWatcher =
            from thingEventWindow in 
                Observable.Merge(
                    bridge.BufferedAdds.Select(t => Tuple.Create("add", t)), 
                    bridge.BufferedChanges.Select(t => Tuple.Create("change", t)), 
                    bridge.BufferedRemoves.Select(t => Tuple.Create("remove", t))
                )
                .Window(bulkStart, start => bulkEnd)
            from thingEvent in thingEventWindow
            select thingEvent;

        Action<Thing, string> addToList = (thing, msg) => 
        {
            var text = new TextBlock() 
            { 
                Text = string.Format(
                    "At:{0} Key:{1} Msg:{2}", 
                    thing.TimeStamp, 
                    thing.Key, 
                    msg) 
            };
            _panel.Children.Add(text);
        };

        _disposables.Add(bridgeWatcher.ObserveOnDispatcher().Subscribe(tup => 
            {
                addToList(tup.Item2, tup.Item1);
            }));
    }

    public void Dispose()
    {
        if(_disposables != null) _disposables.Dispose();
    }
}


public delegate void EventDelegateAdd(Thing thing);
public delegate void EventDelegateChange(Thing thing);
public delegate void EventDelegateRemove(Thing thing);

public delegate void EventDelegateBulkChangesStart();
public delegate void EventDelegateBulkChangesEnd();

// The "Things" that are stored in MyCustomDataSource

public class Thing
{
    public DateTime TimeStamp {get; set;}
    public string Key { get; set; }
    public string Title { get; set; }
    public object OtherStuff { get; set; }
    public override string ToString()
    {
        return string.Format("At:{0} Key:{1} Title:{2}", this.TimeStamp, this.Key, this.Title);        
    }
}

// A custom observable data source with events that indicate when Things are
// added, changed, or removed.

public class MyCustomDataSource
{
    public event EventDelegateAdd AddingThing = delegate { };
    public event EventDelegateChange ChangingThing = delegate { };
    public event EventDelegateRemove RemovingThing = delegate { };

    // The rest of the class that manages the database of Things...
    public void FireAdd(Thing toAdd)
    {
        AddingThing(toAdd);
    }
    public void FireChange(Thing toChange)
    {
        ChangingThing(toChange);
    }
    public void FireRemove(Thing toRemove)
    {
        RemovingThing(toRemove);
    }
}

// This class forms a configurable event bridge between the MyCustomDataSource and
// the GUI.  The goal is to cache, filter, and throttle the events so that the GUI
// updates only occasionally with bulk changes that are relevant for that control.

public class MyEventCachingBridge : IDisposable
{
    private enum ThingEventType
    {
        Add,
        Change,
        Remove
    }

    private MyCustomDataSource mSource;
    private IScheduler _scheduler;

    public event EventDelegateBulkChangesStart BulkChangesStart = delegate { };
    public event EventDelegateBulkChangesEnd BulkChangesEnd = delegate { };

    public IObservable<Thing> RawAdds {get; private set;}
    public IObservable<Thing> RawChanges {get; private set;}
    public IObservable<Thing> RawRemoves {get; private set;}

    public Subject<Thing> BufferedAdds {get; private set;}
    public Subject<Thing> BufferedChanges {get; private set;}
    public Subject<Thing> BufferedRemoves {get; private set;}

    private readonly IObservable<IList<Tuple<Thing,ThingEventType>>> _buffer;
    private List<IList<Tuple<Thing,ThingEventType>>> _eventQueue;
    private static object SyncRoot = new object();

    private readonly CompositeDisposable _disposables;
    private readonly SerialDisposable _watcherDisposable;

    public MyEventCachingBridge(MyCustomDataSource source, int eventRelayInterval, IScheduler scheduler)
    {
        _disposables = new CompositeDisposable();
        mSource = source;
        _scheduler = scheduler;
        _eventQueue = new List<IList<Tuple<Thing,ThingEventType>>>();

        // Magical Reactive Extensions code goes here that subscribes to all 3 events...
        //
        //   mSource.AddingThing
        //   mSource.ChangingThing
        //   mSource.RemovingThing
        // 
        //  ...filters and records a list of the events as they are received ( maintaining order of events too ),
        //  then every eventRelayInterval milliseconds, plays back the events in bulk to update the GUI 
        //  ( on the GUIs thread ).  Note that LINQ will be used to filter the Things so that a subset of
        //  Thing changes are relayed to the GUI - i.e. - not all Thing events are observed by the GUI.
        RawAdds = Observable.FromEvent<EventDelegateAdd, Thing>(
            ev => new EventDelegateAdd(ev),
            h => mSource.AddingThing += h,
            h => mSource.AddingThing -= h);
        BufferedAdds = new Subject<Thing>();

        RawChanges = Observable.FromEvent<EventDelegateChange, Thing>(
            ev => new EventDelegateChange(ev),
            h => mSource.ChangingThing += h,
            h => mSource.ChangingThing -= h);
        BufferedChanges = new Subject<Thing>();

        RawRemoves = Observable.FromEvent<EventDelegateRemove, Thing>(
            ev => new EventDelegateRemove(ev),
            h => mSource.RemovingThing += h,
            h => mSource.RemovingThing -= h);
        BufferedRemoves = new Subject<Thing>();

        _buffer = Observable.Merge(
                    _scheduler,
                    RawAdds.Select(e => Tuple.Create(e, ThingEventType.Add)), 
                    RawChanges.Select(e => Tuple.Create(e, ThingEventType.Change)), 
                    RawRemoves.Select(e => Tuple.Create(e, ThingEventType.Remove)))
            .Buffer(TimeSpan.FromMilliseconds(eventRelayInterval), _scheduler);

        _watcherDisposable = new SerialDisposable();
        _watcherDisposable.Disposable = _buffer
            .ObserveOn(_scheduler)
            .Subscribe(batch => 
            { 
                lock(SyncRoot) { _eventQueue.Add(batch); }
            });
        _disposables.Add(_watcherDisposable);

        var pulse = Observable.Interval(TimeSpan.FromMilliseconds(eventRelayInterval), _scheduler);
        _disposables.Add(pulse.ObserveOn(_scheduler).Subscribe(x => PlayBackCachedEvents()));
    }

    private void PlayBackCachedEvents()
    {
        BulkChangesStart();   // Raise Event to notify GUI to suspend screen updates

        try
        {            
            //_eventQueue.Dump();
            lock(SyncRoot)
            {
                foreach(var batch in _eventQueue)
                {
                    // Play back the list of events to push changes to ListView, TreeView, graphs, etc...            
                    foreach(var evt in batch)
                    {
                        switch(evt.Item2)
                        {
                            case ThingEventType.Add: BufferedAdds.OnNext(evt.Item1); break;
                            case ThingEventType.Change: BufferedChanges.OnNext(evt.Item1);break;
                            case ThingEventType.Remove: BufferedRemoves.OnNext(evt.Item1);break;
                        }
                    }
                }
                _eventQueue.Clear();
            }
        }
        catch(Exception ex)
        {
            Console.WriteLine("Exception during playback:" + ex);
        }
        BulkChangesEnd();   // Raise Event to notify GUI to allow control refresh
    }

    public void Dispose()
    {
        _disposables.Dispose();
    }
}
void Main()
{
var scheduler=scheduler.Default;
var rnd=新随机数();
var canceller=新的CancellationTokenSource();
var source=新的MyCustomDataSource();
var eventRelayInterval=2000;
var桥=新的MyEventCachingBridge(源、eventRelayInterval、调度程序);
var窗口=新的桥接消费者(桥接);
window.Closed+=(o,e)=>{canceller.Cancel();window.Dispose
public class MyEventCachingBridge : IDisposable
{
    CompositeDisposable _disposables;

    public void Dispose()
    {
        _disposables.Dispose();
    }
public void PlayBackCachedEvents()
{
    BulkChangesStart();   // Raise Event to notify GUI to suspend screen updates

    // Play back the list of events to push changes 
    lock(SyncRoot)
    {
        foreach(var batch in _eventQueue)
        {
            // Play back the list of events to push changes to ListView, TreeView, graphs, etc...            
            foreach(var evt in batch)
            {
                switch(evt.Item2)
                {
                    case ThingEventType.Add: BufferedAdds.OnNext(evt.Item1); break;
                    case ThingEventType.Change: BufferedChanges.OnNext(evt.Item1);break;
                    case ThingEventType.Remove: BufferedRemoves.OnNext(evt.Item1);break;
                }
            }
        }
        _eventQueue.Clear();
    }
    BulkChangesEnd();   // Raise Event to notify GUI to allow control refresh
}
public class BridgeConsumer : Window, IDisposable
{
    private readonly CompositeDisposable _disposables;
    private IScheduler _scheduler;
    private StackPanel _panel;

    public void OnLoaded(object sender, RoutedEventArgs ea)
    {
        _panel = new StackPanel();
        this.Content = _panel;
    }

    public BridgeConsumer(MyEventCachingBridge bridge, IScheduler scheduler)
    {
        // for cleanup of any subscriptions
        _disposables = new CompositeDisposable();
        _disposables.Add(bridge);
        _scheduler = scheduler;

        Loaded += OnLoaded;

        // setup a listener for the bulk start/end events on the bridge
        var bulkStart = Observable.FromEvent(
                h => bridge.BulkChangesStart += new EventDelegateBulkChangesStart(h),
                h => bridge.BulkChangesStart -= new EventDelegateBulkChangesStart(h));
        var bulkEnd = Observable.FromEvent(
                h => bridge.BulkChangesEnd += new EventDelegateBulkChangesEnd(h),
                h => bridge.BulkChangesEnd -= new EventDelegateBulkChangesEnd(h));

        // the "meaty bit" - 
        //    1. create a "window" defined by bulk start/end events
        //    2. inside that "window", trap any occurrences on a 
        //          merged view of adds/changes/removes
        //    3. foreach event in that window, select that event
        //         (i.e., give us window contents as a stream of sorts)
        var bridgeWatcher =
            from thingEventWindow in 
                Observable.Merge(
                    bridge.BufferedAdds.Select(t => Tuple.Create("add", t)), 
                    bridge.BufferedChanges.Select(t => Tuple.Create("change", t)), 
                    bridge.BufferedRemoves.Select(t => Tuple.Create("remove", t))
                )
                .Window(bulkStart, start => bulkEnd)
            from thingEvent in thingEventWindow
            select thingEvent;

        // this could just as easily be a method, a bound call to the viewmodel, etc
        Action<Thing, string, DateTimeOffset> addToList = (thing, msg, ts) => 
        {
            var text = new TextBlock() 
            { 
                Text = string.Format(
                    "At:{0} Key:{2} Msg:{3} - nowTime = {1}", 
                    thing.TimeStamp, 
                    ts, 
                    thing.Key, 
                    msg) };
            _panel.Children.Add(text);
        };

        _disposables.Add(bridgeWatcher
            // CAREFUL! "ObserveOn" means what you'd think "SubscribeOn" would
            .ObserveOnDispatcher()
            .Subscribe(tup => 
            {
                addToList(tup.Item2, tup.Item1, _scheduler.Now);
            }));
    }

    public void Dispose()
    {
        // clean up
        if(_disposables != null) _disposables.Dispose();
    }
}
void Main()
{
    var scheduler = Scheduler.Default;
    var rnd = new Random();
    var canceller = new CancellationTokenSource();

    var source = new MyCustomDataSource();    
    var eventRelayInterval = 2000;
    var bridge = new MyEventCachingBridge(source, eventRelayInterval, scheduler);

    var window = new BridgeConsumer(bridge);
    window.Closed += (o,e) => { canceller.Cancel(); window.Dispose(); };
    window.Show();

    Task.Factory.StartNew(
        () => 
        {
            while(true)
            {
                var thing = new Thing() 
                { 
                    Key = "added thing " + rnd.Next(0, 100), 
                    Title = "title for added thing", 
                    TimeStamp = scheduler.Now.DateTime 
                };
                source.FireAdd(thing);
                Thread.Sleep(rnd.Next(1,10) * 100);
            }
        }, canceller.Token);            
}

public class BridgeConsumer : Window, IDisposable
{
    private readonly CompositeDisposable _disposables;
    private StackPanel _panel;

    public void OnLoaded(object sender, RoutedEventArgs ea)
    {
        _panel = new StackPanel();
        this.Content = _panel;
    }

    public BridgeConsumer(MyEventCachingBridge bridge)
    {
        _disposables = new CompositeDisposable();
        _disposables.Add(bridge);

        Loaded += OnLoaded;

        var bulkStart = Observable.FromEvent(
                h => bridge.BulkChangesStart += new EventDelegateBulkChangesStart(h),
                h => bridge.BulkChangesStart -= new EventDelegateBulkChangesStart(h));
        var bulkEnd = Observable.FromEvent(
                h => bridge.BulkChangesEnd += new EventDelegateBulkChangesEnd(h),
                h => bridge.BulkChangesEnd -= new EventDelegateBulkChangesEnd(h));
        var bridgeWatcher =
            from thingEventWindow in 
                Observable.Merge(
                    bridge.BufferedAdds.Select(t => Tuple.Create("add", t)), 
                    bridge.BufferedChanges.Select(t => Tuple.Create("change", t)), 
                    bridge.BufferedRemoves.Select(t => Tuple.Create("remove", t))
                )
                .Window(bulkStart, start => bulkEnd)
            from thingEvent in thingEventWindow
            select thingEvent;

        Action<Thing, string> addToList = (thing, msg) => 
        {
            var text = new TextBlock() 
            { 
                Text = string.Format(
                    "At:{0} Key:{1} Msg:{2}", 
                    thing.TimeStamp, 
                    thing.Key, 
                    msg) 
            };
            _panel.Children.Add(text);
        };

        _disposables.Add(bridgeWatcher.ObserveOnDispatcher().Subscribe(tup => 
            {
                addToList(tup.Item2, tup.Item1);
            }));
    }

    public void Dispose()
    {
        if(_disposables != null) _disposables.Dispose();
    }
}


public delegate void EventDelegateAdd(Thing thing);
public delegate void EventDelegateChange(Thing thing);
public delegate void EventDelegateRemove(Thing thing);

public delegate void EventDelegateBulkChangesStart();
public delegate void EventDelegateBulkChangesEnd();

// The "Things" that are stored in MyCustomDataSource

public class Thing
{
    public DateTime TimeStamp {get; set;}
    public string Key { get; set; }
    public string Title { get; set; }
    public object OtherStuff { get; set; }
    public override string ToString()
    {
        return string.Format("At:{0} Key:{1} Title:{2}", this.TimeStamp, this.Key, this.Title);        
    }
}

// A custom observable data source with events that indicate when Things are
// added, changed, or removed.

public class MyCustomDataSource
{
    public event EventDelegateAdd AddingThing = delegate { };
    public event EventDelegateChange ChangingThing = delegate { };
    public event EventDelegateRemove RemovingThing = delegate { };

    // The rest of the class that manages the database of Things...
    public void FireAdd(Thing toAdd)
    {
        AddingThing(toAdd);
    }
    public void FireChange(Thing toChange)
    {
        ChangingThing(toChange);
    }
    public void FireRemove(Thing toRemove)
    {
        RemovingThing(toRemove);
    }
}

// This class forms a configurable event bridge between the MyCustomDataSource and
// the GUI.  The goal is to cache, filter, and throttle the events so that the GUI
// updates only occasionally with bulk changes that are relevant for that control.

public class MyEventCachingBridge : IDisposable
{
    private enum ThingEventType
    {
        Add,
        Change,
        Remove
    }

    private MyCustomDataSource mSource;
    private IScheduler _scheduler;

    public event EventDelegateBulkChangesStart BulkChangesStart = delegate { };
    public event EventDelegateBulkChangesEnd BulkChangesEnd = delegate { };

    public IObservable<Thing> RawAdds {get; private set;}
    public IObservable<Thing> RawChanges {get; private set;}
    public IObservable<Thing> RawRemoves {get; private set;}

    public Subject<Thing> BufferedAdds {get; private set;}
    public Subject<Thing> BufferedChanges {get; private set;}
    public Subject<Thing> BufferedRemoves {get; private set;}

    private readonly IObservable<IList<Tuple<Thing,ThingEventType>>> _buffer;
    private List<IList<Tuple<Thing,ThingEventType>>> _eventQueue;
    private static object SyncRoot = new object();

    private readonly CompositeDisposable _disposables;
    private readonly SerialDisposable _watcherDisposable;

    public MyEventCachingBridge(MyCustomDataSource source, int eventRelayInterval, IScheduler scheduler)
    {
        _disposables = new CompositeDisposable();
        mSource = source;
        _scheduler = scheduler;
        _eventQueue = new List<IList<Tuple<Thing,ThingEventType>>>();

        // Magical Reactive Extensions code goes here that subscribes to all 3 events...
        //
        //   mSource.AddingThing
        //   mSource.ChangingThing
        //   mSource.RemovingThing
        // 
        //  ...filters and records a list of the events as they are received ( maintaining order of events too ),
        //  then every eventRelayInterval milliseconds, plays back the events in bulk to update the GUI 
        //  ( on the GUIs thread ).  Note that LINQ will be used to filter the Things so that a subset of
        //  Thing changes are relayed to the GUI - i.e. - not all Thing events are observed by the GUI.
        RawAdds = Observable.FromEvent<EventDelegateAdd, Thing>(
            ev => new EventDelegateAdd(ev),
            h => mSource.AddingThing += h,
            h => mSource.AddingThing -= h);
        BufferedAdds = new Subject<Thing>();

        RawChanges = Observable.FromEvent<EventDelegateChange, Thing>(
            ev => new EventDelegateChange(ev),
            h => mSource.ChangingThing += h,
            h => mSource.ChangingThing -= h);
        BufferedChanges = new Subject<Thing>();

        RawRemoves = Observable.FromEvent<EventDelegateRemove, Thing>(
            ev => new EventDelegateRemove(ev),
            h => mSource.RemovingThing += h,
            h => mSource.RemovingThing -= h);
        BufferedRemoves = new Subject<Thing>();

        _buffer = Observable.Merge(
                    _scheduler,
                    RawAdds.Select(e => Tuple.Create(e, ThingEventType.Add)), 
                    RawChanges.Select(e => Tuple.Create(e, ThingEventType.Change)), 
                    RawRemoves.Select(e => Tuple.Create(e, ThingEventType.Remove)))
            .Buffer(TimeSpan.FromMilliseconds(eventRelayInterval), _scheduler);

        _watcherDisposable = new SerialDisposable();
        _watcherDisposable.Disposable = _buffer
            .ObserveOn(_scheduler)
            .Subscribe(batch => 
            { 
                lock(SyncRoot) { _eventQueue.Add(batch); }
            });
        _disposables.Add(_watcherDisposable);

        var pulse = Observable.Interval(TimeSpan.FromMilliseconds(eventRelayInterval), _scheduler);
        _disposables.Add(pulse.ObserveOn(_scheduler).Subscribe(x => PlayBackCachedEvents()));
    }

    private void PlayBackCachedEvents()
    {
        BulkChangesStart();   // Raise Event to notify GUI to suspend screen updates

        try
        {            
            //_eventQueue.Dump();
            lock(SyncRoot)
            {
                foreach(var batch in _eventQueue)
                {
                    // Play back the list of events to push changes to ListView, TreeView, graphs, etc...            
                    foreach(var evt in batch)
                    {
                        switch(evt.Item2)
                        {
                            case ThingEventType.Add: BufferedAdds.OnNext(evt.Item1); break;
                            case ThingEventType.Change: BufferedChanges.OnNext(evt.Item1);break;
                            case ThingEventType.Remove: BufferedRemoves.OnNext(evt.Item1);break;
                        }
                    }
                }
                _eventQueue.Clear();
            }
        }
        catch(Exception ex)
        {
            Console.WriteLine("Exception during playback:" + ex);
        }
        BulkChangesEnd();   // Raise Event to notify GUI to allow control refresh
    }

    public void Dispose()
    {
        _disposables.Dispose();
    }
}