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!

Numeric and Boolean Expression Parser Using Conversion From Infix to Postfix using C#

Dears

Below is a source code to the expression parser with ability to parse combined expressions, that is numeric and booleans, and variable support. See Main method for demo purposes.


Code Snippet
  1. using System;
  2. using System.Collections.Generic;
  3.  
  4. public class ExpressionEvaluator
  5. {
  6.     #region class AtomicExpression
  7.     /// <summary>
  8.     /// This class holds atomic expressions. It holds either variables, values or operators.
  9.     /// If the class holds operator, it can provide result given the input values
  10.     /// </summary>
  11.     public class AtomicExpression
  12.     {
  13.         private string _value;
  14.  
  15.         /// <summary>
  16.         /// Parsed value of the strig value used during evaluation
  17.         /// </summary>
  18.         public object ParsedValue { get; set; }
  19.  
  20.         /// <summary>
  21.         /// Value of the current expression
  22.         /// It is either numeric value or name of the variable (e.g. 1, A, Aleph)
  23.         /// </summary>
  24.         public string Value
  25.         {
  26.             get
  27.             {
  28.                 return _value;
  29.             }
  30.             set
  31.             {
  32.                 double outd;
  33.                 if (double.TryParse(value, out outd)) {
  34.                     ParsedValue = outd;
  35.                 }
  36.                 bool outb;
  37.                 if (bool.TryParse(value, out outb)) {
  38.                     ParsedValue = outb;
  39.                 }
  40.                 _value = value;
  41.             }
  42.         }
  43.         /// <summary>
  44.         /// String representation of the operator (e.g. '==' or '||')
  45.         /// </summary>
  46.         public string Operator { get; set; }
  47.  
  48.         /// <summary>
  49.         /// Evaluates the unary operator with provided value
  50.         /// </summary>
  51.         /// <param name="value">Value for unary operator</param>
  52.         /// <returns>Evaluated expression of the unary operator</returns>
  53.         public object Evaluate(object value)
  54.         {
  55.             if (Operator == "!") {
  56.                 return !((bool)value);
  57.             }
  58.             throw new NotImplementedException(Operator + " operator not implemented");
  59.         }
  60.  
  61.         /// <summary>
  62.         /// Evaluates the binary operator expression with provided values
  63.         /// </summary>
  64.         /// <param name="val1">Left-side value for the operator</param>
  65.         /// <param name="val2">Right-side value for the operator</param>
  66.         /// <returns>Output value of the binary expression</returns>
  67.         public object Evaluate(object val1, object val2)
  68.         {
  69.             Console.Write("{0} {1} {2} = ", val1, Operator, val2);
  70.             switch (Operator) {
  71.                 case "+":
  72.                     return (double)val1 + (double)val2;
  73.                 case "-":
  74.                     return (double)val1 - (double)val2;
  75.                 case "*":
  76.                     return (double)val1 * (double)val2;
  77.                 case "/":
  78.                     return (double)val1 / (double)val2;
  79.                 case ">":
  80.                     return (double)val1 > (double)val2;
  81.                 case ">=":
  82.                     return (double)val1 >= (double)val2;
  83.                 case "<":
  84.                     return (double)val1 < (double)val2;
  85.                 case "<=":
  86.                     return (double)val1 <= (double)val2;
  87.                 case "==":
  88.                     return (double)val1 == (double)val2;
  89.                 case "!=":
  90.                     return (double)val1 != (double)val2;
  91.                 case "&&":
  92.                     return (bool)val1 && (bool)val2;
  93.                 case "||":
  94.                     return (bool)val1 || (bool)val2;
  95.  
  96.             }
  97.             throw new NotImplementedException(Operator + " operator not implemented");
  98.         }
  99.  
  100.         public override string ToString()
  101.         {
  102.             return string.Format("{0}{1}", Value, Operator);
  103.         }
  104.     }
  105.     #endregion
  106.  
  107.     #region Evaluate(List<AtomicExpression> list, Dictionary<string, object> variables)
  108.     /// <summary>
  109.     /// Evaluates the input AtomicExpression (see AtomicExpression) list with the list of provided variables
  110.     /// and calculates the numeric or boolean output. List of expressions is sorted in infix order.
  111.     /// </summary>
  112.     /// <param name="list">List of input expressions. Expressions are evaluated in the postfix manner.</param>
  113.     /// <param name="variables">List of variables and their values used during the evaluation</param>
  114.     /// <returns></returns>
  115.     public object Evaluate(List<AtomicExpression> list, Dictionary<string, object> variables)
  116.     {
  117.         var stack = new Stack<object>();
  118.  
  119.         foreach (var expression in list) {
  120.             // double value
  121.             if (expression.ParsedValue != null) {
  122.                 stack.Push(expression.ParsedValue);
  123.                 continue;
  124.             }
  125.             // variable
  126.             if (!string.IsNullOrEmpty(expression.Value)) {
  127.                 stack.Push(variables[expression.Value]);
  128.                 continue;
  129.             }
  130.             // unary operator
  131.             object result;
  132.             if (expression.Operator == "!") {
  133.                 result = expression.Evaluate(stack.Pop());
  134.                 Console.WriteLine(result);
  135.             }
  136.                 // binary operator
  137.             else {
  138.                 var p2 = stack.Pop();
  139.                 var p1 = stack.Pop();
  140.                 result = expression.Evaluate(p1, p2);
  141.                 Console.WriteLine(result);
  142.             }
  143.             stack.Push(result);
  144.         }
  145.         Console.WriteLine("Result: " + stack.Peek());
  146.         return stack.Pop();
  147.     }
  148.     #endregion
  149.  
  150.     #region CheckParenthesis(string expression)
  151.     /// <summary>
  152.     /// Checks if given expression is well formed with parenthesis
  153.     /// </summary>
  154.     /// <param name="expression">Input expression (e.g. A+B-(2*2)>0)</param>
  155.     /// <returns></returns>
  156.     public virtual bool CheckParenthesis(string expression)
  157.     {
  158.         var stack = new Stack<string>();
  159.  
  160.         foreach (var c in expression) {
  161.             switch (c) {
  162.                 case '(':
  163.                     stack.Push(c.ToString());
  164.                     break;
  165.                 case ')':
  166.                     if (stack.Count == 0) {
  167.                         return false;
  168.                     }
  169.                     stack.Pop();
  170.                     break;
  171.             }
  172.         }
  173.         return stack.Count <= 0;
  174.     }
  175.     #endregion
  176.  
  177.     #region List<AtomicExpression> Postfix(string expression)
  178.     /// <summary>
  179.     /// Converts the input infix expression into the postfix and creates the list
  180.     /// of atomic expressions in this infix order
  181.     /// </summary>
  182.     /// <param name="expression">Infix expression (e.g. A+B-(2*2)>0||B)</param>
  183.     /// <returns>List of atomic expressions sorted in the infix order</returns>
  184.     public virtual List<AtomicExpression> Postfix(string expression)
  185.     {
  186.         var expressions = new List<AtomicExpression>();
  187.         var variable = string.Empty;
  188.         var stack = new Stack<string>();
  189.         // remove spaces
  190.         expression = expression.Replace(" ", string.Empty);
  191.         char c;
  192.         string sc;
  193.  
  194.         for (var i = 0; i < expression.Length; i++) {
  195.             c = expression[i];
  196.  
  197.             // variable or number
  198.             if (char.IsLetterOrDigit(c)) {
  199.                 variable += c;
  200.             }
  201.                 // left parthesis
  202.             else if (c == '(') {
  203.                 stack.Push(c.ToString());
  204.             }
  205.                 // right parenthesis
  206.             else if (c == ')') {
  207.                 // add variable
  208.                 if (!string.IsNullOrEmpty(variable)) {
  209.                     expressions.Add(new AtomicExpression { Value = variable });
  210.                 }
  211.                 variable = string.Empty;
  212.                 while (!stack.Peek().Equals("(")) {
  213.                     expressions.Add(new AtomicExpression { Operator = stack.Pop() });
  214.                 }
  215.                 stack.Pop();
  216.             }
  217.                 // operator
  218.             else {
  219.                 sc = c.ToString();
  220.  
  221.                 // add variable
  222.                 if (!string.IsNullOrEmpty(variable)) {
  223.                     expressions.Add(new AtomicExpression { Value = variable });
  224.                 }
  225.                 variable = string.Empty;
  226.  
  227.                 // check for operators having 2 characters
  228.                 if (i < expression.Length - 1 && !char.IsLetterOrDigit(expression[i + 1]) && expression[i + 1] != '(' && expression[i + 1] != ')') {
  229.                     sc += expression[i + 1];
  230.                     i++;
  231.                 }
  232.                 // add all expressions with higher or same priority
  233.                 while (stack.Count > 0 && Priority(stack.Peek()) >= Priority(sc)) {
  234.                     expressions.Add(new AtomicExpression { Operator = stack.Pop() });
  235.                 }
  236.                 stack.Push(sc);
  237.             }
  238.         }
  239.         while (stack.Count > 0) {
  240.             expressions.Add(new AtomicExpression { Operator = stack.Pop() });
  241.         }
  242.         return expressions;
  243.     }
  244.     #endregion
  245.  
  246.  
  247.     // main method
  248.  
  249.     #region Main(string[] args)
  250.     public static void Main(string[] args)
  251.     {
  252.         ExpressionEvaluator s = new ExpressionEvaluator();
  253.         var list = s.Postfix("A && !(B > 2 + 1) || ( !A && !(B==2))");
  254.  
  255.         Console.WriteLine("------------------------------------------");
  256.         foreach (var expression in list) {
  257.             Console.Write(expression.ToString());
  258.         }
  259.         Console.WriteLine("\r\n------------------------------------------");
  260.         var dict = new Dictionary<string, object>();
  261.         dict.Add("A", true);
  262.         dict.Add("B", 3d);
  263.         s.Evaluate(list, dict);
  264.         Console.ReadKey();
  265.     }
  266.     #endregion
  267.  
  268.     // private methods
  269.  
  270.     #region Priority(string x)
  271.     private static int Priority(string x)
  272.     {
  273.         if (x.Equals("||"))
  274.             return 0;
  275.         if (x.Equals("&&"))
  276.             return 1;
  277.         if (x[0].Equals('<') || x[0].Equals('>') || x[0].Equals('=') || x[0].Equals('!'))
  278.             return 2;
  279.         if (x.Equals("+") || x.Equals("-"))
  280.             return 3;
  281.         if (x.Equals("*") || x.Equals("/"))
  282.             return 4;
  283.         if (x.Equals("(") || x.Equals(")"))
  284.             return -1;
  285.         throw new NotImplementedException(x + " is not implemented");
  286.     }
  287.     #endregion
  288. }