As your Silverlight projects grow complex you’ll soon start to feel the need to have a solid logging system. In my WPF and Windows Forms project I’m now used to have Castle Windsor + Log4Net as my logging system and I really miss it in my Silverlight applications.

We don’t have a porting of Log4Net in Silverlight yet, but we do have Castle Windsor now. Given the fact I’ll use it in my production environment I decided to roll my version of a simple logging framework that mimic some of the features that Log4Net have and it’s based on Castle Windsor logging capabilities (at first…then if I’ll have the need to switch my IoC/DI framework I can abstract the whole logging system and write the integration facilities).

The goal is to have a simple system that can be configured adding and removing component to the Windsor container.

The logger infrastructure will be like this:

LoggingThe Logger class derives directly from Castle’s LevelFilteredLogger (which implement the default ILogger interface). The logger also has a collection of appenders, each IAppender object simply exposes a simple Log() function which will accept some parameters and perform the logging operation.

A simple implementation of the IAppender interface is given by the BrowserConsoleAppender class: this class will log the actions to the IE Developer’s Toolbar console script window, or to the Firebug console window.

To simply the logging configuration I’ve added a LoggingFacility class that is able to configure a default application logger or can provide a simple way to configure multiple loggers and appenders if you need a fine grained configuration.

Let’s start simple: in this first version we’ll reuse interfaces and members directly from the Castle namespaces. The IAppender interface will be like this:

/// <summary>
/// interface for our custom appenders
/// </summary>
public interface IAppender
{
	void Log(LoggerLevel loggerLevel, string loggerName, string message, Exception exception);
}

The BrowserConsoleAppender is quite simple too, with the actual code that logs to the browser’s console stolen from some articles around the web :D:

public class BrowserConsoleAppender : IAppender
{
	public void Log(global::Castle.Core.Logging.LoggerLevel loggerLevel, string loggerName, string message, Exception exception)
	{
		HtmlWindow window = HtmlPage.Window;
		//only log is a console is available (IE and FF)
		var isConsoleAvailable = (bool)window.Eval("typeof(console) != 'undefined' && typeof(console.log) != 'undefined'");
		if (isConsoleAvailable)
		{
			var console = (window.Eval("console.log") as ScriptObject);
			if (console != null)
			{
				DateTime dateTime = DateTime.Now;
				string output;
				if (exception == null)
					output = string.Format("{0} [{1}] '{2}' {3}", dateTime.ToString("u"), loggerLevel, loggerName, message).SanitizeForBrowser();
				else
					output = string.Format("{0} [{1}] '{2}' {3}:\n{4}\n{5}", dateTime.ToString("u"), loggerLevel, loggerName, exception.GetType().FullName,
					                       exception.Message, exception.StackTrace).SanitizeForBrowser();

				console.InvokeSelf(output);
			}
		}
	}
}

The Logger implementation is simple too:

public class Logger : LevelFilteredLogger
{
	public Logger()
	{
	}

	public Logger(string name)
		: base(name)
	{
	}

	public Logger(LoggerLevel loggerLevel)
		: base(loggerLevel)
	{
	}

	public Logger(string loggerName, LoggerLevel loggerLevel)
		: base(loggerName, loggerLevel)
	{
	}

	public Logger(LoggerLevel loggerLevel, IList<IAppender> appenders)
		: base(loggerLevel)
	{
		_appenders = appenders;
	}

	public Logger(string loggerName, LoggerLevel loggerLevel, IList<IAppender> appenders)
		: base(loggerName, loggerLevel)
	{
		_appenders = appenders;
	}

	public override ILogger CreateChildLogger(string loggerName)
	{
		if (loggerName == null)
			throw new ArgumentNullException("loggerName", "To create a child logger you must supply a non null name");

		return new Logger(String.Format(CultureInfo.CurrentCulture, "{0}.{1}", Name, loggerName), Level, Appenders);
	}

	private readonly IList<IAppender> _appenders = new List<IAppender> { new BrowserConsoleAppender() };
	public IList<IAppender> Appenders
	{
		get { return _appenders; }
	}

	protected override void Log(LoggerLevel loggerLevel, string loggerName, string message, Exception exception)
	{
		foreach (var appender in Appenders)
			appender.Log(loggerLevel, loggerName, message, exception);
	}
}

It directly derives from the basic Castle implementation which will give us some logging methods for free (we just have to override the Log() function); we have a bunch of constructors that allows you to configure the logger, the most complete one will accept a threshold level, a logger name and an array of appenders.

Basically this is all that you need to log something in Silverlight; to use this logging framework with Castle you can configure it like this:

Container = new WindsorContainer();
Container.Register(
// register one or more appenders
Component.For<IAppender>().ImplementedBy<BrowserConsoleAppender>().Named("Default"),
// register and configure the loggers
Component.For<ILogger>().ImplementedBy<Logger>().DynamicParameters((k, d) =>
			                   	{
			                   		d["loggerLevel"] = LoggerLevel.Debug;
			                   		IAppender[] appenders = k.ResolveAll<IAppender>();
			                   	})
);

And then use it with normal resolution or dependency injection.

To simply the configuration stage I built up a LoggingFacility that if used without any parameter will configure a single unnamed Logger with the default console appender (if you register more appenders they will be automatically used the first time you resolve the Logger service); alternatively you can pre-configure the Logger services you want to have passing an array of LoggerConfig objects to the facility.

Here’s the implementation code:

public class LoggingFacility : AbstractFacility
{
	public LoggingFacility()
	{ }

	public LoggingFacility(LoggerConfig config)
	{
		_configuredLoggers = new[] { config };
	}

	public LoggingFacility(LoggerConfig[] configs)
	{
		_configuredLoggers = configs;
	}

	private const LoggerLevel DefaultLoggerLevel = LoggerLevel.Warn;
	private readonly LoggerConfig[] _configuredLoggers;

	protected override void Init()
	{
		// if we do not have any explicit configuration, we register a default logger
		if ((_configuredLoggers == null) || (_configuredLoggers.Length == 0))
			Kernel.Register(
				Component.For<ILogger>().ImplementedBy<Logger>()
					.DynamicParameters((k, d) =>
					                   	{
					                   		d["loggerLevel"] = DefaultLoggerLevel;
					                   		IAppender[] appenders = k.ResolveAll<IAppender>();
					                   		// if we do not have registered any appender we provide a default console one
					                   		if (appenders.Length == 0)
					                   			appenders = new IAppender[] { new BrowserConsoleAppender() };
					                   		d["appenders"] = appenders;
					                   	})
				);
		else
		{
			// we need to register more than one logger
			foreach (var loggerConfig in _configuredLoggers)
			{
				LoggerConfig config = loggerConfig;
				Kernel.Register(Component.For<ILogger>()
				                	.ImplementedBy<Logger>()
				                	.DynamicParameters((k, d) =>
				                	                   	{
				                	                   		if (!string.IsNullOrEmpty(config.Name))
				                	                   			d["loggerName"] = config.Name;
				                	                   		d["loggerLevel"] = config.Level;
				                	                   		IAppender[] appenders = null;
				                	                   		/* if we have appenders defined..resolve them */
				                	                   		if ((config.AppendersNames != null) && (config.AppendersNames.Length > 0))
				                	                   		{
				                	                   			List<IAppender> aps = new List<IAppender>();
				                	                   			for (int i = 0; i < config.AppendersNames.Length; i++)
				                	                   				aps.Add(k.Resolve<IAppender>(config.AppendersNames[i]));
				                	                   			appenders = aps.ToArray();
				                	                   		}
				                	                   		/* if not..resolve all the available */
				                	                   		if ((appenders == null) || (appenders.Length == 0))
				                	                   			appenders = k.ResolveAll<IAppender>();
				                	                   		/* if we do not have registered any appender we provide a default console one */
				                	                   		if (appenders.Length == 0)
				                	                   			appenders = new IAppender[] { new BrowserConsoleAppender() };
				                	                   		d["appenders"] = appenders;
				                	                   	}));
			}
		}
	}
}

To use the facility:

Container = new WindsorContainer();

// simple configuraion: just set the facility (this ocnfigures a default logger)
Container.AddFacility<LoggingFacility>();

// advanced configuration: specify some options for the default logger
Container.Kernel.AddFacility("LoggingFacility", new LoggingFacility(new LoggerConfig { Level = LoggerLevel.Debug }));

// also if you specify some more appenders they will be used too
Container.Register(
            // register one or more appenders
            Component.For<IAppender>().ImplementedBy<BrowserConsoleAppender>().Named("Appender1"),
				Component.For<IAppender>().ImplementedBy<BrowserConsoleAppender>().Named("Appender2"),
				Component.For<IAppender>().ImplementedBy<BrowserConsoleAppender>().Named("Appender3"),
				);

As usual it started as a short post and became quite long in the end…let’s see an example of usage, in a Silverlight application you can modify a Page to accept a constructor dependency on the logger (this isn’t a good design solution when it comes to logging services…but this is just an operational demo so it’s ok):

public partial class MainPage : UserControl
{
	public MainPage(ILogger logger)
	{
		InitializeComponent();
		_logger = logger;

		_logger.Info("MainPage created");
	}

	private ILogger _logger;

	private void button1_Click(object sender, RoutedEventArgs e)
	{
		_logger.Info("Second action taken");
	}

	private void button2_Click(object sender, RoutedEventArgs e)
	{
		_logger.Info("First action taken");
	}
}

You then need to rely on the container to configure the actual instance of the class:

// register the type in the container, so we can resolve it
Container.Register(
	Component.For<MainPage>()
	);
	
// later on: ask the container to build an instance of the page
private void Application_Startup(object sender, StartupEventArgs e)
{
	InitContainer();
	Container.Resolve<ILogger>().Info("Application started");
	this.RootVisual = Container.Resolve<MainPage>();
}

To see the Logging service in action I created a simple Silverlight project which configures and uses it, the solution is attached at the end of this article, here’s a screenshot of the IE8 Developer Toolbar script console window that shows how the actions are logged.

SilverlightLogging

You can take this code and extends it with any other logging capabilities you like just implementing your own Appender classes, I have one that calls a WCF service and log the exception using Elmah for example; further extension to this system will include XML configuration support for the LoggingFacility and the introduction of Exception and Message formatters.

Here is the complete solution project:

Related Content