Correct Event handling in C# - c#

this is basically a follow up to a previous question (Triggering an event in c# from c++ and declaring LPCWSTR). I've revised my code based on the answers and comments I have received and I solved the initial issue, which was passing the event to the GpioSetupInterruptPin from a gpio api. I don't have a lot of documentation on the api but what i'm trying to achieve is: have a form with a white label; after pressing a switch, the label turns yellow.
The problem i'm having now is the event seems to trigger as soon as it's created (the "execute" message is passed to the debug dialog and the label turns yellow) but it doesn't do anything when i toggle the switch. I was told in the last question to use WaitForSingleObject but i'm not really sure where to call it and this article only added to my confusion.
public partial class Form1 : Form
{
// P/Invoke CreateEvent and WaitForSingleObject
private void GPIO_Open() //get handle for gpio
private void GPIO_Output() //output pin declaration
private void button1_Click(object sender, EventArgs e)
{
Interrupt_Setup();
}
private void Interrupt_Setup()
{
hGPIO = GPIOapi.GpioOpenHandle(); //returns a handle to the gpio
GIPO_ON = true;
Debug.WriteLine("Driver open \n" + hGPIO);
GPIO_Output(); //set output pins
GPIO_Interrupt(Trigger); //configure interrupt
}
private void GPIO_Interrupt(string trigger)
{
bool ok;
_Main();
//INTERRUPT DECALRATION
ok = GPIOapi.GpioSetupInterruptPin(hGPIO, port6, 4, GPIOapi.INT_TRIGGER_MODE.TRIGGER_MODE_EDGE,
GPIOapi.INT_TRIGGER_POLARITY.TRIGGER_POL_HIGH_RISING, trigger, true);
Thread waitThread=new Thread(WaitForTrigger);
waitThread.Start();
if (!ok)
Debug.WriteLine("NO interrupt");
else
Debug.WriteLine("Interrupt set for:" + port6 + "04" + " at " + hGPIO);
}
public static string Trigger = "InputProcessUpdateHandler";
public static IntPtr handle = CreateEvent(IntPtr.Zero, false, false, Trigger); //used P/Invoke
private static InputProcessor inputProcessor = null;
public Color[] color =
{
Color.Orchid, Color.DarkOrchid, Color.GreenYellow, Color.CornflowerBlue, Color.SteelBlue,Color.Crimson
};
public int i = 0;
public void WaitForTrigger()
{
while(true)
{try
{
if (WaitForSingleObject(handle, 0xFFFFFFFF) == false)
{
BeginInvoke(((System.Action)(() =>label2.BackColor = color[i])));
i++;
if (i > 4)
i = 0;
}
Thread.Sleep(300);
}
catch (Exception e)
{ Debug.WriteLine("exception: " + e); }}
}
}
private void _Main()
{
inputProcessor = new InputProcessor();
ShowToggle showToggle = new ShowToggle(inputProcessor);
inputProcessor.Process(label1);
}
public class ShowToggle
{
private InputProcessor _inputProcessor = null;
public ShowToggle(InputProcessor inputProcessor)
{
_inputProcessor = inputProcessor;
_inputProcessor.updateHandledBy += InputProcessUpdateHandler;
}
private void InputProcessUpdateHandler(Label label)
{
label.BackColor = Color.Yellow;
Debug.Write("execute");
}
}
public class InputProcessor
{
public delegate void InputProcessUpdateHandler(Label label);
public event InputProcessUpdateHandler updateHandledBy = null;
public void Process(Label label)
{
if (updateHandledBy != null)
updateHandledBy(label);
}
}
If anyone could help me with this, I would be very grateful.
*** I got it working but it looks a right mess. Could anyone help me straighten it out?

You code is really confusing to me. I think what you want is something like this. Bear in mind I'm typing this into the SO text editor, so don't expect it to compile and just work - it's a guide. Consider it a step above pseudocode.
public class DeviceInterrupt
{
IntPtr m_gpio;
string m_eventName;
public event EventHandler OnInterrupt;
public DeviceInterrupt(int port)
{
// get a driver handle
m_gpio = GPIO_Open();
// generate some unique event name
m_eventName = "GPIO_evt_" + port;
// wire up the interrupt
GpioSetupInterruptPin(m_gpio, port, m_eventName, ...);
// start a listener
new Thread(EventListenerProc)
{
IsBackground = true,
Name = "gpio listener"
}
.Start();
}
public void Dispose()
{
// TODO: release the handle
}
private void EventListenerProc()
{
// create the event with the name we sent to the driver
var wh = new WaitHandle(false, m_eventName);
while (true)
{
// wait for it to get set by the driver
if (wh.WaitOne(1000))
{
// we have an interrupt
OnInterrupt.Fire(this, EventArgs.Empty);
}
}
}
}
Usage would then be something like this:
var intr = new DeviceInterrupt(4);
intr.OnInterrupt += MyHandler;
....
void MyHandler(object sender, EventArgs a)
{
Debug.WriteLine("Interrupt occurred!");
}
Note
The Compact Framework doesn't support actual named system events, so the named WaitHandle I use in my code above is not a CF-supplied WaitHandle. Instead I'm using the one from the Smart Device Framework. You could also P/Invoke to CreateEvent and WaitForSingleObject yourself.

Related

How to create a new subscription that removes any previous subscribers C# Visual Studio

I'm studying for an exam and I came across a question I couldn't figure out. It asks to Create a TurnOnRadio method for the Radio class. This method should remove any TV subscribers to the remote control object. I thought I could do this with just = without the += or -=. When I go to do this is says This event " RemoteControl.channelChange " can only be on the left hand side of += or -= (except when used from within the type 'Remote Control') Any help on accomplishing this task would be appreciated. Code posted below.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace RemoteControlApp2
{
class RemoteControl
{
public delegate void ChannelChanged(object remote, RemoteEventsArgs re);
public event ChannelChanged channelChange;
private int currentChannel;
public void ChangeTheCrrentChannel(int newChannel)
{
RemoteEventsArgs newRe = new RemoteEventsArgs(newChannel);
if (channelChange!=null)
{
channelChange(this, newRe);
}
}
}
class RemoteEventsArgs : EventArgs
{
public int newChannel;
public RemoteEventsArgs(int nc)
{
this.newChannel = nc;
}
}
class Television
{
private int tvChannel;
//Your code here
public void TurnOnTV(RemoteControl Remote)
{
Remote.channelChange += new RemoteControl.ChannelChanged(TVChannelChanged);
Console.WriteLine(Remote.ToString() + " is detected");
}
public void TurnOffTV(RemoteControl Remote)
{
Remote.channelChange -= new RemoteControl.ChannelChanged(TVChannelChanged);
Console.WriteLine(Remote.ToString() + " is no longer detected");
}
public void TVChannelChanged(Object Remote, RemoteEventsArgs nc)
{
Console.WriteLine("The TV channel is changed. New channel is: {0}", nc.newChannel);
}
}
class Radio
{
private int radioChannel;
//Your code here
public void TurnOnRadio(RemoteControl Remote)
{
Remote.channelChange = new RemoteControl.ChannelChanged(TVChannelChanged);
Console.WriteLine(Remote.ToString() + " is deteceted")
}
//May need to write RadioChannelChanged method
}
class Program
{
static void Main(string[] args)
{
RemoteControl rc = new RemoteControl();
Television tv = new Television();
tv.TurnOnTV(rc);
rc.ChangeTheCrrentChannel(29);
rc.ChangeTheCrrentChannel(32);
tv.TurnOffTV(rc);
Console.ReadKey();
}
}
}
I took out event from public event ChannelChanged channelchange;
So now it is public ChannelChanged channelchange;
Next I finished the radio class and TurnOnRadio method and now that event has been removed I can use = to remove all other subscriptions and now subscribes whatever channel the remote is changed to in main. Radio class code posted below.
class Radio
{
private int radioChannel;
//Your code here
public void TurnOnRadio(RemoteControl Remote)
{
Remote.channelChange = new RemoteControl.ChannelChanged(RadioChannelChanged);
//Console.WriteLine(Remote.ToString() + " is deteceted");
}
public void RadioChannelChanged(object Remote,RemoteEventsArgs re)
{
radioChannel = re.newChannel;
Console.WriteLine("Radio channel is changed. New channel is :{0}", re.newChannel);
}
//May need to write RadioChannelChanged method
}

C# Cross threading. IRC stream thread to Main UI thread

I've been trying to get this little IRC program working but for some reason I'm having issues with VS and cross threading. I'm not sure if I'm not doing it the proper way or what. Here are the parts causing the issue.
Main Thread:
public partial class MainUI : Form
{
private static IRC irc = new IRC();
public MainUI()
{
InitializeComponent();
}
public static void StartIRC()
{
irc.Start();
}
}
IRC Thread:
class IRC
{
private Thread ircThread;
private bool _running = true;
private NetworkStream stream;
private StreamWriter writer;
private StreamReader reader;
private TcpClient irc;
public IRC(){
ircThread = new Thread(new ThreadStart(Run));
ircThread.IsBackground = true;
}
public void Run(){
while (_running) {
parseInStream(reader.ReadLine());
}
}
public void Start()
{
ircThread.Start();
}
private void parseInStream(String inText)
{
String[] text = inText.Split(' ');
String name;
String message;
if (text[1].Equals("PRIVMSG")) {
name = capsFirstChar(getUser(inText));
message = inText.Substring(inText.IndexOf(":", 1) + 1);
sendToChatBox(capsFirstChar(name) + ": " + message, Color.Black);
}
else if (text[1].Equals("JOIN")) {
name = getUser(inText);
sendToChatBox(capsFirstChar(name) + " has joined the channel.", Color.LimeGreen);
}
else if (text[1].Equals("PART")) {
name = getUser(inText);
sendToChatBox(capsFirstChar(name) + " has left the channel.", Color.Red);
}
}
public void sendToChatBox(String text, Color color)
{
//Trying to send the text to the chatbox on the MainUI
//Works if the MainUI.Designer.cs file has it set to static
if (MainUI.txtMainChat.InvokeRequired) {
MainUI.txtMainChat.Invoke((MethodInvoker)delegate() {
sendToChatBox(text, color);
});
}
else {
MainUI.txtMainChat.SelectionColor = color;
MainUI.txtMainChat.AppendText(text);
}
}
private String getUser(String msg)
{
String[] split = msg.Split('!');
user = split[0].Substring(1);
return capsFirstChar(user);
}
private String capsFirstChar(String text)
{
return char.ToUpper(text[0]) + text.Substring(1).ToLower();
}
}
The only way I am able to get it to work is if I enter the MainUI.Designer.cs file and change the textbox to static and then change everything from this.txtMainChatto MainUI.txtMainChat.
My main problem is that when I make any changes on the visual side all the things labeled static or things named MainUI are deleted. I'm trying to figure out what I need to do to keep this from happening. Am I doing it the right way, or is there a better way? I tried using a background worker but it was using a lot of processing power to work that way for some reason.
I've looked around the web and can't seem to find out how one might relate to my setup. I see people calling a thread from the main thread and then sending things from the main thread to the thread it called but not the other way around. There is nothing else being written to the text box so there won't be an issue with it being used by two threads at the same time.
On my main UI thread I passed in "this" so I could reference the main window from my IRC Class. MainUI.txtMainChat
irc = new IRC(this);
Then in my IRC class
MainUI main;
public IRC(MainUI main){
this.main = main;
ircThread = new Thread(new ThreadStart(Run));
ircThread.IsBackground = true;
}
Then I was able to Change
//MainUI.txtMainChat to
main.txtMainChat
Like Cameron said, Though I know I was told it's not the best approach it gets me started.
Your designer file is rebuilt every time you change your UI in the designer.
You'll need to pass your MainUi to your IRC class, or give it an abstraction of it using an interface (best option).
public interface IMainUI
{
void AddText(string text, Color color);
void UiThread(Action code);
}
public class MainUI : IMainUI
{
// Whatever else
public void AddText(string text, Color color)
{
UiThread( () =>
{
// Same code that was in your Irc.SendToChatBox method.
});
}
public void UiThread(Action code)
{
if (InvokeRequired)
{
BeginInvoke(code);
return;
}
code.Invoke();
}
}
public class IRC
{
IMainUI _mainUi;
//Other properties, and fields
public IRC(IMainUI mainUi)
{
this._mainUi = mainUi;
// Other constructor stuff.
}
// Other logic and methods
}

How to get back to function from timed event

Okay so I have a function called readSensor which you guessed it.. reads a sensor.
But the sensors usually take about 100ms to respond. So in the readSensor function I am basically just starting a timer.
On the timed event I read the serialport and get my response.
However this means that my response is in the onTimedEvent when I want it to be in the readSensor function..
Basically from the main form I want to be able to do this.
value = readSensor()
when at the minute all I can do is readSensor() and then I can see the response is coming back by displaying it in a messagebox once the timedEvent fires.
here is my code. (I have missed out loads of serialport setup and stuff but hopefully you can see the problem I am in)
I don't want to just wait in the function for 100ms though polling the timer as that will make my program slow..
I want to somehow get the response back to the readSensor function and then back to the form.
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO.Ports;
using System.Timers;
namespace readSensor
{
public partial class readSens : UserControl
{
public readSens()
{
InitializeComponent();
}
private System.Timers.Timer rTimer;
SerialPort sp = new SerialPort();
private void setupTimer()
{
// Create a timer with a 100ms response.
rTimer = new System.Timers.Timer(100);
rTimer.SynchronizingObject = this;
// Hook up the Elapsed event for the timer.
rTimer.Elapsed += new ElapsedEventHandler(OnTimedEvent);
}
private void OnTimedEvent(object source, ElapsedEventArgs e)
{
string response = getResponse();
}
public string getResponse()
{
string status = "";
byte[] readBuffer = new byte[255];
if (sp.IsOpen)
{
if (sp.BytesToRead > 0) //there is data to read
{
int length = sp.BytesToRead;
for (int i = 0; i < length; i++)
{
readBuffer[i] = (byte)sp.ReadByte();
status = "pass";
return status;
}
}
}
public void readSensor(byte addr)
{
if (!sp.IsOpen)
{
openPort();
readSensor(addr); // calls itself again once port is opened
}
else if (sp.IsOpen)
{
rTimer.Start();
}
else
{
MessageBox.Show("Port not opened yet");
}
}
}
}
In the main form I am basically just saying
setupTimer();
readSensor();
on a button click.
I don't think you can do it without some callback mechanism. You could implement a while loop but that is not eficient as it would introduce spinning.
My advice is to implement a proper async pattern or something simple like:
ReadSensor(addr, DoSomethingWithResult);
public void DoSomethingWithResult(string result)
{
Console.WriteLine (result);
}
public partial class ReadSens : UserControl
{
private Action<string> _responseCallback;
public void ReadSensor(byte addr, Action<string> responseCallback)
{
_responseCallback = responseCallback;
// initiate timer
}
private void OnTimedEvent(object source, ElapsedEventArgs e)
{
string response = getResponse();
_responseCallback(response);
}
}
Start a separate thread, then from that thread write into a queue the results back in your main thread.
class Game1
{
//We declare a queue, which is like an array that we can extract and enter data easily in a FIFO (first in, first out) style list.
Queue<string> q = new Queue<string>();
public void threadStart(object obj)
{
//We get the result of your function, while our main function is still looping and waiting.
string result = readInput()
//We tell C# that the parameter we passed in, is in fact the Game1 class passed from "t.Start"
Game1 game = (Game1)obj;
//This puts our "result" into the queue.
game.q.Enqueue(result);
}
public void start()
{
//Declares a new thread, which will run "threadStart" function.
System.Threading.Thread t = new System.Threading.Thread(threadStart);
//We start the other thread (that will run in parallel) and pass "this" as the parameter.
t.Start(this);
//We loop over and over, sleeping, whilst the other function runs at the same time. This is called "multi- threading"
while (q.Count == 0)
{
System.Threading.Thread.Sleep(10);
}
//This gets the last-entered (oldest) value from the queue q.
string result = q.Deque();
}
}
So this sets off a thread to get the result, and then in my version, polls the queue for a while until the results come back, but in yours could do a bunch of stuff, as long as you check the queue every now and again for new data.
Edit: Added commenting to hopefully alleviate some of your questions.
Could be this approach a valid solution for you? I think you only are using Timer to wait the serialPort to be open, but it can be self-controlled with raising event.
public class SensorReader
{
private Sensor sensor;
private string lastResponse;
public SensorReader(SerialPort serialPort)
{
this.serialPort = aSerialPort.
this.sensor = new Sensor(serialPort);
this.sensor.PortOpen += PortOpenEventHandler(OnPortOpen);
}
private void OnPortOpen()
{
this.ReadPort();
}
public string ReadPort(byte address)
{
if (!this.sensor.IsOpen)
{
this.sensor.OpenPort();
this.lastResponse = "The serial port doesn't respond... yet!";
}
else
{
// Read response at this point.
this.lastResponse = this.GetResponse();
}
return this.lastResponse;
}
}
public class Sensor
{
private SerialPort serialPort;
public Sensor(SerialPort aSerialPort)
{
this.serialPort = aSerialPort;
}
public bool IsOpen
{
get { return this.serialPort.IsOpen; }
}
public delegate void PortOpenEventHandler(object sender, EventArgs e);
public event PortOpenEventHandler PortOpen;
public void OpenPort()
{
// Open port here...
// ... and throw the PortOpen event.
if (this.PortOpen != null)
{
this.PortOpen(this, EventArgs.Empty);
}
}
}

update a richtextbox from a static class

I have the following code:
namespace SSS.RemoteTruckService
{
public partial class Startup : Form
{
private Timer _gpsTimer;
private Timer _ppsTimer;
private Timer _creditCardTimer;
private Timer _iniTimer;
public string Message
{
get { return richTextBox_Message.Text; }
set
{
richTextBox_Message.Invoke((MethodInvoker)(()
=> richTextBox_Message.Text = DateTime.Now + " " +
value + Environment.NewLine + richTextBox_Message.Text));
}
}
public Startup()
{
InitializeComponent();
}
private void ButtonStartClick(object sender, EventArgs e)
{
StartRemoteTruck();
}
private void ButtonPauseClick(object sender, EventArgs e)
{
if (_gpsTimer.Enabled) _gpsTimer.Enabled = false;
if (_ppsTimer.Enabled) _ppsTimer.Enabled = false;
if (_creditCardTimer.Enabled) _creditCardTimer.Enabled = false;
if (_iniTimer.Enabled) _iniTimer.Enabled = false;
ProcessIniFile.StopProcess();
}
public void StartRemoteTruck()
{
Message = "RemoteTruck started.";
if (Settings.GlobalSettings == null)
{
Message = "GlobalSettings was null or not loaded. Cannot continue.";
Logging.Log("GlobalSettings was null or not loaded. Cannot continue.", "RemoteTruck", Apps.RemoteTruckService);
Environment.Exit(0);
}
if (Settings.GlobalSettings.IniFileWatcherEnabled)
{
ProcessIniFile.StartProcess();
}
CreateTimers();
}
And in the ProcessIniFile.StartProcess() I have the code:
namespace SSS.RemoteTruckService.inifile
{
public static class ProcessIniFile
{
private static DateTime _iniLastWriteTime;
private static readonly string Inifile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "sss.ini");
private static FileSystemWatcher _watcher;
public static void StartProcess()
{
ReadIniFile();
SaveCurrentIniReadings();
CreateIniFileWatcher();
}
public static void StopProcess()
{
if (_watcher != null)
{
_watcher.EnableRaisingEvents = false;
_watcher = null;
}
}
private static void CreateIniFileWatcher()
{
_watcher = new FileSystemWatcher
{
Path = Environment.GetFolderPath(Environment.SpecialFolder.Windows),
NotifyFilter = NotifyFilters.LastWrite,
Filter = "sss.ini"
};
_watcher.Changed += SssIniWatcherChanged;
_watcher.EnableRaisingEvents = true;
}
I'd like to pass back the the calling form the status of the reads of the file watcher.
Maybe I'm overthinking this, but if I want to add to the Message on the main form, how do I get to it?
You can use Events for that. Your process can send events and your form can handle them.
More info: http://msdn.microsoft.com/en-us/library/awbftdfh.aspx
The simple but not pretty way I like to use is to make that part of the form static as well. For example, creating a static variable WriteMessage, and in your Form Load or Startup(), you can set it:
WriteMessage = (s) => Message = s;
Sure this has some issues, but it's a quick way to get it done. One of those issues is that, you may need to use Dispatcher.invoke if you're not on the UI thread.

Elegant Log Window in WinForms C#

I am looking for ideas on an efficient way to implement a log window for a windows forms application. In the past I have implemented several using TextBox and RichTextBox but I am still not totally satisfied with the functionality.
This log is intended to provide the user with a recent history of various events, primarily used in data-gathering applications where one might be curious how a particular transaction completed. In this case, the log need not be permanent nor saved to a file.
First, some proposed requirements:
Efficient and fast; if hundreds of lines are written to the log in quick succession, it needs to consume minimal resources and time.
Be able to offer a variable scrollback of up to 2000 lines or so. Anything longer is unnecessary.
Highlighting and color are preferred. Font effects not required.
Automatically trim lines as the scrollback limit is reached.
Automatically scroll as new data is added.
Bonus but not required: Pause auto-scrolling during manual interaction such as if the user is browsing the history.
What I have been using so far to write and trim the log:
I use the following code (which I call from other threads):
// rtbLog is a RichTextBox
// _MaxLines is an int
public void AppendLog(string s, Color c, bool bNewLine)
{
if (rtbLog.InvokeRequired)
{
object[] args = { s, c, bNewLine };
rtbLog.Invoke(new AppendLogDel(AppendLog), args);
return;
}
try
{
rtbLog.SelectionColor = c;
rtbLog.AppendText(s);
if (bNewLine) rtbLog.AppendText(Environment.NewLine);
TrimLog();
rtbLog.SelectionStart = rtbLog.TextLength;
rtbLog.ScrollToCaret();
rtbLog.Update();
}
catch (Exception exc)
{
// exception handling
}
}
private void TrimLog()
{
try
{
// Extra lines as buffer to save time
if (rtbLog.Lines.Length < _MaxLines + 10)
{
return;
}
else
{
string[] sTemp = rtxtLog.Lines;
string[] sNew= new string[_MaxLines];
int iLineOffset = sTemp.Length - _MaxLines;
for (int n = 0; n < _MaxLines; n++)
{
sNew[n] = sTemp[iLineOffset];
iLineOffset++;
}
rtbLog.Lines = sNew;
}
}
catch (Exception exc)
{
// exception handling
}
}
The problem with this approach is that whenever TrimLog is called, I lose color formatting. With a regular TextBox this works just fine (with a bit of modification of course).
Searches for a solution to this have never been really satisfactory. Some suggest to trim the excess by character count instead of line count in a RichTextBox. I've also seen ListBoxes used, but haven't successfully tried it.
I recommend that you don't use a control as your log at all. Instead write a log collection class that has the properties you desire (not including the display properties).
Then write the little bit of code that is needed to dump that collection to a variety of user interface elements. Personally, I would put SendToEditControl and SendToListBox methods into my logging object. I would probably add filtering capabilities to these methods.
You can update the UI log only as often as it makes sense, giving you the best possible performance, and more importantly, letting you reduce the UI overhead when the log is changing rapidly.
The important thing is not to tie your logging to a piece of UI, that's a mistake. Someday you may want to run headless.
In the long run, a good UI for a logger is probably a custom control. But in the short run, you just want to disconnect your logging from any specific piece of UI.
Here is something I threw together based on a much more sophisticated logger I wrote a while ago.
This will support color in the list box based on log level, supports Ctrl+V and Right-Click for copying as RTF, and handles logging to the ListBox from other threads.
You can override the number of lines retained in the ListBox (2000 by default) as well as the message format using one of the constructor overloads.
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading;
using System.Text;
namespace StackOverflow
{
public partial class Main : Form
{
public static ListBoxLog listBoxLog;
public Main()
{
InitializeComponent();
listBoxLog = new ListBoxLog(listBox1);
Thread thread = new Thread(LogStuffThread);
thread.IsBackground = true;
thread.Start();
}
private void LogStuffThread()
{
int number = 0;
while (true)
{
listBoxLog.Log(Level.Info, "A info level message from thread # {0,0000}", number++);
Thread.Sleep(2000);
}
}
private void button1_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Debug, "A debug level message");
}
private void button2_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Verbose, "A verbose level message");
}
private void button3_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Info, "A info level message");
}
private void button4_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Warning, "A warning level message");
}
private void button5_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Error, "A error level message");
}
private void button6_Click(object sender, EventArgs e)
{
listBoxLog.Log(Level.Critical, "A critical level message");
}
private void button7_Click(object sender, EventArgs e)
{
listBoxLog.Paused = !listBoxLog.Paused;
}
}
public enum Level : int
{
Critical = 0,
Error = 1,
Warning = 2,
Info = 3,
Verbose = 4,
Debug = 5
};
public sealed class ListBoxLog : IDisposable
{
private const string DEFAULT_MESSAGE_FORMAT = "{0} [{5}] : {8}";
private const int DEFAULT_MAX_LINES_IN_LISTBOX = 2000;
private bool _disposed;
private ListBox _listBox;
private string _messageFormat;
private int _maxEntriesInListBox;
private bool _canAdd;
private bool _paused;
private void OnHandleCreated(object sender, EventArgs e)
{
_canAdd = true;
}
private void OnHandleDestroyed(object sender, EventArgs e)
{
_canAdd = false;
}
private void DrawItemHandler(object sender, DrawItemEventArgs e)
{
if (e.Index >= 0)
{
e.DrawBackground();
e.DrawFocusRectangle();
LogEvent logEvent = ((ListBox)sender).Items[e.Index] as LogEvent;
// SafeGuard against wrong configuration of list box
if (logEvent == null)
{
logEvent = new LogEvent(Level.Critical, ((ListBox)sender).Items[e.Index].ToString());
}
Color color;
switch (logEvent.Level)
{
case Level.Critical:
color = Color.White;
break;
case Level.Error:
color = Color.Red;
break;
case Level.Warning:
color = Color.Goldenrod;
break;
case Level.Info:
color = Color.Green;
break;
case Level.Verbose:
color = Color.Blue;
break;
default:
color = Color.Black;
break;
}
if (logEvent.Level == Level.Critical)
{
e.Graphics.FillRectangle(new SolidBrush(Color.Red), e.Bounds);
}
e.Graphics.DrawString(FormatALogEventMessage(logEvent, _messageFormat), new Font("Lucida Console", 8.25f, FontStyle.Regular), new SolidBrush(color), e.Bounds);
}
}
private void KeyDownHandler(object sender, KeyEventArgs e)
{
if ((e.Modifiers == Keys.Control) && (e.KeyCode == Keys.C))
{
CopyToClipboard();
}
}
private void CopyMenuOnClickHandler(object sender, EventArgs e)
{
CopyToClipboard();
}
private void CopyMenuPopupHandler(object sender, EventArgs e)
{
ContextMenu menu = sender as ContextMenu;
if (menu != null)
{
menu.MenuItems[0].Enabled = (_listBox.SelectedItems.Count > 0);
}
}
private class LogEvent
{
public LogEvent(Level level, string message)
{
EventTime = DateTime.Now;
Level = level;
Message = message;
}
public readonly DateTime EventTime;
public readonly Level Level;
public readonly string Message;
}
private void WriteEvent(LogEvent logEvent)
{
if ((logEvent != null) && (_canAdd))
{
_listBox.BeginInvoke(new AddALogEntryDelegate(AddALogEntry), logEvent);
}
}
private delegate void AddALogEntryDelegate(object item);
private void AddALogEntry(object item)
{
_listBox.Items.Add(item);
if (_listBox.Items.Count > _maxEntriesInListBox)
{
_listBox.Items.RemoveAt(0);
}
if (!_paused) _listBox.TopIndex = _listBox.Items.Count - 1;
}
private string LevelName(Level level)
{
switch (level)
{
case Level.Critical: return "Critical";
case Level.Error: return "Error";
case Level.Warning: return "Warning";
case Level.Info: return "Info";
case Level.Verbose: return "Verbose";
case Level.Debug: return "Debug";
default: return string.Format("<value={0}>", (int)level);
}
}
private string FormatALogEventMessage(LogEvent logEvent, string messageFormat)
{
string message = logEvent.Message;
if (message == null) { message = "<NULL>"; }
return string.Format(messageFormat,
/* {0} */ logEvent.EventTime.ToString("yyyy-MM-dd HH:mm:ss.fff"),
/* {1} */ logEvent.EventTime.ToString("yyyy-MM-dd HH:mm:ss"),
/* {2} */ logEvent.EventTime.ToString("yyyy-MM-dd"),
/* {3} */ logEvent.EventTime.ToString("HH:mm:ss.fff"),
/* {4} */ logEvent.EventTime.ToString("HH:mm:ss"),
/* {5} */ LevelName(logEvent.Level)[0],
/* {6} */ LevelName(logEvent.Level),
/* {7} */ (int)logEvent.Level,
/* {8} */ message);
}
private void CopyToClipboard()
{
if (_listBox.SelectedItems.Count > 0)
{
StringBuilder selectedItemsAsRTFText = new StringBuilder();
selectedItemsAsRTFText.AppendLine(#"{\rtf1\ansi\deff0{\fonttbl{\f0\fcharset0 Courier;}}");
selectedItemsAsRTFText.AppendLine(#"{\colortbl;\red255\green255\blue255;\red255\green0\blue0;\red218\green165\blue32;\red0\green128\blue0;\red0\green0\blue255;\red0\green0\blue0}");
foreach (LogEvent logEvent in _listBox.SelectedItems)
{
selectedItemsAsRTFText.AppendFormat(#"{{\f0\fs16\chshdng0\chcbpat{0}\cb{0}\cf{1} ", (logEvent.Level == Level.Critical) ? 2 : 1, (logEvent.Level == Level.Critical) ? 1 : ((int)logEvent.Level > 5) ? 6 : ((int)logEvent.Level) + 1);
selectedItemsAsRTFText.Append(FormatALogEventMessage(logEvent, _messageFormat));
selectedItemsAsRTFText.AppendLine(#"\par}");
}
selectedItemsAsRTFText.AppendLine(#"}");
System.Diagnostics.Debug.WriteLine(selectedItemsAsRTFText.ToString());
Clipboard.SetData(DataFormats.Rtf, selectedItemsAsRTFText.ToString());
}
}
public ListBoxLog(ListBox listBox) : this(listBox, DEFAULT_MESSAGE_FORMAT, DEFAULT_MAX_LINES_IN_LISTBOX) { }
public ListBoxLog(ListBox listBox, string messageFormat) : this(listBox, messageFormat, DEFAULT_MAX_LINES_IN_LISTBOX) { }
public ListBoxLog(ListBox listBox, string messageFormat, int maxLinesInListbox)
{
_disposed = false;
_listBox = listBox;
_messageFormat = messageFormat;
_maxEntriesInListBox = maxLinesInListbox;
_paused = false;
_canAdd = listBox.IsHandleCreated;
_listBox.SelectionMode = SelectionMode.MultiExtended;
_listBox.HandleCreated += OnHandleCreated;
_listBox.HandleDestroyed += OnHandleDestroyed;
_listBox.DrawItem += DrawItemHandler;
_listBox.KeyDown += KeyDownHandler;
MenuItem[] menuItems = new MenuItem[] { new MenuItem("Copy", new EventHandler(CopyMenuOnClickHandler)) };
_listBox.ContextMenu = new ContextMenu(menuItems);
_listBox.ContextMenu.Popup += new EventHandler(CopyMenuPopupHandler);
_listBox.DrawMode = DrawMode.OwnerDrawFixed;
}
public void Log(string message) { Log(Level.Debug, message); }
public void Log(string format, params object[] args) { Log(Level.Debug, (format == null) ? null : string.Format(format, args)); }
public void Log(Level level, string format, params object[] args) { Log(level, (format == null) ? null : string.Format(format, args)); }
public void Log(Level level, string message)
{
WriteEvent(new LogEvent(level, message));
}
public bool Paused
{
get { return _paused; }
set { _paused = value; }
}
~ListBoxLog()
{
if (!_disposed)
{
Dispose(false);
_disposed = true;
}
}
public void Dispose()
{
if (!_disposed)
{
Dispose(true);
GC.SuppressFinalize(this);
_disposed = true;
}
}
private void Dispose(bool disposing)
{
if (_listBox != null)
{
_canAdd = false;
_listBox.HandleCreated -= OnHandleCreated;
_listBox.HandleCreated -= OnHandleDestroyed;
_listBox.DrawItem -= DrawItemHandler;
_listBox.KeyDown -= KeyDownHandler;
_listBox.ContextMenu.MenuItems.Clear();
_listBox.ContextMenu.Popup -= CopyMenuPopupHandler;
_listBox.ContextMenu = null;
_listBox.Items.Clear();
_listBox.DrawMode = DrawMode.Normal;
_listBox = null;
}
}
}
}
I'll store this here as a help to Future Me when I want to use a RichTextBox for logging colored lines again. The following code removes the first line in a RichTextBox:
if ( logTextBox.Lines.Length > MAX_LINES )
{
logTextBox.Select(0, logTextBox.Text.IndexOf('\n')+1);
logTextBox.SelectedRtf = "{\\rtf1\\ansi\\ansicpg1252\\deff0\\deflang1053\\uc1 }";
}
It took me way too long to figure out that setting SelectedRtf to just "" didn't work, but that setting it to "proper" RTF with no text content is ok.
My solution to creating a basic log window was exactly as John Knoeller suggested in his answer. Avoid storing log information directly in a TextBox or RichTextBox control, but instead create a logging class which can be used to populate a control, or write to a file, etc.
There are a few pieces to this example solution:
The logging class itself, Logger.
Modification of the RichTextBox control to add scroll-to-bottom functionality after an update; ScrollingRichTextBox.
The main form to demonstrate its use, LoggerExample.
First, the logging class:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
namespace Logger
{
/// <summary>
/// A circular buffer style logging class which stores N items for display in a Rich Text Box.
/// </summary>
public class Logger
{
private readonly Queue<LogEntry> _log;
private uint _entryNumber;
private readonly uint _maxEntries;
private readonly object _logLock = new object();
private readonly Color _defaultColor = Color.White;
private class LogEntry
{
public uint EntryId;
public DateTime EntryTimeStamp;
public string EntryText;
public Color EntryColor;
}
private struct ColorTableItem
{
public uint Index;
public string RichColor;
}
/// <summary>
/// Create an instance of the Logger class which stores <paramref name="maximumEntries"/> log entries.
/// </summary>
public Logger(uint maximumEntries)
{
_log = new Queue<LogEntry>();
_maxEntries = maximumEntries;
}
/// <summary>
/// Retrieve the contents of the log as rich text, suitable for populating a <see cref="System.Windows.Forms.RichTextBox.Rtf"/> property.
/// </summary>
/// <param name="includeEntryNumbers">Option to prepend line numbers to each entry.</param>
public string GetLogAsRichText(bool includeEntryNumbers)
{
lock (_logLock)
{
var sb = new StringBuilder();
var uniqueColors = BuildRichTextColorTable();
sb.AppendLine($#"{{\rtf1{{\colortbl;{ string.Join("", uniqueColors.Select(d => d.Value.RichColor)) }}}");
foreach (var entry in _log)
{
if (includeEntryNumbers)
sb.Append($"\\cf1 { entry.EntryId }. ");
sb.Append($"\\cf1 { entry.EntryTimeStamp.ToShortDateString() } { entry.EntryTimeStamp.ToShortTimeString() }: ");
var richColor = $"\\cf{ uniqueColors[entry.EntryColor].Index + 1 }";
sb.Append($"{ richColor } { entry.EntryText }\\par").AppendLine();
}
return sb.ToString();
}
}
/// <summary>
/// Adds <paramref name="text"/> as a log entry.
/// </summary>
public void AddToLog(string text)
{
AddToLog(text, _defaultColor);
}
/// <summary>
/// Adds <paramref name="text"/> as a log entry, and specifies a color to display it in.
/// </summary>
public void AddToLog(string text, Color entryColor)
{
lock (_logLock)
{
if (_entryNumber >= uint.MaxValue)
_entryNumber = 0;
_entryNumber++;
var logEntry = new LogEntry { EntryId = _entryNumber, EntryTimeStamp = DateTime.Now, EntryText = text, EntryColor = entryColor };
_log.Enqueue(logEntry);
while (_log.Count > _maxEntries)
_log.Dequeue();
}
}
/// <summary>
/// Clears the entire log.
/// </summary>
public void Clear()
{
lock (_logLock)
{
_log.Clear();
}
}
private Dictionary<Color, ColorTableItem> BuildRichTextColorTable()
{
var uniqueColors = new Dictionary<Color, ColorTableItem>();
var index = 0u;
uniqueColors.Add(_defaultColor, new ColorTableItem() { Index = index++, RichColor = ColorToRichColorString(_defaultColor) });
foreach (var c in _log.Select(l => l.EntryColor).Distinct().Where(c => c != _defaultColor))
uniqueColors.Add(c, new ColorTableItem() { Index = index++, RichColor = ColorToRichColorString(c) });
return uniqueColors;
}
private string ColorToRichColorString(Color c)
{
return $"\\red{c.R}\\green{c.G}\\blue{c.B};";
}
}
}
The Logger class incorporates another class LogEntry which keeps track of the line number, timestamp, and desired color. A struct is used to build a Rich Text color table.
Next, here is the modified RichTextBox:
using System;
using System.Runtime.InteropServices;
namespace Logger
{
public class ScrollingRichTextBox : System.Windows.Forms.RichTextBox
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(
IntPtr hWnd,
uint Msg,
IntPtr wParam,
IntPtr LParam);
private const int _WM_VSCROLL = 277;
private const int _SB_BOTTOM = 7;
/// <summary>
/// Scrolls to the bottom of the RichTextBox.
/// </summary>
public void ScrollToBottom()
{
SendMessage(Handle, _WM_VSCROLL, new IntPtr(_SB_BOTTOM), new IntPtr(0));
}
}
}
All I am doing here is inheriting a RichTextBox and adding a "scroll to bottom" method. There are various other questions about how to do this on StackOverflow, from which I derived this approach.
Finally, an example of using this class from a form:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace Logger
{
public partial class LoggerExample : Form
{
private Logger _log = new Logger(100u);
private List<Color> _randomColors = new List<Color> { Color.Red, Color.SkyBlue, Color.Green };
private Random _r = new Random((int)DateTime.Now.Ticks);
public LoggerExample()
{
InitializeComponent();
}
private void timerGenerateText_Tick(object sender, EventArgs e)
{
if (_r.Next(10) > 5)
_log.AddToLog("Some event to log.", _randomColors[_r.Next(3)]);
}
private void timeUpdateLogWindow_Tick(object sender, EventArgs e)
{
richTextBox1.Rtf = _log.GetLogAsRichText(true);
richTextBox1.ScrollToBottom();
}
}
}
This form is created with two timers, one to generate log entries pseudo-randomly, and one to populate the RichTextBox itself. In this example, the log class is instantiated with 100 lines of scroll-back. The RichTextBox control colors are set to have a black background with white and various color foregrounds. The timer to generate text is at a 100ms interval while the one to update the log window is at 1000ms.
Sample output:
It is far from perfect or finished, but here are some caveats and things that could be added or improved (some of which I have done in later projects):
With large values for maximumEntries, performance is poor. This logging class was only designed for a few hundred lines of scroll-back.
Replacing the text of the RichTextBox can result in flickering. I always keep the refresh timer at a relatively slow interval. (One second in this example.)
Adding to #2 above, some of my projects check if the log has any new entries before redrawing the RichTextBox content, to avoid unnecessarily refreshing it.
The timestamp on each log entry can be made optional and allow different formats.
There is no way to pause the log in this example, but many of my projects do provide a mechanism for pausing the scrolling behavior, to allow users to manually scroll, select, and copy text from the log window.
Feel free to modify and improve upon this example. Feedback is welcome.
I would say ListView is perfect for this (in Detail viewing mode), and its exactly what I use it for in a few internal apps.
Helpful tip: use BeginUpdate() and EndUpdate() if you know you will be adding/removing a lot of items at once.
I recently implemented something similar. Our approach was to keep a ring buffer of the scrollback records and just paint the log text manually (with Graphics.DrawString). Then if the user wants to scroll back, copy text, etc., we have a "Pause" button that flips back to a normal TextBox control.
If you want highlighting and color formatting, I'd suggest a RichTextBox.
If you want the auto scrolling, then use the ListBox.
In either case bind it to a circular buffer of lines.

Categories

Resources