views:

1293

answers:

3

I have a WPF application that connects via a socket to a device and gets streaming text data (approx 1 message per second). This data is then displayed on the UI. The user can create rules like "If the data contains 'abc' highlight the line" or "…make it bold", so plain text output will not do, it needs to be "rich" text.

My current solution to this is to have a FlowDocument in my ViewModel that contains the formatted output. The View has a FlowDocumentScrollViewer that is bound to the FlowDocument in the ViewModel.

This works, but when the FlowDocument gets large (~6,000 lines) performance starts to degrade. The current algorithm caps the number of lines at 10,000, but then things get even worse, to the point where the app is unusable. Once it reaches 10,000 lines, then I remove a line for every line that is added resulting in the FlowDocumentScrollViewer getting 2 update notifications for every new line.

I tried to find a way to batch delete (when we reach 10,000 lines delete the oldest 1,000 lines), but there is no bulk delete on the FlowDocument. Looping 1,000 times and doing the delete results in 1,000 update notifications and locks up the UI.

That’s my problem, here’s my question:

What is the best way to display streaming rich text content with WPF? I get ~ 1 message per second, each message is ~150 characters, and I want to keep the last 10000 messages. Am I going about this the wrong way? Are there other controls/objects that would perform better?

EDIT: Here are some more requirements

  • Need to be able to print the output text
  • Need to be able to select and copy the output text so it can be pasted into another document
A: 

FlowDocumentScrollViewers may have overhead due to the ability to display content in columns, etc. Is there a reason that a normal WPF RichTextBox won't work? Also, do you have .NET 3.5 SP1? The following link indicates that there have been big performance improvements for FlowDocuments in SP1: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/a116da54-ce36-446a-8545-3f34e9b9038d.

kvb
It was a RichTextBox originally, but I changed it to get printing, zooming ... for free. Based on the testing I have done, about half the performance hit comes from the FlowDocumentScrollViewer and the other half from the FlowDocument itself.
John Myczek
Which version of the framework are you using? Also, for further text optimization tips, see http://msdn.microsoft.com/en-us/library/bb613560.aspx.
kvb
I'm using 3.5 SP1
John Myczek
A: 

This idea complicates things significantly, but my thought would be to create one viewer per message and create only as many viewers as are necessary to display the currently visible messages. I think the VirtualizingStackPanel control would be a good tool to manage this. I found a series describing the implementation of a VirtualizingStackPanel here.

Obviously, this implies maintaining the message buffer in a separate data structure.

EDIT: I just realized that the standard ListBox control uses a VirtualizingStackPanel in its implementation. With this in mind, my revised suggestion is:

  1. Create a data structure to contain the source of each message.
  2. Create a property on the data structure that creates a FlowDocument from the message source "on the fly"
  3. Bind a ListBox to a collection of said data structures.
  4. Define the ItemTemplate for the ListBox with a FlowDocumentScrollViewer where the Document property is bound to the aforementioned data structure's property.

EDIT 2: Regarding printing/zooming: I can't help you much with printing in WPF (something involving VisualBrush, maybe?), but zooming should be pretty easy to do. I created a ZoomListBox to test this idea. The XAML looks like this:

<ListBox
    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"
    mc:Ignorable="d"
    x:Class="Test.ZoomListBox"
    d:DesignWidth="640" d:DesignHeight="480"
    x:Name="ThisControl">
    <ListBox.ItemsPanel>
     <ItemsPanelTemplate>
      <VirtualizingStackPanel IsItemsHost="True">
       <VirtualizingStackPanel.LayoutTransform>
        <ScaleTransform ScaleX="{Binding ElementName=ThisControl, Path=Zoom}" ScaleY="{Binding ElementName=ThisControl, Path=Zoom}" />
       </VirtualizingStackPanel.LayoutTransform>
      </VirtualizingStackPanel>
     </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

And the code behind is this:

public partial class ZoomListBox
{
 public ZoomListBox()
 {
  this.InitializeComponent();
 }

    public double Zoom
    {
        get { return (double)GetValue(ZoomProperty); }
        set { SetValue(ZoomProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Zoom.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ZoomProperty =
        DependencyProperty.Register("Zoom", typeof(double), typeof(ZoomListBox), new UIPropertyMetadata(1.0));
}

And an example of using it:

<Grid x:Name="LayoutRoot">
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <l:ZoomListBox x:Name="ZoomList">
        <Button>Foo</Button>
        <Button>Foo</Button>
        <Button>Foo</Button>
        <Button>Foo</Button>
        <Button>Foo</Button>
        <Button>Foo</Button>
        <Button>Foo</Button>
    </l:ZoomListBox>
    <Slider Grid.Row="1" Value="{Binding ElementName=ZoomList, Path=Zoom}" Minimum="0.5" Maximum="5" /> 
</Grid>
Daniel Pratt
Interesting idea, thanks for thinking "outside the box". I'll look into something like that, but I need to be able to select/copy data from the output area. Also, I am getting printing and zooming for free now, so I would need to come up with a solution for that.
John Myczek
+3  A: 

The performance breakdown seemed to be caused by the high number of Blocks in the FlowDocument. For every message received I was creating a Run, adding that run to a Paragraph and adding the paragraph to the document.

I changed the algorithm so now it creates a Paragraph then adds 250 Runs to that Paragraph, then creates a new Paragraph ... adds 250 Runs ... and so on. This essentially cuts the number of blocks in half.

This also has an added benefit when I reach the max number of lines (10,000). Instead of deleting a single line for each new line added (and pegging the CPU), I can just delete the oldest Paragraph and that instantly deletes the oldest 250 lines.

This relatively simple change brought the performance well within the acceptable range. Instead of pegging the CPU and locking up the UI, now the CPU stays relatively low with spikes around 15%.

John Myczek