I'm building a Xamarin Forms app and have a problem with the network activity spinner in the Status Bar (iOS) continuing to spin forever. Below is the code I'm using to display a loading screen.
private async Task ExecuteConfigureCampaignCommand()
{
if (IsBusy)
{
return;
}
IsBusy = true;
//ConfigureCampaignCommand.ChangeCanExecute();
bool showAlert = false;
string campaign = null;
try
{
campaign = CrossSecureStorage.Current.GetValue(SettingNames.Campaign);
if (!String.IsNullOrEmpty(campaign))
{
StatusMessage = "Blah";
OnPropertyChanged("StatusMessage");
await Task.Delay(1000);
StatusMessage = "Blah";
OnPropertyChanged("StatusMessage");
await Task.Delay(700);
StatusMessage = "Blah";
OnPropertyChanged("StatusMessage");
await Task.Delay(500);
MessagingService.Current.SendMessage<string>(SettingNames.NavigationSales, campaign);
}
}
catch (Exception e)
{
var abc = e;
showAlert = true;
}
finally
{
IsBusy = false;
//ConfigureCampaignCommand.ChangeCanExecute();
}
if (showAlert)
{
await page.DisplayAlert("Uh Oh :(", "Unable to load campaign.", "OK");
}
}
This is called from a command:
public Command ConfigureCampaignCommand
{
get
{
return configureCampaignCommand ?? (configureCampaignCommand = new Command(async () => await ExecuteConfigureCampaignCommand(), () => { return !IsBusy; }));
}
}
Which is called from OnAppearing:
protected override void OnAppearing()
{
base.OnAppearing();
viewModel.ConfigureCampaignCommand.Execute(null);
}
The logic is fine - the View that I expect is loaded, however the network activity spinner continues spinning. If I add a 1 second delay:
Device.StartTimer(TimeSpan.FromMilliseconds(1), () =>
{
MessagingService.Current.SendMessage<string>(SettingNames.NavigationSales, campaign);
return false;
});
The spinner stops once the new view is loaded. If I remove the await Task.Delay's, the spinner stops once the view is loaded, however the loading messages are not visible to the user.
My question is, why does the network spinner continue?
Whenever you change IsBusy
OnPropertyChanged("IsBusy");
As you are calling this for your other properties but not for IsBusy I guess that this is probably the reason the change doesn't get picked up.
Related
I'm using QR Code Scanner in Xamarin App, when it scans the qr code, there are some operation it does which takes about a minute, while it's performing operation, I want to show a loading dialog on the screen. But, it isn't showing on the screen, and elsewhere in the app, it's working perfectly.
Code
var expectedFormat = ZXing.BarcodeFormat.QR_CODE;
var opts = new ZXing.Mobile.MobileBarcodeScanningOptions { PossibleFormats = new List<ZXing.BarcodeFormat> { expectedFormat } };
var scanner = new ZXing.Mobile.MobileBarcodeScanner();
var result = await scanner.Scan(opts);
if (result == null)
{
// code
return null;
}
else
{
using (UserDialogs.Instance.Loading("Processing"))
{
// code
}
}
UPDATE CODE SAMPLE
public async Task Verification(object sender, EventArgs e)
{
try
{
var expectedFormat = ZXing.BarcodeFormat.QR_CODE;
var opts = new ZXing.Mobile.MobileBarcodeScanningOptions { PossibleFormats = new List<ZXing.BarcodeFormat> { expectedFormat } };
var scanner = new ZXing.Mobile.MobileBarcodeScanner();
var result = await scanner.Scan(opts);
if (result == null)
{
// Code
return null;
}
else
{
try
{
// QR Scan Result
string qr_scan = result.Response;
var result = JsonConvert.DeserializeObject<QRScan>(qr_scan);
await CreateConnection();
}
catch (Exception error)
{ }
finally
{
// navigate to next page
await NavigationService.NavigateToAsync<NextViewModel>();
}
}
}
catch (Exception error)
{ }
return null;
}
public async Task CreateConnection()
{
UserDialogs.Instance.Loading("Processing");
if ()
{
try
{
// Code
}
catch (Exception error)
{
// Code
}
finally
{
await CreateFolder(default, default);
}
}
}
public async Task CreateFolder(object sender, EventArgs e)
{
UserDialogs.Instance.Loading("Processing");
try
{
// Code
}
catch (Exception error)
{
// Code
}
return null;
}
You can use Xamarin.Essentials' MainThread class and more specifically - the InvokeOnMainThreadAsync method. The idea of this method is to not only execute a code on the UI/main thread, but to also to await it's code. This way you can have both async/await logic and main thread execution.
try
{
// QR Scan Result
string qr_scan = result.Response;
var result = JsonConvert.DeserializeObject<QRScan>(qr_scan);
await MainThread.InvokeOnMainThreadAsync(() => CreateConnection());
}
catch (Exception error)
{ }
finally
{
// navigate to next page
await NavigationService.NavigateToAsync<NextViewModel>();
}
Keep in mind that if the method CreateConnection takes a long time to execute, then it would be better to execute on the main thread only the dialog presentation (UserDialogs.Instance.Loading("")).
Try something like this
Device.BeginInvokeOnMainThread(async () =>
{
try
{
using (UserDialogs.Instance.Loading(("Processing")))
{
await Task.Delay(300);
//Your Service code
}
}
catch (Exception ex)
{
var val = ex.Message;
UserDialogs.Instance.Alert("Test", val.ToString(), "Ok");
}
});
I am sending an intent I am receiving through a BroadcastReceiver to multiple Views in my shared-project. The DisplayResult-method implements the MessagingCenter.Send.
public class MyBroadcast : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
String action = intent.Action;
if (action.Equals(MainActivity.Instance.Resources.GetString(intentString)))
{
//Device.BeginInvokeOnMainThread(() => MainActivity.Instance.DisplayResult(intent));
//Task.Run(() => MainActivity.Instance.DisplayResult(intent));
//MainActivity.Instance.RunOnUiThread(() => MainActivity.Instance.DisplayResult(intent));
}
}
}
If a message is received I want to update my Views e.g. like this:
MessagingCenter.Subscribe<object, Model>(this, "HI", (sender, arg) =>
{
var dt = DateTime.Now;
_logger.Debug($"Task started: {dt}");
ActivityIndicator.IsVisible = true;
ActivityIndicator.IsRunning = true;
Task.Run(async () =>
{
await SomeTask();
}).GetAwaiter().GetResult();
_logger.Debug($"Task finished: {DateTime.Now - dt}");
ActivityIndicator.IsRunning = false;
ActivityIndicator.IsVisible = false;
}
});
The issue is if I use the approach with Task.Run(...) my Views are only showing up once but the ActivityIndicator is running. That means if I close a View and go back to the MainPage and navigate to another or the same View again it only shows a white screen.
If I use either Device.BeginInvokeOnMainThread(...) or RunOnUiThread(...) I get the skipped frames error. And my app is frozen until every Task within the MessagingCenter.Subscribe in my Views is finished. The ActivityIndicator is Not showing up.
You can have a try with using async method to invoke these method as follow:
MessagingCenter.Subscribe<PageMain, string>(this, "HI", async (sender, arg) =>
{
var dt = DateTime.Now;
_logger.Debug($"Task started: {dt}");
ActivityIndicator.IsVisible = true;
ActivityIndicator.IsRunning = true;
await SomeTask();
_logger.Debug($"Task finished: {DateTime.Now - dt}");
ActivityIndicator.IsVisible = false;
ActivityIndicator.IsRunning = false;
});
I have the following api call in my MainActivity.cs -
public async Task DoSomething()
{
var progressDialog = ShowLoading(_instance);
await Task.Factory.StartNew(() =>
{
var objectResponse = string.Empty;
string _apiUrl = "https://my-web-service-endpoint";
var request = (HttpWebRequest)WebRequest.Create(_apiUrl);
request.Method = "GET";
using (var response = _request.HttpWebResponse(request))
{
objectResponse = _request.HttpWebResponseBody(response);
}
progressDialog.Dismiss();
});
}
I am calling this method from a button click in SliderControl.cs -
public void loadTestView(View view)
{
Button btnDoSomething = view.FindViewById<Button>(Resource.Id.btnDoSomething);
if (btnDoSomething != null)
{
btnDoSomething .Click += async (sender, e) =>
{
await MainActivity.GetInstance().DoSomething();
};
}
}
However loadTestView is not called in the MainActivity.cs rather it is called when a used swipes onto a specific view in the application and handled in the SliderControl.cs , it is set like -
public override Java.Lang.Object InstantiateItem(ViewGroup container, int position)
{
var selectedView = Resource.Layout.Locator;
if (position == 0)
{
selectedView = Resource.Layout.Main;
}
if (position == 1)
{
selectedView = Resource.Layout.TestView;
}
View view = LayoutInflater.From(container.Context).Inflate(selectedView, container, false);
container.AddView(view);
if (position == 0)
{
loadMainView(view);
}
else if (position == 1)
{
loadTestView(view);
}
return view;
}
When I set a different buttons click event logic in the MainActivity.cs everything working accordingly, however when setting the button click login as it is above, I am met with the error -
Only the original thread that created a view hierarchy can touch its views.
The odd thing is the loader does actually come into view, however when I step into the await Task.Factory.StartNew(() => line the error hits.
I have tried changing the line to use Task.Run instead but same error, and also found some info on Device.BeginInvokeOnMainThread hwoever believe this is for Xamarin Forms. How can I get this to work?
You should use the UI Thread to update the UI / or any task that involve the UI:
Xamarin Forms Example:
Device.BeginInvokeOnMainThread (() => {
label.Text = "Async operation completed";
});
John wrote the great examples describing how to use Async/Await pattern. You may want to read more here https://johnthiriet.com/configure-await/#
Update: For Xamarin Native -
Problem:
new System.Threading.Thread(new System.Threading.ThreadStart(() => {
lblTest.Text = "updated in thread";
// Doesn't work because you can't modify UILabel on background thread!
})).Start();
Solution:
new System.Threading.Thread(new System.Threading.ThreadStart(() => {
InvokeOnMainThread (() => {
label1.Text = "updated in thread";
// this works! We are invoking in Main thread.
});
})).Start();
I am using MVVMLight in an UWP application, I have two screens: on MainScreen I have a button to open a second screen, and I also have another button to send some data from MainScreen to the second screen. I am using
Messenger.Default.Send(someobject)
and similarly
Messenger.Default.Register<Some>(this, (action) => ReceiveMsg(action));
I have to click the button on the main screen and send data to other view.
The problem is the data is not getting updated on the second screen, and causing an exception
The application called an interface that was marshalled for a different thread
I tried several ways to update UI, like this, this in fact I have tried all these possibilities like following
private async void ReceiveMsg(Some action)
{
try
{
//await Task.Factory.StartNew(() => {
// T1 = action.T1;
// RaisePropertyChanged(() => T1);
//});
//SharingData.UpdateScore(action);
//DispatcherHelper.Initialize();
//await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal
// , () =>
// {
// T1 = action.T1;
// RaisePropertyChanged(() => T1);
// });
//await Dispatcher.DispatchAsync(() =>
//{
// T1 = action.T1;
// RaisePropertyChanged(() => T1);
//},1000, Windows.UI.Core.CoreDispatcherPriority.Normal);
//await Window.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
//{
// T1 = action.T1;
// RaisePropertyChanged(() => T1);
//});
//var views= Windows.ApplicationModel.Core.CoreApplication.Views.ToList()[0];
//await views.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => {
// T1 = action.T1;
// RaisePropertyChanged(() => T1);
//});
var thiswindow = Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher;
await thiswindow.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
team1 = action.Team1;
this.RaisePropertyChanged("Team1");
});
//DispatcherHelper.CheckBeginInvokeOnUI(
//() =>
//{
// T1 = action.T1;
// RaisePropertyChanged(() => T1);
//});
//DispatcherHelper.Reset();
//DispatcherHelper.Initialize();
}
catch (Exception ex)
{
//DispatcherHelper.Reset();
//Console
}
}
I have tried all the above segments one by one, but nothing works and still getting the "marshalled for a different thread" error.
Please tell me what I am doing wrong?
The property is getting update, like T1='Some Value' but not reflecting on UI and on RaisePropertyChanged its giving the exception.
I have to click the button on the main screen and send data to other view. The problem is the data is not getting updated on the second screen, and causing an exception
The application called an interface that was marshalled for a different thread.
The Send and Receive action of Messenger will be invoked in same thread. When you click the button in the MainScreen, the CoreWindow.Dispatcher is dispatcher of MainScreen. If you use this dispatcher to modify the content of second screen, it will throw exception.
To resolve it, you should invoke Send method in the main thread of second screen.
private async void Button_Click(object sender, RoutedEventArgs e)
{
newView = CoreApplication.CreateNewView();
int newViewId = 0;
await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
var id = Environment.CurrentManagedThreadId;
Frame frame = new Frame();
frame.Navigate(typeof(SecondPage), null);
Window.Current.Content = frame;
Window.Current.Activate();
newViewId = ApplicationView.GetForCurrentView().Id;
});
bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
}
private async void SendBtn_Click(object sender, RoutedEventArgs e)
{
await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
var id = Environment.CurrentManagedThreadId;
Messenger.Default.Send("Test send message!!!");
});
}
I've been running into a Callback problems with async programming in WPF .Net 4.5.
There should be a way of doing this code in a more understandable way (I have suppressed a big part of it to make it easier to see what is going on).
I don't think there is a way to simply remove code because I need to call Dispatcher in order to manipulate WPF controls and calls like in TbSequence.Focus() and Utils.ShowMessageBox.
private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
{
try
{
Controller.Busy = true;
System.Threading.Tasks.Task.Run(() =>
{
try
{
Controller.SaveItem();
}
catch (BdlDbException ex)
{
if (ex.ExceptionSubType == DbExceptionSubTypes.UniqueViolation)
{
HandleUniqueViolation(ex);
}
else
{
string errorMessage = "";
errorMessage = ex.Message;
Dispatcher.BeginInvoke(new Action(() => Utils.ShowMessageBox(t_MessageBox.Attention, errorMessage)));
}
}
Controller.Busy = false;
});
}
catch (FieldException ex)
{
if (ex.FieldName == "FirstName")
{
TbFirstName.ValidationError = true;
TbFirstName.ApplyErrorToolTip(ex.Message);
}
}
}
public void Init(UcEdit container, Employee entity = null)
{
Controller.Busy = true;
System.Threading.Tasks.Task.Run(() =>
{
try
{
Controller.Init(entity);
}
catch (BdlEntryNotFoundException ex)
{
HandleNotFoundException(ex);
}
Container.Dispatcher.BeginInvoke(new Action(() =>
{
Container.DataContext = Controller;
// Instructions order for focusing TbSequence after load should be different in case we have an existent item
// because we have to focus the control AFTER it is filled with data, in order to set the caret position correctly.
if (Controller.IsNewItem)
{
this.DataContext = Controller;
TbSequence.Focus();
Controller.Busy = false;
}
else
{
TbSequence.TextChanged += TbSequence_TextChanged;
this.DataContext = Controller;
SetButtons();
}
}));
});
}
private void HandleUniqueViolation(BdlDbException ex)
{
string errorMessage = "";
bool isSequence = false; // if true, it's name.
if (ex.Fields[1] == "Sequence")
{
errorMessage = "There is already an Employee with the sequence \"" + Controller.Item.Sequence + "\".";
isSequence = true;
}
else
{
errorMessage = "There is already an Employee named \"" + Controller.Item.FirstName +
" " + Controller.Item.LastName + "\".";
}
errorMessage += "\r\nLoad it from Database?\r\n(All the changes on this window will be lost.)";
Dispatcher.BeginInvoke(new Action(() =>
{
MessageBoxResult res = Utils.ShowMessageBox(t_MessageBox.Question, errorMessage, MessageBoxButton.YesNo);
switch (res)
{
case MessageBoxResult.Yes:
if (isSequence)
{
System.Threading.Tasks.Task.Run(() =>
{
Controller.GetEmployeeBySequence(Controller.Item.Sequence);
Init(Container, Controller.OriginalItem);
});
}
else
{
System.Threading.Tasks.Task.Run(() =>
{
Controller.GetEmployeeByName(Controller.Item.FirstName, Controller.Item.LastName);
Init(Container, Controller.OriginalItem);
});
}
break;
case MessageBoxResult.No:
break;
}
}));
}
As you can see, there is a major Callback problem here that behaves like this:
Save_Executed (UI) -> HandleUniqueViolation (Task) -> ShowMessageBox (UI) -> Controller.GetEmployeeBySequence (Task) -> Controller.Init ...
And so it goes.
Is there a way to make this code more easy to read?
Thank you.
You're starting off your code by putting (more or less) the entirety of your method body in a call to Task.Run and then explicitly marshalling to the UI thread all over the place within that callback.
Just narrow the scope of your Task.Run call so that your UI code is just outside the call to Run rather than inside both it and a call to Invoke:
private async void Save_Executed(object sender, ExecutedRoutedEventArgs e)
{
try
{
Controller.Busy = true;
try
{
await Task.Run(() => Controller.SaveItem());
}
catch (BdlDbException ex)
{
if (ex.ExceptionSubType == DbExceptionSubTypes.UniqueViolation)
{
HandleUniqueViolation(ex);
}
else
{
string errorMessage = "";
errorMessage = ex.Message;
Utils.ShowMessageBox(t_MessageBox.Attention, errorMessage);
}
}
Controller.Busy = false;
}
catch (FieldException ex)
{
if (ex.FieldName == "FirstName")
{
TbFirstName.ValidationError = true;
TbFirstName.ApplyErrorToolTip(ex.Message);
}
}
}
Here you're running the actual long running business operation that you have in a thread pool thread, but doing all of your error handling in the UI thread.
You can do the same thing throughout your application. Rather than putting everything into a background thread and then explicitly marshaling, just only ever execute operations in a background thread that should be entirely executed in a background thread.