I'm implementing a click-and-drag type operation for panning a camera in a graphical application.
I would like to keep track of whether or not we are panning in another stream, that can be checked by other operations for filtering purposes (e.g. we shouldn't allow rubber band selection if we are panning).
The code looks something like this:
MouseClicked
.Where(startClick => startClick.Action == MouseAction.LeftDown)
.SelectMany(_ =>
MouseMoved
.Select(endMove => (_.startClick, endMove))
.TakeUntil(MouseReleased))
.Subscribe(_ => PanCamera(_.startClick, _.endMove));
My solution is to add the following two lines of code
MouseClicked
.Where(startClick => startClick.Action == MouseAction.LeftDown)
.Do(_ => _isPanning.OnNext(true)) // Add this
.SelectMany(_ =>
MouseMoved
.Select(endMove => (_.startClick, endMove))
.TakeUntil(MouseReleased)
.Finally(() => _isPanning.OnNext(false)) // Add this
)
.Subscribe(_ => PanCamera(_.startClick, _.endMove));
Where _isPanning
is a Subject
. This works great, but I was wondering if there was a better approach, without having to use a Subject.
Instead of using a Subject you could fold the panning state into a single observable with multiple subscribers. For example:
public IDisposable PanningBehaviour()
{
var observable = MouseClicked
.Where(startClick => startClick.Action == MouseAction.LeftDown)
.SelectMany(start =>
MouseMoved
.Select(current => (Start: start.Position, Current: current.Position, Panning: PanningState.Panning))
.TakeUntil(MouseReleased)
.Concat(Observable.Return((Start: Point.Empty, Current: Point.Empty, Panning: PanningState.NotPanning))))
.Publish();
var cameraSubscription = observable
.Where(tuple => tuple.Panning == PanningState.Panning)
.Subscribe(tuple => PanCamera(tuple.Start, tuple.Current));
var notPanningSubscription = observable
.Select(tuple => tuple.Panning == PanningState.NotPanning)
.Subscribe(allowOperations => { /* Allow / disallow actions */ });
return new CompositeDisposable(
notPanningSubscription,
cameraSubscription,
observable.Connect()
);
}
In the /* Allow / disallow actions */
you can put whatever you like. In an MVVM app, this approach would work particularly well with an ICommand implementation that allows external code to control it's "CanExecute" status.
FWIW, my MVx.Observable package features an Observable.Command which could directly subscribe to the "notPanning" status as shown below:
private MVx.Observable.Command _allowSelection = new MVx.Observable.Command();
public IDisposable PanningBehaviour()
{
var observable = MouseClicked
.Where(startClick => startClick.Action == MouseAction.LeftDown)
.SelectMany(start =>
MouseMoved
.Select(current => (Start: start.Position, Current: current.Position, Panning: PanningState.Panning))
.TakeUntil(MouseReleased)
.Concat(Observable.Return((Start: Point.Empty, Current: Point.Empty, Panning: PanningState.NotPanning))))
.Publish();
var cameraSubscription = observable
.Where(tuple => tuple.Panning == PanningState.Panning)
.Subscribe(tuple => PanCamera(tuple.Start, tuple.Current));
var notPanningSubscription = observable
.Select(tuple => tuple.Panning == PanningState.NotPanning)
.Subscribe(_allowSelection);
return new CompositeDisposable(
notPanningSubscription,
cameraSubscription,
observable.Connect()
);
}
public ICommand AllowSelection => _allowSelection;
Hope it helps.