18

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.

Community
  • 1
  • 1
AllSolutions
  • 1,176
  • 5
  • 19
  • 40
  • 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 Jan 01 '13 at 23:06
  • 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 Jan 01 '13 at 23:10

7 Answers7

22

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.

Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
  • Looks good...but how will FormName be able to identify which instance of a Form class, in case the same Form class is instantiated multiple times. And I will need to port the last few lines to .Net 2.0 – AllSolutions Jan 01 '13 at 23:22
  • @AllSolutions if there are several forms with same name, then `OpenForms[FormName]` will return form which was opened first. You can search textbox like this `_textBox = form.Controls[TextBoxName] as TextBox` – Sergey Berezovskiy Jan 01 '13 at 23:28
  • Excellent answer. But I think to be more correct, you should be using `Layout.Format(writer, loggingEvent);` (with appropriate StringWriter constructed) – Steve Folly Nov 10 '13 at 19:19
18

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>   
klodoma
  • 4,181
  • 1
  • 31
  • 42
  • Why didn't you show me this hours and HOURS ago? 10x simpler than the monstrosity I was building AND it hooks into the config file. Minor changes to work with RichTextBox as well. Spot on. – WernerCD Jun 10 '14 at 15:19
  • Just a comment for anyone doing this in VB.Net instead of C#: FormName and TextBoxName must be declared as Property for it to work. – JonS Jun 26 '17 at 23:22
  • I have added a WPF version of this below - thanks @klodoma – GreyCloud Jan 09 '19 at 12:25
6

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);
Dorin
  • 69
  • 1
  • 1
  • If the log event is coming from outside of the UI thread, it hangs – Alexey Zimarev Sep 06 '13 at 12:30
  • @AlexeyZimarev: it looks like you have to call BeginInvoke instead. See http://social.msdn.microsoft.com/Forums/vstudio/en-US/a35e5298-33c4-4461-b956-bf265484219e/controlinvoke-hangs-the-application for details. – Chris R. Donnelly Mar 21 '14 at 17:09
  • Yes, BeginInvoke solves the hanging issue. See the complete example I've posted. – klodoma Apr 08 '14 at 15:19
  • Thanks. I used this, and also added `if (!_textBox.IsDisposed)` around the Invoke method call. I had problems on application shutdown. The form being logged to wasn't the main form, so the FormClosing event never got trigged. – Steve Folly Jul 04 '15 at 06:49
  • This does not work for me at all... I setup log4net.config rather than your config in directly by code, I enabled debug, everything -_- Also I can write to the file, but in the UI nothing. – Mecanik Feb 21 '18 at 11:49
2

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

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...

Roger Hill
  • 3,677
  • 1
  • 34
  • 38
2

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)

GreyCloud
  • 3,030
  • 5
  • 32
  • 47
1

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>
shanmuga raja
  • 685
  • 6
  • 19