简体   繁体   中英

Silverlight 4 RichTextBox scroll to bottom

I am writing a chat application with Silverlight. I am adding the content dynamically to the rich text box and I need to scroll it down to show the last messages. But there is no way to do that. All documentation/codes that I have found on internet are old.

Any solution will be gladly accepted.

I struggled with this for a long time in my own application. The Dispatcher.BeginInvoke method wasn't reliable for me; sometimes it would scroll to the bottom, other times it wouldn't. The only way to make it work was to nest two Dispatcher.BeginInvoke calls inside each other, but I didn't like that solution.

Instead of using Dispatcher, I abuse event handlers. In this example, the text box will scroll to the bottom only if it was there at the time the new Inline was added (a useful behavior for chat boxes, to allow users to read previous messages if they want). You can pass a Run object as the Inline parameter.

private void AddInline(Inline inline)
{
    var viewer = textBox.GetChildByType<ScrollViewer>(item => item.Name == "ContentElement") as ScrollViewer;
    bool scrollToBottom = viewer.VerticalOffset == viewer.ScrollableHeight;

    // Creating the paragraph isn't necessary.
    // In my own application, I only add a single paragraph to the RichTextBox that displays my chat messages.
    // Then I just add new Inlines to the single paragraph.
    Paragraph p = new Paragraph();
    p.Inlines.Add(inline);

    if (scrollToBottom)
        textBox.LayoutUpdated += ScrollRichTextBoxToBottom;

    // Adding the paragraph triggers a layout update.
    // In the LayoutUpdated handler, the viewer will be scrolled to the bottom, triggering a second layout pass.
    textBox.Blocks.Add(p);
}

private void ScrollRichTextBoxToBottom(object sender, EventArgs e)
{
    // The LayoutUpdated handler needs to be removed, otherwise the RichTextBox becomes unscrollable.
    textBox.LayoutUpdated -= ScrollRichTextBoxToBottom;

    var viewer = textBox.GetChildByType<ScrollViewer>(item => item.Name == "ContentElement") as ScrollViewer;
    viewer.ScrollToVerticalOffset(viewer.ScrollableHeight);
}

Note: GetChildByType is just an extension method to get the ScrollViewer. (I did not create this extension method.)

public static class Extensions
{
    public static T GetChildByType<T>(this UIElement element, Func<T, bool> condition)
        where T : UIElement
    {
        List<T> results = new List<T>();
        GetChildrenByType<T>(element, condition, results);
        if (results.Count > 0)
            return results[0];
        else
            return null;
    }

    private static void GetChildrenByType<T>(UIElement element, Func<T, bool> condition, List<T> results)
        where T : UIElement
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
        {
            UIElement child = VisualTreeHelper.GetChild(element, i) as UIElement;
            if (child != null)
            {
                T t = child as T;
                if (t != null)
                {
                    if (condition == null)
                        results.Add(t);
                    else if (condition(t))
                        results.Add(t);
                }
                GetChildrenByType<T>(child, condition, results);
            }
        }
    }
}

You need to dig out the ScrollViewer that is part of the RichTextBox template. You can do this with the help of some code based of VisualTreeHelper . Get the code for my VisualTreeEnumeration class here .

Now with this class in your project you can do this:-

ScrollViewer sv = rtb.Descendents().OfType<ScrollViewer>().FirstOrDefault();

Full Example

Create a new application and include the VisualTreeEnumeration class.

In MainPage.xaml use the following xaml:-

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <RichTextBox x:Name="rtb" />
    <Button Content="Click Me" Click="Button_Click" Grid.Row="1" />
</Grid>

In its code behind add this:-

    int i = 0;
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Paragraph p = new Paragraph();
        p.Inlines.Add(new Run() { Text = "Hello " + (i++).ToString() });
        rtb.Blocks.Add(p);

        Dispatcher.BeginInvoke(() =>
        {
            ScrollViewer sv = rtb.Descendents().OfType<ScrollViewer>().FirstOrDefault();
            sv.ScrollToVerticalOffset(sv.ScrollableHeight);
            sv.UpdateLayout();
        });
    }

Note the use of BeginInvoke to allow the RTB to sort itself out after having added the text. Using BeginInvoke queues up the rest of the code on the UI thread dispatcher.

Well, you need neither extensions methods nor dispatchers. The easiest way to scroll the RichTexBox control to the bottom is as follows:

textBox.Selection.Select(textBox.ContentEnd, textBox.ContentEnd);

You can achieve the "scroll to top" behaviour in the similar fashion.

Hope that helps.

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