Button is disabled when binding - xamarin

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>();
}

Related

State of RefreshView is invoking RefreshCommand in .NET MAUI

I have a CollectionView in my .NET MAUI app and I placed it inside a RefreshView. When I call my API to populate this CollectionView, I cache the data so that I don't have to keep making API calls everytime the user hits this page.
In order to streamline my code, I created a private method in my view model that calls my API. The view model looks like this:
public partial MyViewModel : BaseViewModel
{
ObservableCollection<MyModel> MyData { get; } = new();
[RelayCommand]
async Task RefreshData()
{
IsBusy = true;
await GetData(true);
IsBusy = false;
}
private async Task GetData(bool shouldGetFreshData)
{
// Makes API call to get data, then assigns it to MyData collection
}
public async void Init()
{
IsBusy = true;
await GetData(false);
IsBusy = false;
}
}
The XAML for the page looks like this:
<RefreshView
IsRefreshing={Binding IsBusy}
Command={Binding RefreshDataCommand}>
<CollectionView>
...
</CollectionView>
</RefreshView>
I also wired the page to use the MyViewModel as its view model AND OnAppearing(), I call the Init() method of the view model.
Here's what I was expecting which is NOT what's happening:
I thought, the Init() would get called first which then calls the GetData() method with false input parameter. This way, I could use the cached data. And whenever, the user refreshes the CollectionView by pulling it down, the RefreshData() method would be called with true as the input parameter which would force the code to make an API call.
Instead of what I was expecting, here's what's happening:
The Init() method gets called first and as a result, the line with IsBusy = true executes.
This then ends up invoking the RefreshData() method
Then the await GetData(false) in Init() method executes
Then the await GetData(true) in RefreshData() method executes
As a result of all this, the GetData() method gets called twice.
I think, what's triggering this is the IsBusy. I thought IsBusy would only serve as an indicator but not necessarily invoke the RefreshData() method which is bound to the Command of my RefreshView.
Is this normal behavior or am I missing something here?
Apparently, this is "normal" behavior because I'm manually setting IsBusy to true. I decided to leave this question here because this may be a pitfall that affects others.
Here's the actual section in documentation that states this:
And here's the documentation: https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/refreshview
So, all I had to do is remove the IsBusy = true in Init() method.

testing navigation from one view model to other viewmodel

I am trying to create test which be able to check navigation between two viewmodels.
Some code from ResultViewModel
private IMvxCommand homeCommand;
public IMvxCommand HomeCommand => homeCommand ?? (homeCommand = new MvxAsyncCommand(ShowHomePage));
private async Task ShowHomePage()
{
await _navigationService.Navigate<HomeViewModel>();
}
Testing code
private Mock mvxNavigationService = new Mock();
private Mock resultService = new Mock();
[Test]
public void Test1()
{
var viewModel = new ResultViewModel(mvxNavigationService.Object, resultService.Object);
viewModel.HomeCommand.Execute();
mvxNavigationService.Verify(service => service.Navigate<HomeViewModel>());
Assert.Pass();
}
I am getting error
"An expression tree cannot contain a call or invocation that uses optional arguments."
on line
mvxNavigationService.Verify(service => service.Navigate());
Not sure where the issue is.
In Xamarin, a dedicated page navigation method is officially provided.
You can navigate between pages like:
await Navigation.PushAsync (new Page());
You can pop pages from the navigation stack like:
await Navigation.PopAsync ();
For more usage, please refer to: Hierarchical Navigation.

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();
}
}

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

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
});

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.

Resources