Xamarin ZXing scanner write logic after scan is complete - c#

I am working on a C# Xamarin project. In the project, I have used the ZXing.Net.Mobile class library in order to implement a QR code Scanner.
After the user scans the QR code, a url is revealed. Which is used to send data to a webservice. My issue is: midway during the execution of the method connectToBackend the thread BeginInvokeOnMainThread expires. As a consequence the execution of connectToBackend never finishes. I need help about how to handle this thread scenario so that I can process my server request.
public void ShowScannerPage() {
ZXingScannerPage scanPage = new ZXingScannerPage();
scanPage.OnScanResult += (result) => {
// stop scanning
scanPage.IsScanning = false;
ZXing.BarcodeFormat barcodeFormat = result.BarcodeFormat;
string type = barcodeFormat.ToString();
// This thread finishes before the method, connectToBackend, inside has time to finish
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => {
//_pageService.PopAsync();
_pageService.PopAsync();
App.UserDialogManager.ShowAlertDialog("The Barcode type is : " + type, "The text is : " + result.Text, " OK");
// problem here
connectToBackend(result.Text);
});
// If I put connectToBackend here there is still a problem
};
_pageService.PushAsync(scanPage);
}
To provide more information, I am using the MVVM approach. I have a page and when the users clicks on the scan button, the method ShowScannerPage opens up the scanner view on their mobile using the ZXing.Net Mobile library. I have a pasted my class below.
public class WorkoutViewModel {
public ICommand ScanCommand { get; private set; }
public readonly IPageService _pageService;
public WorkoutViewModel(IPageService pageService) {
this._pageService = pageService;
ScanCommand = new Command(ShowScannerPage);
}
public void ShowScannerPage() {
ZXingScannerPage scanPage = new ZXingScannerPage();
scanPage.OnScanResult += (result) => {
// stop scanning
scanPage.IsScanning = false;
ZXing.BarcodeFormat barcodeFormat = result.BarcodeFormat;
string type = barcodeFormat.ToString();
// This thread finishes before the method, connectToBackend, inside has time to finish
Xamarin.Forms.Device.BeginInvokeOnMainThread(() => {
//_pageService.PopAsync();
_pageService.PopAsync();
App.UserDialogManager.ShowAlertDialog("The Barcode type is : " + type, "The text is : " + result.Text, " OK");
// problem here
connectToBackend(result.Text);
});
// If I put connectToBackend here there is still a problem
};
_pageService.PushAsync(scanPage);
}
public async void connectToBackend(String nodes) {
// call api ...
}
}

You are calling an async function (async void connectToBackend(String nodes)) without the await keyword.
I think you can define "async" the event scanPage.OnScanResult += (result) => { so you can async the connectToBackend function.
I think BeginInvokeOnMainThread is not necessary

Related

Issue with MessagingCenter firing multiple times Xamarin

I have looked all over for a solution to an issue. I have noticed that in my android app, every time I fire an event from <button Clicked="GPSToggle_Clicked">, for some reason it increments the number of times my methods get called. So after I compile and load this on my phone, I hit my "GPSToggle_Clicked" button, and then to stop hit that button again. On the first "stop", I'll get a single instance of the below output:
---------------------------------------------------------------Attempting string parsing
---------------------------------------------------------------Sending string to SubmitGeneratedGPX
---------------------------------------------------------------path: /storage/emulated/0/Download/GPX/2022-10-27-02-44-06.gpx
---------------------------------------------------------------GPX File creation success
---------------------------------------------------------------:RawBufferToJsonString: [{"Timestamp":"2022-10-27T18:43:52.849+00:00","Latitude":41.5263818,"Longitude":-81.6507923,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7990270853042603,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:53.696+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:54.526+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:55.374+00:00","Latitude":41.5263819,"Longitude":-81.6507921,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7697961330413818,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2},{"Timestamp":"2022-10-27T18:43:56.21+00:00","Latitude":41.5263811,"Longitude":-81.650792,"Altitude":153.29998779296875,"Accuracy":20.0,"VerticalAccuracy":1.7160584926605225,"Speed":null,"Course":null,"IsFromMockProvider":false,"AltitudeReferenceSystem":2}]
Every subsequent time I hit start/stop on the app, I get the real-time data in the output multiplied by the number of times I've started/stopped since the last compiling.
the main app page button event thats fired:
private async void GPSToggle_Clicked(object sender, EventArgs e)
{
var LocationPermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.LocationAlways>();
var FileReadPermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.StorageRead>();
var FileWritePermissionStatus = await Xamarin.Essentials.Permissions.RequestAsync<Xamarin.Essentials.Permissions.StorageWrite>();
if(LocationPermissionStatus == Xamarin.Essentials.PermissionStatus.Denied)
{
// TODO
return;
}
// run if device is android
if(Device.RuntimePlatform == Device.Android)
{
if (!CrossGeolocator.Current.IsGeolocationAvailable || !CrossGeolocator.Current.IsGeolocationEnabled)
{
// gps is not enabled, throw alert
Console.WriteLine("---------------------------------------------------------------GPS is DISABLED");
await DisplayAlert("Error", "GPS is not enabled. You must enable GPS to use this feature", "Ok");
}
else
{
// set our IsTracking = true flag
if (!IsTracking)
{
// start background listening for GPS
await StartListening();
Console.WriteLine("---------------------------------------------------------------Listening: " + CrossGeolocator.Current.IsListening);
StartService();
Console.WriteLine("---------------------------------------------------------------Service initiated");
IsTracking = true;
Console.WriteLine("---------------------------------------------------------------Tracking initiated");
GPSToggle.Text = "Stop Tracking";
}
else
{
//
// verify that the submittal wasn't done in error, before stopping services and submitting data
bool DoneInError = await DisplayAlert("Finish?", "Are you sure you want to stop services and submit?", "No", "Yes");
if (!DoneInError)
{
await StopListening();
Console.WriteLine("---------------------------------------------------------------listening:" + CrossGeolocator.Current.IsListening);
IsTracking = false;
Console.WriteLine("---------------------------------------------------------------Tracking ceased");
// stop the gps service
StopService();
Console.WriteLine("---------------------------------------------------------------Service ceased");
// stop the background listening for gps
Console.WriteLine("---------------------------------------------------------------Attempt GPX parse from buffer obj");
GPSToggle.Text = "Start Tracking";
}
}
}
}
}
Specifically the line:
StartService();
Fires this method off within the same class, specifically the MessagingCenter.Send<>, which initiates my foreground service to handle logging the gps data into a buffer:
private void StartService()
{
var startServiceMessage = new StartServiceMessage();
MessagingCenter.Send(startServiceMessage, "ServiceStarted");
Preferences.Set("LocationServiceRunning", true);
StatusLabel.Text = "Location service has been started";
Console.WriteLine("---------------------------------------------------------------location service has been started. preferences saved");
}
and
StopService();
Fires this method off to stop the services and retrieve the gps buffer data from the foreground to the main thread:
private void StopService()
{
var stopServiceMessage = new StopServiceMessage();
MessagingCenter.Unsubscribe<App, List<Location>>(this, "GPXBufferData");
MessagingCenter.Subscribe<App, List<Location>>(this, "GPXBufferData", (sender, args) =>
{
RawGPXData = args;
Generate_CreateGPX_File(RawGPXData);
RawBufferToJsonString = GPXParse.GenerateJSON_GPXPoints(RawGPXData);
Console.WriteLine("---------------------------------------------------------------:RawBufferToJsonString: " + RawBufferToJsonString);
PromptForSubmission_GPXPoints_API();
});
Console.WriteLine("--------------------------------------------------------------------------");
MessagingCenter.Send(stopServiceMessage, "ServiceStopped");
Preferences.Set("LocationServiceRunning", false);
Console.WriteLine("---------------------------------------------------------------Location service stopped. preferences saved");
}
In the above snippet, this line is subscribed to in the GPSLocationService.cs file:
MessagingCenter.Send(stopServiceMessage, "ServiceStopped");
This is a portion of my GPSLocationService.cs file that is relevant to this:
public async Task Run(CancellationToken token)
{
int ObjCount = 0;
await Task.Run(async () => {
// if the task was stopped
// check the buffer for data, if data, send to GPXGenerator
MessagingCenter.Subscribe<StopServiceMessage>(this, "ServiceStopped",
message =>
{
if (GPSBufferObj != null)
{
Device.BeginInvokeOnMainThread(() =>
{
MessagingCenter.Unsubscribe<App, List<Location>>((App)Xamarin.Forms.Application.Current, "GPXBufferData");
MessagingCenter.Send<App, List<Location>>((App)Xamarin.Forms.Application.Current, "GPXBufferData", GPSBufferObj);
});
}
});
return;
}, token);
}
I believe I have tracked down where the issue is starting. In my StopService() method, I have the following line (just to keep track of where Im at in the buffer) and it is only sent to output once.
Console.WriteLine("--------------------------------------------------------------------------");
BUT if I place that same line within the pasted portion of my GPSLocationService.cs file, I will get the incremented output. I'm leaning towards the nested task being the issue, I wrote this based losely off of this example repro:
https://github.com/jfversluis/XFBackgroundLocationSample
You don't have MessagingCenter.Unsubscribe<StopServiceMessage> anywhere in your code. StopServiceMessage is what you are accumulating subscriptions to.
You need to make sure Unsubscribe is unsubscribing the instance that was previously subscribed to. It sounds to me like there are multiple instances of GPSLocationService. [In which case, this is no longer referring to the original instance. Unsubscribe won't do anything, unless you have the this that was originally Subscribed.]
If so, instead create an instance of GPSLocationService ONCE, and store it in a static variable. Re-use it. start/stop it, but don't discard it.
Alternatively, if you only want a message ONE TIME from each Subscribe, then Unsubscribe as soon as you receive each message:
MessagingCenter.Subscribe<StopServiceMessage>(this, "ServiceStopped",
message =>
{
MessagingCenter.Unsubscribe<StopServiceMessage>(this, "ServiceStopped");
... your code ...
});
Use this same pattern EVERYWHERE you have a Subscribe (unless you Subscribe ONLY ONE TIME at app start, as Jason suggested.)

How do I call an async dependency service from my xaml.cs code behind?

I am overriding the OnBackButtonPressed() event in my code-behind because I add some custom navigation behaviour.
My target is to call a function from my bluetooth classic dependency service (specifically in .android) when I leave the current page by pressing the android back button (triangle). I disabled the navigation back arrow because the user already confirmed to be in a critical process and the user should not return unless he confirms it.
This dependency function is only called if I add a breakpoint inside the bluetooth function and delay further processes. Without a breakpoint the function call is skipped which makes me believe I got an error in assigning await/async and invoking threads.
The bluetooth command protocol I'm using to communicate with the device is not good but I can't change it. If I'd have programmed the bl classic controller I would've implemented proper set and get methods but there's no way to change that anymore. I wouldn't even use bl classic for that purpose...
Code behind
In my code behind I call a helper class which in return calls the dependency service. This function works as supposed to if I call it from my viewmodels.
public partial class ProcessExecutionPage : ContentPage
{
public ProcessExecutionPage()
{
InitializeComponent();
}
protected override bool OnBackButtonPressed()
{
Device.InvokeOnMainThreadAsync(async () =>
{
//Do some stuff with binding viewmodel which is working fine
if (await DisplayAlert("Go back", "Go back?", "Yes", "No"))
{
await BluetoothHelper.SetLockedAsync(false); //this function isn't called properly
base.OnBackButtonPressed();
//Do some stuff (updating db etc.) -> working fine
await Shell.Current.GoToAsync("MyNextPage"); //working fine
}
});
return true;
}
}
BluetoothHelper function Here I'm getting my bluetooth device status and set it accordingly
public static async Task<bool> SetLockedAsync(bool status)
{
int delay = 300;
System.Console.WriteLine("BluetoothHelper: SetLockedAsync(" + status.ToString() + ")");
//Status check
App.BluetoothService.Write("*");
System.Console.WriteLine("Writing * - " + DateTime.Now.Millisecond.ToString());
await Task.Delay(delay);
int messageCounter = App.MessageList.Count;
System.Console.WriteLine("messageCounter: " + messageCounter.ToString());
App.BluetoothService.Write("*");
System.Console.WriteLine("Writing * - " + DateTime.Now.Millisecond.ToString());
await Task.Delay(delay);
bool lockedStatus;
System.Console.WriteLine("Getting lockedStatus - " + DateTime.Now.Millisecond.ToString());
if (App.MessageList.Count > messageCounter) lockedStatus = false;
else lockedStatus = true;
System.Console.WriteLine("Check locked status");
if (status != lockedStatus)
{
System.Console.WriteLine("Writing * - " + DateTime.Now.Millisecond.ToString());
App.BluetoothService.Write("*");
await Task.Delay(delay);
}
return true;
}
Printing to console returns:
BluetoothHelper: SetLockedAsync(True)
Writing * - 167
Bluetooth: Listening received: 10
messageCounter: 7
Writing * - 507
Getting lockedStatus - 856
Check locked status
All delay() calls are skipped and the function isn't called asynchronous.
EDIT: This is wrong, I didn't check for milliseconds and the delays are working
Interface
public interface IBluetoothService
{
//...
void Write(string text);
//...
}
Android bluetooth implementation
public class AndroidBluetoothService : IBluetoothService
{
/*private defines*/
private BluetoothDevice device;
private string connectedDeviceName;
private BluetoothSocket socket;
private Stream outputStream;
//...
/*public defines*/
public bool IsConnected { get; private set; }
//...
public void Write(string text)
{
if (!IsConnected || connectedDeviceName == string.Empty || device == null || socket == null) return;
byte[] bytes = Encoding.UTF8.GetBytes(text);
try
{
outputStream.Write(bytes, 0, bytes.Length);
}
catch(System.Exception ex) { System.Console.WriteLine("Bluetooth: ERROR writing message: " + ex.Message); }
}
The bluetooth functions all work nicely when I use them from my MVVM.
Debugger isn't showing any exceptions

CefSharp Javascript async response too fast

My approach is to visit each link and if all are visited to receive a return value from.
The problem is, when I start the code, I get instantly a response, clearly empty because not all the links are visited.
private async void ibtn_start_visiting_Click(object sender, EventArgs e)
{
string js = "var ele = document.querySelectorAll('#profiles * .tile__link');document.getElementsByClassName('js-scrollable')[0].scrollBy(0,30);ele.forEach(function(value,index){setTimeout(function(){if(index < ele.length-1){ele[index].click();}else{document.querySelectorAll('.search-results__item').forEach(e => e.parentNode.removeChild(e));document.getElementsByClassName('js-close-spotlight')[0].click();return 'hallo';}},1000 * index)})";
await browser.EvaluateScriptAsync(js).ContinueWith(x =>
{
var response = x.Result;
if (response.Success)
{
this.Invoke((MethodInvoker)delegate
{
var res = (string)response.Result;
Console.WriteLine("Response: " + res);
});
}
else {
Console.WriteLine("NO");
}
});
}
This is the javascript:
var ele = document.querySelectorAll('#profiles * .tile__link');
document.getElementsByClassName('js-scrollable')[0].scrollBy(0,30);
ele.forEach(function(value,index){
setTimeout(function(){
if(index < ele.length-1){
ele[index].click();
}
else{
document.querySelectorAll('.search-results__item').forEach(e => e.parentNode.removeChild(e));
document.getElementsByClassName('js-close-spotlight')[0].click();
alert('hallo');
}
},1500 * index)
})
Oh, I see. Your javascript is using setTimeout, which is kind of equivalent to making the function you pass to it also be async. CefSharp doesn't know when those setTimeout tasks are completed, hence the early return. The pended javascript code does execute, eventually. To know when that's been completed you've got a couple of options:
Make your async javascript code synchronous by getting rid of setTimeout completely.
Set some global variable in your async javascript code and periodically check your webpage in C# to see if that variable is set.
Register some JS handler and call that when your async javascript is completed.
#3 is my favorite, so you might register that handler in C# like so:
public class CallbackObjectForJs{
public void showMessage(string msg){
// we did it!
}
}
webView.RegisterJsObject("callbackObj", new CallbackObjectForJs());
And your JS might look something like:
var totalTasks = 0;
function beginTask() {
totalTasks++;
}
function completeTask() {
totalTasks--;
if (totalTasks === 0) {
callbackObj("we finished!"); // this function was registered via C#
}
}
var ele = document.querySelectorAll('#profiles * .tile__link');
document.getElementsByClassName('js-scrollable')[0].scrollBy(0,30);
ele.forEach(function(value,index){
beginTask(); // NEW
setTimeout(function(){
... // work
completeTask();
}, 1500 * index);
})
To make this cleaner you may want to look into Javascript's Promise.all().

WinRT DownloadProgress callback Progress Status

I am writing a universal app primarily targeting Windows Phone using SQLite-net.
During the course of operation, the user is presented with an option to download multiple files. At the end of each file download, I need to mark the file in the db as completed. I am using BackgroundDownloader in order to download files - the WP8.0 app used Background Transfer Service and that worked great. Files can be huge (some 200+ mbs, user content) and i am not looking forward to wrapping the downloads in the HttpClient or WebClient.
However, it seems that the progress callback doesn't work with awaits unless I actually breakpoint in the method.
The following are listings from a sample app i quickly put together that demonstrates the behaviour:
Model:
public class Field
{
[PrimaryKey]
[AutoIncrement]
public int Id { get; set; }
public bool Done { get; set; }
}
MainPage codebehind (i am creating a db here only for the purposes of this example!):
private async void Button_Click(object sender, RoutedEventArgs e)
{
using (var db = new SQLiteConnection(Windows.Storage.ApplicationData.Current.LocalFolder.Path + "//Main.db"))
{
db.CreateTable<Field>();
db.Commit();
}
this.DbConnection = new SQLiteAsyncConnection(Windows.Storage.ApplicationData.Current.LocalFolder.Path + "//My.db");
var dl = new BackgroundDownloader();
dl.CostPolicy = BackgroundTransferCostPolicy.Always;
var transferUri = new Uri("http://192.168.1.4/hello.world", UriKind.Absolute);
var folder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(
"Content",
CreationCollisionOption.OpenIfExists);
var localFile = await folder.CreateFileAsync("cheesecakes.file", CreationCollisionOption.ReplaceExisting);
var d = dl.CreateDownload(transferUri, localFile);
d.Priority = BackgroundTransferPriority.High;
var progressCallback = new Progress<DownloadOperation>(this.DownloadProgress);
await d.StartAsync().AsTask(progressCallback);
}
private async void DownloadProgress(DownloadOperation download)
{
Debug.WriteLine("Callback");
if (download.Progress.Status == BackgroundTransferStatus.Completed)
{
var f = new Field();
f.Done = true;
await this.DbConnection.InsertAsync(f);
Debug.WriteLine("DONE");
}
}
If i breakpoint inside the DownloadProgress and then press F5 i get both Debug messages, and my db gets a new record.
However, if i just let the code execute, i never see "DONE" printed to me and neither is my db updated.
I tried wrapping the code in a new task:
await Task.Run(
async () =>
{
Debug.WriteLine("taskrun");
.... OTHER CODE FROM ABOVE...
});
But again, i only get to see 'taskrun' if i breakpoint in the callback.
UPDATE I actually think this is more related to checking the status. E.g. the statements outside of the check are executed, but only once, whereas anything inside the check is not executed.
Is there any way to force that callback to be invoked when the download is completed?
private async void DownloadProgress(DownloadOperation download)
{
Debug.WriteLine("Callback");
var value = download.Progress.BytesReceived * 100 download.Progress.TotalBytesToReceive;
new System.Threading.ManualResetEvent(false).WaitOne(1000);
if (download.Progress.Status == BackgroundTransferStatus.Completed )
{
var f = new Field();
f.Done = true;
await this.DbConnection.InsertAsync(f);
Debug.WriteLine("DONE");
}
}
I had this problem too, and I solved this by sleeping for 1000 ms, which worked really well for me.
Not sure what is causing this, but I was able to get the sample app to work reliably by manually checking the bytes to download as opposed to relying on the DownloadOperation.Progress.Status:
private async void DownloadProgress(DownloadOperation download)
{
Debug.WriteLine("Callback");
var value = download.Progress.BytesReceived * 100 / download.Progress.TotalBytesToReceive;
if (download.Progress.Status == BackgroundTransferStatus.Completed || value >= 100)
{
var f = new Field();
f.Done = true;
await this.DbConnection.InsertAsync(f);
Debug.WriteLine("DONE");
}
This gets me to 'DONE' every time.

Hand back control to main UI thread to update UI after asynchronus image download

I have 2 asynchronous downloads in a Downloader class. Basically the code first makes a simple http based API request to get some data containing a url, and then uses this url to download an image - the last function call - Test(adImage) tries to pass the UIImage back to a function in the main ViewController class, so that it can update a UIImageView with the downloaded image. When I try to do this, I get an ArgumentNullException at the line
string result = System.Text.Encoding.UTF8.GetString (e.Result);
I think this is because I need to use the main UI thread to update the main VC, and can't do it from this object running on another asynchronous thread. If I take the Test function out, everything runs fine and the image is downloaded - just not used for anything.
How do I pass the image back to the mainVC and get it to update the image on the main UI thread?
(This is related to a question I asked before, but I think I was totally barking up the wrong tree before, so I felt it better to re-express the problem in the different way).
public class Downloader : IImageUpdated {
UIImage adImage;
Manga5ViewController mainVC;
public void DownloadWebData(Uri apiUrl, Manga5ViewController callingVC)
{
mainVC = callingVC;
WebClient client = new WebClient();
client.DownloadDataCompleted += DownloadDataCompleted;
client.DownloadDataAsync(apiUrl);
}
public void DownloadDataCompleted(object sender, DownloadDataCompletedEventArgs e)
{
string result = System.Text.Encoding.UTF8.GetString (e.Result);
string link = GetUri(result);
Console.WriteLine (link);
downloadImage(new Uri (link));
}
public void downloadImage (Uri imageUri) {
var tmp_img = ImageLoader.DefaultRequestImage (imageUri, this);
if (tmp_img != null)
{
adImage = tmp_img;
Console.WriteLine ("Image already cached, displaying");
Console.WriteLine ("Size: " + adImage.Size);
mainVC.Test (adImage);
}
else
{
adImage = UIImage.FromFile ("Images/downloading.jpg");
Console.WriteLine ("Image not cached. Using placeholder.");
}
}
public void UpdatedImage (System.Uri uri) {
adImage = ImageLoader.DefaultRequestImage(uri, this);
Console.WriteLine ("Size: " + adImage.Size);
mainVC.Test (adImage);
}
....
}
Damn, after working on this for hours, I finally figured it out a few minutes after posting this.
It was as simple as wrapping the UI code like so:
InvokeOnMainThread (delegate {
// UI Update code here...
});

Categories

Resources