又一个核心实用组件 - 应用中的日志记录、异常处理、配置和本地化






4.47/5 (7投票s)
核心实用组件 - 本地化、配置、日志记录和异常处理。
引言
在启动新的软件项目时,我们需要能够执行本地化、配置以及日志记录和异常处理等核心实用组件的代码。
本文简要介绍了这些组件在一个集成应用中的应用。为了简化,我删除了大部分注释,以便代码尽可能清晰。
本地化
本地化(Localization)通常是指将产品或服务适应特定语言、文化和期望的本地“外观和感觉”的过程,即使其能够支持您的应用程序的多语言。微软使用不同的本地化机制,但这并非我在示例源代码中实现的。
我在这里展示的是如何在运行时以编程方式切换用户界面语言。因此,要使用示例演示,您需要安装以下三种语言的键盘:英语(美国)、阿拉伯语(埃及)和德语(德国),如下图所示。
好吧,让我们从一个例子开始。假设您为一个拥有许多不同国籍同事的跨国企业工作,您希望让用户轻松地在用户界面中使用他们自己的母语,以及他们的默认键盘布局。那么您就可以构建一个示例如下图片的样本窗体。
此外,还需要在解决方案的所有项目中实现完整的本地化,包括向用户显示的警告和错误消息、日志记录活动等,如下图所示。
为了简化,我将展示如何在字典中设置语言,但通常情况下会使用外部实体,如数据库表、CSV 文件或文本文件来存储同一消息在不同语言中的引用。
以下代码初始化一个字典,并设置其在英语、阿拉伯语和德语三种语言下的消息值。
private void InitDictionary()
{
eDictionary = new Dictionary>
{
{
"Text",
new List {"Login Details ...", "بيانات الدخول ...", "Login-Daten ..."}
},
{
"_sIsNullOrEmptyMsg",
new List
{"Argument can't be Null or Empty",
"المعامل لا يمكن أن يكون فارغ أو عديم القيمة",
"Dem kann nicht null oder leer sein"
}
},
{
"_sUserNameMsg",
new List
{"The [User Name] value can't be Null or Empty ...",
"إسم المستخدم لا يمكن أن يكون فارغ أو عديم القيمة",
"Die [Benutzername] kann nicht null oder leer sein ..."
}
},
{
"_sUserPasswordMsg",
new List
{"The [Password] value can't be Null or Empty ...",
"كلمة السر لا يمكن أن يكون فارغ أو عديم القيمة",
"Die [Kennwort] kann nicht null oder leer sein ..."
}
},
{
"_sOkayMsg",
new List
{"Very good, everything went Okay ...",
"هايل ، كل شيء مر على ما يرام",
"Sehr gut, ging alles in Ordnung ..."
}
},
{
"lblUserName",
new List {"User Name", "إسم المستخدم", "Benutzername"}
},
{
"lblPassword",
new List {"Password", "كلمة السر", "Kennwort"}
},
{
"btnOK",
new List {"OK", "موافق", "ja"}
},
{
"btnClose",
new List {"Cancel", "خروج", "kündigen"}
},
{
"btnGenerateException",
new List {"Generate Exception", "توليد إعتراض", "Erzeugen Ausnahme"}
}
};
}
以下代码根据枚举切换语言。有关更多详细信息,请参阅源代码。
private void InitMessagesLanguage(LanguageName languageName)
{
int iIndex = 0;
switch (languageName)
{
case LanguageName.English:
iIndex = 0;
break;
case LanguageName.Arabic:
iIndex = 1;
break;
case LanguageName.German:
iIndex = 2;
break;
}
Text = eDictionary["Text"][iIndex];
_sIsNullOrEmptyMsg = eDictionary["_sIsNullOrEmptyMsg"][iIndex];
_sUserNameMsg = eDictionary["_sUserNameMsg"][iIndex];
_sUserPasswordMsg = eDictionary["_sUserPasswordMsg"][iIndex];
_sOkayMsg = eDictionary["_sOkayMsg"][iIndex];
lblUserName.Text = eDictionary["lblUserName"][iIndex];
lblPassword.Text = eDictionary["lblPassword"][iIndex];
btnOK.Text = eDictionary["btnOK"][iIndex];
btnClose.Text = eDictionary["btnClose"][iIndex];
btnGenerateException.Text = eDictionary["btnGenerateException"][iIndex];
}
配置
要利用 VS 中的配置功能,您需要在应用程序引用中添加对 System.Configuration 的引用,如下图所示。
对于示例项目,添加 App.Config 文件并按如下方式设置。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="Global.Setting.AppLang" value="US" />
<add key="Global.Setting.LoggingFolderName" value="LogFolder" />
<add key="Global.Setting.LoggingFolderParentPath" value="c:\" />
<add key="Global.Setting.EnableLoggingAppFlow" value="True" />
<add key="Global.Setting.EnableLoggingData" value="True" />
<add key="Global.Setting.EnableLoggingErrors" value="True" />
</appSettings>
</configuration>
好了,我在一个名为 ConfigHandler 的独立类中提供了足够多的方法,该类实现了 IDisposable 接口。
该类在初始化时有两个构造函数重载。以下演示了如何保存配置值。我们必须始终检查可能出现的 null 或空值,并且不要忘记刷新 ConfigurationManager 部分,以便在应用程序生命周期内使用保存的数据。
public bool SaveSectionSettingsValueByKey(string sKey, string sKeyValue, string sSection = "appSettings")
{
try
{
if (string.IsNullOrEmpty(sKey) || string.IsNullOrEmpty(sKeyValue) || string.IsNullOrEmpty(sSection))
throw new ArgumentOutOfRangeException(_sKeyValueSectionMsg);
AppSettingsSection section = (AppSettingsSection)_config.GetSection(sSection);
section.Settings[sKey].Value = sKeyValue;
//Save the configuration file.
_config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection(sSection);
//Op done successfully
return true;
}
catch (Exception ex)
{
//bubble error.
throw new Exception(_sSaveSectionSettingsValueByKeyMsg, ex);
}
}
以下演示了如何通过键获取配置值。您会注意到,默认情况下,节名称为“appSettings”,但如果您愿意,也可以是任何其他节。
public string GetSectionSettingsValueByKey(string sKey, string sSection = "appSettings")
{
try
{
if (string.IsNullOrEmpty(sKey) || string.IsNullOrEmpty(sSection))
throw new ArgumentOutOfRangeException(_sKeySectionMsg);
AppSettingsSection section = (AppSettingsSection)_config.GetSection(sSection);
return section.Settings[sKey].Value;
}
catch (Exception ex)
{
//bubble error.
throw new Exception(_sGetSectionSettingsValueByKeyMsg, ex);
}
}
异常处理
对于异常处理,我区分了以下两种不同的异常机制:
-
首先,构建一个自定义窗体来接收来自任何线程的应用程序异常。这种机制使您能够完全控制您的应用程序并捕获粗心的代码异常。
-
其次,一种通用的成功、警告和错误消息框机制,基于 Microsoft MessageBox 组件,可以根据用户界面的从右到左/从左到右的要求改变其布局,正如您在代码中将看到的。
关于第一点,我创建了三个静态方法来显示自定义线程异常窗体,用于处理 AppDomain 未捕获的异常和其他用户界面异常。请回看静态类“Program”。
第二种机制是构建一个 ExceptionHandler 类,使您能够根据您的需求显示 MessageBox 对话框。
以下代码展示了如何生成 RTL/LTR MessageBox 对话框,并用您喜欢的标题和内容变量填充它。
private void ShowMessageBox(string sOutPut, string sHeading, MessageBoxIcon messageBoxIcon = MessageBoxIcon.Information, bool bRightToLeftLayout = false)
{
if (bRightToLeftLayout)
MessageBox.Show(sOutPut, sHeading, MessageBoxButtons.OK, messageBoxIcon, MessageBoxDefaultButton.Button1,
MessageBoxOptions.RtlReading | MessageBoxOptions.RightAlign);
else
MessageBox.Show(sOutPut, sHeading, MessageBoxButtons.OK, messageBoxIcon, MessageBoxDefaultButton.Button1);
}
以下方法根据枚举显示正确的 MessageBox。
public void ShowException(string sExceptionMessage, OperationResult opResult)
{
string currOpStatus = String.Empty;
string sCharSeparatorLine = UtilityHandler.DrawSeparatorLine();
MessageBoxIcon messageBoxIcon = MessageBoxIcon.Information;
switch (opResult)
{
case OperationResult.Success:
currOpStatus = _sSuccessMsg;
_sOutput = _sOutputSuccessMsg + Environment.NewLine + sCharSeparatorLine + Environment.NewLine;
_sOutput += sExceptionMessage + Environment.NewLine;
_sOutput += sCharSeparatorLine;
messageBoxIcon = MessageBoxIcon.Information;
break;
case OperationResult.Warning:
currOpStatus = _sWarningMsg;
_sOutput = _sOutputWarningMsg + Environment.NewLine + sCharSeparatorLine + Environment.NewLine;
_sOutput += sExceptionMessage + Environment.NewLine;
_sOutput += sCharSeparatorLine;
messageBoxIcon = MessageBoxIcon.Warning;
break;
case OperationResult.Errors:
currOpStatus = _sErrorMsg;
_sOutput = _sOutputErrorMsg + Environment.NewLine + sCharSeparatorLine + Environment.NewLine;
_sOutput += sExceptionMessage + Environment.NewLine;
_sOutput += sCharSeparatorLine;
messageBoxIcon = MessageBoxIcon.Error;
break;
}
ShowMessageBox(_sOutput, currOpStatus, messageBoxIcon, _bRightToLeftLayout);
}
下图显示了自定义 MessageBox 的实际效果。
日志记录
日志记录是识别您的应用程序是否按计划运行的重要因素。对于日志处理,我选择将日志记录到配置文件中指定的专用文件夹中。当然,您可以使用事件查看器或数据库表来保存日志,但在示例代码中,我使用了文件日志记录。
以下代码创建了一个日志文件夹,并且为了简化,我没有考虑创建文件夹时可能面临的安全问题,以及创建的文件夹是在同一台机器上还是在网络驱动器上等等。
public bool CreateLogFolder(string sLoggingFolderParentFullPath = "Default", string sLogFolderName = "LogFolder")
{
if (string.IsNullOrEmpty(sLogFolderName))
throw new ArgumentOutOfRangeException("LogFolderName", _sCantBeNullOrEmptyMsg);
if (sLoggingFolderParentFullPath.ToLower() == "default")
{
sLoggingFolderParentFullPath = Environment.CurrentDirectory;
}
try
{
if (!(Directory.Exists(Path.Combine(sLoggingFolderParentFullPath, sLogFolderName))))
{
Directory.CreateDirectory(Path.Combine(sLoggingFolderParentFullPath, sLogFolderName));
}
using (ConfigHandler configHandler = new ConfigHandler())
{
configHandler.SaveSectionSettingsValueByKey(UtilityHandler.SLoggingFolderNameKey, sLogFolderName);
configHandler.SaveSectionSettingsValueByKey(UtilityHandler.SLoggingFolderParentPathKey, sLoggingFolderParentFullPath);
}
return true;
}
catch (Exception ex)
{
//bubble error.
throw new Exception(_sCreateLogFolderMsg, ex);
}
}
以下代码根据枚举创建日志文件。嗯,您创建三个日志文件:一个用于错误,一个用于数据,第三个用于活动监控日志。
public bool CreateLogFileInstance(LogFileType logFileType, bool bOverwriteCurrent = false)
{
if (!GetLoggingEnableDisableStatus(logFileType))
return false; //Logging is Forbidden, do not do anything
try
{
string sToBeWritten = UtilityHandler.DrawSeparatorLine(99, "*") + Environment.NewLine;
switch (logFileType)
{
case LogFileType.Error:
sToBeWritten += _sCreatingNewMsg + _sErrorLogMsg + _sInstanceAtMsg + DateTime.Now.ToString();
break;
case LogFileType.Logs:
sToBeWritten += _sCreatingNewMsg + _sActivityLogMsg + _sInstanceAtMsg + DateTime.Now.ToString();
break;
case LogFileType.Data:
sToBeWritten += _sCreatingNewMsg + _sDataLogMsg + _sInstanceAtMsg + DateTime.Now.ToString();
break;
default:
sToBeWritten += _sCreatingNewMsg + _sGenericLogMsg + _sInstanceAtMsg + DateTime.Now.ToString();
break;
}
sToBeWritten += Environment.NewLine + UtilityHandler.DrawSeparatorLine(99, "*") + Environment.NewLine;
string strLoggingFileName = GetLoggingFileName(logFileType);
string strLoggingFileNameWithPath = Path.Combine(_strLoggingFolderFullPath, strLoggingFileName);
if (bOverwriteCurrent)
{
if (File.Exists(strLoggingFileNameWithPath))
File.Delete(strLoggingFileNameWithPath); //remove the file.
}
//Establish a Logger file.
using (FileStream fStream = new FileStream(strLoggingFileNameWithPath, FileMode.Append, FileAccess.Write, FileShare.None))
using (StreamWriter swLogWritter = new StreamWriter(fStream, Encoding.UTF8))
swLogWritter.WriteLine(sToBeWritten);
return true;
}
catch (Exception ex)
{
//bubble error.
throw new Exception(_sCreateLogFileInstance01Msg, ex);
}
}
同时,也基于枚举,您可以在需要时写入任何日志文件,正如您在下面的 WriteLogLine 方法中所看到的。
public bool WriteLogLine(LogFileType logFileType, string sToBeWritten)
{
if (!GetLoggingEnableDisableStatus(logFileType))
return false; //Logging is Forbidden, do not do anything
string strLogFileName = GetLoggingFileName(logFileType);
string strLoggingFileNameWithPath = Path.Combine(_strLoggingFolderFullPath, strLogFileName);
try
{
//Create a Logger file if it does not exist.
if (!File.Exists(strLoggingFileNameWithPath))
CreateLogFileInstance(logFileType);
using (FileStream fStream = new FileStream(strLoggingFileNameWithPath, FileMode.Append, FileAccess.Write, FileShare.None))
using (StreamWriter swLogWritter = new StreamWriter(fStream, Encoding.UTF8))
swLogWritter.WriteLine(sToBeWritten);
return true;
}
catch (Exception ex)
{
//bubble error.
throw new Exception(_sWriteLogLine01Msg, ex);
}
}
结论
任何有价值的应用程序都必须具备本地化、配置、日志记录和异常处理机制等核心实用组件。这些组件必须具备足够的标准来丰富您的应用程序,并且必须基于足够的面向对象技术,以便您可以依赖它们。