Xamarin Forms ListView Programatic Refresh Not Stopping on Android When Page Loaded
Asked Answered
R

3

9

I have Portable project in Visual Studio 2015 with a ListView that gets populated with some data via an API call through the following function refreshData:

async Task refreshData()
{
    myListView.BeginRefresh();
    var apiCallResult = await App.Api.myApiGetCall();            
    myListView.ItemsSource = apiCallResult;
    myListView.EndRefresh();    
}

refreshData() is called in

protected override void OnAppearing()
{
    base.OnAppearing();
    refreshData();
}

Everything is working fine except on Android where the refresh indicator is not stopping or disappearing on EndRefresh() when the page is initially loaded. The page is in a TabbedPage so I can go to a different tab and then return to this page and the refresh indicator properly starts and stops with completion of my API call.

Why is refresh is not stopping when the page initially loads on Android? Any help would be appreciated.

Note: This works perfectly fine when I run on iOS.

So far I've tried:

  1. replacing myListView.BeginRefresh() with myListView.IsRefreshing = true and myListView.EndRefresh() with myListView.IsRefreshing = false

  2. Using Device.BeginInvokeOnMainThread(() => {//update list and endRefresh}).

  3. Using async void refreshData() instead of async Task refreshData().

Restorative answered 10/2, 2017 at 17:25 Comment(14)
try to use Device.BeginInvokeOnMainThread (//your code related update list view);Cordie
just tried, didn't work.Restorative
try to use only myListView.EndRefresh(); ..remove myListView.Isreferhing=falseCordie
I've tried that as well. Doesn't make a difference.Restorative
try a simple list without asyn and await . maybe async is the reasonCordie
I've tried already without async when building it out and it was fine.Restorative
Let us continue this discussion in chat.Cordie
protected override void OnAppearing() { Device.BeginInvokeOnMainThread(()=> myListView.BeginRefresh()); } myListView.Refreshing += (object sender, EventArgs e) => { refreshData(); }; void refreshData() { Device.BeginInvokeOnMainThread ( var apiCallResult = App.Api.myApiGetCall(); myListView.ItemsSource = apiCallResult; myListView.EndRefresh();); }Cordie
@DerFlickscter did you try above codeCordie
Did you find a solutionCordie
I tried to reproduce your issue, by my side, if I put the ListView page under a TabbedPage, it will refresh twice when the page is initialized, and if it is not under a TabbedPage, it works fine. What you mean then by "not stop refresh"? Could you please share us a minimal reproducible demo?Chretien
@GraceFeng-MSFT When I mean by "not stop refresh", I mean the refreshing spinning indicator does not disappear.Restorative
@DerFlickschter, then how will your refreshing spinning indicator be shown? By default, I can't see any refreshing spinning indicator. Each time when you load data, how many items will you load?Chretien
@GraceFeng-MSFT The refresh spinning indicator is built into the ListView. When I load data it will be generally in the range of 0-10 items.Restorative
A
4

Personally I can get this problem when I start ListView refreshing in the Page Contructor and stop it after the data is loaded. Sometimes (quite often) Xamarin.Forms ListView doesn't cancel refreshing animation.

I believe you faced with a quite common issue with Android SwipeRefreshLayout: it may not stop refreshing animation after setRefreshing(false) called. Native Android developers use the following approach:

swipeRefreshLayout.post(new Runnable() {
    @Override
        public void run() {
            mSwipeRefreshLayout.setRefreshing(refreshing);
    }
});

Interestingly, Xamarin.Forms uses this approach when it sets initial refreshing status (code); however, it is not enough. You need a custom renderer:

public class ExtendedListViewRenderer : ListViewRenderer
{
    /// <summary>
    /// The refresh layout that wraps the native ListView.
    /// </summary>
    private SwipeRefreshLayout _refreshLayout;

    public ExtendedListViewRenderer(Android.Content.Context context) : base(context)
    {
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _refreshLayout = null;
        }
        base.Dispose(disposing);
    }

    protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
    {
        base.OnElementChanged(e);
        _refreshLayout = (SwipeRefreshLayout)Control.Parent;
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == ListView.IsRefreshingProperty.PropertyName)
        {
            // Do not call base method: we are handling it manually
            UpdateIsRefreshing();
            return;
        }
        base.OnElementPropertyChanged(sender, e);
    }

    /// <summary>
    /// Updates SwipeRefreshLayout animation status depending on the IsRefreshing Element 
    /// property.
    /// </summary>
    protected void UpdateIsRefreshing()
    {
        // I'm afraid this method can be called after the ListViewRenderer is disposed
        // So let's create a new reference to the SwipeRefreshLayout instance
        SwipeRefreshLayout refreshLayoutInstance = _refreshLayout;

        if (refreshLayoutInstance == null)
        {
            return;
        }

        bool isRefreshing = Element.IsRefreshing;
        refreshLayoutInstance.Post(() =>
        {
            refreshLayoutInstance.Refreshing = isRefreshing;
        });
    }
}
Abm answered 13/7, 2018 at 17:19 Comment(0)
G
2

You need to follow MVVM Pattern

On your ViewModel you need to:

  • Implement INotifyPropertyChanged

  • Define properties like:

     private bool _IsRefreshing;
     public bool IsRefreshing
     {
         get { return _IsRefreshing; }
         set { SetProperty(ref _IsRefreshing, value; } 
         /*So every time the property changes the view is notified*/
     }
    
  • Define the method that fetch your data, in your case refreshData()

  • Toggle the IsRefreshing true/false when needed

On your Page you need to:

  • Bind the listview itemSource to a VM property with the SetPropertyValue

  • Bind ListView.IsRefreshing to ViewModel's IsRefreshing:

    MyListView.SetBinding<MyViewModel>(ListView.IsRefreshing, vm => vm.IsRefreshing);

Here is a great article talking about INotifyPropertyChanged

Gunstock answered 25/2, 2017 at 20:26 Comment(3)
Why has this answer received a down vote? This is how it should be done, even though the code shared uses an additional framework. But the idea is to make a one way binding for the IsRefreshing property, and make it implement INotifyPropertyChanged, and set it to false after your code that updates your list.Whittington
@Elisabeth that's true, but I can confirm that this can lead to an endless refreshing indicator in a tabbed page. I have it inside a Task.Run(async () => await //.. and changing the "IsRefreshing" bool property of the VM thats bound to the ListView will sometimes not propergate the change event to the View. What Ingenator described is as he said just a "MVVM pattern". Nothing more.Agog
@Agog Beware when updating in a task. If you have run off the main thread or didn’t complete the work, it is when your property change will never propagate to the view. Use ‘Device.BeginInvokeOnMainThread’ to update.Whittington
D
2

Try this:

async void refreshData()
{
    Device.BeginInvokeOnMainThread(() => {
        myListView.BeginRefresh();
        var apiCallResult = await App.Api.myApiGetCall();
        myListView.ItemsSource = apiCallResult;
        myListView.EndRefresh();
    });
}

Apparently, there is no need for "Task" anymore.

If the error occurs only in Android, it's possible that it's just the way Android handles threads. It does not allow threads to change visual elements directly. Sometimes when we try to do that it throws a silent exception and the code action have no practical effect.

Dasya answered 1/6, 2017 at 1:2 Comment(2)
> Aparantly you don't need of the "Task" return too. Well, you probably need it, otherwise you would execute network call in the UI threadAbm
You're right @NikolaiDoronin. It make sense. So the whole approach in my answer is wrong. It's not the best way to do thatDasya

© 2022 - 2024 — McMap. All rights reserved.