Can I change the UI from a method that gets data (not async) that has been called with await? - xamarin

I have this code:
protected override async void OnAppearing()
{
base.OnAppearing();
Subscribe();
vm.IsBusy = true;
if (Change.firstTimeCardsTab == true)
{
SetButtons(Settings.cc.ShortText());
await Task.Run(() => GetOnAppearingData());
}
}
private void GetOnAppearingData()
{
App.DB.GetData();
AddDetailSection();
SetPageDetails();
Change.firstTimeCardsTab = false;
vm.IsBusy = false;
}
vm.IsBusy = true; shows an activity indicator on the page and vm.IsBusy = false; would I hoped turn it off.
However when I run the code I get a message saying:
UIKit Consistency error: you are calling a UIKit method that can only
be invoked from a UI thread.
Can someone give me advice on how I can set the activity indicator to show, then get the data and then set it to not show? Note that AddDetailSection(); adds some elements to the XAML. Could this be the problem?

UI can be manipulated only from the main thread. There is a method Device.BeginInvokeOnMainThread(Action) that can help to solve your issue, more can be found in the official documentation.
Just wrap all UI related actions by it:
Device.BeginInvokeOnMainThread(() => {
SetPageDetails();
// Any other actions
});

Related

Blazor client side refresh component

I'm trying to figure out how to refresh the client-side component after button click.
Repo Link with example: https://github.com/ovie91/RefreshComponent
Site /test or from nav menu test
So I have OnInitializedAsync method that is retrieving data from API
protected override async Task OnInitializedAsync()
{
result = await (some API Call);
}
Then I have a method connected to the button
private async void ButtonClick()
{
await (some API Call);
result = null;
this.StateHasChanged(); <--- Doesnt work :<
}
I have tried to use this.StateHasChanged(); but there is no reaction.
As a workaround, I can force you to navigate again to the same website but this refresh "Whole" website but not a component.
Any ideas on how to deal with it?
whole code (stripped to minimum):
#page "/test"
#inject HttpClient Http
#if (result == null)
{
<p>Loading...<p>
}
else
{
#result
<button #onclick="(() => ButtonClick())">Click</button>
}
#code {
private APIObject result;
protected override async Task OnInitializedAsync()
{
result = await (some API Call);
}
private async void ButtonClick()
{
await (some API Call);
result = null;
this.StateHasChanged(); <--- Doesnt work :<
}
}
Update
I want to refresh component so OnInitializedAsync would be triggered again and that would mean I don't have to run the same code again after button click. Hope you understand what I mean.
To get the desired output you just have to shuffle the lines a little, from:
private async void ButtonClick()
{
await (some API Call); // UI checks if an update is needed (No)
result = null; // now an update is needed
this.StateHasChanged(); <--- Doesnt work :< // actually: not needed
}
to:
private async Task ButtonClick()
{
result = null; // change the state
//this.StateHasChanged(); // not needed, a request is pending
await (some API Call); // should show '<h3>Loading</h3>' now
}
Note that the UI is updated when an await releases the Thread.
however, from your answer we get
var APICall = await Http.GetAsync("SomeAPI");
Thread.Sleep(2000);
This should work when Http.GetAsync("SomeAPI"); really is an async call and not just some stand-in pseudo code. Because Thread.Sleep(2000); will really freeze things.
If you want to make sure:
private async Task GetData()
{
await Task.Delay(1); // release the thread for rendering
var APICall = await Http.GetAsync("SomeAPI");
Random rnd = new Random();
Thread.Sleep(2000); // Task.Delay() is much preferred
result = "Random Number: " + rnd.Next();
}
Thread.Sleep() is appropriate to simulate some CPU (not I/O) intensive code. So I'm not saying it's wrong but be aware of the difference.
And it is much better to make eventhandlers async Task instead of async void but that is not the direct problem here.
From here:
Blazor uses a synchronization context (SynchronizationContext) to enforce a single logical thread of execution. A component's lifecycle methods and any event callbacks that are raised by Blazor are executed on the synchronization context.
Blazor Server's synchronization context attempts to emulate a single-threaded environment so that it closely matches the WebAssembly model in the browser, which is single threaded. At any given point in time, work is performed on exactly one thread, giving the impression of a single logical thread. No two operations execute concurrently.
So as enet asnwered, you should use async Task signature instead of async void.
I have moved API call to another Method and inside of OnInitializedAsync I called it.
Then when I reset the result variable to see Loading state I'm able to "refresh" component to achieve that you need to add. this.StateHasChanged()
Now I have a responsive component to updates that are happening :)
#page "/test"
#using System.Threading;
#inject HttpClient Http
#if (result == null)
{
<h3>Loading</h3>
}
else
{
#result
<button #onclick="(() => ButtonClick())">Click</button>
}
#code {
private string result;
protected override async Task OnInitializedAsync()
{
await GetData();
}
private async Task GetData()
{
var APICall = await Http.GetAsync("SomeAPI");
Random rnd = new Random();
Thread.Sleep(2000);
result = "Random Number: " + rnd.Next();
}
private async Task ButtonClick()
{
await Http.GetAsync("SomeAPIcall");
result = null; // required to see loading state.
this.StateHasChanged(); // when added model is refreshed and Loading state is visible.
await GetData();
}
}

Button is disabled when binding

I have a IMvxAsyncCommand method in my ViewModel to perform a server call and depending the result navigate to a ViewModel or other. I don't know why but it is disabling the button where the click event is bind.
public IMvxAsyncCommand Register
{
var runner = Task.Run(() => _service.Register());
runner.Wait();
if (runner.Result.Status == SUCCESS)
{
return new MvxAsyncCommand(() => _navigationService.Navigate<NextViewModel>());
}
else
{
return new MvxAsyncCommand(() => _navigationService.Navigate<ErrorViewModel>());
}
}
I tried moving the runner into an async method and set a global variable with the response but I continue to experience this issue. If I comment the service call and leave just the navigationService code then the button is enabled so I guess the problem is the way I am making the service call into this method.
First of all your code snippet does not compile. If it were a Property Getter, it would block until _service.Register() completes, which is probably not what you want. Instead you should put that call into the command and await it:
private IMvxAsyncCommand _register;
public ICommand Register => _register = _register ?? new MvxAsyncCommand(DoRegisterCommand);
private async Task DoRegisterCommand()
{
var result = await _service.Register();
if (result.Status == SUCCESS)
await _navigationService.Navigate<NextViewModel>();
else
await _navigationService.Navigate<ErrorViewModel>();
}

Akavache always calling the fetchFunc

I have the following code using Akavache in a Xamarin app and it's not behaving the way I would think it should. Probably my misunderstanding of how it should be but it's driving me crazy.
So in my viewmodel I'm making the call to FetchNewsCategories and specifying a cache of 5 minutes for the item. What I'd expect to happen is that if the cache item is not there, it would make a call to the fetchFunc (ie. FetchNewsCategoriesAsync) but if I call the service any number of times inside the cache timeout of 5 minutes, it should just give me the cached item and not do the server call. In all cases that I've tried, it keeps doing the rest call and never gives me the cached item. I've also tried this with GetAndFetchLatest and if there is a cached item, it doesn't make the rest call but it also doesn't make the call in the subscribe event in the viewmodel.
Any ideas what I'm doing wrong here?
EDIT: I tested this same code on Android (Nexus 5 KitKat API19) and it's working flawlessly. I'm going to reset my IOS emulator and see if something was just out of whack.
NewsService.cs
public static async Task<ServiceResponse<List<ArticleCategoryInfo>>> FetchNewsCategoriesAsync(BlogSourceType blogSource)
{
return await ServiceClient.POST<List<ArticleCategoryInfo>>(Config.ApiUrl + "news/categories", new
{
ModuleId = Int32.Parse(Config.Values[blogSource == BlogSourceType.News ? ConfigKeys.KEY_NEWS_MODULE_ID : ConfigKeys.KEY_BLOG_MODULE_ID])
});
}
public static IObservable<ServiceResponse<List<ArticleCategoryInfo>>> FetchNewsCategories(BlogSourceType blogSource)
{
var cache = BlobCache.LocalMachine;
var cachedCategories = cache.GetOrFetchObject("categories" + blogSource,
async () => await FetchNewsCategoriesAsync(blogSource),
DateTime.Now.AddMinutes(5));
return cachedCategories;
}
NewsViewModel.cs
public async Task LoadCategories()
{
var cachedCategories = NewsService.FetchNewsCategories(blogSource);
cachedCategories.Subscribe((obj) => { Device.BeginInvokeOnMainThread(() => DisplayCategories(obj.Result,"Subscribe"));});
return;
}
private void DisplayCategories(IList<ArticleCategoryInfo> categories, string source)
{
Categories.Clear();
System.Diagnostics.Debug.WriteLine("Redisplaying categories from " + source);
foreach (var item in categories)
{
Categories.Add(item);
}
}
Just wanted to add my resolution to the issue I experienced above for reference to others with this problem.
The ServiceResponse object that I was trying to cache had an HttpResponseMessage in it which I suspect was causing a serialization error, probably a cyclical reference, so it never did get cached and ended up calling the endpoint every time. I ended up putting an [IgnoreDataMemberAttribute] on that property so it wasn't serialized and the problems went away.
I ended up handling the subscribe in the following manner to handle errors and to make sure the activity indicator bound to the IsBusy property was updated properly.
public async Task LoadActivities(bool refresh)
{
IsBusy = true;
if (refresh) OlderThanJournalId = int.MaxValue;
var cached = ActivityService.FetchJournalItems(GroupId, OlderThanJournalId, refresh);
cached.Subscribe((result) => { Device.BeginInvokeOnMainThread(() => {
DisplayActivities(result);
}); }, (err) => HandleError(err), () => IsBusy = false);
}
public void HandleError(Exception ex) {
IsBusy = false;
DialogService.ShowErrorToast(AppResources.ErrorMessage, "Unable to load activity stream.");
System.Diagnostics.Debug.WriteLine(ex.Message);
}
private void DisplayActivities(ServiceResponse<List<JournalItem>> response)
{
if (!response.IsConnected) {
DialogService.ShowInfoToast(AppResources.ErrorMessage, AppResources.NotConnected);
return;
}
if (!response.Authorized) {
App.LoginManager.Logout();
}
Activities.Clear();
foreach (var item in response.Result)
{
Activities.Add(item);
}
}
BeginInvokeOnMainThread is used to make sure that the updates to the ObservableCollection in DisplayActivities are seen in the UI.

How to intercept Navigation Bar Back Button Clicked in Xamarin Forms?

I have a xamarin form page where a user can update some data in a form. I need to intercept the Navigation Bar Back Button Clicked to warn the user if some data have not been saved.How to do it?
I'm able to intercept the hardware Bar Back Button Clicked in Android using the Android.MainActivity.OnBackPressed(), but that event is raised only on hardware Bar Back Button Clicked, not on Navigation Bar Back Button Clicked.
I tried also to override Xamarin.Forms.NavigationPageOnBackButtonPressed() but it doesn't work. Why?
Any one have already solved that issue?
I also tried by overriding OnDisappear(), there are two problems:
The page has already visually disappeared so the "Are you sure?" dialog appears over the previous page.
Cannot cancel the back action.
So, is it possible to intercept the navigation bar back button press?
I was able to show a confirmation dialog that could cancel navigation by overriding the following methods in the FormsApplicationActivity.
// navigation back button
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == 16908332)
{
var currentViewModel = (IViewModel)navigator.CurrentPage.BindingContext;
currentViewModel.CanNavigateFromAsync().ContinueWith(t =>
{
if (t.Result)
{
navigator.PopAsync();
}
}, TaskScheduler.FromCurrentSynchronizationContext());
return false;
}
else
{
return base.OnOptionsItemSelected(item);
}
}
// hardware back button
public async override void OnBackPressed()
{
var currentViewModel = (IViewModel)navigator.CurrentPage.BindingContext;
// could display a confirmation dialog (ex: "Cancel changes?")
var canNavigate = await currentViewModel.CanNavigateFromAsync();
if (canNavigate)
{
base.OnBackPressed();
}
}
The navigator.CurrentPage is a wrapper around the INavigation service. I do not have to cancel navigation from modal pages so I am only handling the NavigationStack.
this.navigation.NavigationStack[this.navigation.NavigationStack.Count - 1];
The easiest, as #JordanMazurke also somewhat mentions, since the event for the back button cannot be handled currently (other than the physical back button for Android), is to either:
NavigationPage.ShowHasBackButton(this, false)
Pushing a Modal instead of a Page
Then afterwards, you can add an ActionbarItem from where you can handle the Event.
I personally spoke to the iOS team from Xamarin concerning this matter, and they basically told me we shouldn't expect support for handling the Event for the BackButtonPressed in the NavigationBar. The reason being, that on iOS, it's bad practice for the users to receive a message when Back is pressed.
Best way I have found is adding my own NavigationRenderer to intercept the navigation methods and a simple Interface
[assembly: ExportRenderer(typeof(NavigationPage), typeof(CustomerMobile.Droid.NavigationPageRenderer))]
public class NavigationPageRenderer : Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer
{
public Activity context;
public NavigationPageRenderer(Context context)
: base(context)
{}
protected override Task<bool> OnPushAsync(Page page, bool animated) {...}
protected override Task<bool> OnPopToRootAsync(Page page, bool animated){...}
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
// if the page implements my interface then first check the page
//itself is not already handling a redirection ( Handling Navigation)
//if don't then let the handler to check whether to process
// Navitation or not .
if (page is INavigationHandler handler && !handler.HandlingNavigation
&& handler.HandlePopAsync(page, animated))
return Task.FromResult(false);
return base.OnPopViewAsync(page, animated);
}
}
Then my INavigationHandler interface would look like this
public interface INavigationHandler
{
public bool HandlingNavigation { get; }
public bool HandlePopAsync(Xamarin.Forms.Page view, bool animated);
public bool HandlePopToRootAsync(Xamarin.Forms.Page view, bool animated);
public bool HandlePuchAsync(Xamarin.Forms.Page view, bool animated);
}
Finally in any ContentView, in this example when trying to navigate back I'm just collapsing a menu and preventing a navigation back.
public partial class MenusList : INavigationHandler
{
public bool HandlingNavigation { get; private set; }
public bool HandlePopAsync(Page view, bool animated)
{
HandlingNavigation = true;
try
{
if (Menu.Expanded)
{
Menu.Collapse();
return true;
}
else return false;
}
finally
{
HandlingNavigation = false;
}
}
}
This is an inherently difficult task and the only way I got around it was to remove the back button entirely and then handle the backwards navigation from a 'save' button.
I have done a brief search of the Xamarin.Forms forum and the following has been suggested:
public override bool OnOptionsItemSelected(Android.Views.IMenuItem item)
{
return false;
}
The link for the post is as follows:
https://forums.xamarin.com/discussion/21631/is-there-nay-way-of-cancelling-the-back-button-event-from-the-navigationpage
As has already been said - you cannot do this cross-platform. However, you can handle it natively with arguably not so much effort: https://theconfuzedsourcecode.wordpress.com/2017/03/12/lets-override-navigation-bar-back-button-click-in-xamarin-forms/
The article covers iOS and Android. If you have a UWP project you'll have to hammer your own solution for it.
Edit:
Here is the UWP solution! It actually turned out to be pretty easy – there is just one back button and it’s supported by Forms so you just have to override ContentPage’s OnBackButtonPressed:
protected override bool OnBackButtonPressed()
{
if (Device.RuntimePlatform.Equals(Device.UWP))
{
OnClosePageRequested();
return true;
}
else
{
base.OnBackButtonPressed();
return false;
}
}
async void OnClosePageRequested()
{
var tdvm = (TaskDetailsViewModel)BindingContext;
if (tdvm.CanSaveTask())
{
var result = await DisplayAlert("Wait", "You have unsaved changes! Are you sure you want to go back?", "Discard changes", "Cancel");
if (result)
{
tdvm.DiscardChanges();
await Navigation.PopAsync(true);
}
}
else
{
await Navigation.PopAsync(true);
}
}
Back button appearance and behavior can be redefined by setting the BackButtonBehavior attached property to a BackButtonBehavior object.
<Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding GoBackCommand}"/></Shell.BackButtonBehavior>
GobackCommand defined in your View Model.
You can follow below link for more details:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/navigation
In Xamarin.Forms, the Page class has an OnBackButtonPressed() method that you can tap into for all platforms

How to wait for an async call to complete on WP7?

I'm loading pivot items based on a call a webservice call. Given that all I have is asynchronous calls available, how do I go about catching when it's finished?
My main reason is that I'd like to keep a loading dialog up while it's waiting for the callback. However, I'm loading in a viewmodel class, and obviously the loading bar is in the page class.
Honestly, if I could just know when one pivot item was loaded, that would be fine, however setting an event handler on loadedpivotitem never seems to trigger.
I assume you are databinding your View to your ViewModel. In that case all you need to do is create a bool property and set it to true while loading/awaiting the async call. You could do something like this:
private bool isSyncing;
public bool IsSynchronizing
{
get { return this.isSyncing; }
set
{
this.isSyncing = value;
this.RaisePropertyChanged(() => this.IsSynchronizing); //Use appropriate RaisePropertyChanged method for your MVVM implementation
}
}
Before starting the async call you would set IsSynchronizing = true. At the end of the eventhandler set IsSynchronizing = false;
From your view you can bind to this bool. For the loadingbar it could be like this:
<ProgressBar Visibility="{Binding IsSynchronizing, Converter={StaticResource booleanToVisibilityConverter}}" IsIndeterminate="{Binding IsSynchronizing}" Style="{StaticResource PerformanceProgressBar}" />
In your scenario you can use an inverted BooleanToVisibilityConverter to hide the pivot while it is still loading.
Hope this helps, let me know if you need more info on using the BooleanToVisibilityConverters
You would need to hook up an event handler similar to as shown in this block of code:
public void LoadData()
{
SampleDataServiceClient client = new SampleDataServiceClient();
client.GetDataCompleted += new EventHandler<GetDataCompletedEventArgs>(client_GetDataCompleted);
client.GetDataAsync();
}
void client_GetDataCompleted(object sender, GetDataCompletedEventArgs e)
{
this.DataContext = e.Result;
}

Resources