18
votes

This is in followup to my question: Flexible Logging Interface...

I now want to write a custom log4net appender for a multiline TextBox, for my WinForms 2.0 application. One of the StackOverflow members devdigital has already pointed me to this link:

TextBox Appender

However, the article does not describe how to configure such an appender via an Xml file. The unique problem in configuring this appender is that we need to pass a reference to a TextBox object to this appender.

So is it at all possible to configure it using an Xml file? Or can such appenders be only configured programmatically? What are the options to make it as configurable or loosely coupled as possible, may be using a combination of Xml file and code?

Thanks.

7
In the xml config file, there are named params. Couldn't you use this to get the Name of textbox? And then use: Control[] Items = Controls.Find("textBoxLog4Net", false); to get access at runtime?Steve Wellens
But as far as I know, Controls is a property of the Form; then the question becomes which form should the appender refer to, or how will the appender get a reference to the Form object from the Xml file?AllSolutions

7 Answers

22
votes

It depends on the way how you configure log4net, but usually there will be no forms created(and thus textBoxes) when log4net reads configuration. So, you need to create properties for form and textbox names. And you should check if form is opened and it has provided textbox just before appending logging event. Also it's better to inherit from AppenderSkeleton than implement IAppender from scratch:

public class TextBoxAppender : AppenderSkeleton
{
    private TextBox _textBox;
    public string FormName { get; set; }
    public string TextBoxName { get; set; }

    protected override void Append(LoggingEvent loggingEvent)
    {
        if (_textBox == null)
        {
            if (String.IsNullOrEmpty(FormName) || 
                String.IsNullOrEmpty(TextBoxName))
                return;

            Form form = Application.OpenForms[FormName];
            if (form == null)
                return;

            _textBox = form.Controls[TextBoxName] as TextBox;
            if (_textBox == null)
                return;

            form.FormClosing += (s, e) => _textBox = null;
        }

        _textBox.AppendText(loggingEvent.RenderedMessage + Environment.NewLine);
    }
}

Configuration is simple (log4net will read xml elements and provide values for properties with same names):

<appender name="textbox" type="Foo.TextBoxAppender, Foo">
  <formName value="Form1"/>
  <textBoxName value="textBox1"/>
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%date %-5level %logger - %message" />
  </layout>      
</appender>
<root>
  <level value="INFO" />
  <appender-ref ref="textbox"/>
</root>

I didn't provide any error handling code or code related to multi-threading and threads synchronization, because question is about appender configuration.

17
votes

here is an updated version of all upper comments: thread safe, doesn't lock the application and uses the conversion pattern:

namespace MyNamespace
{

    public class TextBoxAppender : AppenderSkeleton
    {
        private TextBox _textBox;
        public TextBox AppenderTextBox
        {
            get
            {
                return _textBox;
            }
            set
            {
                _textBox = value;
            }
        }
        public string FormName { get; set; }
        public string TextBoxName { get; set; }

        private Control FindControlRecursive(Control root, string textBoxName)
        {
            if (root.Name == textBoxName) return root;
            foreach (Control c in root.Controls)
            {
                Control t = FindControlRecursive(c, textBoxName);
                if (t != null) return t;
            }
            return null;
        }

        protected override void Append(log4net.Core.LoggingEvent loggingEvent)
        {
            if (_textBox == null)
            {
                if (String.IsNullOrEmpty(FormName) ||
                    String.IsNullOrEmpty(TextBoxName))
                    return;

                Form form = Application.OpenForms[FormName];
                if (form == null)
                    return;

                _textBox = (TextBox)FindControlRecursive(form, TextBoxName);
                if (_textBox == null)
                    return;

                form.FormClosing += (s, e) => _textBox = null;
            }
            _textBox.BeginInvoke((MethodInvoker)delegate
            {
                _textBox.AppendText(RenderLoggingEvent(loggingEvent));
            });
        }
    }

}

The configuration, place this in app.config:

<appender name="textboxAppender" type="MyNamespace.TextBoxAppender, MyNamespace">
  <formName value="MainForm"/>
  <textBoxName value="textBoxLog"/>
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
  </layout>
</appender>
<root>
  <level value="DEBUG" />
  <appender-ref ref="RollingFileAppender" />
  <appender-ref ref="textboxAppender" />
</root>   
6
votes

I modified the appender to work with multithreading. Also, I attached the code configuration.

Regards, Dorin

Appender:

public class TextBoxAppender : AppenderSkeleton
{
    private TextBox _textBox;
    public TextBox AppenderTextBox
    {
        get
        {
            return _textBox;
        }
        set
        {
            _textBox = value;
        }
    }
    public string FormName { get; set; }
    public string TextBoxName { get; set; }

    private Control FindControlRecursive(Control root, string textBoxName)
    {
        if (root.Name == textBoxName) return root;
        foreach (Control c in root.Controls)
        {
            Control t = FindControlRecursive(c, textBoxName);
            if (t != null) return t;
        }
        return null;
    }

    protected override void Append(log4net.Core.LoggingEvent loggingEvent)
    {
        if (_textBox == null)
        {
            if (String.IsNullOrEmpty(FormName) ||
                String.IsNullOrEmpty(TextBoxName))
                return;

            Form form = Application.OpenForms[FormName];
            if (form == null)
                return;

            _textBox = (TextBox)FindControlRecursive(form, TextBoxName);
            if (_textBox == null)
                return;

            form.FormClosing += (s, e) => _textBox = null;
        }
        _textBox.Invoke((MethodInvoker)delegate
        {
            _textBox.AppendText(loggingEvent.RenderedMessage + Environment.NewLine);
        });
    }
}

Configuration:

           var textBoxAppender = new Util.TextBoxAppender();
        textBoxAppender.TextBoxName = "textLog";
        textBoxAppender.FormName = "MainTarget";
        textBoxAppender.Threshold = log4net.Core.Level.All;
        var consoleAppender = new log4net.Appender.ConsoleAppender { Layout = new log4net.Layout.SimpleLayout() };
        var list = new AppenderSkeleton[] { textBoxAppender, consoleAppender };
        log4net.Config.BasicConfigurator.Configure(list);
2
votes

The actual line that appends to the textbox should be...

_textBox.AppendText(RenderLoggingEvent(loggingEvent));

...if you want to take advantage of a pattern layout. Otherwise, it just sends the text of the message (the default layout).

2
votes

Above sample by Klodoma is quite good. If you change the textbox to a richtextbox, you can do more with the output. Here is some code to color code messages by level:

        System.Drawing.Color text_color;

        switch (loggingEvent.Level.DisplayName.ToUpper())
        {
            case "FATAL":
                text_color = System.Drawing.Color.DarkRed;
                break;

            case "ERROR":
                text_color = System.Drawing.Color.Red;
                break;

            case "WARN":
                text_color = System.Drawing.Color.DarkOrange;
                break;

            case "INFO":
                text_color = System.Drawing.Color.Teal;
                break;

            case "DEBUG":
                text_color = System.Drawing.Color.Green;
                break;

            default:
                text_color = System.Drawing.Color.Black;
                break;
        }

        _TextBox.BeginInvoke((MethodInvoker)delegate
        {
            _TextBox.SelectionColor = text_color;
            _TextBox.AppendText(RenderLoggingEvent(loggingEvent));
        });

If you really want to, the colors could be mapped from the log4net config in the same fashion as the ColorConsoleAppender, but I leave that for the next coder to stumble onto this sample...

2
votes

Here is a WPF/XAML version of klodoma's answer

  public class TextBoxAppender : AppenderSkeleton {
    private TextBox AppenderTextBox { get; set; }
    private Window window;

    public string WindowName { get; set; }
    public string TextBoxName { get; set; }

    private T FindControl<T>(Control root, string textBoxName) where T:class{
        if (root.Name == textBoxName) {
            return root as T;
        }

        return root.FindName(textBoxName) as T;
    }

    protected override void Append(log4net.Core.LoggingEvent loggingEvent) {
        if (window == null || AppenderTextBox == null) {
            if (string.IsNullOrEmpty(WindowName) ||
                string.IsNullOrEmpty(TextBoxName))
                return;

            foreach (Window window in Application.Current.Windows) {
                if (window.Name == WindowName) {
                    this.window = window;
                }
            }
            if (window == null)
                return;

            AppenderTextBox = FindControl<TextBox>(window, TextBoxName);
            if (AppenderTextBox == null)
                return;

            window.Closing += (s, e) => AppenderTextBox = null;
        }
        window.Dispatcher.BeginInvoke( new Action(delegate {
            AppenderTextBox.AppendText(RenderLoggingEvent(loggingEvent));
        }));
    }

and the log config

 <appender name="textboxAppender" type="Namespace.TextBoxAppender, Namespace">
<windowName value="Viewer"/>
<textBoxName value="LogBox"/>
<layout type="log4net.Layout.PatternLayout">
  <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
</layout>

Don't forget to give your window a name (must be different to the window type name)

1
votes

I would prefer the below approach if you want to do the logging at multiple places in your application. This approach gives the flexibility to change the control instance dynamically through code.

TextBoxAppender

public class TextBoxAppender : AppenderSkeleton
    {
        public RichTextBox RichTextBox { get; set; }

        protected override void Append(LoggingEvent loggingEvent)
        {
            Action operation = () => { this.RichTextBox.AppendText(RenderLoggingEvent(loggingEvent)); };
            this.RichTextBox.Invoke(operation);
        }
    }

The code to assign the textbox instance. Do this before you start the process that does the logging.

 var appender = LogManager.GetRepository().GetAppenders().Where(a => a.Name == "TextBoxAppender").FirstOrDefault();
 if (appender != null)
       ((TextBoxAppender)appender).RichTextBox = this.richTextBoxLog;

The configuration

<log4net debug="false">
    <appender name="TextBoxAppender" type="SecurityAudit.UI.TextBoxAppender">
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
      </layout>
    </appender>
    <root>
      <priority value="DEBUG" />
      <appender-ref ref="TextBoxAppender" />
    </root>
  </log4net>