Search code examples
wpfgridsplitter

WPF GridSplitter gets jammed when ListView gets focus


The GridSplitters in my WPF app are behaving strangely. My application has a 3-panel layout, implemented as a grid with 5 columns, where 0/2/4 have content and 1/3 have GridSplitters. The middle panel is a Grid with a ListView.

Everything works fine until the ListView gets focus, at which point the splitters mostly stop moving. You can drag them a pixel or two, then they freeze. The app sees an initial motion event, but nothing further. You can repeat this a few times to move the splitter multiple pixels, but if you try to move another splitter the previous one jumps back where it was. There are two more splitters in the side panels that continue to work normally.

Reloading the project causes the ListView to lose focus, and the splitters start working again.

That's probably hard to visualize, so a made a short video.

The XAML is pretty straightforward. The only quirk is that the center panel is defined twice, with mutually-exclusive visibility.

<Grid Name="triptychGrid" DockPanel.Dock="Top">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" MinWidth="100"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*" MinWidth="150"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*" MinWidth="100"/>
    </Grid.ColumnDefinitions>

    <GridSplitter Grid.Column="1" Width="4" HorizontalAlignment="Left"/>
    <GridSplitter Grid.Column="3" Width="4" HorizontalAlignment="Center"/>

    <Grid Grid.Column="0" Name="leftPanel"> ... </Grid>

    <Grid Grid.Column="2" Name="launchPanel" Visibility="{Binding Path=LaunchPanelVisibility}"> ... </Grid>

    <Grid Grid.Column="2" Name="codeListGrid" Visibility="{Binding Path=CodeListVisibility}"> ... </Grid>

    <Grid Grid.Column="4" Name="rightPanel"> ... </Grid>
</Grid>

There are event listeners on the panel widths so they get remembered between runs. I restore the widths of the panels when the application first starts, but don't touch them after that.

Everything works just fine until the ListView gets focus. It would be less weird if it froze up entirely. Moving a couple of pixels at a time and then jumping back when other controls are touched seems weird.

I have reproduced this on a desktop running the latest Win10 (.NET reports 10.0.18362) and a virtual machine running the latest Win7. The project is open source.


Solution

  • The problem occurs because you are handling the ItemContainerGenerator.StatusChanged event. When the listView gets resized because of the grid splitter, the even get generated for each pixel or so which mess up the smoothness of the operation.

    A quick and lazy fix to that is to cancel that handler when the sliding is in progress, you can achieve that simply by using a boolean, set it to true or false by handling the DragStarted and DragOver of the GridSplitters:

      <GridSplitter Name="leftSplitter" Width="4" Grid.Column="1" HorizontalAlignment="Left" DragStarted="LeftSplitter_OnDragStarted" DragOver="LeftSplitter_OnDragOver"/>
      <GridSplitter Name="rightSplitter" Width="4" Grid.Column="3" HorizontalAlignment="Center" DragStarted="LeftSplitter_OnDragStarted" DragOver="LeftSplitter_OnDragOver"/>
    

    The handlers look something like that:

    private bool _isBeingDraged = false;
    private void LeftSplitter_OnDragStarted(object sender, DragStartedEventArgs e)
    {
            _isBeingDraged = true;
    }
    
    private void LeftSplitter_OnDragOver(object sender, DragEventArgs e)
    {
            _isBeingDraged = false;
    }
    

    Then update your ItemContainerGenerator_StatusChanged to consider that boolean:

     private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e) {
            if(_isBeingDraged)
                return;
            if (codeListView.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) {
                int index = codeListView.SelectedIndex;
    
                if (index >= 0) {
                    ListViewItem item =
                        (ListViewItem)codeListView.ItemContainerGenerator.ContainerFromIndex(index);
    
                    if (item != null) {
                        item.Focus();
                    }
                }
            }
        }
    

    There might be something more sophisticated to do with the ItemContainerGenerator, but that requires more diggings.