Notes客户端Print相同信息到状态栏的问题



Notes客户端Print相同信息到状态栏的问题。这可能是个微不足道的问题,不过其他人也应该遇到过。在Notes客户端用LotusScript的Print语句输出信息到状态栏时,如果多次调用传入的参数实际相同,状态栏只会显示该信息一次。例如:

Print “a”

Print “a”

Print “a”

不会输出三行a,而只有一行。如果作为参数的变量内容相同,多次调用也只会输出一次。例如:

Print view.Toplevelentrycount

Print view.Toplevelentrycount

Print view.Toplevelentrycount

你 或许会问,即使这有些不正常,也没什么妨碍,重复输出有什么意义呢?原因如下。在客户端应用程序中,这些对Print的调用都是为了在程序运行时给用户 (不论是普通用户还是开发者人员)信息和反馈。Print重复的静态信息的用例会出现在报告事件和处理进度时,例如在批量审批流程、处理附件、查找特定记 录等等时,可以在发生或处理完一个事件后,调用这样的语句:Print”A purchase workflow is reviewed.”,Print “A XXX record is found.”。以上情况还可以通过在输出的信息中添加序列号来解决。而受这种输出丢失影响更大的则是Print的参数为变量的情况。尤其是在调试程序 时,往往会在代码的不同处Print同一变量,以理解某些语句的效果或弄清错误的原因。这时应该的多行输出变为一行,令人困扰。当然可以人为添加一些注释 使得每次输出都有差别,但很麻烦。

Print只在客户端运行的代码中,也就是输出到客户端状态栏时,有这种行为。包含它的代码运行在服务器 端时,按触发方式或者输出到服务器控制台,或者返回给浏览器,都没有这个问题。所以这是Print语句的特性,抑或是Notes客户机状态栏的问题,似乎 更有可能是后者。

这种行为是不是bug?或者是出于某些限制或考量有意设计如此?非计算机科班出身的我,想不出来,在网上也没有查到。不过 我印象中Firefox的Firebug插件的控制台也曾有同样行为,用console.log()方法输出重复信息时,只会显示一条。后来有了改进,在 输出信息的左边会在括号里标注重复输出的次数,例如(5)Show me again.

剩下的就是如何应对,即workaround。

首 先容易想到的就是写一个函数包装Print,在实际要输出的信息前附上当前时间。除却我们的目的,显示输出信息的时间本身也是有益的。遗憾的是 LotusScript里的日期时间类型Variant只精确到秒,如果重复信息的输出是在同一秒内,问题依旧。LotusScript可获得的最精确的 当前时间是Timer函数的返回值,精确到百分之一秒。于是我们可以写出如下函数:

  1. Public Function PrintMsg(msg As Variant)  
  2.     Dim strTime As String  
  3.     strTime=Format(Now, “hh:mm:ss”)  
  4.     Dim hs As Long   
  5.     hs=Timer Mod 100  
  6.     strTime=strTime & “:” & hs  
  7.     Print(strTime & “ - ” & msg)  
  8. End Function  

但在应用时会发现,百分之一秒对于计算机和LotusScript仍然是很漫长的(这是件好事),所以在类似下面的代码中,输出的信息对于Print语句仍然是相同的,也就会发生丢失。

  1. call PrintMsg(view.Toplevelentrycount)  
  2. Call CreateDoc(db)  
  3. call PrintMsg(view.Toplevelentrycount)  

LotusScript 里的时间只精确到百分之一秒,是因为Notes和Domino是跨平台的,不同操作系统支持的时间精度不等,但最少都在十毫秒数量级。利用 GetThreadInfo函数凭藉操作系统的clocktick概念实际能获得时间段的更高精度。下面是网上曾有人对不同系统的测试结果 (http://searchdomino.techtarget.com/tip/Accurate-LotusScript-timing-technique):


Ticks per second:

Win2000 Domino 6.0 (ticks per second1000)

Win2000 Domino 5.0.9a (ticks per second1000)

AIX 4.3.3 ML 9 with Domino 5.0.11 (ticks per second = 1,000,000)

Solaris 8 with Domino 5.0.11 (ticks per second = 1,000,000)

SuSE Linux 8.1 with Domino 6.0.1 (ticks per second 1000)

SunOS 5.8 Generic_108528-18 with Domino Release 6.0.1; (ticks per second 1000)

因此下面的代码演示了如何获得一段时间小于一秒的部分,并将它转化到毫秒单位。

  1. dim tick1 as Long, tick2 as Long, tps as Long  
  2. tps=GetThreadInfo(7)  
  3. tick1=GetThreadInfo(6)  
  4. ‘do something  
  5. tick2=GetThreadInfo(6)  
  6. dim dif as Long ‘The difference of the part of milliseconds   
  7. dif=((tick2-tick1) Mod tps)/tps*1000  

但是因为clock tick表示的是系统时钟自开机以来的时钟周期数,并不能由它直接确定当前时间,所以不能用在我们上面的函数以获得更高的时间精度。

为了彻底解决本文所说的问题,只有在每次输出前附上一个序列号。为此需要将PrintMsg函数包装在一个对象里,从而能够操作作为对象字段的序列号。

  1. Private printNum As Integer ‘Used for distinguish printing same contents  
  2. Public Function PrintMsg(msg As Variant)  
  3.     Dim strTime As String  
  4.     strTime=Format(Now, “hh:mm:ss”)  
  5.     Dim hs As Long   
  6.     hs=Timer Mod 100  
  7.     strTime=strTime & “:” & hs  
  8.     printNum=printNum+1  
  9.     Print(“[" & printNum & "]“ & strTime & “ - ” & msg)  
  10. End Function  

调用它输出重复内容的效果如下:

[1]16:52:36:24 – 81

[2]16:52:36:24 – 81

[3]16:52:36:24 – 81

我把这个方法添加到以前曾介绍过的Log4Dom类里,当然诸位也可以单独为它创建一个更精简的类。

  1. Class Log4Dom     
  2.     public logLevel As Integer  
  3.     public module As String         ‘module   
  4.     ‘the Log Destination, set as a variant then depending on the log type,   
  5.     ‘set as either a LogDB, logNotesFile, logStatus or logPrompt      
  6.     Private logFile As Variant                    
  7.       
  8.     Private s As NotesSession   
  9.     Private db As NotesDatabase ‘current database  
  10.     Private profile As NotesDocument ‘Log4Dom profile document  
  11.     public logName As String ‘log name from the profile  
  12.       
  13.     Private printNum As Integer ‘Used for distinguish printing same contents  
  14.       
  15.     Sub New ()  
  16.         Set s=New NotesSession  
  17.         logLevel = LEVEL_DEBUG  
  18.     End Sub  
  19.       
  20.     %REM  
  21.         Add a log destination.  
  22.         As the de facto only used log destination is Notes DB,  
  23.         I didn‘t handle the case of multiple log destinations of different types.  
  24.     %END REM  
  25.     Public Function AddLogFile(file As Variant)  
  26.         Set logFile = file  
  27.           
  28.         If TypeName(file)=“LOGDB” then  
  29.             ‘read parameter from Log4Dom profile by starrow  
  30.             Set db=s.CurrentDatabase  
  31.             Set profile=db.GetProfileDocument(“Log4DomProfile”)  
  32.             If Not profile Is Nothing Then  
  33.                 If profile.GetItemValue(“LogLevel”)(0)><“” then  
  34.                     logLevel=profile.GetItemValue(“LogLevel”)(0)  
  35.                 End if  
  36.                 logName=profile.GetItemValue(“LogName”)(0)  
  37.             End If  
  38.             ‘if no parameter provided, try the agent name  
  39.             If logName=“” Then  
  40.                 If Not s.CurrentAgent Is Nothing Then  
  41.                     logName=s.CurrentAgent.Name  
  42.                 End If  
  43.             End If  
  44.               
  45.             logFile.LogName=logName   
  46.         End if    
  47.     End Function  
  48.       
  49.     ‘logging at the different levels, INFO, WARN etc  
  50.     Public Function Info(message As StringAs Integer            
  51.         Info = WriteLog(LEVEL_INFO, message)      
  52.     End Function  
  53.       
  54.     Public Function Warn(message As StringAs Integer  
  55.         Warn = WriteLog(LEVEL_WARN, message)  
  56.     End Function  
  57.       
  58.     Public Function Debug(message As StringAs Integer  
  59.         Debug = WriteLog(LEVEL_DEBUG, message)    
  60.     End Function  
  61.       
  62.     ‘Can’t use error as the function name because it’s a reserved word.  
  63.     ‘If used for logging a runtime error, leave the argument as an empty string.      
  64.     Public Function LogError(message As StringAs Integer  
  65.         If message=“” Then  
  66.             ‘Generate the message for a runtime error.  
  67.             ‘The GetThreadInfo(LSI_THREAD_CALLPROC) must be called here instead of   
  68.             ‘calling an universal function to get the error message because that would  
  69.             ‘always return LogError as the function raising the error.    
  70.             ‘LSI_THREAD_CALLMODULE=11, LSI_THREAD_CALLPROC=10  
  71.             message = GetThreadInfo(11) & “>” & GetThreadInfo(10) & “: ” & _  
  72.             “Error(“ & Err() & “): ” & Error() & “ at line ”& Erl()  
  73.         End If  
  74.         LogError = WriteLog(LEVEL_ERROR, message)     
  75.     End Function  
  76.       
  77.     Public Function Fatal(message As StringAs Integer  
  78.         Fatal = WriteLog(LEVEL_FATAL, message)        
  79.     End Function  
  80.       
  81.     ‘user level logging, for specific level logging  
  82.     ‘@param level integer - the level 10 is the most detail, 1 the lowest level  
  83.     ‘message - a string to be logged. Number, boolean and date values would be  
  84.     ‘automatically converted to strings by LotusScript. Other types should be manually converted.  
  85.     Public Function WriteLog(level As Integer, message As StringAs Integer  
  86.         Dim theDate As String  
  87.         Dim theLevel As String  
  88.         Dim theMessage As String  
  89.         theDate = Cstr(Now)  
  90.         theLevel = “["+GetLevelString(level)+"] ”         
  91.         theMessage = theDate+“ ”+theLevel+“ ”+module+“ - ”+message  
  92.         ‘ check that logging is turned on for this level  
  93.         ‘ otherwise there is no need to log  
  94.         If level <= logLevel Then  
  95.             Call logFile.writelog(theMessage)  
  96.         End If  
  97.     End Function  
  98.       
  99.     ‘closes the log, saves notes doc or closes file  
  100.     Public Function Close  
  101.         logFile.close  
  102.     End Function  
  103.       
  104.       
  105.     ‘convert from level numbers into string  
  106.     Private Function GetLevelString(level As IntegerAs String  
  107.         Select Case level  
  108.             Case LEVEL_INFO : GetLevelString = LEVEL_INFO_STRING  
  109.             Case LEVEL_DEBUG : GetLevelString = LEVEL_DEBUG_STRING  
  110.             Case LEVEL_WARN : GetLevelString = LEVEL_WARN_STRING  
  111.             Case LEVEL_ERROR : GetLevelString = LEVEL_ERROR_STRING  
  112.             Case LEVEL_FATAL : GetLevelString = LEVEL_FATAL_STRING  
  113.             Case Else : GetLevelString = “LEVEL ”+Cstr(level)  
  114.         End Select  
  115.     End Function  
  116.       
  117.     %REM      
  118.             If the argument is the same, multiple calls to Print are executed only once.  
  119.         This function appends the current time before the message to be printed. The  
  120.         time is rounded to the nearest hundredth of second. However, for some circumstances,  
  121.         this is still not enough to avoid the same content to be printed. Thus, a serial  
  122.         number is appended.  
  123.     %END REM  
  124.     Public Function PrintMsg(msg As Variant)  
  125.         Dim strTime As String  
  126.         strTime=Format(Now, “hh:mm:ss”)  
  127.         Dim hs As Long   
  128.         hs=Timer Mod 100  
  129.         strTime=strTime & “:” & hs  
  130.         printNum=printNum+1  
  131.         Print(“[" & printNum & "]“ & strTime & “ - ” & msg)  
  132.     End Function  
  133. End Class  
  134. ‘Get a logger instance that writes to the specified db.  
  135. Public Function GetLogger(db As NotesDatabase) As log4Dom  
  136.     Dim logger As log4dom  
  137.     Set logger = New log4dom()  
  138.     Dim logFile As New LogDB(db)  
  139.     Call logger.AddLogFile(logFile)   
  140.     Set GetLogger=logger  
  141. End Function  
  142. Dim logger As Log4Dom  
  143. Set logger=GetLogger(Nothing)  
  144. call logger.PrintMsg(81)  
  145. call logger.PrintMsg(81)  
  146. call logger.PrintMsg(81)