简体   繁体   中英

WPF: Horizontal ScrollViewer on DataGrid snapping to right side on reinstantation of the bound ObservableCollection

I am currently experiencing an issue in WPF where the horizontal ScrollViewer of a DataGrid snaps to the right side of the possible scroll space (showing the right most content of the DataGrid) when reinstantiating the bound ObservableCollection.

Even if I trigger a behavior to manually set the HorizontalOffset to 0 when a bound event is invoked, and invoke the event immediately after rebinding the list, the 0 is ignored and the snap goes to the right side again. I assume this is something to do with the order of operations and command queue inside the ScrollViewer.

This seems like something that should be default behaviour (I'm not sure why you'd ever want a scroll bar to snap to right side by default when the data is populated). Does anyone know of a workaround to this issue?

As requested, code files from my replication project.

MainWindow.xaml

<Window x:Class="WpfScrollViewer.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    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:local="clr-namespace:WpfScrollViewer"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
    <local:MainViewModel/>
</Window.DataContext>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <StackPanel Grid.Row="0" Orientation="Horizontal">
        <Button Content="Rebind" Command="{Binding RebindCommand}"/>
        <Button Content="Clear and Set" Command="{Binding ClearCommand}"/>
    </StackPanel>

    <DataGrid ItemsSource="{Binding People}" Grid.Row="1" HorizontalScrollBarVisibility="Visible" FontSize="30">

    </DataGrid>
</Grid>

Person.cs

namespace WpfScrollViewer
{
    public class Person
    {
        public string FirstNames { get; set; }

        public string LastName { get; set; }

        public int Age { get; set; }

        public string Address { get; set; }

        public string PostCode { get; set; }

        public string PhoneNumber { get; set; }
    }
}

DelegateCommand.cs

using System;
using System.Windows.Input;

namespace WpfScrollViewer
{
    public class DelegateCommand : ICommand
    {
        private readonly Action _fn;
        private readonly Func<bool> _canExecute;

        public event EventHandler CanExecuteChanged;

        public DelegateCommand(Action fn, Func<bool> canExecute = null)
        {
            _fn = fn;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameter)
        {
            if (_canExecute == null)
            {
                return true;
            }

            return _canExecute();
        }

        public void Execute(object parameter)
        {
            _fn();
        }
    }
}

MainViewModel.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;

namespace WpfScrollViewer
{
    public class MainViewModel : INotifyPropertyChanged
    {
        private readonly Random _random = new Random();

        private ObservableCollection<Person> _people;

        public ObservableCollection<Person> People
        {
            get => _people;
            set
            {
                if (_people == value)
                {
                    return;
                }

                _people = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(People)));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public ICommand RebindCommand { get; }

        public ICommand ClearCommand { get; }

        public MainViewModel()
        {
            RebindCommand = new DelegateCommand(RebindPeople);
            ClearCommand = new DelegateCommand(ClearAndSetPeople);
        }

        private void RebindPeople()
        {
            People = new ObservableCollection<Person>(GetPeople());
        }

        private void ClearAndSetPeople()
        {
            var people = GetPeople();
            People.Clear();
            foreach (var person in people)
            {
                People.Add(person);
            }
        }

        private List<Person> GetPeople()
        {
            var people = new List<Person>
            {
                new Person
                {
                    FirstNames = "John",
                    LastName = "Doe",
                    Address = "17 Random Street",
                    PostCode = "RN32 2JR",
                    Age = 31,
                    PhoneNumber = "07647123456"
                },
                new Person
                {
                    FirstNames = "Jane",
                    LastName = "Doe",
                    Address = "17 Random Street",
                    PostCode = "RN32 2JR",
                    Age = 30
                },
                new Person
                {
                    FirstNames = "Jack",
                    LastName = "Freens",
                    Address = "37 Badboi Lane",
                    Age = 30
                },
                new Person
                {
                    FirstNames = "Richard",
                    LastName = "Brodget",
                    Address = "69 Meme Street",
                    Age = 31
                },
                new Person
                {
                    FirstNames = "Sam",
                    LastName = "Orfitt",
                    Address = "16 Withernsea Road",
                    Age = 29
                },
                new Person
                {
                    FirstNames = "Tom",
                    LastName = "Orfitt",
                    Address = "16 Withernsea",
                    Age = 27
                }
            };

            var rmCount = _random.Next(1, 4);
            for (var i = 0; i < rmCount; i++)
            {
                people.RemoveAt(_random.Next(people.Count));
            }

            return people;
        }
    }
}

Using the "Rebind" button shows the behaviour I've described above. Make sure you scroll slightly over to the right, and the horizontal scroll bar will snap to the right on rebind. If the bar is fully over to the left, the horizontal scroll bar will correctly snap to the left, as I'd like it to do in all situations.

Cheers

It is caused by Columns Auto Generation feature. Everytime you change data it will drop all columns (I gues that in this stage is scrollbar location lost) and creates new columns based on passed data. If you define columns staticaly and disables that feature using AutoGenerateColumns="False" parameter it wont reset scrollbar position.

<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding People}" Grid.Row="1" HorizontalScrollBarVisibility="Visible" FontSize="30">
    <DataGrid.Columns>
        <DataGridTextColumn Header="FirstNames" Binding="{Binding FirstNames}" />
        <DataGridTextColumn Header="LastName" Binding="{Binding LastName}" />
        <DataGridTextColumn Header="Age" Binding="{Binding Age}" />
        <DataGridTextColumn Header="Address" Binding="{Binding Address}" />
        <DataGridTextColumn Header="PostCode" Binding="{Binding PostCode}" />
        <DataGridTextColumn Header="PhoneNumber" Binding="{Binding PhoneNumber}" />
    </DataGrid.Columns>
</DataGrid>

If you want to generate columns dynamicaly and also need to remember scrollbar position you can generate Columns from code behind using reflection. Disadvantage is that you cannot bind it but must it generate manually. For example

<DataGrid AutoGenerateColumns="False" Loaded="DataGrid_Loaded" ItemsSource="{Binding People}" Grid.Row="1" HorizontalScrollBarVisibility="Visible" FontSize="30">
</DataGrid>

and DataGrid_Loaded:

DataGrid dg = (DataGrid)sender;
MainViewModel mvm = (MainViewModel)this.DataContext;
Type classType = typeof(Person);
PropertyInfo[] properties = classType.GetProperties();
foreach (PropertyInfo prop in properties) {
    dg.Columns.Add(new DataGridTextColumn() {
        Binding = new Binding(prop.Name),
        Header = prop.Name
    });
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM