WPF Scrollviewer DesiredSize does not increase whe

2019-06-09 11:24发布

问题:

I have a WPF UserControl (inside an ElementHost) with a ScrollViewer that contains an ItemsControl . The HorizontalScrollbarVisibility is set to Auto, so if no scrolling is necessary, the ScrollBargets hidden.

My requirement is, that if the ScrollBar gets shown/hidden, the ElementHost does adjust it's height accordingly. To achieve that, I'm listening to the SizeChanged event, I get the DesiredSize of the ScrollViewer in the EventHandler, then I pass DesiredSize.Height to the ElementHost.

  1. 2. 3.

One way, this works: With ScrollBar visible (situation 1), I enlarge my window until all items of the ItemsControl are visible, the ScrollBar disappears, the ElementHost adjusts to reduced height (situation 2). DesiredSize actually got smaller the moment the ScrollBar is hidden.

The other way, though, it doesn't work: With ScrollBar not visible (situation 2), I reduce my window size until a ScrollBar is necessary and appears. DesiredSize stays the same, and the ElementHost does not adjust (situation 3).

Any ideas?

This is the xaml of the Scrollviewer, with some MVVM stuff, but don't get hung up on this, the point really is, why does the DesiredSize not increase when the ScrollBar appears? Why is it shrink only?

<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Hidden" >
    <i:Interaction.Behaviors>
        <beh:HeightChangedBehavior HeightChangedCommand="{Binding HeightChangedCommand}" />
    </i:Interaction.Behaviors>
    <ItemsControl ItemsSource="{Binding TabHeaders}" >
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel IsItemsHost="True" Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate DataType="models:TabHeaderButtonModel">
                <RadioButton Content="{Binding Caption}" IsChecked="{Binding IsChecked, Mode=TwoWay}" GroupName="Tabs" 
                            Command="{Binding SelectionChangedCommand}" CommandParameter="{Binding}"
                            Style="{StaticResource TabHeaderToggleButtonStyle}">
                </RadioButton>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

ScrollViewer style (basically default WPF):

<Style x:Key="ScrollViewerStyle1" TargetType="{x:Type ScrollViewer}">
    <Setter Property="Template" >
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ScrollViewer}">
                <Grid x:Name="Grid" Background="{TemplateBinding Background}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <Rectangle x:Name="Corner" Grid.Column="1" Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Grid.Row="1"/>
                    <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" CanHorizontallyScroll="False" CanVerticallyScroll="False" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" Grid.Column="0" Margin="{TemplateBinding Padding}" Grid.Row="0"/>
                    <ScrollBar x:Name="PART_VerticalScrollBar" AutomationProperties.AutomationId="VerticalScrollBar" Cursor="Arrow" Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Grid.Row="0" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}"/>
                    <ScrollBar x:Name="PART_HorizontalScrollBar" AutomationProperties.AutomationId="HorizontalScrollBar" Cursor="Arrow" Grid.Column="0" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Orientation="Horizontal" Grid.Row="1" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportWidth}" Style="{DynamicResource ScrollBarStyle1}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="IsEnabled" Value="false">
            <Setter Property="Foreground" Value="White"/>
        </Trigger>
    </Style.Triggers>
</Style>

回答1:

I had to calculate the desired height by getting the ScrollViewer's content desired height and adding the ScrollBar height if visible.

This still feels slightly awkward to me, so if you have a better solution, I'll gladly change the accepted answer.

public class HeightChangedBehavior : Behavior<ScrollViewer>
{
    public ICommand HeightChangedCommand { get { return (ICommand)GetValue(HeightChangedCommandProperty); } set { SetValue(HeightChangedCommandProperty, value); } }
    public static readonly DependencyProperty HeightChangedCommandProperty = DependencyProperty.Register("HeightChangedCommand", typeof(ICommand), typeof(HeightChangedBehavior), new PropertyMetadata(null));


    protected override void OnAttached()
    {
        this.AssociatedObject.ScrollChanged += AssociatedObject_ScrollChanged;
        base.OnAttached();
    }

    /// <summary>
    /// Calculates the desired height for the scrollviewer, as the sum of its content
    /// desired height and, if visible, the horizontal scrollbar height.
    /// </summary>
    void AssociatedObject_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        ScrollViewer sv = (ScrollViewer)sender;

        // get content height
        double height = ((FrameworkElement)sv.Content).DesiredSize.Height;

        if (sv.ComputedHorizontalScrollBarVisibility == Visibility.Visible)
        {
            // add scrollbar height
            height += (double)sv.FindResource(SystemParameters.HorizontalScrollBarHeightKey); // template of scrollbar should use this key
        }

        int intHeight = (int)Math.Ceiling(height); // whole pixels

        // execute the command
        ICommand cmd = this.HeightChangedCommand;
        if (cmd != null && intHeight != sv.ActualHeight)
            cmd.Execute(intHeight);
    }
}