AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.34k stars 2.2k forks source link

Changing Selected TabItem resets the scroll position of a ListBox #5651

Closed derekantrican closed 1 year ago

derekantrican commented 3 years ago

Describe the bug This is definitely a weird one. I was able to simplify down to a minimum example but I still don't see what the issue is. I'm not sure if it's how I'm doing things or if it's a bug on Avalonia's side.

Basically, in my setup, if you scroll the listbox then change tabs - the listbox scroll position gets reset. ListBox.LayoutUpdated is getting called so I think that's part of it (not actually that the scroll position is being reset) but the question is still: why?

Screen recording:

To Reproduce Here's my minimum reproducible example:

MainWindow.axaml:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" Width="600" Height="450"
        x:Class="AvaloniaTabChangeResettingControls.MainWindow">
  <Grid RowDefinitions="*,Auto" ColumnDefinitions="Auto,*">
    <ListBox Width="200" Items="{Binding ListBoxItems}"/>
    <TabControl Grid.Column="1">
      <TabItem Header="Tab1">
        Tab1&#10;Content
      </TabItem>
      <TabItem Header="Tab2">
        Tab2 Content
      </TabItem>
    </TabControl>
    <Grid Grid.Row="1" Grid.ColumnSpan="2"/>
  </Grid>
</Window>

MainWindow.xaml.cs:

using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace AvaloniaTabChangeResettingControls
{
    public class MainWindow : Window
    {
        public MainWindow()
        {
            AvaloniaXamlLoader.Load(this);
            this.DataContext = this;
        }

        public List<string> ListBoxItems { get; } = new List<string>
        {
            "Item1",
            "Item2",
            "Item3",
            "Item4",
            "Item5",
            "Item6",
            "Item7",
            "Item8",
            "Item9",
            "Item10",
            "Item11",
            "Item12",
            "Item13",
            "Item14",
            "Item15",
        };
    }
}

Some things that oddly seem to be essential to reproducing the error:

I do not think it matters how ListBox.Items is set (binding vs codebehind using FindControl)

Expected behavior I would expect the ListBox to not update it's layout (change scroll position) when tab selection is changed.

Desktop (please complete the following information):

derekantrican commented 3 years ago

I found a workaround by creating two inner grids: a row grid and a column grid. So the problem still exists (should definitely still be able to use the above code without issues), but can worked arond by doing something like this:

<Grid RowDefinitions="*,Auto">
  <Grid ColumnDefinitions="Auto,*">
    <ListBox x:Name="listBox" Width="200"/>
    <TabControl Grid.Column="1">
      <TabItem Header="Tab1">
        Tab1&#10;Content
      </TabItem>
      <TabItem Header="Tab2">
        Tab2 Content
      </TabItem>
    </TabControl>      
  </Grid>
  <Grid Grid.Row="1"/>
</Grid>

Note that the last Grid (<Grid Grid.Row="1"/>) actually contains content in my application so I want to preserve it

maxkatz6 commented 3 years ago

Scroll is invalidated on VirtualizingStackPanel.MeasureOverride, which is invoked twice with different availableSize for the same control. On first call available size is not constrained by window size, which triggers scroll invalidation.

I don't understand Grid source code good enough, but it seems like first measuring it calls for intermediate calculations with infinite available size, and second call to get actual control size.

Not sure which logic is wrong. Probably scroll should be invalidated on arranging, not on measuring.

Fido789 commented 3 years ago

Yes, I can confirm the issue, here is the content of my tabs:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:design="clr-namespace:Forpixa.Editor.UI.Views.Design"
             mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300"
             x:Class="Forpixa.Editor.UI.Views.Design.DesignerControl">
    <Design.DataContext>
        <design:DesignerMockup/>
    </Design.DataContext>
    <Grid Background="{DynamicResource ThemeControlMidColor}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition />
            <ColumnDefinition Width="20" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
            <RowDefinition Height="20" />
        </Grid.RowDefinitions>
        <ContentControl Grid.Column="1" Grid.Row="0" Height="20" Content="{Binding TopRuler}" IsVisible="{Binding ShowRulers}"/>
        <ContentControl Grid.Column="0" Grid.Row="1" Width="20" Content="{Binding LeftRuler}" IsVisible="{Binding ShowRulers}"/>
        <ContentControl Grid.Column="1" Grid.Row="1" Content="{Binding Canvas}"/>
        <ScrollBar Grid.Column="2" Grid.Row="1" Orientation="Vertical" Minimum="{Binding Canvas.OriginBounds.Top}" Maximum="{Binding Canvas.OriginBounds.Bottom}"
                   Value="{Binding Canvas.OriginY}" ViewportSize="{Binding Canvas.VisibleBounds.Height}"></ScrollBar>
        <DockPanel Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" LastChildFill="True" >
                <ContentControl DockPanel.Dock="Left" Content="{Binding ZoomEdit}"/>
                <ScrollBar Orientation="Horizontal" Minimum="{Binding Canvas.OriginBounds.Left}" Maximum="{Binding Canvas.OriginBounds.Right}"
                           Value="{Binding Canvas.OriginX}" ViewportSize="{Binding Canvas.VisibleBounds.Width}"></ScrollBar>
        </DockPanel>
    </Grid>
</UserControl>

image

grokys commented 1 year ago

Just tried this repro on current master and it appears that it's fixed šŸŽ‰ .