Thursday, June 9, 2011

Creating Awesome Logging Control with NLog, WPF, C# and Memory Target

First we need to define new "reactive" memory target. This target will notify us when some log item is received in memory.

Code Snippet
  1. using System;
  2. using NLog;
  3. using NLog.Targets;
  4.  
  5. namespace VWBT.Controls.Log
  6. {
  7.     public class MemoryEventTarget : Target
  8.     {
  9.         public event Action<LogEventInfo> EventReceived;
  10.  
  11.         /// <summary>
  12.         /// Notifies listeners about new event
  13.         /// </summary>
  14.         /// <param name="logEvent">The logging event.</param>
  15.         protected override void Write(LogEventInfo logEvent)
  16.         {
  17.             if (EventReceived != null) {
  18.                 EventReceived(logEvent);
  19.             }
  20.         }
  21.     }
  22. }

Now we are ready to define our logging control. We will keep last 50 log messages in the ObservableColection, which we will bind to the ListView control. We also register event for our memory target inwhich we update our collection.



Code Snippet
  1. using System;
  2. using System.Collections.ObjectModel;
  3. using System.Windows.Controls;
  4. using NLog;
  5. using VWBT.Controls.Log;
  6.  
  7. namespace VWBT.Controls
  8. {
  9.     /// <summary>
  10.     /// Interaction logic for LoggingControl.xaml
  11.     /// </summary>
  12.     public partial class LoggingControl : UserControl
  13.     {
  14.         readonly MemoryEventTarget _logTarget;  // My new custom Target (code is attached here MemoryQueue.cs)
  15.  
  16.         public static ObservableCollection<LogEventInfo> LogCollection { get; set; }
  17.  
  18.  
  19.         public LoggingControl()
  20.         {
  21.             LogCollection = new ObservableCollection<LogEventInfo>();
  22.  
  23.             InitializeComponent();
  24.  
  25.             // init memory queue
  26.             _logTarget = new MemoryEventTarget();
  27.             _logTarget.EventReceived += EventReceived;
  28.             NLog.Config.SimpleConfigurator.ConfigureForTargetLogging(_logTarget, LogLevel.Debug);
  29.         }
  30.  
  31.         private void EventReceived(LogEventInfo message)
  32.         {
  33.             Dispatcher.Invoke(new Action(() => {
  34.                 if (LogCollection.Count >= 50) LogCollection.RemoveAt(LogCollection.Count - 1);
  35.                 LogCollection.Add(message);
  36.             }));
  37.         }
  38.     }
  39. }


Following is the simple design of the logging control. Please note the binding.


Code Snippet
  1. <UserControl x:Class="VWBT.Controls.LoggingControl"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:Log="clr-namespace:VWBT.Controls.Log" mc:Ignorable="d"
  6.              d:DesignHeight="230" d:DesignWidth="457"
  7.              DataContext="{Binding RelativeSource={RelativeSource Self}}">
  8.     <UserControl.Resources>
  9.         <Log:LogItemBgColorConverter x:Key="LogItemBgColorConverter" />
  10.         <Log:LogItemFgColorConverter x:Key="LogItemFgColorConverter" />
  11.     </UserControl.Resources>
  12.     <Grid>
  13.         <!--<TextBox IsReadOnly="True" AcceptsReturn="True"  Height="Auto" HorizontalAlignment="Stretch" Name="dgLog" VerticalAlignment="Stretch" Width="Auto"/>-->
  14.         <ListView ItemsSource="{Binding LogCollection}" Name="logView">
  15.             <ListView.ItemContainerStyle>
  16.                 <Style TargetType="{x:Type ListViewItem}">
  17.                     <Setter Property="ToolTip" Value="{Binding FormattedMessage}" />
  18.                     <Setter Property="Background" Value="{Binding Level, Converter={StaticResource LogItemBgColorConverter}}" />
  19.                     <Setter Property="Foreground" Value="{Binding Level, Converter={StaticResource LogItemFgColorConverter}}" />
  20.                     <Style.Triggers>
  21.                         <Trigger Property="IsSelected" Value="True">
  22.                             <Setter Property="Background" Value="DarkOrange"/>
  23.                             <Setter Property="Foreground" Value="black"/>
  24.                         </Trigger>
  25.                         <Trigger Property="IsMouseOver" Value="True">
  26.                             <Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Path=Background}"/>
  27.                             <Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource Self}, Path=Foreground}"/>
  28.                         </Trigger>
  29.                     </Style.Triggers>
  30.                 </Style>
  31.             </ListView.ItemContainerStyle>
  32.             <ListView.View>
  33.                 <GridView>
  34.                     <GridView.Columns>
  35.                         <!--<GridViewColumn DisplayMemberBinding="{Binding LoggerName}" Header="Logger"/>-->
  36.                         <GridViewColumn DisplayMemberBinding="{Binding Level}" Header="Level"/>
  37.                         <GridViewColumn DisplayMemberBinding="{Binding FormattedMessage}" Width="500" Header="Message"/>
  38.                         <GridViewColumn DisplayMemberBinding="{Binding Exception}" Header="Exception"/>
  39.                     </GridView.Columns>
  40.                 </GridView>
  41.             </ListView.View>
  42.         </ListView>
  43.         <!--<ListBox Height="Auto" HorizontalAlignment="Stretch" Name="dgLog" VerticalAlignment="Stretch" Width="Auto" />-->
  44.     </Grid>
  45. </UserControl>


Previous XAML code uses couple converters which allow us to display messages in different color.


Code Snippet
  1. using System;
  2. using System.Globalization;
  3. using System.Windows.Data;
  4. using System.Windows.Media;
  5.  
  6. namespace VWBT.Controls.Log
  7. {
  8.     public class LogItemBgColorConverter : IValueConverter
  9.     {
  10.         public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  11.         {
  12.             if ("Warn" == value.ToString()) {
  13.                 return Brushes.Yellow;
  14.             } else if ("Error" == value.ToString()) {
  15.                 return Brushes.Tomato;
  16.             }
  17.             return Brushes.White;
  18.         }
  19.  
  20.         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  21.         {
  22.             throw new NotImplementedException();
  23.         }
  24.     }
  25. }


Code Snippet
  1. using System;
  2. using System.Globalization;
  3. using System.Windows.Data;
  4. using System.Windows.Media;
  5.  
  6. namespace VWBT.Controls.Log
  7. {
  8.     public class LogItemFgColorConverter : IValueConverter
  9.     {
  10.         public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  11.         {
  12.             if ("Error" == value.ToString()) {
  13.                 return Brushes.Black;
  14.             }
  15.             return Brushes.Black;
  16.         }
  17.  
  18.         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  19.         {
  20.             throw new NotImplementedException();
  21.         }
  22.     }
  23. }

That's all folks. With this control you are ready to display your logs directly in your application! Comments welcome!

4 comments:

Laurent M said...
This comment has been removed by the author.
Laurent M said...
This comment has been removed by the author.
Laurent M said...

Hi,
I've managed to make it work now but I have another problem. Once I use your setup, I lose all the other setup I've made in the NLog.conf, would there be a way to keep these?
Thanks for this awesome control!
Laurent

Unknown said...

Laurent,

You can achieve this by adding this to your app initialization (before you log anything, e.g. in public App()):

ConfigurationItemFactory.Default.Targets.RegisterDefinition("MemoryEvent", typeof(MemoryEventTarget)); // Not sure if this is needed.
LoggingConfiguration config = NLog.LogManager.Configuration;
MemoryEventTarget _logTarget = new MemoryEventTarget();
config.AddTarget("memoryevent", _logTarget);
config.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, _logTarget));
LogManager.Configuration = config;

The replace

_logTarget = new MemoryEventTarget();
_logTarget.EventReceived += EventReceived;
NLog.Config.SimpleConfigurator.ConfigureForTargetLogging(_logTarget, LogLevel.Debug);

by

foreach (Target target in NLog.LogManager.Configuration.AllTargets)
{
if (target is MemoryEventTarget)
{
((MemoryEventTarget)target).EventReceived += EventReceived;
}
}

It least it worked for me...