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

Binding to a single object.

So today, on CodeProject, one of the WPF regulars posed a question about whether or not you could only bind to ObservableCollection objects, and if you could bind to single objects, how could you guarantee two way databinding goodness. As you may well imagine, I promptly answered that you could indeed bind to a single object and implement two way databinding with it. The discussion also posed the question, could WPF bind to properties that linked into child objects and display these – from separate usercontrols.

This seemed to me to be an ideal candidate for demonstrating the beauty and wonder that is the WPF databinding infrastructure. If you’ve only ever been exposed to WinForms or ASP.NET databinding, then you are in for a major treat – WPF (and Silverlight) provide so much more bang for your buck with binding that it’s hard to see how we managed before. First of all, let’s take a look at our business object classes, Benefit and Customer. Both of these classes implement INotifyPropertyChanged so that we get access to the notification mechanism that supports two way binding.

First of all, here’s Benefit.cs:

Benefit.cs

And here’s Customer.cs:

Customer.cs

As you can see, there’s nothing out of the ordinary in these classes – they don’t do anything clever. They are just your bog standard, basic model classes.

Now, here’s the main window XAML:

Window1.xaml

The TextBoxes are bound to the relevant entries in the Customer class using the familiar {Binding Path=…} syntax. But wait, where’s the Benefit class in all this? I added it for a reason, yet there’s no sign of it. Well, as you can see – there’s a reference to a UserControl (BenefitsControl), and we are going to perform the actual binding in this class. Here’s the definition of BenefitsControl.xaml:

benefitscontrol.xaml

Look – there’s the binding Benefits.ProviderName, but would it surprise you to know that I’m not going to put the data class anywhere near this control. I’m going to set the DataContext on the parent control and let the usercontrol “find” the data there. Here’s the implementation of the code behind the parent XAML (Window1.cs):

window1.cs

So – here’s the application in all its glory:

runningapplication

Well – now that we’ve covered the code, how does the databinding actually work? How does the user control actually get its binding to work? This is where the wonder that is WPF actually comes into play. If the binding can’t find data in the data context at the current level, it works its way back up the logical tree until it can. Seriously – I love this stuff, and I’d hate to go back from it.

The code for this application can be downloaded here. Note that you’ll need to change the extension from .doc to .zip.

customerboundsamplezip

The notification gotcha.

When your application uses DataBinding in WPF, it’s great how little you actually have to do to have full two-way databinding running, with multiple views watching the same data, and reacting based on data changes. This amazingly powerful feature is, to me, one of the key selling features of WPF and Silverlight; no longer do you have to write huge amounts of code in your Windows Forms or ASP.NET application to manage all the different views on the same data. The underlying platform takes care of it for you.

One of the key components in this “magic”, is the use of INotifyPropertyChanged, which is responsible for notifying the data consumers that properties have changed. In your property setter, all you need to do is call the PropertyChanged event, telling it which property has changed and off you go – support for two-way databinding. This means that the following code is a common site

On the surface, there doesn’t appear to be much wrong with it, but there is a flaw with this approach. What happens if your value doesn’t change, but the setter is called – this can happen if the user retypes a value that is already in the field for instance, WPF will trigger the setter even though nothing has changed, which results in the PropertyChanged event being raised, triggering rebinds. In a desktop application, this normally isn’t that significant, but it could be much more significant in a Silverlight app with heavy data binding. We know what the problem is, the Change method being called when nothing has changed, and the fix is easy. Check to see if the old value of the property differs from the new value, and call the Changed method only when they do. This simple change means that databindings only occur when something has actually changed.

Reusable transaction handling in WPF with IEditableCollectionView.

One of the banes of my life as a professional developer is handling grid editing where it can react intelligently to the user cancelling a change in the middle of an add or edit. Fortunately, if you know where to look, .NET can do a lot of the heavy lifting for you. With a little bit of IEditableObject goodness and a couple of helper classes, it’s now easy to add change handling to your WPF applications.

The attached project contains a library with two classes in it that you can use to do the work for you. The first class, EditableObject, manages the commit and rollback phases. The behaviour of this class is interesting, so I’d like to take a few minutes to go over it with you. The first thing to note is that it’s abstract, so your data classes inherit from it. When you start an add or edit operation, a copy of the current data class is created and the values are stored in the copy before the edit can progress. This gives us a copy to go back to without having to manually copy fields out. It uses reflection to do this, which can be a little bit slow on complex types, but when you’re dealing with reflecting on one object it’s not too bad and it was a compromise I was prepared to live with in return for the flexibility it provides.

Important note: As we’re using reflection here, I’m only serializing the fields of the current object so the hierarchy doesn’t get traversed. This means that your dataclasses must derive from EditableObject directly in order to get access to all the features of this wrapper.

EditableObject implements the INotifyPropertyChanged interface with a bit of a twist. In normal operations, you would raise the PropertyChanged event as soon as you changed the property, but we don’t want this to happen until we’ve committed the edit/add. This means we have to step outside the normal behaviour a little bit, and to do this I’ve changed the usual implementation of INotifyPropertyChanged so that it just records the properties that have changed – then it plays them back when the edit is committed. Obviously, if it’s rolled back then the notification doesn’t go ahead.

The second class, EditableCollectionHandler, encapsulates IEditableCollectionView which was introduced in .NET 3.5 SP 1. Basically, this is a view that supports adding, editing and removing items from a collection. The add and edit are transactional, so we can hook this up to EditableObject directly. It provides the following methods, Add, Edit, Remove, CommitChanges and CancelChanges as well as two properties; DefaultView and EditView.

In the sample, I’ve created a ListBox inside a grid. The grid contains two DataTemplates to be used with the ListBox, the default template used to display the data, and the EditTemplate, to be used when we are adding or editing a line of data.

When you set up the link from your WPF application to EditableCollectionHandler, you specify the object you want the collection view to work against, the container of the templates and the type of item you want to specify for editing the items when you are adding or editing. You also set up the name of the default template and the edit template (if you have one), and then you’re good to go.

Requirements:

  • Visual Studio 2008
  • .NET 3.5 SP 1

Downloads

Be sure to change the extension from doc to zip and then uncompress it.

editablecollectionzip