Log window from scratch
Welcome to a new entry in the Log window from scratch series, dear reader!
In the previous entry we converted our WPF window into a
Class Library and created a host program that used it to display some sample messages. This time, we’ll expose some configuration to the host program and add some new functionality to the window. This way we can support several projects and make it more useful.
Ready? Set? Go!
Window position and dimensions
Let’s start with a simple one: setting the position and size of our window.
Imagine you are developing a game and it has a window where it gets rendered. You don’t want your log window to be created over it (or under it!), but side to side instead.
We could add two new methods to our
SetSize, or we could pass these parameters to the
Initialize method and have them set from the start. For now, let’s pass them to
Initialize since we don’t plan on resizing it programatically after it’s shown.
Initialize like so:
So we can call it like so:
Of course, we’d need to update our
LoggerUI’s constructor to match the new parameter:
The only addition has been the block where we set the dimensions.
And this is it! We can now position and size it however we want.
Bonus idea: we could ask the window about these properties (which would be updated when resizing or moving the window) from the host program and persist its values so we could start from the last configuration when we launch the program again.
Auto-scroll to bottom
This will be the first addition that will modify our window’s layout.
If you were to use the window as it is now, you would notice the scroll isn’t moving unless you do it manually. This may be useful if you are reading some of the messages, but most of the time you may want it to scroll to the last message automatically.
We’ll be adding a checkbox to our window that lets us activate or deactivate this functionality. This isn’t something the host program can choose to have or not, but something that’s built-in. So our layout will be something like this:
Let’s start with the built-in block. This is the updated
MainWindow.xaml file with some parts commented out for the sake of brevity:
As you can see, we’ve wrapped everything into a
DockPanel which allows us to resize the window and keep every control stretched as we want while keeping some parts fixed. This is how it looks:
Now we have to connect that checkbox to some
ViewModel. We’ll update its XAML definition as:
With that, we’ve tied a yet-to-be-created variable called
IsAutoScrollEnabled to our checkbox. Let’s add it in our
If you were to do this, it wouldn’t work because we’re trying to bind a variable from a
null by default. To fix that we must do this in the
Now it will work as expected.
For now, we’ve used our own log capability to show a message in the window whenever we modify the property. If we were to run it and click on the checkbox several times we’d get something like this:
Scroll to bottom
By default, when a new item is added to the
ListView that contains our log messages no scroll is performed. We’d want to know when an item is added to the list so we can perform the scroll ourselves.
LogEntries variable (which is an
ObservableCollection) has a
CollectionChanged event handler we can subscribe to. So, first of all let’s create a method in our
MainWindow.xaml.cs to handle the scroll:
Basically, we ask the
ListView for its children, get the scroll control for the list and ask it to go to the bottom. We can also check the
NotifyCollectionChangedEventArgs to know whether it was triggered because an item was added or deleted, if we wanted to.
Now, to tie it with our checkbox, add this:
We can now, also, refactor
IsAutoScrollEnabled with just this:
Congratulations, you’ve got an auto-scrolling
Per system filters
So far the new features are great additions, but how about being able to filter messages by their system?
Imagine you’re working on program and there’s some bug you’re tracking down related to the net communication. Wouldn’t it be awesome to filter out everything else and only read the logs for that system? Something easy like clicking on different checkboxes?
However, the goal of this log window is to be used in more than a single project and each one of them may have completely diferent systems. So, how do we do it?
Configuration from the host
LoggerUI entry point? We’re adding a new method to let the host program tell us which systems it will be using so we can create as many checkboxes. This could be the outline:
MainWindow.ConfigureSystems doesn’t exist, so we must create it. But first, let’s think about what we need at the Window level.
We’re going to have a list of checkboxes, one for each system we’ve received and we want to know whether they are checked to filter the messages. Phew! Let’s digest all that.
We could have a new
LogSystem which contains the name of the system (as received from the host) as well as the state of the checkbox we’ll present to the user. Something like:
And this way, we can have a
ObservableCollection<LogSystem>) in our
MainWindow that contains all of the entries. By having that, we could have
MainWindow.ConfigureSystems do this:
List<LogEntry> where we store all of the log messages we receive? Well, we want to filter it now. To do that, we’ll create a new
ICollectionView from it.
ICollectionView lets us sort, filter or group data from a given collection via predicate. This predicate is a function that decides, for each element in the collection, whether it belongs to the filtered data.
By the way, don’t let the
View part of the name fool you. This doesn’t have anything to do with visual representation, it’s just the way to call this kind of filtered collection.
So, we’ve got to create it in our
And initialize it:
As you can see, we take
LogEntries and create a view of the data. By the way, you may want to transfer the
CollectionChanged event to this one so we apply the scroll when this one changes and not the unfiltered one. Then we assign our filter, which looks like this:
In other words, we keep any
LogEntry unless the matching
LogSystem is disabled.
This implementation has an unexpected behavior at this point, but let’s continue and discover it later on.
We said we’d like to have as many checkboxes as systems so we could filter them to fit our needs. So, we need to add them somewhere. Maybe… to the bottom like this?
For that, and again skipping some of the XML attributes, we have to update our layout to this:
And remember to set the
ItemsSource of the
And this is how it would look like:
Impressive, isn’t it? Except it has a bug.
If you asked me, before even executing the log window, What should it do if I disable the TEST system for a while and then enable it? I’d say it would be silent for that while and then all the muted messages would pop at once. Do you mind if we test it now?
First, let it show some messages then disable the
After 5 seconds, let’s enable it again.
Can you spot the issues?
The first screenshot shows how we wanted to filter out the messages with the
TEST system but the ones before we disabled it are still there.
The second one shows how the messages between timestamps
8797 are gone! It’s like they weren’t registered at all! Why is that?
If you remember, we created an
ICollectionView from the
LogEntries list. When we created it, the filter was executed and it’s executed again when a new
LogEntry is added to the original
When we disabled the
TEST system, new items didn’t fulfill the predicate but we didn’t re-evaluate it for the existent ones. When we enabled it again, only the new ones are evaluated and not the ones that were discarded while it was disabled (although they were still stored in the
So, how do we fix this issue?
We’ve seen how our
LogSystem.Enabled property was modified when we interacted with the checkbox. Our goal is to re-evaluate the filter whenever this happens so it’s applied to the whole
To do this we need to allow whoever is interested to listen to changes in our
Enabled property. WPF has an interface called
INotifyPropertyChanged that lets us execute an event whenever this happens. We’d have to refactor our
LogSystem class as:
When inheriting from this interface we have a new event handler called
PropertyChanged that lets others listen to the changes we decide to trigger.
MainWindow.ConfigureSystems method where we created our
LogSystem entries? That’s where we’ll subscribe to changes.
OnSystemEnableChanged would look like this:
Refresh recreates the view by re-evaluating the predicate against the source collection. This way, when we disable the
TEST system we’ll have an empty list and when we re-enable it we’ll have all items even when we weren’t seeing them.
Great job! Now you’ve got filters by system in an auto-scrolling
We’ve gone through some useful functionalities for our window, but we’re missing an interesting one: log levels with colors!
Mind joining me in the next post of the series to complete it?
Thanks for reading!