Saturday, 23 May 2015

Logging WPF binding errors to the Event Log and testing this

WPF developers know that WPF binding errors are elusive. The XAML code is populated with binding expressions that are not strongly bound, but written as text. Whereas Razor MVC views support strongly bound MVC views, WPF developers must keep a keen eye to the debugger output view as their application run to spot WPF binding errors. These binding errors are not thrown and hard to track down. In addition, if there is present one WPF binding error per item in an items control, the WPF application will quickly behave slow, indicating a possible WPF binding error or several as is often the case. It is possible to track binding errors to the Event Log. In this article I will present code I use to track or trace binding errors to the Event Log and a NUnit integration test that actually test out the binding trace listener. Let's first review the code for the WPF binding error trace listener:


 public class BindingErrorTraceListener : DefaultTraceListener
    {
        private static BindingErrorTraceListener _Listener;

        public static void SetTrace()
        { SetTrace(SourceLevels.Error, TraceOptions.None); }

        public static void SetTrace(SourceLevels level, TraceOptions options)
        {
            if (_Listener == null)
            {
                _Listener = new BindingErrorTraceListener();
                PresentationTraceSources.DataBindingSource.Listeners.Add(_Listener);
            }

            _Listener.TraceOutputOptions = options;
            PresentationTraceSources.DataBindingSource.Switch.Level = level;
        }

        public static void CloseTrace()
        {
            if (_Listener == null)
            { return; }

            _Listener.Flush();
            _Listener.Close();
            PresentationTraceSources.DataBindingSource.Listeners.Remove(_Listener);
            _Listener = null;
        }



        private StringBuilder _Message = new StringBuilder();

        private BindingErrorTraceListener()
        { }

        public override void Write(string message)
        { _Message.Append(message); }

        public override void WriteLine(string message)
        {
            _Message.Append(message);

            var final = "WPF DataBinding Error detected: " +
                Environment.NewLine + _Message.ToString();
            _Message.Length = 0;

            var logger = new EventLogProvider();
            if (logger != null)
                logger.Log("OpPlan 4.x Client WPF Data Binding Error found: " + final, Microsoft.Practices.Prism.Logging.Category.Warn,
                     Microsoft.Practices.Prism.Logging.Priority.Medium);
        }
    }

The class above BindingErrorTraceListener inherits from DefaultTraceListener in the System.Diagnostics namespace. It is adding itself to the PresentationSources.DataBindingSource.Listeners property collection, this is also in the System.Diagnostics namespace. The class uses another class called EventLogProvider, which implements ILoggerFacade (used in PRISM applications), which will be presented below. Please note that the DefaultTraceListener is already added to this collection. The following code will add the implementation above to the listener collection in the OnStartup method of the WPF application class codebehind - App.xaml.cs:

public partial class App {

.. 


 protected override void OnStartup(StartupEventArgs e) {
  
  if (Debugger.IsAttached){
   BindingErrorTraceListener.SetTrace();   
  }

 }

}

Note that the listener is added to the listener collection if a debugger is attached. WPF binding errors should be detected by the developer and resolved before software is published and/or shipped to the customers. The EventLogProvider class is simply a wrapper to the EventLog class in System.Diagnostics namespace.

using System.Configuration;
using System.Diagnostics;
using Hemit.Diagnostics;
using Microsoft.Practices.Prism.Logging;

namespace Hemit.OpPlan.Client.Infrastructure
{
    //[Export(typeof(ILoggerFacade))]
    //[PartCreationPolicy(CreationPolicy.Shared)]
    public class EventLogProvider : ILoggerFacade
    {
        public const int MaxLogMessageLength = 32765;
        public string EventLogSourceName { get; set; }


        public EventLogProvider()
        {
            EventLogSourceName = ConfigurationManager.AppSettings[Constants.EventLogSourceNameMefKey];
        }

        private void WriteEntry(string message, Category category, Priority priority)
        {
            int eventID = 0;

            System.Diagnostics.EventLog.WriteEntry(EventLogSourceName, message, GetEventLogEntryType(category), eventID, GetPriorityId(priority));
        }

        private static EventLogEntryType GetEventLogEntryType(Category category)
        {
            switch (category)
            {
                case Category.Debug:
                    return EventLogEntryType.Information;
                case Category.Exception:
                    return EventLogEntryType.Error;
                case Category.Info:
                    return EventLogEntryType.Information;
                case Category.Warn:
                    return EventLogEntryType.Warning;
                default:
                    return EventLogEntryType.Error;
            }
        }

        private static short GetPriorityId(Priority priority)
        {
            switch (priority)
            {
                case Priority.None:
                    return 0;
                case Priority.High:
                    return 1;
                case Priority.Medium:
                    return 2;
                case Priority.Low:
                    return 3;
                default:
                    return 0;
            }
        }

        #region ILoggerFacade Members

        public void Log(string message, Category category, Priority priority)
        {
            WriteEntry(message, category, priority);
        }

        #endregion
    }
}

It is also nice to make an integration test to actually test that the WPF binding error trace listener actually works. I only use the integration to simulate a WPF binding error, but one can extend the EventLogProvider class to retrieve an Event Log Item that matches some certain text


    [TestFixture]
    public class BindingErrorTraceListenerUtilityTest
    {

        [Test]
        [RequiresSTA]
        public void Wpf_BindingError_Causes_Logging_Through_BindingTraceErrorListener()
        {
             
            BindingErrorTraceListener.SetTrace();
           
            var sausages = new[]
            {
                new {Product = "Hot dog", Cost = 25},
                new {Product = "Wiener", Cost = 42},
                new {Product = "Bratwurst", Cost = 53},
                new {Product = "Trailer grill shrimp salad with all extras deluxe edition", Cost = 112 }
            };

            string tbName = "tbGoof";
            var sausageBooth = new ListBox();
            var vt = new FrameworkElementFactory(typeof(TextBlock));
            vt.Name = tbName;
            vt.SetBinding(TextBlock.TextProperty, new Binding("Costt")); //Her setter man en WPF binding error 

            var groupBox = new GroupBox {DataContext = sausages[3]};
            groupBox.SetBinding(HeaderedContentControl.HeaderProperty, "Costt");

            var bindingExpressionGroupHeader = BindingOperations.GetBindingExpressionBase(groupBox, HeaderedContentControl.HeaderProperty);
            if (bindingExpressionGroupHeader != null)
                bindingExpressionGroupHeader.UpdateTarget();

            sausageBooth.ItemTemplate = new DataTemplate()
            {
                VisualTree = vt
            };
            sausageBooth.ItemsSource = sausages;

            sausageBooth.UpdateLayout();

            IItemContainerGenerator generator = sausageBooth.ItemContainerGenerator;
            GeneratorPosition position = generator.GeneratorPositionFromIndex(0);
            using (generator.StartAt(position, GeneratorDirection.Forward, true))
            {
                foreach (object o in sausageBooth.Items)
                {
                    DependencyObject dp = generator.GenerateNext();
                    generator.PrepareItemContainer(dp);
                }
            }

            ListBoxItem firstComboBoxItem = (ListBoxItem) sausageBooth.ItemContainerGenerator.ContainerFromIndex(0);
            Assert.IsNotNull(firstComboBoxItem);
            firstComboBoxItem.ContentTemplate.Seal();
            var dt = firstComboBoxItem.ContentTemplate.LoadContent();
            Assert.IsNotNull(dt);
            var textblock = dt as TextBlock;
            Assert.IsNotNull(textblock);
            textblock.SetBinding(TextBlock.TextProperty, new Binding("Costt"));
            var bindingTextBlock = BindingOperations.GetBindingExpressionBase(textblock, TextBlock.TextProperty);
            Assert.IsNotNull(bindingTextBlock);
            bindingTextBlock.UpdateTarget();

        }


Note that the integration above will run through and not assert anything at the end if the Event Log item was really added, but it will be easy to inspect the Event Log using the eventvwr command and check that the WPF binding error was really added. The BindingErrorTraceListener class adds wpf binding errors as warnings in the event log, as the following screen shot display:

No comments:

Post a Comment