Rx - Keeping track of a mouse panning operation - reactive-programming

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.

Related

How to subscribe to .NET MAUI LifeCycle Events from a ViewModel?

I am using the MauiCommunityToolkit and builder.ConfigureLifeCycleEvents in MauiProgram.cs like this:
// Initialise the toolkit
builder.UseMauiApp<App>().UseMauiCommunityToolkit();
// the rest of the logic...
builder.ConfigureLifecycleEvents(events =>
{
#if ANDROID
events.AddAndroid(android => android
.OnStart((activity) => MyOnStart(activity))
.OnCreate((activity, bundle) => MyOnCreate(activity, bundle))
.OnResume((activity) => MyOnResume(activity))
.OnBackPressed((activity) => MyOnBackPressed(activity))
.OnPause((activity) => MyOnPause(activity))
.OnStop((activity) => MyOnStop(activity))
.OnDestroy((activity) => MyOnDestroy(activity)));
#endif
});
This is all good, but is there some way to subscribe to these events directly from a ViewModel?
I could use the messenger service to let the ViewModel know if these events are fired if not.
Is there a better way?
I am new to MAUI (and this may be a C# question anyway).
It seems you can't subscribe the LifeCycle Events which happened before the instantiation of the ViewModel such as the OnStart in the viewmodel. But you can subscribe the LifeCycle Events which happened after it.
According to the official document about the app lifecyce event, the event is attach to the Window. So you can get the Window in the viewmodel. Such as:
In the App.cs:
public partial class App : Application
{
      public App()
      {
            InitializeComponent();
            MainPage = new AppShell();
      }
      public static Window Window { get; private set; }
      protected override Window CreateWindow(IActivationState activationState)
      {
            Window window = base.CreateWindow(activationState);
            Window = window;
            return window;
      }
}
In the viewmodel;
var window = App.Window;
            window.Stopped += (s, e) =>
            {
                  Debug.WriteLine("=========stopped");
            };
            window.Resumed += (s, e) =>
            {
                  Debug.WriteLine("=========resumed");
            };
            window.Destroying += (s, e) =>
            {
                  Debug.WriteLine("=========destorying");
            };
You can try to use the Dependency Injection in the maui to make the instantiation of the viewmodel run before the App's construction method. You can create the instance of the Window in the viewmodel and then retrun the viewmodel's Window in the app's CreateWindow method.
I haven’t tried it, but I assume this will work
App.Current.OnStart += MyEventHandler;

ReactiveUI - Create a "IsReseting" Observable for IReactiveDerivedList

How could I create an IObservable<bool> "IsReseting" which is true as soon as ShouldReset fires for the IReactiveDerivedList and back to false once the derived list is recomputed (and may not have any items)?
I'd like to display some activity indicator while filters apply.
I've done the following:
_isReseting= MyDerivedList
.ShouldReset
.Select(_ => true)
.Merge(MyDerivedList
.Changed
.Select(_ => false))
.ToProperty(this, x => x.IsReseting);

Ionic2: Unsubscribe Event to Avoid Duplicate Entries?

I've followed this tutorial which outlines adding monitoring beacons in an Ionic 2 application. I have it working great: when the view loads, it initializes and begins listening for beacons:
home.ts
ionViewDidLoad() {
this.platform.ready().then(() => {
this.beaconProvider.initialise().then((isInitialised) => {
if (isInitialised) {
this.listenToBeaconEvents();
}
});
});
}
This calls the listenToBeaconEvents function which populates a list in the view with all of the beacons:
home.ts
listenToBeaconEvents() {
this.events.subscribe(‘didRangeBeaconsInRegion’, (data) => {
// update the UI with the beacon list
this.zone.run(() => {
this.beacons = [];
let beaconList = data.beacons;
beaconList.forEach((beacon) => {
let beaconObject = new BeaconModel(beacon);
this.beacons.push(beaconObject);
});
});
});
}
I'm able to stop ranging using this.beaconProvider.stopRanging() that calls a function from the below function:
beacon-provider.ts
stopRanging() {
if (this.platform.is('cordova')) {
// stop ranging
this.ibeacon.stopRangingBeaconsInRegion(this.region)
.then(
() => {
console.log('Stopped Ranging');
},
error => {
console.error('Failed to stop monitoring: ', error);
}
);
}
}
The problem I'm having is this - in the original tutorial the beacon list is shown at the root, there's no other navigation. I've moved it to a different view, and if the user exits and re-enters the view, it re-initializes and loads everything, resulting in duplicate list entries.
I've tried creating a function within beacon-provider.ts to call before the view exits, but I can't figure out how to keep the subscriptions/events from duplicating.
I've tried this.delegate.didRangeBeaconsInRegion().unsubscribe(), and some other variations but they all result in runtime errors.
In your case you are using Ionic's Events API which has its own unsubscribe(topic, handler) function.
In your component, whenever you need to unsubscribe, you should call this with the same topic:
this.events.unsubscribe(‘didRangeBeaconsInRegion’);
This will remove all handlers you may have registered for the didRangeBeaconsInRegion.
If you want to unsubscribe one specific function, you will have to have registered a named handler which you can send with unsubscribe.
this.events.unsubscribe(‘didRangeBeaconsInRegion’,this.mySubscribedHandler);
And your home.ts would look like:
mySubscribedHandler:any = (data) => {
// update the UI with the beacon list
this.zone.run(() => {
this.beacons = [];
let beaconList = data.beacons;
beaconList.forEach((beacon) => {
let beaconObject = new BeaconModel(beacon);
this.beacons.push(beaconObject);
});
});
}
listenToBeaconEvents() {
this.events.subscribe(‘didRangeBeaconsInRegion’,this.mySubscribedHandler);
}

RxJs Observable with infinite scroll OR how to combine Observables

I have a table which uses infinite scroll to load more results and append them, when the user reaches the bottom of the page.
At the moment I have the following code:
var currentPage = 0;
var tableContent = Rx.Observable.empty();
function getHTTPDataPageObservable(pageNumber) {
return Rx.Observable.fromPromise($http(...));
}
function init() {
reset();
}
function reset() {
currentPage = 0;
tableContent = Rx.Observable.empty();
appendNextPage();
}
function appendNextPage() {
if(currentPage == 0) {
tableContent = getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; });
} else {
tableContent = tableContent.combineLatest(
getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; }),
function(o1, o2) {
return o1.concat(o2);
}
)
}
}
There's one major problem:
Everytime appendNextPage is called, I get a completely new Observable which then triggers all prior HTTP calls again and again.
A minor problem is, that this code is ugly and it looks like it's too much for such a simple use case.
Questions:
How to solve this problem in a nice way?
Is is possible to combine those Observables in a different way, without triggering the whole stack again and again?
You didn't include it but I'll assume that you have some way of detecting when the user reaches the bottom of the page. An event that you can use to trigger new loads. For the sake of this answer I'll say that you have defined it somewhere as:
const nextPage = fromEvent(page, 'nextpage');
What you really want to be doing is trying to map this to a stream of one directional flow rather than sort of using the stream as a mutable object. Thus:
const pageStream = nextPage.pipe(
//Always trigger the first page to load
startWith(0),
//Load these pages asynchronously, but keep them in order
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
//One option of how to join the pages together
scan((pages, p) => ([...pages, p]), [])
)
;
If you need reset functionality I would suggest that you also consider wrapping that whole stream to trigger the reset.
resetPages.pipe(
// Used for the "first" reset when the page first loads
startWith(0),
//Anytime there is a reset, restart the internal stream.
switchMapTo(
nextPage.pipe(
startWith(0),
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
scan((pages, p) => ([...pages, p]), [])
)
).subscribe(x => /*Render page content*/);
As you can see, by refactoring to nest the logic into streams we can remove the global state that was floating around before
You can use Subject and separate the problem you are solving into 2 observables. One is for scrolling events , and the other is for retrieving data. For example:
let scrollingSubject = new Rx.Subject();
let dataSubject = new Rx.Subject();
//store the data that has been received back from server to check if a page has been
// received previously
let dataList = [];
scrollingSubject.subscribe(function(page) {
dataSubject.onNext({
pageNumber: page,
pageData: [page + 10] // the data from the server
});
});
dataSubject.subscribe(function(data) {
console.log('Received data for page ' + data.pageNumber);
dataList.push(data);
});
//scroll to page 1
scrollingSubject.onNext(1);
//scroll to page 2
scrollingSubject.onNext(2);
//scroll to page 3
scrollingSubject.onNext(3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.all.js"></script>

How to trigger Form Validators in Angular2

In angular2 I want to trigger Validators for some controls when a another control is changed. Is there some way that I can just tell the form to re-validate? Better still, can I request validation of specific fields?
Example:
Given Checkbox X and input P.
Input P has a validator that behaves differently based on the model value of X.
When X is checked/unchecked I need to invoke the validator on P. The Validator on P will look at the model to determine the state of X and will validate P accordingly.
Here's some code:
constructor(builder: FormBuilder) {
this.formData = { num: '', checkbox: false };
this.formGp = builder.group({
numberFld: [this.formData.num, myValidators.numericRange],
checkboxFld: [this.formData.checkbox],
});
}
this.formGp.controls['checkboxFld'].valueChanges.observer({
next: (value) => {
// I want to be able to do something like the following line:
this.formGp.controls['numberFld'].validator(this.formGp.controls['numberFld']);
}
});
Anybody have a solution? Thanks!
I don't know if you are still looking for an answer, so here is my suggestions:
Have a look at this: Angular 2 - AbstractControl
I think what you could do is following:
this.formGp.controls['checkboxFld'].valueChanges.observer({
next: (value) => {
this.formGp.controls['numberFld'].updateValueAndValidity();
}
});
This should trigger and run the validators. Furthermore the state gets updated as well. Now you should be able to consult the checkbox value within your validator logic.
Validaton-Guide
FormControl Documentation
with my ControlGroup I do this because I have errors divs checking if touched
for (var i in this.form.controls) {
this.form.controls[i].markAsTouched();
}
(this.form is my ControlGroup)
With the help of this blog
blog link
I have came across a solution with the combine of Nightking answer
Object.keys(this.orderForm.controls).forEach(field => {
const control = this.orderForm.get(field);
control.updateValueAndValidity();
});
this.orderForm is the form group
This did the trick for me
this.myForm.markAllAsTouched();
There are more elegant ways of modeling this behavior - for example, putting your state into a ReplaySubject and observing that, and then using async validators observing the state - but the pseudo-coded approach below should work. You simply observe the value changes in the checkbox, update the model as appropriate, then force a re-validation of the numberFld with the updateValueAndValidity cal.
constructor(builder: FormBuilder) {
this.formData = { num: '', checkbox: false };
const numberFld = builder.control(this.formData.num, myValidators.numericRange);
const checkbox = builder.control(this.formData.checkbox);
checkbox.valueChanges.map(mapToBoolean).subscribe((bool) => {
this.formData.checked = bool;
numberFld.updateValueAndValidity(); //triggers numberFld validation
});
this.formGp = builder.group({
numberFld: numberFld,
checkboxFld: checkbox
});
}
You can trigger validation in this way:
this.myform.get('myfield').updateValueAndValidity();
static minMaxRange(min: number, max: number): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (Validators.min(min)(control)) { // if min not valid
return Validators.min(min)(control);
} else {
return Validators.max(max)(control);
}
};
}
Here is another similar way that also uses markAsDirty and updateValueAndValidity, particularly good if you use angular material where markAsTouched is not enough.
export function forceValidation(form: AbstractControl) {
if (form instanceof FormGroup || form instanceof FormArray) {
for (const inner in form.controls) {
const control = form.get(inner);
control && forceValidation(control);
}
} else {
form.markAsDirty();
form.markAsTouched();
form.updateValueAndValidity();
}
}