假设你正在会议室里演示即时编译的程序。有什么最佳的方式来进行演示呢?
你还在往代码窗口里输入代码?这样,你得依赖听众的想像力 ―― 他们在脑海中构造这个程序是怎样运行的。此外,你还得依赖他们相信你的代码真的像和你所说的一样运作。
或者你要不停的运行你的代码,这样程序的输出窗口会弹出,然后听众可以看到代码的实际工作情况?这是有风险的,因为每次切换代码和输出窗口会中断流程。并且你得依赖听众会记得他们每次看到的输出信息所对应的代码。
我想这是一个可以被技术所解决的问题!我为Visual Studio 2008 写了一个小插件。它关注的是当前文本缓冲中所包含的代码,并在后台编译它,然后在前端窗口中显示输出。这一系列动作大概每2秒钟重复一次。你甚至不需要保存再重新编译代码就能看到输出 了。这个插件特意制做成独立的控制台程序因为它用不着输入。这是一幅截图:
源代码很小而且简单易懂,并且可以从上面的链接中获得。
有两个关键的地方。第一是要使用多线程进行处理。我想让源代码在后台线程中编译因此它并不会影响Visual Studio UI. 但是要获取当前缓冲的文本你必须使用UI线程,而且要显示输出你也得使用它。我使用System.Timers.Timer在后台线程触发它的事件,同时为需要UI线程的任务调用form.Invoke(…)。
我还使用了“non-AutoReset”计时器。我让它获取源码并进行编译+运行+显示,暂停2秒后,再去获取源码并进行编译+运行+显示,如此循环。换句话说,计时器的时间间隔必须设为2 秒在处理完先前的定时事件之后。
''' <summary>
''' OnTimer 处理non-autoreset计时器信号。它运行在后台线程中。从当前的缓冲区中获取
''' 源代码并编译它,然后显示输出。
''' </summary>
''' <remarks></remarks>
Sub OnTimer() Handles t.Elapsed
Try
Dim oldsrc = src
'我们在后台线程中。但只能从 UI 线程获取源...
'此委托将获取源代码并将它存储在"src"字段中
f.Invoke(New Action(AddressOf GetSource))
If src <> oldsrc Then
Dim oldoutput = output
' 在后台编译并运行
output = CompileAndRun(src)
If output <> "" OrElse oldoutput = "" Then
' 在屏幕上显示输出必须在UI线程中完成
' 此委托从"output"字段中得到要输出的内容并显示它
f.Invoke(New Action(AddressOf ShowOutput))
End If
End If
Finally
t.Start()
End Try
End Sub
另一个关键时刻与如何运行代码并捕获它的输出有关。在“My”命名空间中,VB在这方面有很多有用的函数。我的主要问题是合理的处理异常不留任何遗漏。(注意:获取临时文件名的代码并不完全准确:事实上你在某个状态之前得到一个未使用的临时文件名并不意味着这个文件名会一直不被使用;同样,它也并不意味着添加了“.vb”扩展名的临时文件名会一直不被使用。但是把这种情况处理的更准确看直来并不十分划算;不管怎样,在异常处理中我们将所有的问题恢复到正常状态。)
Function CompileAndRun(ByVal src As String) As String
Dim fn_exe = ""
Dim fn_src = ""
Dim vbc As System.Diagnostics.Process = Nothing
Dim exe As System.Diagnostics.Process = Nothing
Try
' 准备编译
fn_src = My.Computer.FileSystem.GetTempFileName() & ".vb"
My.Computer.FileSystem.WriteAllText(fn_src, src, False)
fn_exe = My.Computer.FileSystem.GetTempFileName() & ".exe"
Dim framework = Environment.ExpandEnvironmentVariables("%windir%\Microsoft.Net\Framework")
Dim latest_framework = (From d In My.Computer.FileSystem.GetDirectories(framework) Where d Like "*\v*" Select d).Last
' 编译
vbc = System.Diagnostics.Process.Start(New ProcessStartInfo _
With {.CreateNoWindow = True, _
.UseShellExecute = False, _
.FileName = latest_framework & "\vbc.exe", _
.Arguments = String.Format("/out:""{0}"" /target:exe ""{1}""", fn_exe, fn_src)})
Dim vbc_done = vbc.WaitForExit(3000)
If Not vbc_done Then Return ""
If vbc.ExitCode <> 0 Then Return ""
' 运行
Dim pinfo = New ProcessStartInfo With {.CreateNoWindow = True, _
.UseShellExecute = False, _
.FileName = fn_exe, _
.RedirectStandardOutput = True}
exe = New System.Diagnostics.Process With {.StartInfo = pinfo}
exe.Start()
Dim output = exe.StandardOutput.ReadToEnd
Dim exe_done = exe.WaitForExit(3000)
If Not exe_done Then Return ""
Return output
Finally
' 我们可以巧妙的关闭VBC进程
If vbc IsNot Nothing Then
If Not vbc.HasExited Then
Try : vbc.Kill() : Catch ex As Exception : End Try
Try : vbc.WaitForExit() : Catch ex As Exception : End Try
End If
Try : vbc.Close() : Catch ex As Exception : End Try
vbc = Nothing
End If
' 我们可以巧妙的关闭EXE
If exe IsNot Nothing Then
If Not exe.HasExited Then
Try : exe.Kill() : Catch ex As Exception : End Try
Try : exe.WaitForExit() : Catch ex As Exception : End Try
End If
Try : exe.Close() : Catch ex As Exception : End Try
exe = Nothing
End If
' 删除剩余的文件
Try : My.Computer.FileSystem.DeleteFile(fn_exe) : Catch ex As Exception : End Try
Try : My.Computer.FileSystem.DeleteFile(fn_src) : Catch ex As Exception : End Try
End Try
End Function