Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Commit

Permalink
ListView: avoid that disabling RefreshAllowed cancels refresh indicat…
Browse files Browse the repository at this point in the history
…or on Android, fixes #8384 (#14816)

* Add test for issue #8384

* ListView: avoid that disabling RefreshAllowed cancels refresh
indicator on Android, fixes #8384

RefreshAllowed is bound to ListView.RefreshCommand.CanExecute().
Often an implementation of RefreshCommand might update its CanExecute status
after execution starts. This caused ListViewRenderer to immediately
disable the SwipeRefreshLayout, thereby cancelling the refresh/activity
indicator on top of the list view.

The solution is to NOT disable it while refreshing, but waiting for the
next chance when current refresh activity/command is done.

* Only enable Issue8384 constructor for XF controls app build (not for UI unit tests)

Otherwise we get 'InitializeComponent' not found error when compiling UI unit test projects.
  • Loading branch information
cpraehaus committed Nov 4, 2021
1 parent e3faa59 commit 085ad87
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 1 deletion.
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Test 8384"
x:Class="Xamarin.Forms.Controls.Issues.Issue8384">

<StackLayout>
<Label Text="Test for issue #8384" />
<ListView IsPullToRefreshEnabled="True" ItemsSource="{Binding Items}" RefreshCommand="{Binding Refresh}"
IsRefreshing="{Binding IsRefreshing}">
</ListView>
<StackLayout>
<Label Text="Test steps (Android):" />
<Label Text="1. Swipe to refresh the list (refresh takes 4 seconds)" />
<Label Text="2. Refresh/busy indicator appears at the top" />
<Label Text="3. Refresh/busy indicator should remain visible until list content changes (refresh is finished).
due to issue #8384 the indicator vanishes too early if Command.CanExecute toggles (in this test after 1 second)." />
<Label Text="4. Repeat steps 1 to 3 multiple times" />
</StackLayout>
</StackLayout>
</ContentPage>
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;

using Xamarin.Forms;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;

namespace Xamarin.Forms.Controls.Issues
{
// Learn more about making custom code visible in the Xamarin.Forms previewer
// by visiting https://aka.ms/xamarinforms-previewer
[DesignTimeVisible(false)]
[Preserve(AllMembers = true)]
[Issue(IssueTracker.Github, 8384,
"[Bug] [5.0] [Android] [Bug] ListView RefreshCommand ActivityIndicator does disappear on Android if CanExecute is changed to false",
PlatformAffected.Android)]
public partial class Issue8384 : ContentPage
{
public Issue8384()
{
#if APP
InitializeComponent();
BindingContext = new ViewModelIssue8384();
#endif
}
}

class ViewModelIssue8384 : INotifyPropertyChanged
{
public class MyCommand : Command
{
private bool _allow;

public MyCommand(Action<object> execute, Func<object, bool> canExecute) : base(execute, canExecute)
{
Allow = true;
}

public bool Allow
{
get
{
return _allow;
}
set
{
_allow = value;
ChangeCanExecute();
}
}
}

private List<string> _items;
private bool _isRefreshing;
private MyCommand _refresh;

static readonly List<string> FIRST_LIST = new List<string>() {
"one", "two", "three"
};

static readonly List<string> SECOND_LIST = new List<string>() {
"four", "five", "six"
};

public bool IsRefreshing
{
get
{
return _isRefreshing;
}
set
{
_isRefreshing = value;
OnPropertyChanged("IsRefreshing");
}
}

public ViewModelIssue8384()
{
Items = FIRST_LIST;

Refresh = new MyCommand(Execute, CanExecute);
}

public event PropertyChangedEventHandler PropertyChanged;

private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

private async void Execute(object parameter)
{
IsRefreshing = true;
Debug.WriteLine("Refresh start");
await Task.Delay(1000).ConfigureAwait(false);

// Side note: doing this off the main thread throws an exception
Device.BeginInvokeOnMainThread(() => { _refresh.Allow = false; });

await Task.Delay(3000).ConfigureAwait(false);
Items = (Items == FIRST_LIST) ? SECOND_LIST : FIRST_LIST;

Debug.WriteLine("Refresh end");
IsRefreshing = false;

Device.BeginInvokeOnMainThread(() => { _refresh.Allow = true; });
}

private bool CanExecute(object parameter)
{
return _refresh.Allow;
}

public List<string> Items
{
get
{
return _items;
}

set
{
_items = value;
OnPropertyChanged("Items");
}
}

public MyCommand Refresh
{
get
{
return _refresh;
}
set
{
_refresh = value;
OnPropertyChanged("Refresh");
}
}
}
}
Expand Up @@ -1809,6 +1809,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Issue14697.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8383.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8383-2.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue8384.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue13577.xaml.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue14505.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue14505-II.cs" />
Expand Down Expand Up @@ -2320,6 +2321,9 @@
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue8383-2.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue8384.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue13577.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
Expand Down
16 changes: 15 additions & 1 deletion Xamarin.Forms.Platform.Android/Renderers/ListViewRenderer.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using Android.Content;
using Android.Runtime;
using Android.Views;
Expand Down Expand Up @@ -428,13 +429,26 @@ void UpdateIsRefreshing(bool isInitialValue = false)
}
else
_refresh.Refreshing = isRefreshing;

// Allow to disable SwipeToRefresh layout AFTER refresh is done
UpdateIsSwipeToRefreshEnabled();
}
}

void UpdateIsSwipeToRefreshEnabled()
{
if (_refresh != null)
_refresh.Enabled = Element.IsPullToRefreshEnabled && (Element as IListViewController).RefreshAllowed;
{
var isEnabled = Element.IsPullToRefreshEnabled && (Element as IListViewController).RefreshAllowed;
_refresh.Post(() =>
{
// NOTE: only disable while NOT refreshing, otherwise Command bindings CanExecute behavior will effectively
// cancel refresh animation. If not possible right now we will be called by UpdateIsRefreshing().
// For details see https://github.com/xamarin/Xamarin.Forms/issues/8384
if (isEnabled || !_refresh.Refreshing)
_refresh.Enabled = isEnabled;
});
}
}

void UpdateFastScrollEnabled()
Expand Down

0 comments on commit 085ad87

Please sign in to comment.