1

My app features a data-bound read-only TextBox as a means to capture and display its "logged" activity. Each time something is to be logged, it is concatenated to the bound string. This works well enough for limited amounts of logged text, but as the quantity of text grows, it (understandably) bogs down. I've seen the suggestion in previous questions: Efficient live log-viewer in WPF and What is a fast way to render a log view in WPF? of using a ListBox. I could do this, but I'd lose a nice feature - allowing the user to select and copy arbitrary blobs of text. Is there any other solution?

C Robinson
  • 411
  • 4
  • 18
  • you should set a limit on how much text you want to keep in the log and start removing them as application runs. All log histories should be written to a text file for users to copy – Steve Nov 20 '17 at 21:22
  • At which point it bogs down (on how much lines)? What are the requirements (max number of lines to display)? – Evk Nov 20 '17 at 21:36
  • @Evk A bit subjective, but as the string length gets to about 100k or so (not sure how many lines, maybe only a 1000 or so), it begins to be noticeable. Would be nice if it could still work with a couple of megs - 10,000 - 20,000 lines. – C Robinson Nov 20 '17 at 22:47

2 Answers2

6

You can use a Listbox and allow the user to copy parts of the log by using some ItemTemplate:

<ListBox Name="viewList">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBox Text="{Binding Mode=OneWay}" IsReadOnly="True"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Then fill it with some ObservableCollection:

ObservableCollection<string> mvList = new ObservableCollection<string>();
viewList.ItemsSource = mvList;

Good to know: the ListBox automatically implements some virtualization that ensures good performances with very long lists. Here for more details

P.Manthe
  • 960
  • 1
  • 5
  • 12
  • 1
    I've seen the solution in the answers I referenced, but what it doesn't do that I'm aiming for is for the log to be arbitrarily selectable as if it was one big blob of text.Sure, you can do multi-line selection, but it's a very different user experience to what I'm aiming for. – C Robinson Nov 21 '17 at 15:08
  • Adding an auto-scroll bottom from here, helps a lot too - https://stackoverflow.com/a/28433208/353147 with a DoEvents here - https://stackoverflow.com/a/11899439/353147 – Chuck Savage Oct 28 '21 at 05:42
0

Problem solved simply...

XAML of my new user control:

<UserControl x:Class="DbHelper.Controls.LogControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <TextBox x:Name="TextBox" FontFamily="Consolas" FontSize="12" IsReadOnly="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" TextWrapping="Wrap"></TextBox>
</UserControl>

... and its code-behind:

public partial class LogControl : UserControl
{
    private readonly Queue<string> _logQueue = new Queue<string>();
    private Timer _timer;
    private bool _synced;

    public int MaxLines { get; set; } = 1000;

    public LogControl()
    {
        _timer = new Timer(state => ((LogControl)state).Refresh(), this, 1000, 1000);
        InitializeComponent();
    }

    private void Refresh()
    {
        lock (_logQueue)
        {
            if (!_synced)
            {
                var sb = new StringBuilder();
                foreach (var line in _logQueue)
                {
                    sb.AppendLine(line);
                }

                Dispatcher.Invoke(() =>
                {
                    TextBox.Text = sb.ToString();
                    TextBox.ScrollToEnd();
                });
                _synced = true;
            }
        }
    }

    public void Log(string str)
    {
        lock (_logQueue)
        {
            _logQueue.Enqueue(str);
            while (_logQueue.Count > MaxLines)
            {
                _logQueue.Dequeue();
            }
            _synced = false;
        }
    }

    public void Clear()
    {
        lock (_logQueue)
        {
            _logQueue.Clear();
            _synced = false;
        }
    }
}
C Robinson
  • 411
  • 4
  • 18
  • Nice solution to your problem. A simple improvement to your `Refresh()` function would be to move the `sb.ToString()` call off of the `Dispatcher` thread as well. All that needs to be within the `Dispatcher.Invoke(...)` is the assignment to the `Text` property, and `ScrollToEnd()`, not the building the `String` by `StringBuilder`, which could be time consuming. So pass a `string` into your closure, instead of the `sb` as you have it. – Glenn Slayden Jun 01 '22 at 20:53