Skip to content

KeepExpanded fails when ScrollViewer content contains its own ScrollViewer (TextBox, ListView, etc.) #57

@gilleslabelle

Description

@gilleslabelle

Description:

KeepVerticalExpanded and KeepHorizontalExpanded do not work reliably when the ScrollViewer's content is a control that has its own internal ScrollViewer (e.g. TextBox, ListView). The scrollbars collapse unexpectedly. The same setup works correctly with controls that have no internal ScrollViewer (e.g. ItemsRepeater).

Steps to reproduce:

Zoom in until scrollbars should appear
Scrollbars do not stay expanded — they collapse intermittently
Replace TextBox with ItemsRepeater and the same setup works perfectly.

Root cause:

In ApplyKeepExpandedToScrollViewer (ScrollBarExtensions.KeepExpanded.cs line 101):

if (scrollViewer.FindDescendant(targetScrollBarName) is ScrollBar scrollBar &&
FindDescendant in DependencyObjectExtensions.cs does a depth-first visual tree walk and returns the first match. When the content control (TextBox, ListView) has its own internal ScrollViewer, the depth-first search enters ScrollContentPresenter → content → inner ScrollViewer and finds the inner control's VerticalScrollBar/HorizontalScrollBar first. The outer ScrollViewer's actual scrollbars are never reached.

The same issue affects RemoveVisualStatesFromScrollViewer (line 274), which calls scrollViewer.FindDescendant("Root") and can also find the wrong "Root" Grid.

The equivalent lines in ScrollBarExtensions.Annotations.cs (line 131) have the same bug.

Suggested fix:

When searching for template parts of a ScrollViewer, skip past ScrollContentPresenter to avoid entering user content. For example:

`private static IEnumerable FindTemplateScrollBars(DependencyObject parent)
{
int count = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);

    if (child is ScrollBar sb)
    {
        yield return sb;
        continue;
    }

    // Skip the content host — everything inside is user content
    if (child is ScrollContentPresenter)
        continue;

    foreach (ScrollBar descendant in FindTemplateScrollBars(child))
        yield return descendant;
}

}`

Then use this instead of scrollViewer.FindDescendant(targetScrollBarName) in both KeepExpanded and Annotations.

Affected controls (confirmed):

TextBox
ListView
Not affected:

ItemsRepeater (no internal ScrollViewer)

Package version: 1.1.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions