As useful as it is, there are certain occasions when using the Output window will not suffice. Perhaps we have far too much output to look through now and would like to view it on the way home from work, or maybe we need to see this kind of debug trace information after our application has been deployed. In these cases and others, it's time to enable the WPF Presentation Trace Sources.
There are a number of different trace sources that we can employ to output detailed tracing data for us. The choice is the same as that found in the WPF Trace Settings options and, in fact, after setting the values there, the Output window has already been showing us the debug trace output.
By default, WPF uses a DefaultTraceListener object to send the information to the Output window, but we can override that and/or configure the output to be sent to a text and/or XML file instead or as well.
In order to do this, we need to alter our app.config file, which is found in the root folder of our startup project. We'll need to add a system.diagnostics section and within it, add sources, switches, and sharedlisteners elements. The switches element holds the switch that determines the output level, as specified in the previous section.
The sharedlisteners element specifies which kind of output we want to utilize. The three types are:
- System.Diagnostics.ConsoleTraceListener: Sends the traces to the Output window
- System.Diagnostics.TextWriterTraceListener: Outputs to a text file
- System.Diagnostics.XmlWriterTraceListener: Outputs to an XML file
Finally, we need to add a source element for each trace source that we want to listen to, and specify which switch and listener we want to use with it. Therefore, we are able to output different trace sources to different media and with different levels of output. These trace sources are the same as those found in the WPF Trace Settings options, although in the configuration file, we need to specify their full names.
The choices are as follows:
- System.Windows.Media.Animation
- System.Windows.Data
- System.Windows.DependencyProperty
- System.Windows.Documents
- System.Windows.Freezable
- System.Windows.Interop.HwndHost
- System.Windows.Markup
- System.Windows.NameScope
- System.Windows.ResourceDictionary
- System.Windows.RoutedEvent
- System.Windows.Shell
Let's see an example configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /> </startup> <system.diagnostics> <sources> <source name="System.Windows.Data" switchName="Switch"> <listeners> <add name="TextListener" /> </listeners> </source> </sources> <switches> <add name="Switch" value="All" /> </switches> <sharedListeners> <add name="TextListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="Trace.txt" /> </sharedListeners> <trace indentsize="4" autoflush="true"></trace> </system.diagnostics> </configuration>
Focusing on the system.diagnostics section from the example, we see that there is one source element that is specifying the System.Windows.Data source (for data binding information), the switch named Switch, and the TextListener listener. Looking first in the switches section, we find the switch named Switch and note that it is set with an output level of All.
Below this, in the sharedlisteners element, we see the listener named TextListener. This listener is of type System.Diagnostics.TextWriterTraceListener and this outputs to a text file which is specified by the value of the initializeData attribute. We end with a trace element that sets the tab size of the text document to four spaces and ensures that data is flushed out of the buffer after each write to prevent trace data from being lost due to a crash.
To set a less verbose output, we can simply alter the switch to use one of the other levels of output, as follows:
<add name="Switch" value="Error" />
As mentioned earlier, WPF can use a DefaultTraceListener object to send trace information to the Output window when particular options are set in Visual Studio. The name of this listener is Default. In order to stop the default behavior of this DefaultTraceListener, we can remove it using our source element, as follows:
<source name="System.Windows.Data" switchName="Switch"> <listeners> <add name="TextListener" /> <remove name="Default" /> </listeners> </source>
It's good to be aware of this fact, because if we also configured our own ConsoleTraceListener object, we could end up with our Output window duplicating trace events. However, it is also possible to add multiple listeners into each source element if required:
<source name="System.Windows.Data" switchName="Switch"> <listeners> <add name="TextListener" /> <add name="OutputListener" /> </listeners> </source>
We can also add different listeners for different sources:
<source name="System.Windows.Data" switchName="Switch"> <listeners> <add name="TextListener" /> </listeners> </source> <source name="System.Windows.DependencyProperty" switchName="Switch"> <listeners> <add name="OutputListener" /> </listeners> </source> ... <sharedListeners> <add name="TextListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="Trace.txt" /> <add name="OutputListener" type="System.Diagnostics.ConsoleTraceListener" /> </sharedListeners>
Different output levels for different sources can be added as follows:
<source name="System.Windows.Data" switchName="ErrorSwitch"> <listeners> <add name="TextListener" /> </listeners> </source> <source name="System.Windows.DependencyProperty" switchName="AllSwitch"> <listeners> <add name="OutputListener" /> </listeners> </source> ... <switches> <add name="AllSwitch" value="All" /> <add name="ErrorSwitch" value="Error" /> </switches>
One neat feature that WPF Presentation Trace Sources provide is the ability to create our own custom trace sources:
<source name="CompanyName.ApplicationName" switchName="Switch"> <listeners> <add name="TextListener" /> </listeners> </source>
Note that the DefaultTraceListener was already configured to send information to the Output window in the WPF Trace Settings options mentioned in the previous section, so the traces from this source will also be sent to the Output window automatically. If you have not set those options but want to view the trace output there, then you will need to manually add a reference to the ConsoleTraceListener to this source as shown in the preceding code snippets.
In the code, we are now able to output custom trace information to this source:
TraceSource traceSource = new TraceSource("CompanyName.ApplicationName");
traceSource.TraceEvent(TraceEventType.Information, eventId, "Data loaded"); // Alternative way to output information with an event id of 0 traceSource.TraceInformation("Data loaded");
To specify different levels of importance, we use the TraceEventType enumeration:
traceSource.TraceEvent(TraceEventType.Error, eventId, "Data not loaded");
After outputting the debug information, we can optionally flush the existing listeners to ensure that they receive the events in the buffers before continuing:
traceSource.Flush();
Finally, we need to ensure that we close the TraceSource object to free resources when we have outputted the necessary information:
traceSource.Close();
The best part of this tracing functionality is the fact that we can turn it on and off using the configuration file, either at design time, runtime, or even on production versions of the application. As the configuration file is basically a text file, we can manually edit it and then restart the application so that it reads the new configuration.
Imagine that we had two switches in our file and that our default configuration used the switch named OffSwitch, so that there was no tracing output:
<source name="CompanyName.ApplicationName" switchName="OffSwitch"> <listeners> <add name="TextListener" /> </listeners> </source>
...
<switches> <add name="AllSwitch" value="All" /> <add name="OffSwitch" value="Off" /> </switches>
Now imagine that we have deployed our application and it is installed on a user's computer. It's worth noting at this point that the actual deployed configuration file that is created from the app.config file will have the same name as the executable file. In our case, it would be named CompanyName.ApplicationName.exe.config and would reside in the same folder as the executable file.
If this installed application was not behaving correctly, we could locate this configuration file, and simply change the switch to the one named AllSwitch:
<source name="CompanyName.ApplicationName" switchName="AllSwitch"> <listeners> <add name="TextListener" /> </listeners> </source>
After restarting the application, the new configuration would be read and our custom traces would be written to the specified text file. One alternative to restarting the application would be to call the Refresh method of the Trace class, which has the same effect of initiating a new read of the configuration file:
Trace.Refresh();
This method call can even be connected to a menu item or other UI control to enable tracing to be turned on and off without having to restart the application. Using either of these methods of refreshing the configuration file, we can attain important debug information from our software, even when it is in production. However, great care should be taken to ensure that text or XML file tracing is not permanently enabled on released software, as it will negatively affect performance.
While the WPF Presentation Trace Sources are typically available by default these days, in a few cases, we may need to manually enable this tracing functionality by adding the following registry key:
HKEY_CURRENT_USER\Software\Microsoft\Tracing\WPF
Once the WPF registry key has been added, we need to add a new DWORD value to it, name it ManagedTracing, and set its value to 1. We should then have access to the WPF Presentation Trace Sources. We've now seen a number of ways of finding the information that we need at runtime, but what about if the application won't even run?