Logging display and WPF

A question appeared over on the Code Project forums today about binding the output from log4net into WPF. The question asked was:

“I’m trying to use Log4net to log messages within my application. I’m adding a WPF window and want to stream the messages to the window. Log4net provides a TextWriterAppender that takes a StringWriter and writes logged events to the StringWriter, flushing it after each event.I want to simply connect the output of the StringWriter as the Text property on a TextBox. When I started this, it seemed simple and obvious – now I’m less sure. Ideally, I would simply like to bind the StringWriter to the TextBox, but haven’t found the incantation.

The basic problem is that the StringWriter doesn’t provide something like the INotifyPropertyChanged event to trigger code output a new log message (unless there is something behind the scenes I haven’t found).

I’ve see many examples of binding, all of which seem to presume that I have control over the writer itself. Am I missing something simple (I hope), or is this really not that straightforward.”

This is a very good question, so I thought I’d knock together a quick sample application to demonstrate how to do this. The first thing to remember is that log4net allows you to create your own appenders and use them in your application. The second thing to remember is that you need to hook INotifyPropertyChanged into the mechanism. To that end, I created the following appender:

namespace log4netSample.Logging
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using log4net.Appender;
  using System.ComponentModel;
  using System.IO;
  using System.Globalization;
  using log4net;
  using log4net.Core;

  /// <summary>
  /// The appender we are going to bind to for our logging.
  /// </summary>
  public class NotifyAppender : AppenderSkeleton, INotifyPropertyChanged
  {
    #region Members and events
    private static string _notification;
    private event PropertyChangedEventHandler _propertyChanged;

    public event PropertyChangedEventHandler PropertyChanged
    {
      add { _propertyChanged += value; }
      remove { _propertyChanged -= value; }
    }
    #endregion

    /// <summary>
    /// Get or set the notification message.
    /// </summary>
    public string Notification
    {
      get
      {
        return _notification; ;
      }
      set
      {
        if (_notification != value)
        {
          _notification = value;
          OnChange();
        }
      }
    }

    /// <summary>
    /// Raise the change notification.
    /// </summary>
    private void OnChange()
    {
      PropertyChangedEventHandler handler = _propertyChanged;
      if (handler != null)
      {
        handler(this, new PropertyChangedEventArgs(string.Empty));
      }
    }

    /// <summary>
    /// Get a reference to the log instance.
    /// </summary>
    public NotifyAppender Appender
    {
      get
      {
        return Log.Appender;
      }

    }

    /// <summary>
    /// Append the log information to the notification.
    /// </summary>
    /// <param name="loggingEvent">The log event.</param>
    protected override void Append(LoggingEvent loggingEvent)
    {
      StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
      Layout.Format(writer, loggingEvent);
      Notification += writer.ToString();
    }
  }
}

Whenever a new message is appended, the Notification is updated and the PropertyChangedEventHandler is called to notify the calling application that the binding has been updated. In order to use this appender, you need to hook it into your configuration:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net"
      type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>
  <appSettings>
    <add key="log4net.Internal.Debug" value="false"/>
  </appSettings>
  <system.diagnostics>
    <trace autoflush="true">
      <listeners>
        <add name="textWriterTraceListener"
             type="System.Diagnostics.TextWriterTraceListener"
             initializeData="C:\log4net_internal.log"/>
      </listeners>
    </trace>
  </system.diagnostics>
  <log4net>
    <appender name="NotifyAppender" type="log4netSample.Logging.NotifyAppender" >
      <layout type="log4net.Layout.PatternLayout">
        <param name="Header" value="[Header]\r\n" />
        <param name="Footer" value="[Footer]\r\n" />
        <param name="ConversionPattern" value="%d [%t] %-5p %c %m%n" />
      </layout>
    </appender>

    <root>
      <level value="ALL" />
      <appender-ref ref="NotifyAppender" />
    </root>
  </log4net>
</configuration>

Note that you might want to add the following line into your AssemblyInfo.cs file:

[assembly: log4net.Config.XmlConfigurator(Watch=true)]

I find the following class really helpful when logging:

namespace log4netSample.Logging
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using log4net;
  using log4net.Config;
  using log4net.Appender;
  using log4net.Repository.Hierarchy;

  public enum LogLevel
  {
    Debug = 0,
    Error = 1,
    Fatal = 2,
    Info = 3,
    Warning = 4
  }
  /// <summary>
  /// Write out messages using the logging provider.
  /// </summary>
  public static class Log
  {
    #region Members
    private static readonly ILog _logger = LogManager.GetLogger(typeof(Log));
    private static Dictionary<LogLevel, Action<string>> _actions;
    #endregion

    /// <summary>
    /// Static instance of the log manager.
    /// </summary>
    static Log()
    {
      XmlConfigurator.Configure();
      _actions = new Dictionary<LogLevel, Action<string>>();
      _actions.Add(LogLevel.Debug, WriteDebug);
      _actions.Add(LogLevel.Error, WriteError);
      _actions.Add(LogLevel.Fatal, WriteFatal);
      _actions.Add(LogLevel.Info, WriteInfo);
      _actions.Add(LogLevel.Warning, WriteWarning);
    }

    /// <summary>
    /// Get the <see cref="NotifyAppender"/> log.
    /// </summary>
    /// <returns>The instance of the <see cref="NotifyAppender"/> log, if configured.
    /// Null otherwise.</returns>
    public static NotifyAppender Appender
    {
      get
      {
        foreach (ILog log in LogManager.GetCurrentLoggers())
        {
          foreach (IAppender appender in log.Logger.Repository.GetAppenders())
          {
            if (appender is NotifyAppender)
            {
              return appender as NotifyAppender;
            }
          }
        }
        return null;
      }
    }

    /// <summary>
    /// Write the message to the appropriate log based on the relevant log level.
    /// </summary>
    /// <param name="level">The log level to be used.</param>
    /// <param name="message">The message to be written.</param>
    /// <exception cref="ArgumentNullException">Thrown if the message is empty.</exception>
    public static void Write(LogLevel level, string message)
    {
      if (!string.IsNullOrEmpty(message))
      {
        if (level > LogLevel.Warning || level < LogLevel.Debug)
          throw new ArgumentOutOfRangeException("level");

        // Now call the appropriate log level message.
        _actions[level](message);
      }
    }

    #region Action methods
    private static void WriteDebug(string message)
    {
      if (_logger.IsDebugEnabled)
        _logger.Debug(message);
    }

    private static void WriteError(string message)
    {
      if (_logger.IsErrorEnabled)
        _logger.Error(message);
    }

    private static void WriteFatal(string message)
    {
      if (_logger.IsFatalEnabled)
        _logger.Fatal(message);
    }

    private static void WriteInfo(string message)
    {
      if (_logger.IsInfoEnabled)
        _logger.Info(message);
    }

    private static void WriteWarning(string message)
    {
      if (_logger.IsWarnEnabled)
        _logger.Warn(message);
    }
    #endregion
  }
}

It’s a simple matter then to do something like Log.Write(LogLevel.Info, “This is my message”);

If you download the attached sample, you’ll get to see the whole application running in all its glory, and you can see how updating the log results in the output being updated. Don’t forget to rename the .doc file to .zip when you save it.

log4netsamplezip

Advertisements

12 thoughts on “Logging display and WPF

  1. It’s quite straight forward like Pete has shown us with his application above, things get much more complicated when you have to bind many data sources together with multiple inputs i.e. lots of users.

  2. Lee H

    Hi. trite perhaps, but may I ask how you have set up your code to look so nicely formatted, is that within Visual Studio, or are you just formatting your code to look good on the web with css etc. I really like the alternate row shading, line numbering and font colour schemes you’ve used

  3. Thomas

    This worked great for a while, but now I think I’ve messed it up 😦 I’m having trouble when logging from a backgroundworker. In the backgroundworker.dowork it finds the NotifyAppender, but _propertyChanged eventhandler is null 😦 Is it not possible to log from backgroundworker (other thread than GUI)?

  4. Hi; Great article but I have a question. I am trying to achieve this but I want the log to appear on my mainwindows after the modification of the “_message” property of the LogwritterViewModel.cs. any idea ? The problem I am facing is that the binding between NotificationView.xaml and NotifyAppender.cs does not seems to work. after a changed of “_message”, I execute a procedure to simulate the call of UpdateLogCommand but it doesn’t seems to work. Thanks

  5. Pingback: Fix Wpf Display Error Icon Windows XP, Vista, 7, 8 [Solved]

  6. Pingback: Fix Wpf Show Error Icon Windows XP, Vista, 7, 8 [Solved]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s