--- /dev/null
+<?xml version="1.0" encoding="utf-8" ?>
+<controls:TestContentPage
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:controls="clr-namespace:Xamarin.Forms.Controls"
+ x:Class="Xamarin.Forms.Controls.Issues.Issue7803">
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+
+ <StackLayout Grid.Row="0" Orientation="Vertical" Spacing="5">
+ <Label LineBreakMode="WordWrap" Text="Pull to refresh and verify that the last item shows 19. If not, the test has failed." HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+ <Label LineBreakMode="WordWrap" Text="{Binding Text}" HorizontalTextAlignment="Center"/>
+ </StackLayout>
+
+ <RefreshView Grid.Row="1" IsRefreshing="{Binding IsRefreshing}" Command="{Binding RefreshCommand}">
+ <CollectionView AutomationId="CollectionView7803" ItemsSource="{Binding Items}">
+ <CollectionView.ItemsLayout>
+ <LinearItemsLayout Orientation="Vertical" ItemSpacing="5"/>
+ </CollectionView.ItemsLayout>
+
+ <CollectionView.ItemTemplate>
+ <DataTemplate>
+ <Grid HeightRequest="100" BackgroundColor="Beige">
+ <Label Text="{Binding Position}" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
+ </Grid>
+ </DataTemplate>
+ </CollectionView.ItemTemplate>
+ </CollectionView>
+ </RefreshView>
+ </Grid>
+</controls:TestContentPage>
\ No newline at end of file
--- /dev/null
+using System.Collections.ObjectModel;
+using Xamarin.Forms.CustomAttributes;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.Xaml;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using System.ComponentModel;
+using System;
+
+#if UITEST
+using Xamarin.UITest;
+using Xamarin.UITest.Queries;
+using NUnit.Framework;
+using Xamarin.Forms.Core.UITests;
+using System.Linq;
+#endif
+
+namespace Xamarin.Forms.Controls.Issues
+{
+#if UITEST
+ [NUnit.Framework.Category(UITestCategories.CollectionView)]
+#endif
+#if APP
+ [XamlCompilation(XamlCompilationOptions.Compile)]
+#endif
+ [Preserve(AllMembers = true)]
+ [Issue(IssueTracker.Github, 7803, "[Bug] CarouselView/RefreshView pull to refresh command firing twice on a single pull", PlatformAffected.All)]
+ public partial class Issue7803 : TestContentPage
+ {
+#if APP
+ public Issue7803()
+ {
+
+ InitializeComponent();
+
+ BindingContext = new ViewModel7803();
+ }
+#endif
+
+ protected override void Init()
+ {
+
+ }
+
+#if UITEST
+ [Test]
+ public void DelayedIsRefreshingAndCommandTest_SwipeDown()
+ {
+ var collectionView = RunningApp.WaitForElement(q => q.Marked("CollectionView7803"))[0];
+
+ RunningApp.Pan(new Drag(collectionView.Rect, Drag.Direction.TopToBottom, Drag.DragLength.Medium));
+
+ RunningApp.WaitForElement(q => q.Marked("Count: 20"));
+ RunningApp.WaitForNoElement(q => q.Marked("Count: 30"));
+
+ AppResult[] lastCellResults = null;
+
+ RunningApp.QueryUntilPresent(() =>
+ {
+ RunningApp.DragCoordinates(collectionView.Rect.CenterX, collectionView.Rect.Y + collectionView.Rect.Height - 50, collectionView.Rect.CenterX, collectionView.Rect.Y + 5);
+
+ lastCellResults = RunningApp.Query("19");
+
+ return lastCellResults;
+ }, 10, 1);
+
+ Assert.IsTrue(lastCellResults?.Any() ?? false);
+ }
+#endif
+ }
+
+ [Preserve(AllMembers = true)]
+ public class ViewModel7803 : INotifyPropertyChanged
+ {
+ public ObservableCollection<Model7803> Items { get; set; } = new ObservableCollection<Model7803>();
+
+ private bool _isRefreshing;
+
+ public bool IsRefreshing
+ {
+ get
+ {
+ return _isRefreshing;
+ }
+ set
+ {
+ _isRefreshing = value;
+
+ OnPropertyChanged("IsRefreshing");
+ }
+ }
+
+ private string _text;
+
+ public string Text
+ {
+ get
+ {
+ return _text;
+ }
+ set
+ {
+ _text = value;
+
+ OnPropertyChanged("Text");
+ }
+ }
+
+ public Command RefreshCommand { get; set; }
+
+ public ViewModel7803()
+ {
+ PopulateItems();
+
+ RefreshCommand = new Command(async () =>
+ {
+ IsRefreshing = true;
+
+ await Task.Delay(2000);
+ PopulateItems();
+
+ IsRefreshing = false;
+ });
+ }
+
+ void PopulateItems()
+ {
+ var count = Items.Count;
+
+ for (var i = count; i < count + 10; i++)
+ Items.Add(new Model7803() { Position = i });
+
+ Text = "Count: " + Items.Count;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged(string name)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+ }
+
+ [Preserve(AllMembers = true)]
+ public class Model7803 : INotifyPropertyChanged
+ {
+ private int _position;
+
+ public int Position
+ {
+ get
+ {
+ return _position;
+ }
+ set
+ {
+ _position = value;
+
+ OnPropertyChanged("Position");
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged(string name)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+ }
+}
using System.Threading.Tasks;
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.Internals;
+using System;
#if UITEST
using Xamarin.Forms.Core.UITests;
public class RefreshViewTests : TestContentPage
{
RefreshView _refreshView;
+ Command _refreshCommand;
+
public RefreshViewTests()
{
}
protected override void Init()
{
Title = "Refresh View Tests";
- var scrollViewContent =
- new StackLayout()
- {
- };
+ var scrollViewContent = new StackLayout();
- Enumerable.Range(0, 10).Select(_ => new Label() { HeightRequest = 200, Text = "Pull me down to refresh me" })
+ Enumerable
+ .Range(0, 10)
+ .Select(_ => new Label() { HeightRequest = 200, Text = "Pull me down to refresh me" })
.ForEach(x => scrollViewContent.Children.Add(x));
+
+ bool canExecute = true;
+ _refreshCommand = new Command(async (parameter) =>
+ {
+ if(!_refreshView.IsRefreshing)
+ {
+ throw new Exception("IsRefreshing should be true when command executes");
+ }
+
+ if (parameter != null && !(bool)parameter)
+ {
+ throw new Exception("Refresh command incorrectly firing with disabled parameter");
+ }
+
+ await Task.Delay(2000);
+ _refreshView.IsRefreshing = false;
+ }, (object parameter) =>
+ {
+ return parameter != null && canExecute && (bool)parameter;
+ });
+
_refreshView = new RefreshView()
{
Content = new ScrollView()
Content = scrollViewContent,
AutomationId = "LayoutContainer"
},
- Command = new Command(async () =>
- {
- await Task.Delay(2000);
- _refreshView.IsRefreshing = false;
- })
+ Command = _refreshCommand,
+ CommandParameter = true
};
var isRefreshingLabel = new Label();
var label = new Label { BindingContext = _refreshView };
isRefreshingLabel.SetBinding(Label.TextProperty, new Binding("IsRefreshing", stringFormat: "IsRefreshing: {0}", source: _refreshView));
+ var commandEnabledLabel = new Label { BindingContext = _refreshView };
+ commandEnabledLabel.SetBinding(Label.TextProperty, new Binding("IsEnabled", stringFormat: "IsEnabled: {0}", source: _refreshView));
+
Content = new StackLayout()
{
Children =
{
isRefreshingLabel,
+ commandEnabledLabel,
new Button()
{
Text = "Toggle Refresh",
_refreshView.IsRefreshing = !_refreshView.IsRefreshing;
})
},
+ new Button()
+ {
+ Text = "Toggle Can Execute",
+ Command = new Command(() =>
+ {
+ canExecute = !canExecute;
+ _refreshCommand.ChangeCanExecute();
+ }),
+ AutomationId = "ToggleCanExecute"
+ },
+ new Button()
+ {
+ Text = "Toggle Can Execute Parameter",
+ Command = new Command(() =>
+ {
+ _refreshView.CommandParameter = !((bool)_refreshView.CommandParameter);
+ _refreshCommand.ChangeCanExecute();
+ }),
+ AutomationId = "ToggleCanExecuteParameter"
+ },
+ new Button()
+ {
+ Text = "Toggle Command Being Set",
+ Command = new Command(() =>
+ {
+ if(_refreshView.Command != null)
+ _refreshView.Command = null;
+ else
+ _refreshView.Command = _refreshCommand;
+ }),
+ AutomationId = "ToggleCommandBeingSet"
+ },
_refreshView
}
};
{
RunningApp.WaitForElement(q => q.Marked("IsRefreshing: False"));
- var container = RunningApp.WaitForElement("LayoutContainer")[0];
-
- RunningApp.Pan(new Drag(container.Rect, Drag.Direction.TopToBottom, Drag.DragLength.Medium));
-
+ TriggerRefresh();
RunningApp.WaitForElement(q => q.Marked("IsRefreshing: True"));
RunningApp.Screenshot("Refreshing");
RunningApp.WaitForElement(q => q.Marked("IsRefreshing: False"));
RunningApp.Screenshot("Refreshed");
}
+
+ [Test]
+ public void RefreshDisablesWithCommand()
+ {
+ RunningApp.WaitForElement("IsRefreshing: False");
+ RunningApp.Tap("ToggleCanExecute");
+ RunningApp.WaitForElement("IsEnabled: False");
+ TriggerRefresh();
+
+ var results = RunningApp.Query("IsRefreshing: True");
+ Assert.AreEqual(0, results.Length);
+ results = RunningApp.Query("IsRefreshing: True");
+ Assert.AreEqual(0, results.Length);
+ }
+
+ void TriggerRefresh()
+ {
+ var container = RunningApp.WaitForElement("LayoutContainer")[0];
+ RunningApp.Pan(new Drag(container.Rect, Drag.Direction.TopToBottom, Drag.DragLength.Medium));
+
+ }
#endif
}
}
\ No newline at end of file
<Compile Include="$(MSBuildThisFileDirectory)Issue7865.xaml.cs">
<SubType>Code</SubType>
</Compile>
+ <Compile Include="$(MSBuildThisFileDirectory)Issue7803.xaml.cs">
+ <DependentUpon>Issue7803.xaml</DependentUpon>
+ </Compile>
<Compile Include="$(MSBuildThisFileDirectory)RefreshViewTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Issue7338.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ScrollToGroup.cs" />
<Generator>MSBuild:Compile</Generator>
</EmbeddedResource>
</ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue7803.xaml">
+ <SubType>Designer</SubType>
+ <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+ </EmbeddedResource>
+ </ItemGroup>
</Project>
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using NUnit.Framework;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.Core.UnitTests
+{
+ [TestFixture]
+ public class RefreshViewTests : BaseTestFixture
+ {
+ [TearDown]
+ public override void TearDown()
+ {
+ base.TearDown ();
+ Device.PlatformServices = null;
+ Device.Info = null;
+ }
+
+ [SetUp]
+ public override void Setup ()
+ {
+ base.Setup ();
+ Device.PlatformServices = new MockPlatformServices ();
+ Device.Info = new TestDeviceInfo ();
+ }
+
+ [Test]
+ public void StartsEnabled()
+ {
+ RefreshView refreshView = new RefreshView();
+ Assert.IsTrue(refreshView.IsEnabled);
+ }
+
+ [Test]
+ public void CanExecuteDisablesRefreshView()
+ {
+ RefreshView refreshView = new RefreshView();
+ refreshView.Command = new Command(() => { }, () => false);
+ Assert.IsFalse(refreshView.IsEnabled);
+ }
+
+ [Test]
+ public void CanExecuteEnablesRefreshView()
+ {
+ RefreshView refreshView = new RefreshView();
+ refreshView.Command = new Command(() => { }, () => true);
+ Assert.IsTrue(refreshView.IsEnabled);
+ }
+
+ [Test]
+ public void CanExecuteChangesEnabled()
+ {
+ RefreshView refreshView = new RefreshView();
+
+ bool canExecute = true;
+ var command = new Command(() => { }, () => canExecute);
+ refreshView.Command = command;
+
+ canExecute = false;
+ command.ChangeCanExecute();
+ Assert.IsFalse(refreshView.IsEnabled);
+
+
+ canExecute = true;
+ command.ChangeCanExecute();
+ Assert.IsTrue(refreshView.IsEnabled);
+ }
+
+ [Test]
+ public void CommandPropertyChangesEnabled()
+ {
+ RefreshView refreshView = new RefreshView();
+
+ bool canExecute = true;
+ var command = new Command((p) => { }, (p) => p != null && (bool)p);
+ refreshView.CommandParameter = true;
+ refreshView.Command = command;
+
+ Assert.IsTrue(refreshView.IsEnabled);
+ refreshView.CommandParameter = false;
+ Assert.IsFalse(refreshView.IsEnabled);
+ refreshView.CommandParameter = true;
+ Assert.IsTrue(refreshView.IsEnabled);
+ }
+
+ [Test]
+ public void RemovedCommandEnablesRefreshView()
+ {
+ RefreshView refreshView = new RefreshView();
+
+ bool canExecute = true;
+ var command = new Command(() => { }, () => false);
+ refreshView.Command = command;
+ Assert.IsFalse(refreshView.IsEnabled);
+ refreshView.Command = null;
+ Assert.IsTrue(refreshView.IsEnabled);
+ refreshView.Command = command;
+ Assert.IsFalse(refreshView.IsEnabled);
+ }
+
+ [Test]
+ public void IsRefreshingStaysFalseWithDisabledCommand()
+ {
+ RefreshView refreshView = new RefreshView();
+
+ bool canExecute = true;
+ refreshView.Command = new Command(() => { }, () => false);
+ refreshView.IsRefreshing = true;
+ Assert.IsFalse(refreshView.IsRefreshing);
+ }
+
+ [Test]
+ public void IsRefreshingSettableToTrue()
+ {
+ RefreshView refreshView = new RefreshView();
+ Assert.IsFalse(refreshView.IsRefreshing);
+
+ refreshView.IsRefreshing = true;
+ Assert.IsTrue(refreshView.IsRefreshing);
+ }
+
+ [Test]
+ public void IsRefreshingStaysFalseWithDisabledRefreshView()
+ {
+ RefreshView refreshView = new RefreshView();
+ refreshView.IsEnabled = false;
+ refreshView.IsRefreshing = true;
+ Assert.IsFalse(refreshView.IsRefreshing);
+ }
+
+ [Test]
+ public void IsRefreshingTogglesFalseWhenIsEnabledSetToFalse()
+ {
+ RefreshView refreshView = new RefreshView();
+ refreshView.IsRefreshing = true;
+ refreshView.IsEnabled = false;
+ Assert.IsFalse(refreshView.IsRefreshing);
+ }
+
+ [Test]
+ public void IsRefreshingEventFires()
+ {
+ RefreshView refreshView = new RefreshView();
+ bool eventFired = false;
+ refreshView.Refreshing += (_, __) => eventFired = true;
+ Assert.IsFalse(eventFired);
+ refreshView.IsRefreshing = true;
+ Assert.IsTrue(eventFired);
+ }
+ }
+}
<Compile Include="CommandSourceTests.cs" />
<Compile Include="CommandTests.cs" />
<Compile Include="DependencyResolutionTests.cs" />
+ <Compile Include="RefreshViewTests.cs" />
<Compile Include="MockDispatcherProvider.cs" />
<Compile Include="MockDispatcher.cs" />
<Compile Include="DeviceUnitTests.cs" />
using System;
+using System.Runtime.CompilerServices;
using System.Windows.Input;
using Xamarin.Forms.Platform;
public class RefreshView : ContentView, IElementConfiguration<RefreshView>
{
readonly Lazy<PlatformConfigurationRegistry<RefreshView>> _platformConfigurationRegistry;
+ public event EventHandler Refreshing;
public RefreshView()
{
}
public static readonly BindableProperty IsRefreshingProperty =
- BindableProperty.Create(nameof(IsRefreshing), typeof(bool), typeof(RefreshView), false, BindingMode.TwoWay);
+ BindableProperty.Create(nameof(IsRefreshing), typeof(bool), typeof(RefreshView), false, BindingMode.TwoWay, coerceValue: OnIsRefreshingPropertyCoerced, propertyChanged: OnIsRefreshingPropertyChanged);
+
+ static void OnIsRefreshingPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ bool value = (bool)newValue;
+
+ if (!value)
+ return;
+
+ var refreshView = ((RefreshView)bindable);
+ refreshView.Refreshing?.Invoke(bindable, EventArgs.Empty);
+ if (refreshView.Command != null)
+ refreshView.Command.Execute(refreshView.CommandParameter);
+ }
+
+ static object OnIsRefreshingPropertyCoerced(BindableObject bindable, object value)
+ {
+ RefreshView view = (RefreshView)bindable;
+ bool newValue = (bool)value;
+
+ // IsRefreshing can always be toggled to false
+ if (!newValue)
+ return value;
+
+ if (!view.IsEnabled)
+ return false;
+
+ if (view.Command == null)
+ return value;
+
+ if (!view.Command.CanExecute(view.CommandParameter))
+ return false;
+
+ return value;
+ }
public bool IsRefreshing
{
}
public static readonly BindableProperty CommandProperty =
- BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(RefreshView));
+ BindableProperty.Create(nameof(Command), typeof(ICommand), typeof(RefreshView), propertyChanged: OnCommandChanged);
+
+ static void OnCommandChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ RefreshView refreshView = (RefreshView)bindable;
+ if (oldValue is ICommand oldCommand)
+ oldCommand.CanExecuteChanged -= refreshView.RefreshCommandCanExecuteChanged;
+
+ if (newValue is ICommand newCommand)
+ newCommand.CanExecuteChanged += refreshView.RefreshCommandCanExecuteChanged;
+
+ refreshView.RefreshCommandCanExecuteChanged(bindable, EventArgs.Empty);
+ }
public ICommand Command
{
typeof(object),
typeof(RefreshView),
null,
- propertyChanged: (bindable, oldvalue, newvalue) => ((RefreshView)bindable).RefreshCommandCanExecuteChanged(bindable, EventArgs.Empty));
+ propertyChanged: (bindable, oldvalue, newvalue) => ((RefreshView)(bindable)).RefreshCommandCanExecuteChanged(((RefreshView)(bindable)).Command, EventArgs.Empty));
public object CommandParameter
{
void RefreshCommandCanExecuteChanged(object sender, EventArgs eventArgs)
{
if (Command != null)
- IsEnabled = Command.CanExecute(CommandParameter);
+ SetValueCore(IsEnabledProperty, Command.CanExecute(CommandParameter));
+ else
+ SetValueCore(IsEnabledProperty, true);
}
public static readonly BindableProperty RefreshColorProperty =
{
return _platformConfigurationRegistry.Value.On<T>();
}
+
+ protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ base.OnPropertyChanged(propertyName);
+ if (IsEnabledProperty.PropertyName == propertyName)
+ if (!IsEnabled && IsRefreshing)
+ IsRefreshing = false;
+ }
}
}
\ No newline at end of file
_refreshing = value;
if (RefreshView != null && RefreshView.IsRefreshing != _refreshing)
- RefreshView.IsRefreshing = _refreshing;
+ RefreshView.SetValueFromRenderer(RefreshView.IsRefreshingProperty, _refreshing);
base.Refreshing = _refreshing;
-
- if (base.Refreshing && Element is RefreshView refreshView && refreshView.Command != null && refreshView.Command.CanExecute(refreshView?.CommandParameter))
- refreshView.Command.Execute(refreshView?.CommandParameter);
}
}
CompleteRefresh();
_refreshCompletionDeferral = args.GetDeferral();
Element.SetValueFromRenderer(RefreshView.IsRefreshingProperty, true);
- if (Element?.Command?.CanExecute(Element?.CommandParameter) ?? false)
- {
- Element.Command.Execute(Element?.CommandParameter);
- }
}
}
}
\ No newline at end of file
_isRefreshing = value;
if (Element != null && Element.IsRefreshing != _isRefreshing)
- Element.IsRefreshing = _isRefreshing;
+ Element.SetValueFromRenderer(RefreshView.IsRefreshingProperty, _isRefreshing);
- if (_isRefreshing)
+
+ if (_isRefreshing != _refreshControl.Refreshing)
{
- _refreshControl.BeginRefreshing();
+ if (_isRefreshing)
+ _refreshControl.BeginRefreshing();
+ else
+ _refreshControl.EndRefreshing();
- if (Element is RefreshView refreshView && refreshView.Command != null && refreshView.Command.CanExecute(refreshView?.CommandParameter))
- refreshView.Command.Execute(refreshView?.CommandParameter);
+ TryOffsetRefresh(this, IsRefreshing);
}
- else
- _refreshControl.EndRefreshing();
-
- TryOffsetRefresh(this, IsRefreshing);
}
}