Search code examples
c#wpfscrollviewereventtrigger

Scrolling a ScrollViewer to the bottom in WPF when a new line is added to a TextBlock in XAML


I'm trying to keep the vertical scroll bar at the bottom (the latest entry), but at the moment, the scroll bar just stays in the same place, so as content is being added to the string, the scroll bar moves to the top.

I know that I can use the ServerScroll.ScrollToEnd() property in my code-behind to move the bar to the end. But is there a way to automatically do this is xaml? (so that I don't have to call this property every time I add to the string).

XAML

<ScrollViewer Name="ServerScroll"
              VerticalScrollBarVisibility="Auto">
    <TextBlock Name="serverConsole"
               Margin="5"
               Background="White"
               TextWrapping="Wrap"/>
</ScrollViewer>

Code-behind

private void example_Click(object sender, RoutedEventArgs e)
{
    ServerConsole += "asdf\r\n";      // binded to TextBlock
    ServerScroll.ScrollToEnd();    
}

Solution

  • With a TextBox, reacting to TextBox.TextChanged

    If you want to scroll to the end everytime the Text property of your TextBlock is changed, I would recommend switching to a TextBox so that you can hookup to its TextChanged event using System.Windows.Interactivity:

    <Window x:Class="WpfApp1.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:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
            xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Button Grid.Row="0" Width="50" Height="25" Click="Button_Click"></Button>
            <ScrollViewer Grid.Row="1" Name="ServerScroll"
                          VerticalScrollBarVisibility="Auto">
                <TextBox Name="serverConsole"
                           Margin="5"
                           Background="White"
                           TextWrapping="Wrap">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="TextChanged">
                            <ei:CallMethodAction MethodName="ScrollToEnd" TargetObject="{Binding ElementName=ServerScroll}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </TextBox>
            </ScrollViewer>
        </Grid>
    </Window>
    

    With a TextBlock, reacting to Button.Click

    If you wish to scroll to the end whenever your Button is clicked, you can use the same technique to hook-up to its Click event:

    <Window x:Class="WpfApp1.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:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
            xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Button Grid.Row="0" Width="50" Height="25" Click="Button_Click">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <ei:CallMethodAction MethodName="ScrollToEnd" TargetObject="{Binding ElementName=ServerScroll}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
            <ScrollViewer Grid.Row="1" Name="ServerScroll"
                          VerticalScrollBarVisibility="Auto">
                <TextBlock Name="serverConsole"
                           Margin="5"
                           Background="White"
                           TextWrapping="Wrap">
                </TextBlock>
            </ScrollViewer>
        </Grid>
    </Window>
    

    With a ListView, reacting to CollectionChanged

    It looks like you really want to use an ItemsControl instead of a TextBlock though, because you're talking about entries and everything. You could switch to a ListView and hook-up to its CollectionChanged event as well:

    <Window x:Class="WpfApp1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
            xmlns:ei="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            <Button Grid.Row="0" Width="50" Height="25" Click="Button_Click"/>
    
            <ScrollViewer Grid.Row="1" Name="ServerScroll"
                          VerticalScrollBarVisibility="Auto">
                <ListView x:Name="listView" ItemsSource="{Binding MyList}">
                    <i:Interaction.Triggers>
                        <i:EventTrigger SourceObject="{Binding MyList}" EventName="CollectionChanged">
                            <ei:CallMethodAction MethodName="ScrollToEnd" TargetObject="{Binding ElementName=ServerScroll}"/>
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </ListView>
            </ScrollViewer>
        </Grid>
    </Window>
    

    And in your view model:

    public ObservableCollection<string> MyList { get; } = new ObservableCollection<string>();