在现代应用程序中,后台处理任务负责收集数据和/或控制设备是很常见的需求。对这些任务的监控可以通过多种方式进行,其中一些可能涉及复杂的图形用户界面组件用于状态报告和用户交互控制。顾名思义,LogString类仅提供这类功能的一个子集。
有许多日志框架和API可用,以及处理Windows事件日志的工具。统计了不少于30篇相关的CodeProject文章。虽然可能可以使用其中的一些工具,但它们的功能通常不适用于下面描述的要求。话虽如此,应该指出,项目LogString是为了使用log4net进行系统日志记录而开发的。当然,两者之间的差异在于,这两个日志设施(log4net和LogString)用于完全不同的目的。
本文还详细介绍了如何在解决方案中使用一些基本的.NET2.0框架功能。这里没有技巧或未记录的系统特性。可以将此视为一个C#2初学者教程,展示了一些在日常开发工作中有用的语言特性。
开发LogString类是因为应用程序需要能够让用户实时监控多个后台处理任务的活动。换句话说,他们需要能够查看处理事件的发生。
具体要求如下:
LogString实现为一个单独的类,并且具有非常简单的接口:
LogString GetLogString(string name); // 获取LogString实例
void PersistAll(); // 保存所有LogString实例
void ClearAll(); // 清除所有LogString实例的内容
void RemoveLogString(string name); // 移除命名的LogString实例
void Add(string message); // 添加消息
void Persist(); // 保存此LogString实例
void Clear(); // 清除此LogString实例
string Log; // 这是日志字符串
delegate void LogUpdateDelegate(); // 通知委托
event LogUpdateDelegate OnLogUpdate; // 更新事件
选项属性:见下表。
类型 | 属性名称 | 描述 | 默认值 |
---|---|---|---|
bool | ReverseOrder | 如果为true,则将新条目添加到日志的开头。这使得实时更新更容易查看,因为新项目出现在顶部,而旧条目从底部滚动出去。如果设置为false,则新条目追加到日志文本的末尾。 | true |
bool | LineTerminate | 每个新条目都用CRLF终止。 | true |
bool | TimeStamp | 为每个日志条目添加时间戳。如果为false,则不添加时间戳。 | true |
int | MaxChars | 日志中允许的最大字符数。一旦日志字符串达到这个大小,最旧的文本将被移除:如果ReverseOrder为true,则从末尾移除,如果ReverseOrder为false,则从开头移除。 | 32000 |
GetLogString()方法用于访问命名的LogString实例:
LogString myLogger = LogString.GetLogString("Task1");
返回的对象是单例。后台处理任务通过调用Add()方法添加日志条目:
myLogger.Add("Something important happened!");
监控任务通过Log属性访问整个日志内容。如果textBox1是一个TextBox组件,日志内容将通过以下方式查看:
textBox1.Text = myLogger.Log;
通过向LogString实例的OnLogUpdate事件添加LogUpdateDelegate委托来实现对查看器的自动更新:
myLogger.OnLogUpdate += new LogString.LogUpdateDelegate(this.LogUpdate);
LogUpdate函数通过Invoke()方法更新TextBox组件。这些细节将在下面讨论。
每个日志字符串都可以使用Persist()实例方法单独持久化,但更典型的是使用PersistAll()静态方法在应用程序退出时(或例如在定时间隔)通过单个调用来持久化所有日志:
LogString.PersistAll();
可以使用Clear()方法清除单个日志,或使用ClearAll()清除所有日志。
LogStringTestApp项目,包括LogString类,展示了大部分功能,包括从后台线程进行日志记录。
使用静态Hashtable来维护所有命名的日志字符串:
private static Hashtable m_LogsTable = new Hashtable();
public static LogString GetLogString(string name)
{
if (m_LogsTable.ContainsKey(name))
return (LogString)m_LogsTable[name];
LogString rv = new LogString(name);
m_LogsTable.Add(name, rv);
return rv;
}
返回的每个LogString实例都是单例。可以使用RemoveLogString方法从表中移除LogString实例。这只有在创建了许多唯一命名的日志,以至于表不会用未使用的实例填满时才需要。
注意,LogString构造函数读取其日志文件(如果存在)。这将在实例化时自动恢复命名日志字符串的内容。
为了提供线程安全操作,每当内部日志字符串被读取或修改时,都必须锁定。
public void Clear()
{
lock (m_strLog)
{
m_strLog = string.Empty;
}
WriteLog();
if (OnLogUpdate != null) OnLogUpdate();
}
当一个线程在锁定代码块内时,执行相同对象锁定的另一个线程将被阻塞,直到另一个线程退出其块。在这里使用了C#lock语法,而不是更通用的Mutex(或Monitor)。有几个原因:
有关访问锁定的更多详细信息,请参见Thread Synchronization (C#Programming Guide)。
为希望在LogString实例已更新(即日志字符串已修改)时收到通知的客户端提供了一个公共委托/事件对。
public delegate void LogUpdateDelegate();
public event LogUpdateDelegate OnLogUpdate;
当日志字符串发生更改时,所有已添加到事件的委托将在LogString调用OnLogUpdate时收到通知,前提是有委托存在:
if (OnLogUpdate != null) OnLogUpdate();
客户端将使用以下方式更新TextBox组件:
private System.Windows.Forms.TextBox txtLog;
...
myLogger.OnLogUpdate += new LogString.LogUpdateDelegate(this.LogUpdate);
...
private delegate void UpdateDelegate();
private void LogUpdate()
{
Invoke(new UpdateDelegate(delegate {
txtLog.Text = myLogger.Log;
}));
}
Invoke()调用是必要的,以便UI组件更新可以从其他线程进行。这里显示的匿名委托是.NET2.0的新功能(见C#2 Anonymous Methods)。不必定义额外的委托方法使代码更干净、更容易理解。