ReactiveUI: When using ReactiveCommand.CreateFromObservable() the UI does not update until the command completes - reactiveui

I'm using ReactiveUI 7.0 with WPF and .NET 4.5.2.
I'm trying to create a ReactiveCommand from an Observable. The code DOES work, however, the UI doesn't update until the command is completed. I have a progress bar and a progress window that I want to update as the command runs. In addition, the UI is unresponsive while the ReactiveCommand is executing (I can't click on a cancel button or anything else for that matter). I'm hoping it's something I am overlooking and obvious to somebody smarter than me. Or maybe I'm just doing it wrong.
Thanks for looking.
Here is the relevant code:
My ViewModel declaration:
public ReactiveCommand<Unit, string> PerformSelectedOperationCommand { get; set; }
private StringBuilder sb;
Within my ViewModel constructor:
PerformSelectedOperationCommand = ReactiveCommand.CreateFromObservable(PerformOperationObservable,
this.WhenAnyValue(x => x.SelectedPath, x => x.TotalFilesSelected,
(x, y) => x != null && y > 0));
// listen to the messages and append to output
PerformSelectedOperationCommand.Subscribe(s =>
{
sb.AppendLine(s);
ProgressWindowOutput = sb.ToString();
});
And here is the Observable contained in my ViewModel which runs when clicking on the Go button (Notice it's modifying properties of my ViewModel):
private IObservable<string> PerformOperationObservable()
{
sb.Clear();
return Observable.Create<string>((o) =>
{
using (cts = new CancellationTokenSource())
{
// this creates a copy of the file list which will keep us safe
// if the user is clicking around on the UI and changing the list.
var selectedFiles = FileList.Where(x => x.IsChecked).ToArray();
int total = selectedFiles.Length;
int count = 0;
foreach (var file in selectedFiles)
{
ProgressBarMessage = $"Processing {count + 1} of {total}";
o.OnNext($"Processing file {file.Name}");
SelectedFileOperation.PerformOperation(file.FullPath);
count++;
PercentComplete = (int)(((double)count / total) * 100);
if (cts.IsCancellationRequested)
{
PercentComplete = 0;
ProgressBarMessage = string.Empty;
break;
}
}
ProgressBarMessage = string.Empty;
}
o.OnCompleted();
return Disposable.Empty;
});
}

Observable are inherently single threaded, you need to specify where to do the work. You could do this, I believe:
PerformSelectedOperationCommand
= ReactiveCommand.CreateFromObservable(
PerformOperationObservable.SubscribeOn(RxApp.TaskPoolScheduler), // Move subscription to task pool
this.WhenAnyValue(
x => x.SelectedPath,
x => x.TotalFilesSelected,
(x, y) => x != null && y > 0
)
);
// listen to the messages and append to output
PerformSelectedOperationCommand
.ObserveOnDispather() // Return to UI thread
.Subscribe(s =>
{
sb.AppendLine(s);
ProgressWindowOutput = sb.ToString();
});
You explicitly subscribe to the observable on the task pool, moving the work of the UI thread. Right before using the output, you return to the UI thread to be able to do updates on the UI.

Related

UWP: calculate expected size of screenshot using multiple monitor setup

Right, parsing the clipboard, I am trying to detect if a stored bitmap in there might be the result of a screenshot the user took.
Everything is working fine as long as the user only has one monitor. Things become a bit more involved with two or more.
I am using the routing below to grab all the displays in use. Now, since I have no idea how they are configured to hang together, I do not know how to calculate the size of the screenshot (that Windows would produce) from that information.
I explicitly do not want to take a screenshot myself to compare. It's a privacy promise by my app.
Any ideas?
Here is the code for the size extractor, run in the UI thread.
public static async Task<Windows.Graphics.SizeInt32[]> GetMonitorSizesAsync()
{
Windows.Graphics.SizeInt32[] result = null;
var selector = DisplayMonitor.GetDeviceSelector();
var devices = await DeviceInformation.FindAllAsync(selector);
if (devices?.Count > 0)
{
result = new Windows.Graphics.SizeInt32[devices.Count];
int i = 0;
foreach (var device in devices)
{
var monitor = await DisplayMonitor.FromInterfaceIdAsync(device.Id);
result[i++] = monitor.NativeResolutionInRawPixels;
}
}
return result;
}
Base on Raymonds comment, here is my current solution in release code...
IN_UI_THREAD is a convenience property to see if the code executes in the UI thread,
public static Windows.Foundation.Size GetDesktopSize()
{
Int32 desktopWidth = 0;
Int32 desktopHeight = 0;
if (IN_UI_THREAD)
{
var regions = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView()?.WindowingEnvironment?.GetDisplayRegions()?.Where(r => r.IsVisible);
if (regions?.Count() > 0)
{
// Get grab the most left and the most right point.
var MostLeft = regions.Min(r => r.WorkAreaOffset.X);
var MostTop = regions.Min(r => r.WorkAreaOffset.Y);
// The width is the distance between the most left and the most right point
desktopWidth = (int)regions.Max(r => r.WorkAreaOffset.X + r.WorkAreaSize.Width - MostLeft);
// Same for height
desktopHeight = (int)regions.Max(r => r.WorkAreaOffset.Y + r.WorkAreaSize.Height - MostTop);
}
}
return new Windows.Foundation.Size(desktopWidth, desktopHeight);
}

Is it possible to keep executing test if element wasn't found with Cypress

I have a case when I need to wait for element (advertising), if it's visible then needs to click it, but if element wasn't found after timeout then needs to keep executing a test.
How to handle the situation with Cypress ?
The way Cypress says to check for a conditional element is Element existence
cy.get('body').then(($body) => {
const modal = $body.find('modal')
if (modal.length) {
modal.click()
}
})
Most likely you put that at the top of the test, and it runs too soon (there's no retry timeoout).
You can add a wait say 30 seconds, but the test is delayed every time.
Better to call recursively
const clickModal = (selector, attempt = 0) => {
if (attempt === 100) return // whole 30 seconds is up
cy.get('body').then(($body) => {
const modal = $body.find('modal')
if (!modal.length) {
cy.wait(300) // wait in small chunks
clickModal(selector, ++attempt)
}
})
return // done, exit
}
cy.get('body')
.then($body => clickModal('modal'))
Intercept the advert
Best is if you can find the url for the advert in network tab, use cy.intercept() to catch it and stub it out to stop the modal displaying.
I tried the above solution, but seems that in some cases parameter $body could not contain necessary element, cause it was not loaded when we invoked cy.get('body'). So, I found another solution, using jQuery via Cypress, here is it:
let counter = 0;
const timeOut: number = Cypress.config('defaultCommandTimeout');
const sleep = (milliseconds) => {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
};
while (true) {
if (Cypress.$(element).length > 0 && Cypress.$(element).is(':visible')) {
Cypress.$(element).click();
break;
} else {
sleep(500);
counter = counter + 500;
if (counter >= timeOut) {
cy.log(elementName+ ' was not found after timeout');
break;
}
}
}

Intercepting property changes with CommunityToolKit.Mvvm amd Xamarin Forms

I've been moving code from mvvmlight to the CommunityToolkit.Mvvm framework with my Xamarin Forms project and have hit a snag.
In mvvmlight, I would have a property like this
bool loginOK;
public bool LoginOK
{
get => loginOK;
set => Set(()=>LoginOK, ref loginOK, value, true);
}
in CommunityToolkit.Mvvm, this becomes
bool loginOK;
public bool LoginOK
{
get => loginOK;
set => SetProperty(ref loginOK, value);
}
Accorrding to the docs, if the property changes, the PropertyChanging event is fired
In my code behind in (in Xam.Forms), I have this
protected override void OnAppearing()
{
base.OnAppearing();
ViewModel.PropertyChanged += ObservableObject_PropertyChanged;
}
async void ObservableObject_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "LoginOK":
if (ViewModel.LoginOK)
{
if (ViewModel.SkipWelcome)
{
var d1 = await image.ScaleTo(4, 500);
if (!d1)
{
Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
{
Device.BeginInvokeOnMainThread(async () => await Shell.Current.GoToAsync("//TabBar"));
return false;
});
}
}
}
else
{
var d2 = await image.ScaleTo(8, 500);
if (!d2)
{
var d3 = await image.ScaleTo(0, 500);
if (!d3)
{
Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
{
Device.BeginInvokeOnMainThread(async () => await Shell.Current.GoToAsync("//Welcome"));
return false;
});
}
}
}
break;
}
}
When I run this and set a break point on the line
var d2 = await image.ScaleTo(8,500);
The break is not hit, but the imge shows
Am I doing something wrong to intercept the property changing or is there something I'm missing from the docs?
The main issue you are seeing is because loginOK defaults to false and when a login attempt fails LoginOK does not technically change and therefore SetProperty does not result in raising the PropertyChanged event. If you look at the codebase for the toolkit you will notice that they always check whether the value has changed before raising any notifications.
There are a number of options that you could consider to work around this problem:
1 - switch to bool?
If you switch to using a nullable bool then it's value will default to null and therefore when a login attempt fails false will cause PropertyChanged to fire.
2 - use an enum
Similar to the bool? approach in point 1 but instead use something like a LoginResult enum like the following:
public enum LoginResult
{
None = 0,
Failed,
Success
}
This will make you code much more readable than point 1.
3 - fire a specific event
Rather than rely on PropertyChanged you could create your own event (e.g. LoginAttempted) you could then keep your bool indicating success or not if you so desired.
4 - make use of a Command
Events are sometimes consider a bad concept of the past so you could instead create an ICommand property on your View and assign it to your View Model allowing the View Model to call Execute on it and pass the log result up to the view. Something like:
View
public ViewConstructor()
{
ViewModel.LoginAttemptCommand = new RelayCommand<bool>(() => OnLoginAttempted());
}
public void OnLoginAttempted(bool result)
{
// Old logic on whether login succeeded.
}
ViewModel
public ICommand LoginAttemptCommand { get; set; }
// Wherever you set LoginOK = false/true call:
LoginAttemptCommand.Execute(false); // or true if it succeeds.

Dart component progress bar only updating at the end of loop

I'm developing an app in angular dart and trying to animate a progress bar which shows progress of a file upload. I'm simply parsing the JSON from the file, and sending them off to a service with a _service.create(....) method.
I've put all of this code in an async method which is called when my submit button is clicked:
void uploadFile() async {
submitButton.attributes.addAll({ 'disabled': '' });
progress.value = 0;
FileList files = input.files;
if (files.isEmpty) {
_handleError("No file selected");
//handle error, probably a banner
return;
}
File file = files.item(0);
FileReader reader = new FileReader();
//reader.result
reader.onLoadEnd.listen((e) async {
Map map = json.decode(reader.result);
var combinations = map['combinations'];
progress.max = combinations.length;
int loopCount = 0;
combinations.forEach((e) async {
await _service.create(VJCombination.fromJSON(e)).then((_) {
combinationCount++;
progress.value++;
loopCount++;
if (loopCount == combinations.length) {
submitButton.attributes.remove('disabled');
}
});
});
isLoadSuccessful = true;
});
reader.onError.listen((evt) => print(evt));
reader.readAsText(file);
progress.value = 10;
}
I'm getting the progress and the submitButton elements with the #ViewChild annotations:
#ViewChild('progress')
ProgressElement progress;
#ViewChild('submit')
ButtonElement submitButton;
The code works. The progress bar starts off empty, and after the file is read and the service gets the data, the progress bar is full.
My issue is that the UI is only updated after all of the combinations have been sent to the _service. So it seemingly goes from empty to full in one frame.
This code
combinations.forEach((e) async {
does not make sense, because forEach does not care about the returned Future
Rather use
for(var e in combinations) {
Not sure if this fixes your problem, but such code definitely needs to be changed.

Update MultiselectList selected items

I'm using MultiselectList from Controls.Toolkit. I use it as a favourite selector. I have a list with items, I select the favourites and the next time I open the selection bar I would like to see my favourites already selected. When IsSelectionEnabledChanged event occurs, if IsSelectionEnabled is true (the selection bar is opened) I try to add my favourites to the list's SelectedItems. Here is a code snippet:
private void multiSelectList_IsSelectionEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (multiSelectList.IsSelectionEnabled)
{
foreach (var favourite in FavouritesList)
{
multiSelectList.SelectedItems.Add(multiSelectList.Items.Where(i => ((MyModel)i).id == favourite.id).FirstOrDefault());
}
}
}
I have tested this solution and I found out that the layout does not update the entire list that's why I'm not seeing the items as selected (but they are). Not even the actual visible items in the list. After scrolling for a bit and scrolling back, the selection appears! I've tried to use multiSelectList.UpdateLayout() method programatically but it did not solve it.
I wonder if it is a visualization problem or a CheckBox binding problem (the selection uses CheckBox on the side).
The SelectedItems is just a List<object>, it doesn’t raise any events when you update it.
To update your items manually, you could do something like following instead (untested code):
private void multiSelectList_IsSelectionEnabledChanged( object sender, DependencyPropertyChangedEventArgs e )
{
if( !multiSelectList.IsSelectionEnabled )
return;
var dictSelected = FavouritesList.ToDictionary( f => f.id, f => true );
for( int i = 0; i < multiSelectList.Items.Count; i++ )
{
MyModel m = (MyModel)multiSelectList.Items[ i ];
if( !dictSelected.ContainsKey( m.id ) )
continue; // Not selected
MultiselectItem item = (MultiselectItem)multiSelectList.ItemContainerGenerator.ContainerFromIndex( i );
if( null != item )
item.IsSelected = true; // This should add the item into the SelectedItems collection.
else
multiSelectList.SelectedItems.Add( m ); // The item is virtualized and has no visual representation yet.
}
}

Resources