Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

LogViewer: A Fast WPF Control Displaying Logging Information

5.00/5 (6 votes)
2 Sep 2022CPOL7 min read 13.1K  
Concurrent threads can use LogViewer to show the user scrollable information efficiently
For long running background tasks, it is helpful for the user to know which step is presently executed. LogViewer can collect this information multithreading safe and display it to the user as scrollable text. LogViewer allows the background thread to write formatted text without using any WPF code. When hundreds of messages need to get written per second, WPF would be too slow updating the GUI for each single message. LogViewer collects the messages and sends them 10 times per second to the WPF thread.

Image 1

LogViewer Use Case

It is quite common that a task gets executed in the background which may run for minutes and executes thousands of steps. If the GUI shows no change during that time, the user might think the app is frozen. On the other hand, he might not need to see all the thousands of steps, but only the few major ones and the current one.

Easiest would be just to display the major ones, but then the user has no idea which of the many detail steps took too much time. But if he sees that the current step gets displayed for seconds, he knows that something is wrong.

Since the background task might not know which step will take long and gets displayed, it is easiest if he can just write all messages to the LogViewer, which then executes the following functions:

  • Accept logging messages multithreading safe, not on WPF thread
  • Differentiate between temporary and permanent messages. A temporary message gets overwritten by any following message, while a permanent message is still shown to the user once the background task has finished.
  • Collect all messages received within 0.1 second and transfer them together to the WPF thread. The WPF thread cannot update the GUI hundreds of times per second and the user could not read the information anyway.
  • Translate the messages into WPF Flow<wbr />Document elements. Controlling fonts, sizes, margins, etc. can be rather complicated and the writer of the background task should not need to know WPF. To specify the required formatting, he simply passes with the message also a StringStyleEnum value defined specifically for that app in StyledString.cs.

LogViewer Design

This chapter describes some design considerations. If you are just interested in using the Logviewer, jump to the chapter Using LogViewer in your app.

Implementation of Multithreading

I was considering using some fancy multithreading safe collection to store the messages, but in the end, I decided just to use simple lists and lock every Write() by the background task and every Read() by the WPF thread. As soon explained, the Read() is extremely fast and only executes 10 times a second, meaning there will be hardly ever any simultaneous lock.

Image 2

LogViewer.Write() acquires the lock and then does three activities, using the "active" message buffer:

  1. If the last message in the buffer is a temporary one, remove it.
  2. Add message together with StringStyleEnum value to active buffer
  3. If timer is not started: start timer with 0.1 second delay and, if caller is actually running already on WPF thread, write message immediately to GUI.

In the Tick event of the wpfTimer, first the buffers get locked, then the buffers switched. When Write() was using Buffer1 before the tick occurred, it will use Buffer2 afterwards. The lock now gets released and the WPF thread has all the time it needs to process the "not active" buffer. Once done, the timer restarts itself in another 0.1 sec. Once it gets an empty buffer, it stops restarting.

Supporting Formatted Text

The background task usually belongs to the business layer code, which should not have any dependency from WPF. For that reason, LogViewer defines its own StringStyleEnum:

C#
public enum StringStyleEnum {
  none = 0,
  normal,
  label,
  header1,
  errorHeader,
  errorText,
  stats
}

These values can be different according to the needs of the application. The file StyledString.cs contains also the static method StyledString.ToInline(), which translates the message and its StringStyleEnum value into a WPF flowdocument Run:

C#
case StringStyleEnum.header1:
  styledParagraph.Margin = new Thickness(0, 24, 0, 4);
  inline = new Bold(new Run(message)) {
    FontSize = styledParagraph.FontSize * 1.2
  };
  break;

Adding Formatting to Plain Strings

The background tasks use the following LogViewer methods for writing:

C#
public void WriteLine()
public void WriteLine(string line)
public void WriteTempLine(string line)
public void WriteLine(string line, StringStyleEnum stringStyle)
public void WriteTempLine(string line, StringStyleEnum stringStyle)
public void Write(string text)
public void Write(string text, StringStyleEnum stringStyle)
public void Write(StyledString styledString)
public void Write(params StyledString[] styledStrings)

The Write() contains the message and possibly some information about the format and if an end of line is needed. LogViewer translates this into a StyledString, which then gets forwarded to WPF.

C#
public class StyledString {
  public string String { get; private set; }
  public StringStyleEnum StringStyle { get; private set; }
  public LineHandlingEnum LineHandling { get; private set; }
  public DateTime Created { get; private set; }
}

The values of StringStyleEnum are different for each application. LineHandling can have the three values none, endOfLine and temporaryEOL. Basically, it is possible to write only part of a line with Write(), which is useful if part of the log line should be bold and another part not, in which case the background task would call Write() twice.

Messages marked with temporaryEOL get only shown to the user until the next Write() gets called.

Using LogViewer in your App

You can get the latest version of LogViewer from Github:

https://github.com/PeterHuberSg/LogViewer

Image 3

I put it in its own DLL LogViewerLib. Instead of linking that library into your app, I would recommend just to copy the two files, LogViewer.cs and StyledString.cs. You will need to change the content of StyledString.cs to your app's needs and it might therefore be better if you do not keep syncing your code with LogViewer on GitHub.

LogViewerTestApp is a WPF application I use to test LogViewer.

Testing LogViewer

It might be interesting to have a look at the LogViewerTestApp code to see how to add LogViewer to your app. Running LogViewerTestApp gives you a feeling how LogViewer behaves.

Image 4

Test1 writes some differently formatted text. Note that the last line "tempLine3" is a temporary line. As soon any TestX button gets pressed again, that line will get overwritten.

Test2 writes temporary lines as quickly as possible. Amazingly, LogViewer can handle a million temporary messages a second! After all, it just collects them in RAM, but sends only 1 every 0.1 second to WPF.

Test3 keeps writing new lines. This was helpful to test if the user can stop and continue autoscrolling. If the user wants to inspect a permanent line, he can scroll to it, which stops the autoscrolling. Once he wants to activate the autoscrolling again, he simply scrolls to the end.

Further Reading

If you have read until here, you might be truly interested in WPF, in which case I would warmly recommend some of my other WPF articles. The present one I feel is not that interesting to read, but some of my articles are really helpful, giving you insights into WPF which you will not find anywhere else:

My Projects on Github

I also wrote several opensource projects on Github:

StorageLib Truly amazing replacement of databases for single user WPF apps. The programmer just defines the C# classes he wants to use and the code generator writes all code for creating, updating and deleting the data permanently on a local disk. This speeds up the work of the developer greatly, because he no longer needs to use any SQL, Entity Framework, etc. Queries are done in Linq and run super fast. The code executes much faster than any database. Supports transactions, backups and much more. Runs error free since 5+ years in several projects.
TracerLib Collecting efficiently in real time multithreaded data which can be stored in a file or just stored in RAM and discarded after some time. This is useful for Exception handling, because this allows to tell the user what happened before (!) the Exception occurred.
WpfWindowsLib WPF Controls for data entry, detecting if required data is missing or data has been changed.

Last but Not Least a Great Game

Image 5

I wrote MasterGrab 6 years ago and since then I play it nearly every day before I start programming. It takes about 10 minutes to beat 3 robots who try to grab all 200 countries on a random map. The game finishes once one player owns all the countries. The game is fun and every day fresh because the map looks completely different each time. The robots bring some dynamic into the game, they compete against each other as much as against the human player. If you like, you can even write your own robot, the game is open source. I wrote mine in about 2 weeks, but I am surprised how hard it is to beat them. When playing against them, one has to develop a strategy so that the robots attack each other instead of you. I will write a CodeProject article about it sooner or later, but you can already download and play it. There is good help in the application explaining how to play:

History

  • 3rd September, 2022: Initial version
  • 23rd May, 2023: Removed WPF processing out of lock in WpfTimer_Tick. Details see comments.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)