dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.24k stars 1.76k forks source link

RefreshView executes command on Page loading #6456

Open AndreasReitberger opened 2 years ago

AndreasReitberger commented 2 years ago

Description

I'm using a RefreshView and noticed that the command is fired on the initial page loading. Is this intended?

<RefreshView
            IsRefreshing="{Binding IsChecking}" 
            Command="{Binding CheckConfigurationCommand}"
            />

Steps to Reproduce

  1. Add a RefreshView and bind a command
  2. Start the app

Version with bug

Release Candidate 1 (current)

Last version that worked well

Unknown/Other

Affected platforms

iOS, I was not able test on other platforms

Affected platform versions

iOS

Did you find any workaround?

Add a "IsStartup" boolean to the CanExecute method for the Command binded to the RefreshView

Relevant log output

No response

VincentBu commented 2 years ago

hi @AndreasReitberger, would you like to provide a sample project?

AndreasReitberger commented 2 years ago

hi @AndreasReitberger, would you like to provide a sample project?

Sure, I'll create a sample and link it here.

AndreasReitberger commented 2 years ago

I created a small repo showing the behavior. I cannot reproduce the issue in my app that the Command gets called on startup, however it fires twice. Maybe this is the problem at all. I do call the ConnectCommand in code behind on startup (in my ported app, then also the RefreshView executes the command (called twice then).

Just set a breaking point to LoadingPageViewModel => Task ConnectAction()

Then click the "Refresh" Button. The Command get fires twice, if the same Command is bound to the RefreshView. I'm also not able to pull to refresh in this case.

image

using Issue_6455.Models;
using System.Diagnostics;

namespace Issue_6455.ViewModels
{
    public class LoadingPageViewModel : BaseViewModel
    {
        #region Properties

        int _counter = 0;
        public int Counter
        {
            get => _counter;
            set
            {
                if (_counter == value) return;
                _counter = value;
                OnPropertyChanged();
            }
        }
        #endregion

        #region Constructor
        public LoadingPageViewModel()
        {
            ConnectCommand = new Command(async () => await ConnectAction(), ConnectCommand_CanExcecute);
        }
        #endregion

        #region Commands
        public Command ConnectCommand { get; set; }
        bool ConnectCommand_CanExcecute()
        {
            return !IsConnecting;
        }
        async Task ConnectAction()
        {
            try
            {
                Counter++;
                IsConnecting = true;
                await Task.Delay(2500);
            }
            catch (Exception exc)
            {
                // Log error
                Debug.WriteLine(exc.Message);
            }
            IsConnecting = false;
        }
        #endregion
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="Issue_6455.Views.LoadingPage"

    xmlns:viewModels="clr-namespace:Issue_6455.ViewModels"
    >
    <ContentPage.BindingContext>
        <viewModels:LoadingPageViewModel x:Name="ViewModel" />
    </ContentPage.BindingContext>
    <Grid>
        <RefreshView
            IsRefreshing="{Binding IsConnecting}" 
            Command="{Binding ConnectCommand}"
            >
            <Grid
                RowDefinitions="*,80"
                >
                <Label
                    VerticalTextAlignment="Center"
                    HorizontalTextAlignment="Center"
                    >
                    <Label.FormattedText>
                        <FormattedString>
                            <FormattedString.Spans>
                                <Span Text="Command fired: " />
                                <Span Text="{Binding Counter}" />
                            </FormattedString.Spans>
                        </FormattedString>
                    </Label.FormattedText>
                </Label>

                <Button
                    Grid.Row="1"
                    Text="Refresh"
                    Command="{Binding ConnectCommand}"
                    Margin="20,10"
                    />
            </Grid>
        </RefreshView>
    </Grid>
</ContentPage>

Repo: Issue_6456.zip

v-longmin commented 2 years ago

Verified repro on iOS 15.4 with VS 17.3.0 Preview 1.0 [32414.199.main]. Repro project: Issue_6456.zip

AndreasReitberger commented 2 years ago

Any news when this will be fixed? Thank you!

ggutschi commented 2 years ago

Reproduced on Windows with VS17.3.0 Preview 2.0. I'm using Shell navigation and I call the RefreshCommand on OnNavigatedTo.

Call stack of first call:

[Namespace].Maui.dll![Namespace].Maui.PageModels.EntitiesPageViewModel<[Namespace].Common.Models.TimeEntry, int, [Namespace].Maui.Models.TimeEntryViewModel>.Refresh() Line 35
    at [ProjectPath]\PageModels\EntitiesPageViewModel.cs(35)
CommunityToolkit.Mvvm.dll!CommunityToolkit.Mvvm.Input.AsyncRelayCommand.ExecuteAsync(object parameter)
[Namespace].Maui.dll![Namespace].Maui.Pages.TimeEntriesPage.OnNavigatedTo(Microsoft.Maui.Controls.NavigatedToEventArgs args) Line 21
    at [ProjectPath]\Pages\TimeEntriesPage.xaml.cs(21)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Page.SendNavigatedTo(Microsoft.Maui.Controls.NavigatedToEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Shell.SendNavigated(Microsoft.Maui.Controls.ShellNavigatedEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Shell..ctor.AnonymousMethod__169_0(object _, Microsoft.Maui.Controls.ShellNavigatedEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellNavigationManager.HandleNavigated.__FireNavigatedEvents|1(Microsoft.Maui.Controls.ShellNavigatedEventArgs a, Microsoft.Maui.Controls.Shell shell)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellNavigationManager.HandleNavigated.AnonymousMethod__0()
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BaseShellItem.OnAppearing(System.Action action)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellNavigationManager.HandleNavigated(Microsoft.Maui.Controls.ShellNavigatedEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Shell.Microsoft.Maui.Controls.IShellController.UpdateCurrentState(Microsoft.Maui.Controls.ShellNavigationSource source)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellItem.OnCurrentItemChanged(Microsoft.Maui.Controls.BindableObject bindable, object oldValue, object newValue)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValueActual(Microsoft.Maui.Controls.BindableProperty property, Microsoft.Maui.Controls.BindableObject.BindablePropertyContext context, object value, bool currentlyApplying, Microsoft.Maui.Controls.Internals.SetValueFlags attributes, bool silent)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValueCore(Microsoft.Maui.Controls.BindableProperty property, object value, Microsoft.Maui.Controls.Internals.SetValueFlags attributes, Microsoft.Maui.Controls.BindableObject.SetValuePrivateFlags privateAttributes)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValue(Microsoft.Maui.Controls.BindableProperty property, object value, bool fromStyle, bool checkAccess)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValue(Microsoft.Maui.Controls.BindableProperty property, object value)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellItem.CurrentItem.set(Microsoft.Maui.Controls.ShellSection value)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellItem.CreateFromShellSection(Microsoft.Maui.Controls.ShellSection shellSection)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellItem.implicit operator Microsoft.Maui.Controls.ShellItem(Microsoft.Maui.Controls.ShellSection shellSection)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Handlers.ShellItemHandler.OnNavigationTabChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
Microsoft.WinUI.dll!WinRT._EventSource_global__Windows_Foundation_TypedEventHandler_global__Microsoft_UI_Xaml_Controls_NavigationView__global__Microsoft_UI_Xaml_Controls_NavigationViewSelectionChangedEventArgs_.EventState.GetEventInvoke.AnonymousMethod__1_0(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
Microsoft.Windows.SDK.NET.dll!ABI.Windows.Foundation.TypedEventHandler<Microsoft.UI.Xaml.Controls.NavigationView, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs>.Do_Abi_Invoke<System.IntPtr, System.IntPtr>(void* thisPtr, System.IntPtr sender, System.IntPtr args)
[Native to Managed Transition]
[Managed to Native Transition]
Microsoft.WinUI.dll!ABI.Microsoft.UI.Xaml.IApplicationStaticsMethods.Start(WinRT.IObjectReference _obj, Microsoft.UI.Xaml.ApplicationInitializationCallback callback)
Microsoft.WinUI.dll!Microsoft.UI.Xaml.Application.Start(Microsoft.UI.Xaml.ApplicationInitializationCallback callback)
[Namespace].Maui.dll![Namespace].Maui.WinUI.Program.Main(string[] args) Line 31
    at [ProjectPath]\obj\Debug\net6.0-windows10.0.19041.0\win10-x64\Platforms\Windows\App.g.i.cs(31)

Call stack of second call:

[Namespace].Maui.dll![Namespace].Maui.PageModels.EntitiesPageViewModel<[Namespace].Common.Models.TimeEntry, int, [Namespace].Maui.Models.TimeEntryViewModel>.Refresh() Line 35
    at [ProjectPath]\PageModels\EntitiesPageViewModel.cs(35)
CommunityToolkit.Mvvm.dll!CommunityToolkit.Mvvm.Input.AsyncRelayCommand.ExecuteAsync(object parameter)
CommunityToolkit.Mvvm.dll!CommunityToolkit.Mvvm.Input.AsyncRelayCommand.Execute(object parameter)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.RefreshView.OnIsRefreshingPropertyChanged(Microsoft.Maui.Controls.BindableObject bindable, object oldValue, object newValue)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValueActual(Microsoft.Maui.Controls.BindableProperty property, Microsoft.Maui.Controls.BindableObject.BindablePropertyContext context, object value, bool currentlyApplying, Microsoft.Maui.Controls.Internals.SetValueFlags attributes, bool silent)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValueCore(Microsoft.Maui.Controls.BindableProperty property, object value, Microsoft.Maui.Controls.Internals.SetValueFlags attributes, Microsoft.Maui.Controls.BindableObject.SetValuePrivateFlags privateAttributes)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindingExpression.ApplyCore(object sourceObject, Microsoft.Maui.Controls.BindableObject target, Microsoft.Maui.Controls.BindableProperty property, bool fromTarget)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindingExpression.Apply(bool fromTarget)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindingExpression.BindingExpressionPart.PropertyChanged.AnonymousMethod__49_0()
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.DispatcherExtensions.DispatchIfRequired(Microsoft.Maui.Dispatching.IDispatcher dispatcher, System.Action action)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindingExpression.BindingExpressionPart.PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindingExpression.WeakPropertyChangedProxy.OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
CommunityToolkit.Mvvm.dll!CommunityToolkit.Mvvm.Input.AsyncRelayCommand.ExecutionTask.set(System.Threading.Tasks.Task value)
CommunityToolkit.Mvvm.dll!CommunityToolkit.Mvvm.Input.AsyncRelayCommand.ExecuteAsync(object parameter)
[Namespace].Maui.dll![Namespace].Maui.Pages.TimeEntriesPage.OnNavigatedTo(Microsoft.Maui.Controls.NavigatedToEventArgs args) Line 21
    at [ProjectPath]\Pages\TimeEntriesPage.xaml.cs(21)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Page.SendNavigatedTo(Microsoft.Maui.Controls.NavigatedToEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Shell.SendNavigated(Microsoft.Maui.Controls.ShellNavigatedEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Shell..ctor.AnonymousMethod__169_0(object _, Microsoft.Maui.Controls.ShellNavigatedEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellNavigationManager.HandleNavigated.__FireNavigatedEvents|1(Microsoft.Maui.Controls.ShellNavigatedEventArgs a, Microsoft.Maui.Controls.Shell shell)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellNavigationManager.HandleNavigated.AnonymousMethod__0()
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BaseShellItem.OnAppearing(System.Action action)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellNavigationManager.HandleNavigated(Microsoft.Maui.Controls.ShellNavigatedEventArgs args)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Shell.Microsoft.Maui.Controls.IShellController.UpdateCurrentState(Microsoft.Maui.Controls.ShellNavigationSource source)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellItem.OnCurrentItemChanged(Microsoft.Maui.Controls.BindableObject bindable, object oldValue, object newValue)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValueActual(Microsoft.Maui.Controls.BindableProperty property, Microsoft.Maui.Controls.BindableObject.BindablePropertyContext context, object value, bool currentlyApplying, Microsoft.Maui.Controls.Internals.SetValueFlags attributes, bool silent)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValueCore(Microsoft.Maui.Controls.BindableProperty property, object value, Microsoft.Maui.Controls.Internals.SetValueFlags attributes, Microsoft.Maui.Controls.BindableObject.SetValuePrivateFlags privateAttributes)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValue(Microsoft.Maui.Controls.BindableProperty property, object value, bool fromStyle, bool checkAccess)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.BindableObject.SetValue(Microsoft.Maui.Controls.BindableProperty property, object value)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellItem.CurrentItem.set(Microsoft.Maui.Controls.ShellSection value)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellItem.CreateFromShellSection(Microsoft.Maui.Controls.ShellSection shellSection)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.ShellItem.implicit operator Microsoft.Maui.Controls.ShellItem(Microsoft.Maui.Controls.ShellSection shellSection)
Microsoft.Maui.Controls.dll!Microsoft.Maui.Controls.Handlers.ShellItemHandler.OnNavigationTabChanged(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
Microsoft.WinUI.dll!WinRT._EventSource_global__Windows_Foundation_TypedEventHandler_global__Microsoft_UI_Xaml_Controls_NavigationView__global__Microsoft_UI_Xaml_Controls_NavigationViewSelectionChangedEventArgs_.EventState.GetEventInvoke.AnonymousMethod__1_0(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs args)
Microsoft.Windows.SDK.NET.dll!ABI.Windows.Foundation.TypedEventHandler<Microsoft.UI.Xaml.Controls.NavigationView, Microsoft.UI.Xaml.Controls.NavigationViewSelectionChangedEventArgs>.Do_Abi_Invoke<System.IntPtr, System.IntPtr>(void* thisPtr, System.IntPtr sender, System.IntPtr args)
[Native to Managed Transition]
[Managed to Native Transition]
Microsoft.WinUI.dll!ABI.Microsoft.UI.Xaml.IApplicationStaticsMethods.Start(WinRT.IObjectReference _obj, Microsoft.UI.Xaml.ApplicationInitializationCallback callback)
Microsoft.WinUI.dll!Microsoft.UI.Xaml.Application.Start(Microsoft.UI.Xaml.ApplicationInitializationCallback callback)
[Namespace].Maui.dll![Namespace].Maui.WinUI.Program.Main(string[] args) Line 31
    at [ProjectPath]\obj\Debug\net6.0-windows10.0.19041.0\win10-x64\Platforms\Windows\App.g.i.cs(31)
ggutschi commented 2 years ago

The IsRefreshingchange is executing the RefreshCommand, is that correct?

Currently the IsRefreshingproperty acts as a switch to start the Command (if set from false to true). Maybe that's intended, but imho it's misleading and the OnIsRefreshingPropertyChangedshould not invoke the command. At least it conflicts with the documentation which says:

IsRefreshing, of type bool, which indicates the current state of the RefreshView.

ggutschi commented 2 years ago

@AndreasReitberger as a workaround:

Add if (IsConnecting) return; as the first statement in ConnectAction()

BrunoMoureau commented 2 years ago

I came to this issue too.

However, it is explained here that we can use the CanExecute delegate to enable or disable the RefreshView.Command.

I no longer experience the double call issue since I specified that the command should only execute when it is not already running.

I use CommunityToolkit.Mvvm.Input package and its IAsyncRelayCommand

In GalleryView.xaml

<RefreshView
    IsRefreshing="{Binding RefreshCommand.IsRunning}"
    Command="{Binding RefreshCommand}">

    <CollectionView ItemsSource="{Binding Photos}"
                    ItemSizingStrategy="MeasureFirstItem">

    <!-- ... -->

    </CollectionView>
</RefreshView>

In GalleryViewModel

public IAsyncRelayCommand RefreshCommand { get; }

public GalleryViewModel()
{
    RefreshCommand = new AsyncRelayCommand(LoadGalleryAsync, () => RefreshCommand.IsRunning == false);
}

private async Task LoadGalleryAsync()
{
    // load data from service
}
ghost commented 2 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

AndreasReitberger commented 1 year ago

Still seeing this issue on the latest version. The workaround is working, though. But this should actually be fixed.. The main issue still is, that the RefreshView fires the command twice if the pull down gesture is used. Maybe related to? https://github.com/xamarin/Xamarin.Forms/issues/7803

@PureWeen @jsuarezruiz

janseris commented 1 year ago

The documentation now states that setting IsRefreshing to true on RefreshView will invoke the command. https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/refreshview?view=net-maui-7.0

Manually setting the IsRefreshing property to true will trigger the refresh visualization, and will execute the ICommand defined by the Command property.

It is also the reason why we are experiencing this issue (and probably it is not a bug but by design) because I think always the logic of the app is that we initially call something like ReloadData (which is also our RefreshView command) upon page visit and we set IsRefreshing=true in ViewModel to also display the loading animation (which has a side effect of executing the function ReloadData again).

We could set IsRefreshing=true in the ViewModel instead of calling ReloadData to achieve the correct behavior but that is not a good idea because we cannot assume from the ViewModel that setting property IsRefreshing will call the ReloadData method by coincidence (we don't know in ViewModel that IsRefreshing is bound to a RefreshView control in the View which also by coincidence calls ReloadData via a registered Command).

I think the issue here is that IsRefreshing property on RefreshView has two responsibilities instead of 1 (setting it has two effects).

  1. Animation enabled/disabled
  2. Command called

When we only need 1., we also receive 2. which we don't actually want in the use case I explained.

I think the solution could be:


Example use case in my app:

I have a RefreshView and everytime I visit the page, the command gets executed twice. Repro: https://github.com/janseris/MauiRefreshViewCommandCalledTwiceOnAppearingBug

How to repro repeatedly in the app:

  1. Log in via entering a known name such as All and click Login. Then click Logout in shell flyout menu. This will send you back to Login screen where the Command will be invoked twice again. This will happen everytime you visit the Login page.

How to observe the command being called twice: I added static counters and prints (Debug.WriteLine). Example output:

[0:] 09.04.2023 0:16:14: Total called LoginPage.OnAppearing: 1 times
[0:] 09.04.2023 0:16:14: Called LoginViewModel OnAppearing via toolkit:EventToCommandBehavior hooked to LoginPage.OnAppearing event. This also which calls ReloadUsersCommand. Total: 1 times.
[0:] 09.04.2023 0:16:14: Total started command: 1 times
[0:] 09.04.2023 0:16:14: Total started command: 2 times
AndreasReitberger commented 1 year ago

Thanks for your input. As far as I understood, the RefreshView fires the Command once the IsRefreshing is set true. Coming from my XForm app, I had to set the binded property to true in the command to trigger the loading animation. I need to try what happens if I do not set the IsRefreshing property manually. Is the animation shown then?

For me it does not make sense to trigger the Command by setting the binded boolean to true.

janseris commented 1 year ago

Thanks for your input. As far as I understood, the RefreshView fires the Command once the IsRefreshing is set true. Coming from my XForm app, I had to set the binded property to true in the command to trigger the loading animation. I need to try what happens if I do not set the IsRefreshing property manually. Is the animation shown then?

For me it does not make sense to trigger the Command by setting the binded boolean to true.

The animation is shown is equivalent to IsRefreshing=true

samhouts commented 1 year ago

Hi @janseris !! Thank you for following up!

I removed the triaged and verified labels because we're doing a pass on all older issues to see if they are in a different state in the latest public versions. I'll update this one, since you've kindly done that for us!

ShariatPanah commented 8 months ago

i'm facing the exact same behavior, any update on this? tested with Android

kimhongka commented 3 months ago

I can reproduce this same issue on Android