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.21k stars 1.75k forks source link

Calling ScrollView ScrollToAsync in OnNavigatedTo event or OnAppearing event does not return. #15387

Open cat0363 opened 1 year ago

cat0363 commented 1 year ago

Description

Place a BindableLayout inside the ScrollView and set the BindableLayout's ItemsSource in the constructor.

Layout as shown below.

<Grid RowDefinitions="44,100,*">
    <Button x:Name="btnNext" Grid.Row="0" Text="Next" TextColor="White" BackgroundColor="Blue" Clicked="btnNext_Clicked" />
    <Editor x:Name="txtLog" Grid.Row="1" />
    <ScrollView x:Name="svTestItem" Grid.Row="2" Orientation="Vertical">
        <StackLayout x:Name="slTestItem" Orientation="Vertical">
            <BindableLayout.ItemTemplate>
                <DataTemplate>
                    <Grid HeightRequest="44">
                        <Label Text="{Binding ItemName}" FontSize="20"  />
                    </Grid>
                </DataTemplate>
            </BindableLayout.ItemTemplate>
        </StackLayout>
    </ScrollView>
</Grid>

Call the ScrollView's ScrollToAsync method within the OnNavigatedTo event.

protected override async void OnNavigatedTo(NavigatedToEventArgs args)
{
    base.OnNavigatedTo(args);

    txtLog.Text += ("Before ScrollToAsync" + Environment.NewLine);
    await svTestItem.ScrollToAsync(0, 0, false);
    txtLog.Text += ("After ScrollToAsync" + Environment.NewLine);
}

If the ScrollToAsync method call completes successfully, the following log should be displayed on the screen.

Before ScrollToAsync
After ScrollToAsync

But the actual log is below.

Before ScrollToAsync

The ScrollToAsync method remains called and does not return.

In the reproduction code, no code is written after the ScrollToAsync method, but in reality, initialization processing on the screen is written. Therefore, the code written after the ScrollToAsync method is not executed.

This issue occurs on both Android and iOS.

[.NET MAUI Android]

https://github.com/dotnet/maui/assets/125236133/f00d3340-f5ab-4aab-a95b-c9cb6ce19c3e

[.NET MAUI iOS]

https://github.com/dotnet/maui/assets/125236133/70518d94-a773-499a-94be-fa198a2f0cce

This problem also occurred within the OnAppearing event.

The ScrollToAsync method call does not return in the OnNavigatedTo event or the OnAppearing event at the time of initial display. In the OnNavigatedTo or OnAppearing event when returning from another screen, the ScrollToAsync method call will return. I expected the ScrollToAsync method call to return in the OnNavigatedTo or OnAppearing event.

At least it was working as intended on iOS in Xamarin.Forms. Android on Xamarin.Forms had the same results as Android on .NET MAUI.

[Xamarin.Forms iOS]

https://github.com/dotnet/maui/assets/125236133/3889b178-5e05-47c6-b892-84d4947c885d

Please let me know if there are any points to note during implementation. Any good ideas? Thank you.

Steps to Reproduce

The steps to reproduce are as follows.

  1. Launch the app uploaded to github with the device on Android or iOS.

In step 1, the call to the ScollToAsync method does not return. I expected the ScrollToAsync method call to return in the OnNavigatedTo or OnAppearing event.

Link to public reproduction project repository

https://github.com/cat0363/Maui-IssueScrollToAsync2.git

Version with bug

7.0.86

Last version that worked well

Unknown/Other

Affected platforms

iOS, Android

Affected platform versions

iOS 16.4, Android 11.0

Did you find any workaround?

It is possible to avoid it by calling await Task.Delay(XXX) before the ScrollToAsync call. XXX is any millisecond. However, this is not the preferred workaround.

Relevant log output

No response

ghost commented 1 year ago

We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.

cat0363 commented 1 year ago

Additional Information: At the timing of OnNavigatedTo event or OnAppearing event, the Handler below is null.

[src\Controls\src\Core\ScrollView\ScrollView.cs]

void OnScrollToRequested(ScrollToRequestedEventArgs e)
{
    CheckTaskCompletionSource();
    ScrollToRequested?.Invoke(this, e);
    Handler?.Invoke(nameof(IScrollView.RequestScrollTo), ConvertRequestMode(e).ToRequest());
}

Therefore, the asynchronous scrolling task will never be executed and the caller will be waiting for its completion. I don't know if this solution is correct, but you can solve this problem by staggering the execution timing.

[src\Controls\src\Core\ScrollView\ScrollView.cs]

void OnScrollToRequested(ScrollToRequestedEventArgs e)
{
    CheckTaskCompletionSource();
    ScrollToRequested?.Invoke(this, e);
    if (Handler is not null)
    {
        Handler.Invoke(nameof(IScrollView.RequestScrollTo), ConvertRequestMode(e).ToRequest());
    }
    else
    {
        Loaded += async (s, e2) =>
        {
            await Task.Delay(1);
            Handler?.Invoke(nameof(IScrollView.RequestScrollTo), ConvertRequestMode(e).ToRequest());
        };
    }
}

However, if you don't insert await Task.Delay(1), it won't scroll properly.

Below is the execution result. In the video, the scroll position is intentionally set to (x, y) = (0, 200) because it is unclear whether it was scrolled or remains in the initial display.

[Android]

https://github.com/dotnet/maui/assets/125236133/5716245a-4987-4090-96e4-0c625d6275ff

[iOS]

https://github.com/dotnet/maui/assets/125236133/bdc5ae88-c13b-4599-a932-dfaee6ba5d5b

I suspect that the call to await Task.Delay(1) establishes the required size for scrolling.

By the way, the ScrollView's size was assigned when there was a call to await Task.Delay(1). If not, the size is unassigned and the value of both Width and Height is -1.

I hope it will be a hint for solving this issue. I'm sorry if I'm wrong.

cat0363 commented 1 year ago

Alternatively, writing it like this also worked:

[src\Controls\src\Core\ScrollView\ScrollView.cs]

void OnScrollToRequested(ScrollToRequestedEventArgs e)
{
    CheckTaskCompletionSource();
    ScrollToRequested?.Invoke(this, e);

    if (Handler is not null)
    {
        Handler.Invoke(nameof(IScrollView.RequestScrollTo), ConvertRequestMode(e).ToRequest());
    }
    else
    {                           
        Loaded += (s, e2) =>
        {
            Dispatcher.Dispatch(() =>
            {
                Handler?.Invoke(nameof(IScrollView.RequestScrollTo), ConvertRequestMode(e).ToRequest());
            });
        };
    }
}

Calling await Task.Delay(1) is unnecessary in this case

This solution works only for iOS and Android. On Windows, it works in the NavigatedTo event when displayed for the first time, but it does not work in the NavigatedTo event when transitioning from another page. I requested a scroll in the ChangeView method, but the ViewChanged event is not firing. Because the ViewChanged event will not fire unless there is a change in the scrollbar position. For this reason, ScrollToAsync will not returned if the scrollbar position does not change when returning to the original page from another page.