How to improve the performance of FlowDocumentScrollViewer?

In a previous question, I asked how to get real-time log output in a WPF text element ( WPF add text blocks to user interface thread, but does WinForms not work? ). The answer there led me to use the FlowDocumentScrollViewer , which really was much faster than the RichTextBox . However, I found that running commands with a huge amount of text output (e.g. svn co) lead to a noticeable slowdown in my WPF application. Switching tabs after checking 3 or 4 very large svn branches takes 3-4 seconds, and I'm sure the time will increase with the number of checks I do. Scrolling also has a noticeable lag.

As stated in the related question above, I recently switched my application from Windows Forms to WPF. I like WPF a lot - it provides many advantages that I did not have on Forms. However, performance seems to be quite a problem in WPF, at least for me. In the version of my Forms application, I could print a huge amount of text in a RichTextBox control and had no slowdown in my application. Switching tabs was instant, and scrolling was seamless. This is the experience I want in my WPF application.

So my question is this: how to improve the performance of my FlowDocumentScrollViewer in accordance with the performance of Windows Forms RichTextBox , without losing formatting capabilities, such as bold and italics, and without losing copy / paste functions? I want to switch WPF controls while they offer the formatting options I'm looking for.

Here is my print code, for reference:

 public void PrintOutput(String s) { if (outputParagraph.FontSize != defaultFontSize) { outputParagraph = new Paragraph(); outputParagraph.Margin = new Thickness(0); outputParagraph.FontFamily = font; outputParagraph.FontSize = defaultFontSize; outputParagraph.TextAlignment = TextAlignment.Left; OutputBox.Document.Blocks.Add(outputParagraph); } outputParagraph.Inlines.Add(s); if (!clearOutputButton.IsEnabled) clearOutputButton.IsEnabled = true; } public void PrintImportantOutput(String s) { if (outputParagraph.FontSize != importantFontSize) { outputParagraph = new Paragraph(); outputParagraph.Margin = new Thickness(0); outputParagraph.FontFamily = font; outputParagraph.FontSize = importantFontSize; outputParagraph.TextAlignment = TextAlignment.Left; OutputBox.Document.Blocks.Add(outputParagraph); } String timestamp = DateTime.Now.ToString("[hh:mm.ss] "); String toPrint = timestamp + s; outputParagraph.Inlines.Add(new Bold(new Run(toPrint))); if (!clearOutputButton.IsEnabled) clearOutputButton.IsEnabled = true; } 

I switch the font size and make the text bold when printing "important" text. The reason this code has so many lines is because I try to reuse the same paragraph for all the text until I click on the “important” text; I add a new paragraph with all the “important” text, and then add another paragraph as soon as I return to the non-important text and add it to this paragraph until I type in a more “important” text. I was hoping that reusing the same paragraph would improve performance.

In addition, it should be noted that I print stdout to one FlowDocumentScrollViewer , stderr to another FlowDocumentScrollViewer , and both to a third FlowDocumentScrollViewer at once. Therefore, each line of stdout and stderr is technically printed twice, doubling the load on my application. Again, this is not a problem in WinForms.


Below is a complete sample code as indicated in the comments. It is extremely simple (3 FlowDocumentScrollViewer and simple printing), but it still slows down a considerable time of about 20,000 lines of text and much worse.

EDIT: Sample code removed. In its place is a working code to solve my performance problems. It works the same as FlowDocumentScrollViewer , with one exception: you cannot select substrings of strings. I study this, although it seems difficult.

Bridge.cs

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Documents; using System.Threading; using System.Collections.Concurrent; using System.Threading.Tasks; namespace PerformanceTest { public class Bridge { int counterLimit; public BlockingCollection<PrintInfo> output; public BlockingCollection<PrintInfo> errors; public BlockingCollection<PrintInfo> logs; protected static Bridge controller = new Bridge(); public static Bridge Controller { get { return controller; } } public MainWindow Window { set { if (value != null) { output = value.outputProducer; errors = value.errorProducer; logs = value.logsProducer; } } } public bool Running { get; set; } private Bridge() { //20000 lines seems to slow down tabbing enough to prove my point. //increase this number to get even worse results. counterLimit = 40000; } public void PrintLotsOfText() { new Thread(new ThreadStart(GenerateOutput)).Start(); new Thread(new ThreadStart(GenerateError)).Start(); } private void GenerateOutput() { //There is tons of output text, so print super fast if possible. int counter = 1; while (Running && counter < counterLimit) { if (counter % 10 == 0) PrintImportantOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + "."); else PrintOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + "."); //Task.Delay(1).Wait(); } Console.WriteLine("GenerateOutput thread should end now..."); } private void GenerateError() { int counter = 1; while (Running && counter < counterLimit) { PrintError("I will never forgive your errors as long I live. This is confession #" + counter++ + "."); //Task.Delay(1).Wait(); } Console.WriteLine("GenerateError thread should end now..."); } #region Printing delegate void StringArgDelegate(String s); delegate void InlineArgDelegate(Inline inline); public void PrintOutput(String s) { output.TryAdd(new PrintInfo(s, false)); PrintLog("d " + s); } public void PrintImportantOutput(String s) { output.TryAdd(new PrintInfo(s, true)); PrintLog("D " + s); } public void PrintError(String s) { errors.TryAdd(new PrintInfo(s, false)); PrintLog("e " + s); } public void PrintImportantError(String s) { errors.TryAdd(new PrintInfo(s, true)); PrintLog("E " + s); } public void PrintLog(String s) { logs.TryAdd(new PrintInfo(s, false)); } #endregion } public class PrintInfo { public String Text { get; set; } public bool IsImportant { get; set; } public PrintInfo() { } public PrintInfo(String text, bool important) { Text = text; IsImportant = important; } } } 

MainWindow.xaml

 <Window x:Class="PerformanceTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" xmlns:l="clr-namespace:PerformanceTest" WindowStartupLocation="CenterScreen"> <Grid> <TabControl> <TabControl.Resources> <Style TargetType="ListBox"> <Setter Property="TextElement.FontFamily" Value="Consolas" /> <Setter Property="TextElement.FontSize" Value="12" /> <Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True" /> <Setter Property="VirtualizingStackPanel.VirtualizationMode" Value="Recycling" /> <Setter Property="l:ListBoxSelector.Enabled" Value="True" /> <Setter Property="ContextMenu"> <Setter.Value> <ContextMenu> <MenuItem Command="Copy" /> </ContextMenu> </Setter.Value> </Setter> </Style> <Style TargetType="ListBoxItem"> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="Margin" Value="0" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ListBoxItem"> <TextBlock Text="{Binding Text}" TextWrapping="Wrap" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" FontWeight="{TemplateBinding FontWeight}" /> <ControlTemplate.Triggers> <DataTrigger Binding="{Binding IsImportant}" Value="true"> <Setter Property="TextElement.FontWeight" Value="SemiBold" /> <Setter Property="TextElement.FontSize" Value="14" /> </DataTrigger> <Trigger Property="IsSelected" Value="true"> <Setter Property="Background" Value="{StaticResource {x:Static SystemColors.HighlightBrushKey}}" /> <Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.HighlightTextBrushKey}}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </TabControl.Resources> <TabItem Header="Bridge"> <StackPanel Orientation="Vertical" HorizontalAlignment="Left"> <Button Content="Start Test" Click="StartButton_Click" /> <Button Content="End Test" Click="EndButton_Click" /> </StackPanel> </TabItem> <TabItem Header="Output"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <!--<RichTextBox x:Name="OutputBox" ScrollViewer.VerticalScrollBarVisibility="Auto"/>--> <ListBox Grid.Column="0" ItemsSource="{Binding Output,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" ScrollViewer.HorizontalScrollBarVisibility="Disabled"> <ListBox.CommandBindings> <CommandBinding Command="Copy" Executed="CopyExecuted" /> </ListBox.CommandBindings> </ListBox> <GridSplitter Grid.Column="1" Width="5" ResizeBehavior="PreviousAndNext" /> <ListBox Grid.Column="2" ItemsSource="{Binding Errors,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" ScrollViewer.HorizontalScrollBarVisibility="Disabled"> <ListBox.CommandBindings> <CommandBinding Command="Copy" Executed="CopyExecuted" /> </ListBox.CommandBindings> </ListBox> </Grid> </TabItem> <TabItem Header="Log"> <Grid> <ListBox Grid.Column="0" ItemsSource="{Binding Logs,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" ScrollViewer.HorizontalScrollBarVisibility="Disabled"> <ListBox.CommandBindings> <CommandBinding Command="Copy" Executed="CopyExecuted" /> </ListBox.CommandBindings> </ListBox> </Grid> </TabItem> </TabControl> </Grid> </Window> 

MainWindow.xaml.cs

 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; using System.Threading; namespace PerformanceTest { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { public BlockingCollection<PrintInfo> outputProducer = new BlockingCollection<PrintInfo>(); public BlockingCollection<PrintInfo> errorProducer = new BlockingCollection<PrintInfo>(); public BlockingCollection<PrintInfo> logsProducer = new BlockingCollection<PrintInfo>(); public ObservableCollection<PrintInfo> Output { get; set; } public ObservableCollection<PrintInfo> Errors { get; set; } public ObservableCollection<PrintInfo> Logs { get; set; } protected FontFamily font = new FontFamily("Consolas"); protected int defaultFontSize = 12; protected int importantFontSize = 14; Dispatcher dispatcher; public MainWindow() { Bridge.Controller.Window = this; try { InitializeComponent(); } catch (Exception ex) { Console.WriteLine(ex.InnerException.ToString()); Console.WriteLine(ex.StackTrace); } dispatcher = Dispatcher; Output = new ObservableCollection<PrintInfo>(); Errors = new ObservableCollection<PrintInfo>(); Logs = new ObservableCollection<PrintInfo>(); new Thread(new ThreadStart(() => Print(outputProducer, Output))).Start(); new Thread(new ThreadStart(() => Print(errorProducer, Errors))).Start(); new Thread(new ThreadStart(() => Print(logsProducer, Logs))).Start(); } public delegate void EmptyDelegate(); public void Print(BlockingCollection<PrintInfo> producer, ObservableCollection<PrintInfo> target) { try { foreach (var info in producer.GetConsumingEnumerable()) { dispatcher.Invoke(new EmptyDelegate(() => { if (info.IsImportant) { String timestamp = DateTime.Now.ToString("[hh:mm.ss] "); String toPrint = timestamp + info.Text; info.Text = toPrint; } target.Add(info); }), DispatcherPriority.Background); } } catch (TaskCanceledException) { //window closing before print finish } } private void StartButton_Click(object sender, RoutedEventArgs e) { if (!Bridge.Controller.Running) { Bridge.Controller.Running = true; Bridge.Controller.PrintLotsOfText(); } } private void EndButton_Click(object sender, RoutedEventArgs e) { Bridge.Controller.Running = false; } private void CopyExecuted(object sender, ExecutedRoutedEventArgs e) { ListBox box = sender as ListBox; HashSet<PrintInfo> allItems = new HashSet<PrintInfo>(box.Items.OfType<PrintInfo>()); HashSet<PrintInfo> selectedItems = new HashSet<PrintInfo>(box.SelectedItems.OfType<PrintInfo>()); IEnumerable<PrintInfo> sortedItems = allItems.Where(i => selectedItems.Contains(i)); IEnumerable<String> copyItems = from i in sortedItems select i.Text; string log = string.Join("\r\n", copyItems); Clipboard.SetText(log); } } } 

ListBoxSelector.cs is in @pushpraj's answer.

+8
performance c # text wpf
source share
2 answers

I am running the sample that you provided, in addition to a few problems with threads, the biggest problem is the amount of data. which slows down the rendering of text as it grows.

I tried to rewrite my code differently. I used Tasks, BlockingCollection, and Virtualization to improve performance with the assumption that the main interest of the application is logging speed.

Bridge.cs

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Documents; using System.Threading; using System.Collections.Concurrent; using System.Threading.Tasks; namespace PerformanceTest { public class Bridge { int counterLimit; public BlockingCollection<string> output; public BlockingCollection<string> impOutput; public BlockingCollection<string> errors; public BlockingCollection<string> impErrors; public BlockingCollection<string> logs; protected static Bridge controller = new Bridge(); public static Bridge Controller { get { return controller; } } public MainWindow Window { set { if (value != null) { output = value.outputProducer; impOutput = value.impOutputProducer; errors = value.errorProducer; impErrors = value.impErrorProducer; logs = value.logsProducer; } } } public bool Running { get; set; } private Bridge() { //20000 lines seems to slow down tabbing enough to prove my point. //increase this number to get even worse results. counterLimit = 40000; } public void PrintLotsOfText() { Task.Run(() => GenerateOutput()); Task.Run(() => GenerateError()); } private void GenerateOutput() { //There is tons of output text, so print super fast if possible. int counter = 1; while (Running && counter < counterLimit) { PrintOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + "."); //Task.Delay(1).Wait(); } } private void GenerateError() { int counter = 1; while (Running && counter < counterLimit) { PrintError("I will never forgive your errors as long I live. This is confession #" + counter++ + "."); //Task.Delay(1).Wait(); } } #region Printing delegate void StringArgDelegate(String s); delegate void InlineArgDelegate(Inline inline); public void PrintOutput(String s) { output.TryAdd(s); PrintLog("d " + s); } public void PrintImportantOutput(String s) { impOutput.TryAdd(s); PrintLog("D " + s); } public void PrintError(String s) { errors.TryAdd(s); PrintLog("e " + s); } public void PrintImportantError(String s) { impErrors.TryAdd(s); PrintLog("E " + s); } public void PrintLog(String s) { String text = s; logs.TryAdd(text); } #endregion } } 

MainWindow.xaml

 <Window x:Class="PerformanceTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" WindowStartupLocation="CenterScreen"> <Grid> <TabControl> <TabControl.Resources> <Style TargetType="ListBox"> <Setter Property="TextElement.FontFamily" Value="Consolas" /> <Setter Property="TextElement.FontSize" Value="12" /> <Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" /> <Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" /> </Style> </TabControl.Resources> <TabItem Header="Bridge"> <StackPanel Orientation="Vertical" HorizontalAlignment="Left"> <Button Content="Start Test" Click="StartButton_Click" /> <Button Content="End Test" Click="EndButton_Click" /> </StackPanel> </TabItem> <TabItem Header="Output"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <ListBox Grid.Column="0" ItemsSource="{Binding Output,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" /> <GridSplitter Grid.Column="1" Width="5" ResizeBehavior="PreviousAndNext" /> <ListBox Grid.Column="2" ItemsSource="{Binding Errors,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" /> </Grid> </TabItem> <TabItem Header="Log"> <Grid> <ListBox Grid.Column="0" ItemsSource="{Binding Logs,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" /> </Grid> </TabItem> </TabControl> </Grid> </Window> 

MainWindow.xaml.cs

 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Threading; namespace PerformanceTest { /// /// Interaction logic for MainWindow.xaml /// public partial class MainWindow : Window { public BlockingCollection<string> outputProducer = new BlockingCollection<string>(); public BlockingCollection<string> impOutputProducer = new BlockingCollection<string>(); public BlockingCollection<string> errorProducer = new BlockingCollection<string>(); public BlockingCollection<string> impErrorProducer = new BlockingCollection<string>(); public BlockingCollection<string> logsProducer = new BlockingCollection<string>(); public ObservableCollection<object> Output { get; set; } public ObservableCollection<object> Errors { get; set; } public ObservableCollection<object> Logs { get; set; } Dispatcher dispatcher; public MainWindow() { Bridge.Controller.Window = this; try { InitializeComponent(); } catch (Exception ex) { Console.WriteLine(ex.InnerException.ToString()); Console.WriteLine(ex.StackTrace); } dispatcher = Dispatcher; Output = new ObservableCollection<object>(); Errors = new ObservableCollection<object>(); Logs = new ObservableCollection<object>(); Task.Run(() => Print(outputProducer, Output)); Task.Run(() => Print(errorProducer, Errors)); Task.Run(() => Print(logsProducer, Logs)); } public void Print(BlockingCollection<string> producer, ObservableCollection<object> target) { try { foreach (var str in producer.GetConsumingEnumerable()) { dispatcher.Invoke(() => { target.Insert(0, str); }, DispatcherPriority.Background); } } catch (TaskCanceledException) { //window closing before print finish } } private void StartButton_Click(object sender, RoutedEventArgs e) { if (!Bridge.Controller.Running) { Bridge.Controller.Running = true; Bridge.Controller.PrintLotsOfText(); } } private void EndButton_Click(object sender, RoutedEventArgs e) { Bridge.Controller.Running = false; } } } 

For a complete working example, download PerformanceTest.zip and see how close it is to what you need. I just rewrote only a part. If this example goes in the right direction, we can implement the rest of the functionality. un-comment Task.Delay(1).Wait(); in Bridge.cs, if you can slow down production to view mixed logs, otherwise the log generation will be too fast for it to appear one after the other on the logs tab.

+5
source share

Using FlowDocumentPageViewer will help performance, as it loads the document asynchronously.

0
source share

All Articles