10

In a previous question, I asked how to get real-time logging output in a WPF textbox-like element (WPF append text blocks UI thread heavily but WinForms doesn't?). The answer there led me to using a FlowDocumentScrollViewer, which indeed was much faster than RichTextBox. However, I've found that running commands that have a massive amount of text output (like 'svn co') leads to a noticeable slowdown in my WPF application. Switching tabs after checking out 3 or 4 very large svn branches takes 3-4 seconds, and I'm sure the time would go up with the number of checkouts I do. Scrolling also has noticeable lag.

As stated in the question I linked above, I switched my application from Windows Forms to WPF recently. I like WPF a lot - it gives many advantages I didn't have in Forms. However, performance seems to be quite an issue in WPF, at least for me. In the Forms version of my application, I could print out massive amounts of text to the RichTextBox control and had no slowdown at all in my app. Switching tabs was instant, and scrolling was seamless. This is the experience I want in my WPF app.

So my question is this: How can I improve the performance of my FlowDocumentScrollViewer to match the performance of the Windows Forms RichTextBox, without losing formatting capabilities like bold and italic, and without losing copy/paste functionality? I'm willing to switch WPF controls as long as they offer the formatting capabilities I'm looking for.

Here's my printing 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 font sizes and make text bold when printing "important" text. The reason this code is so many lines is because I'm trying to re-use the same paragraph for all text until I hit "important" text; I add a new paragraph with all the "important" text, then add another paragraph once I switch back to the non-important text and append to that paragraph until I hit more "important" text. I was hoping that re-using the same paragraph would improve performance.

Also, it should be noted that I'm printing stdout to one FlowDocumentScrollViewer, stderr to another FlowDocumentScrollViewer, and both at once to a third FlowDocumentScrollViewer. So each line of stdout and stderr technically gets printed twice, doubling the load on my app. Again, this wasn't an issue in WinForms.


Below is a full code sample, as requested in the comments. It is extremely simple (3 FlowDocumentScrollViewer's and simple printing) but still slows down big time around 20000 lines of text, and much worse past that.

EDIT: The code sample has been removed. In its place is the working code to solve my performance issues. It works just as FlowDocumentScrollViewer would, with one exception: you can't select substrings of lines. I'm looking into fixing that, 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.

Community
  • 1
  • 1
Darkhydro
  • 1,992
  • 4
  • 24
  • 43
  • so, you have 3 `FlowDocument`s? and 3 different `FlowDocumentScrollViewers`? or 3 scrollviewers showing the same document? can you clarify that a little bit? – Federico Berasategui Jul 30 '14 at 18:32
  • @HighCore I have 3 separate FlowDocuments, 1 for stdout, 1 for stderr, and a 3rd for the "Log" tab, which combines the two and adds some helpful logging text as well. Each FlowDocument has its own FlowDocumentScrollViewer as well. – Darkhydro Jul 30 '14 at 18:41
  • **Bounty:** I'm too lazy/busy at the moment to participate in SO other than unhelpful comments (like this one). Please Help the OP. I'm sure I'm not the only one who answers WPF questions.... – Federico Berasategui Aug 20 '14 at 00:24
  • Could you post a working sample which can reproduce the issue? may I try to find some optimizations for you. – pushpraj Aug 20 '14 at 04:20
  • I'm not finding any issues with a simple example I concocted (lots of text, lots of tabs). Let us know when you have a few examples of up! Thanks. – Matt Aug 20 '14 at 19:41
  • @Matt I'll get a sample up within a couple days. – Darkhydro Aug 21 '14 at 01:21
  • @Matt I've posted a sample, it slows down VERY noticeably around 40000 lines of text. – Darkhydro Aug 23 '14 at 01:58
  • @Darkhydro I execute the sample you've provided, apart from few threading issues the biggest issue is amount of data. which slows down the text rendering as the it grows. I have a few question before I may be able to answer the question. how much lines you want to keep at one point of time? is there any maximum limit? – pushpraj Aug 23 '14 at 08:35
  • I'm confused. I don't see any FlowDocumentScrollViewers or FlowDocuments in the XAML. But I'm far from a XAML pro. What am I missing? – Greg Vogel Feb 04 '17 at 22:48
  • @GregVogel See the EDIT at the top of where my code samples start. I've removed the FlowDocumentScrollViewer sample code. – Darkhydro Jul 17 '17 at 23:53

2 Answers2

6

I execute the sample you've provided, apart from few threading issues the biggest issue is amount of data. which slows down the text rendering as the it grows.

I tried to rewrite your code in a different way. I used Tasks, BlockingCollection and Virtualization to improve the performance with assumptions that 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 full working sample download PerformanceTest.zip and see if this is close to what you need. I have only rewritten only some portion. If this example is heading to the desired direction, we can implement rest of the functionality. un-comment Task.Delay(1).Wait(); in Bridge.cs if you may want to slow down the production to see the mixed logs otherwise the logs generation is too fast so it appear one after other in logs tab.

pushpraj
  • 13,458
  • 3
  • 33
  • 50
  • A rather important thing I seem to have somehow missed in my question is that I need copy-paste functionality as well, which ListBox limits greatly. I tried the ListBox approach per HighCore's suggestion earlier, and the virtualization sped things up a lot, but getting copy paste working with that solution is an absolute nightmare and not something I ever completed. – Darkhydro Aug 25 '14 at 18:36
  • I've converted my PerfTest app to use your code, and as I suspected copy paste is a huge issue. The virtualizing ListBox is extremely fast and has the performance I want, but functionality wise I really do need copy/paste. – Darkhydro Aug 25 '14 at 20:31
  • If you need copy and paste, simply modify the item template for the ListBox to use a readonly text box instead. – Clever Neologism Aug 25 '14 at 21:09
  • @Clever Neologism That doesn't change the fact that each ListBoxItem is separated from other items, and selecting across multiple items is near impossible... I'm talking about click and drag copy/paste, not line by line, which is useless in my application. Additionally (for pushpraj's benefit), I'm on .NET 4.0, not 4.5, so I had to modify your code a bit to get it working. – Darkhydro Aug 25 '14 at 21:17
  • got your point so you want the log to be able to be selected and copied too. let's see how we can achieve the same while keeping up the performance. – pushpraj Aug 26 '14 at 03:11
  • I tried to implement the copy behavior using [ListBox-drag-selection](http://www.codeproject.com/Articles/209560/ListBox-drag-selection), here is the updated project [PerformanceTest-2.zip](https://docs.google.com/file/d/0B2f5wPS0jKJPeEJFNVNqX0xMalU/view?pli=1). this will enable drag selection and copy of logs, give it a try and see if that helps. only point to notice is that unlike a textbox the log entry will be copied in multiple of rows instead of a part(substring) of it. I assume part of a log entry may not have enough meaning to be copied. however that can also be achieved if required – pushpraj Aug 27 '14 at 06:23
  • @pushpraj This new project is coming a lot closer to what I'm looking for, however there's still some major issues. For one, this setup doesn't allow me to customize output between PrintOutput and PrintImportantOutput for instance. I want important output to be in bold and larger font, with a timestamp on it. Also, I can envision a lot of issues with spacing using this model, since strings with newlines will go in one ListBoxItem using newline spacing, and then there's a different spacing between ListBoxItems. I also want the copy-paste to be from any point in the substring. – Darkhydro Aug 28 '14 at 20:38
  • @pushpraj Essentially I'm looking for the capabilities of the RichTextBox control without the massive performance penalties it incurs. Here's a list: bold, italic, font size, text spacing, line spacing, and copy paste. – Darkhydro Aug 28 '14 at 20:41
  • apart from copy-paste anywhere, all the other requirements are possible with the current approach. RichTextBox or any other document viewer will come with such penalties while dealing with huge data. – pushpraj Aug 29 '14 at 06:17
  • @pushpraj Ok, I have your example printing bold text with different sizes, and modifying the ListBox style can get me 0 spacing between ListBoxItems. If you can provide me with an example of copy-paste from anywhere (or at least something to start from) I'll give you the answer. – Darkhydro Aug 29 '14 at 18:51
  • @pushpraj Also, I'm having a very difficult time figuring out how to wrap text in the ListBoxItems. I've tried at least 10 different online solutions, including DataTemplates, ItemContainerTemplates, Styles, changing HorizontalContentAlignment to Stretch and disabling the horizontal scrollbar, but nothing works. When text is longer in the ListBoxItem, it doesn't wrap, period. I would greatly appreciate any help on this. – Darkhydro Aug 29 '14 at 19:23
  • @pushpraj Honestly, the text wrap issue is much larger than the copy-paste anywhere issue. I'd really like to be able to copy substrings, but I can force users to copy paste lines at a time and edit them in a different application I guess... – Darkhydro Aug 29 '14 at 22:56
  • @pushpraj I've managed to get wrapping functionality by using a DataTemplate with a Label in it and changing the Label's resources to use a style that force's its inner TextBlock to wrap by default. I ran into an issue with copy paste order (selected items in a listbox are copied in order of selection, not item order), which I've also solved for most cases, but doesn't work for selections greater than an arbitrary length I assigned (2000 in this case). Ctrl+A works however as a special case. If you know how to easily sort selected items please let me know. – Darkhydro Aug 30 '14 at 01:52
  • I was also working on the wrapping issue and also discovered the incorrect order(actually selection order). I also implemented the bold , timestamp thing with more real looking selection. let's see how we can solve the ordering issue. mean while have a look on [PerformanceTest-3.zip](https://drive.google.com/file/d/0B2f5wPS0jKJPUWE4Q0tlcmg3dmc/view) – pushpraj Aug 30 '14 at 02:17
  • I just come up with the sorting, use the code from [Sorting SelectedItems based on original collection](http://pastebin.com/aKZ9f2rN) and merge with the solution. secondly I was not able to figure out the 2000 limit I am able to copy over 10,000 lines with drag select, it take so long to select so I didn;t go much further. with ctrl-A it is able to select all as you mentioned. – pushpraj Aug 30 '14 at 03:15
  • @pushpraj Your sorting solution works much better than mine. I've switched to it. I've also switched my code to use the TextBlock control template, which works very well but introduces a new issue. Shift+Click for ListBoxItem works as intended (selecting everything between the first and last item selected), but with the TextBlock template IF you hit the TextBlock and not the ListBoxItem, Shift+Click simply appends the selected item for copy instead of selecting everything in between. Additionally, Shift+Arrow completely stalls the application. This is good enough for an answer though. – Darkhydro Sep 02 '14 at 20:56
  • @pushpraj I'd still appreciate your help with this additional issues if possible. I can open another question regarding these followup issues if that helps. I've edited my question with the new code to show people how to solve my original question. – Darkhydro Sep 02 '14 at 20:58
  • 1
    @pushpraj I've solved these issues by getting rid of the element inside the TextBlock. Shift+Arrow doesn't highlight up and down as expected, it moves to the top and bottom of the ListBox, but that's fine with me. Shift+Click now works as expected. The only issue left is substring selection inside the ListBoxItems. I'll look around for an answer, but if you find one, please let me know! Thanks for the help @pushpraj! – Darkhydro Sep 02 '14 at 23:44
  • I am glad it worked for you. I used Run element for a natural looking selection otherwise TextBlock is perfectly fine. let's see if we can solve the partial selection. I am not sure how, but would still give it a try. – pushpraj Sep 03 '14 at 04:49
1

Using FlowDocumentPageViewer will help the performance since it loads document asynchronously.

Ekk
  • 5,627
  • 19
  • 27
  • 1
    I misunderstood what this control is... I can't have the output be on multiple pages, it MUST be on a single scrollable page. – Darkhydro Aug 25 '14 at 20:15