Silverlight 中的静默打印






4.78/5 (13投票s)
如何避免从 Silverlight 打印时出现打印对话框。
引言
正如您可能知道的,Silverlight 4 通过其打印 API(基于 PrintDocument
类)引入了基本打印功能。此 API 允许您以位图方式将应用程序屏幕、屏幕的一部分或经过适当构造的替代自定义视觉树发送到打印机。
在 Silverlight 中,出于安全原因,打印支持在某些方面受到限制;例如
- 打印操作必须由用户发起(也就是说:它仅允许在处理用户事件的上下文中进行);
- “打印”对话框(即下图所示的窗口,用户可以在其中选择打印机设置,然后单击“打印”继续,或单击“取消”取消打印操作)始终显示。
特别是后者限制非常令人讨厌,尤其是在用户只需要确认打印对话框提出的所有默认设置,并通过单击“打印”按钮来启动打印操作时。这不仅可能是一个多余的操作,而且在某些情况下,它可能是一个真正的问题。
例如,考虑一个受控的业务线场景,其中 Silverlight 应用程序用于售票台:为了将票交给买家,用户需要输入一些数据,然后打印一张票,该票应打印一次(且仅一次)到默认打印机。当然,在这种情况下,允许用户打印更多副本或更改目标打印机是不受欢迎的,应避免。那么,Silverlight 强制出现的打印对话框就显得是一个很大的限制。此外,作为程序员,您无法知道用户在确认打印操作之前是否更改了打印对话框选项中的某些详细信息;因此,您不仅无法阻止他们进行这些修改,而且您也无法实际知道是否发生了一些修改(事实上,BeginPrintEventArgs
和 PrintPageEventArgs
都不包含此信息)。
因为我发现自己正处于这种情况,所以我试图找到一种方法来确保 Silverlight 的打印操作以静默方式进行,即:用户从应用程序启动打印操作,而无需与打印对话框进行交互,并且基本上自动确认了默认打印对话框的所有选项,而无需用户干预。
这样
- 我确信应用程序将打印到当前配置为默认打印机的打印机;
- 我确信在用户启动打印操作后只会打印一份;
- 将来(如果需要,因为纸张卡住或其他问题)的重新打印将完全由应用程序逻辑驱动(例如,请求进一步的确认或用于重新打印的密码和权限),并在应用程序级别进行适当记录。
我在此提出的解决方案(请注意!)仅适用于某些特定场景(通常是:受控的业务线或企业环境),因为
- 它受到一些限制(它仅在基于 Windows 的 PC 上运行,尽管 Silverlight 的兼容性更广);
- 它利用了 Silverlight 的隔离存储,因此用户不得在其客户端上禁用它;
- 它需要控制最终用户的计算机(需要一个自定义可执行文件在那里运行,以支持从 Silverlight 进行此类静默打印)。
基本思路
为了实现 Silverlight 的静默打印,在假设显示打印对话框是不可避免的情况下,我的基本想法是拦截该对话框的出现,并立即通过模拟对同一对话框的“打印”按钮的点击来模拟对默认设置的用户确认。
为了拦截特定窗口的打开,我们必须在操作系统层面工作,与 Windows OS 在处理窗口时管理的各种消息和事件进行交互。这需要在客户端计算机上运行完全受信任的代码,您可以通过运行本地 .NET 可执行文件或利用 Silverlight 的 COM 互操作来实现(但仅当您选择涉及完全受信任的浏览器外 Silverlight 应用程序的解决方案时)。
对于我必须处理的具体场景(我完全控制了目标计算机的安装和设置,以便在售票台运行),我选择了在客户端计算机上安装一个自定义可执行文件,充当“拦截服务”。它执行监视任何打开的窗口的任务,检查它是否是打印对话框,如果是,则通过模拟点击“打印”按钮来自动确认建议的选项。
拦截打印对话框
为了监视任何打开的窗口,我决定挂钩系统事件 WM_SETFOCUS
(当任何窗口获得用户输入焦点时触发)。为此,我使用了 Microsoft UI Automation 框架(从 .NET Framework 3.5 起可用),特别是 UIAutomationClient.dll 和 UIAutomationTypes.dll 程序集(包含一组适用于托管代码的类型,使 UI Automation 客户端应用程序能够获取有关 UI 的信息并向控件发送输入)。
我准备的拦截服务代码(实际上是在源代码下载中的一个简单 Windows Forms 应用程序中实现的)基本上执行以下操作:
- 它挂钩
WM_SETFOCUS
Windows 事件; - 当处理
WM_SETFOCUS
Windows 事件时 - 它检索当前具有焦点的控件;
- 它向上遍历控件树以查找标题为“Print”的窗口;
- 找到标题为“Print”的窗口后
- 它识别“Print”按钮;
- 它调用“点击”它以自动确认打印操作。
事实上,当这个拦截代码在客户端计算机上运行时,如果我导航到我的 Silverlight 应用程序并使用它打印某些内容,打印对话框会非常短暂地出现(它只是“闪烁”一下),并且会立即被自动确认代码关闭,而没有任何机会让我与之交互或修改任何建议的设置。所以,这似乎奏效了!但我们只走了一半,因为存在以下主要问题:
- 拦截代码只查找一个标题为“Print”的焦点窗口,但目前没有检查打印对话框的所有者;因此,自动确认将适用于任何调用者发起的打印对话框(而不仅仅是指定的 Silverlight 应用程序);这不好,因为如果我通过浏览器的标准打印功能打印网页,或者我想通过 Microsoft Word 等任何应用程序打印文档,如果拦截代码正在运行,那么就会发生自动确认,但我不想这样做;
- 如果我想在同一个 Silverlight 应用程序中提供一些“受控”(即自动确认)的打印功能,并且其他打印功能通过标准打印对话框“开放”给用户正常干预,那么目前我无法实现这一点;
- 如果拦截代码未运行(因为它崩溃了或未启动),Silverlight 应用程序会正常运行,显示打印对话框并允许我修改其中的所有内容;我希望在这种情况下,打印功能被阻止,从而强制拦截代码在 Silverlight 应用程序能够执行我想要“受控”的打印操作的条件下运行。
如果问题 #1 可以通过在检测到刚打开并具有焦点的打印对话框窗口的应用程序身份上实现检查来解决,那么很明显,问题 #2 和 #3 需要拦截应用程序和 Silverlight 应用程序之间的某种通信。事实上,为了区分应该自动确认的打印操作和不应该的打印操作,Silverlight 应用程序应该以某种方式区分对打印 API 的调用,并且拦截代码应该能够因此进行区分。此外,它应该以某种方式向 Silverlight 应用程序发出信号,表明它正在运行,以便 Silverlight 应用程序可以决定是否应该启动受控打印操作(因为拦截代码正在运行并且能够管理自动确认),或者是否应该阻止它(因为拦截代码似乎未运行,因此无法管理自动确认 - 在这种情况下,我们宁愿完全禁止打印操作)。
让 Silverlight 与拦截应用程序通信
浏览器托管的 Silverlight 应用程序和本地可执行文件能够通信吗?我对这个问题的第一个回答是:“当然不能,因为 Silverlight 的沙箱以及浏览器托管应用程序的限制,(在 Silverlight 4 中)不能完全信任!”
但是,有一种简单的通信方法确实存在,我发现它在 Silverlight 的隔离存储中。想想看:Silverlight 应用程序的隔离存储是位于客户端 PC 文件系统中的一个文件结构,位于用户配置文件文件夹的子树中;如果一个 Windows 可执行文件以适当的权限和适当的文件系统访问权限执行,它肯定能够读取和写入 Silverlight 应用程序隔离存储使用的相同文件和文件夹……或者不是?
因此,我决定让 Silverlight 应用程序和实现“拦截服务”的本地可执行文件通过隔离存储进行通信。基本上,我将隔离存储视为一个可以传递和拾取消息的公共场所(一种共享邮箱)。
- Silverlight 应用程序发送的消息实际上承载以下概念内容:“我亲爱的拦截服务,我即将开始一次受控打印操作,并且希望您通过出现打印对话框的自动确认来管理它”。这类消息(在调用
PrintDocument.Print
之前立即发送)仅用于受控打印操作,并且显然应省略用于开放打印操作(从而解决了上述问题 #2,即:区分和区分受控和开放打印操作)。此外,因为 Silverlight 应用程序负责发出其希望由拦截服务管理的打印操作的信号,这也解决了问题 #1;事实上,任何其他打印操作(例如,由其他应用程序发起的)都会生成一个打印对话框窗口 - 即使该窗口被服务检测为刚打开并具有焦点的“Print”窗口 - 也不会被自动确认。 - 拦截服务发送的消息所承载的概念内容是:“我亲爱的 Silverlight 应用程序,我已启动并正常运行,并且状态良好:如果您愿意,您可以启动一次受控打印操作,因为我已经准备好通过出现打印对话框的自动确认来管理它;但是 - 请 - 在启动您希望我作为受控操作进行管理的打印操作之前,请告知我您的意图。”这类消息(定期发送,通常每分钟多次)实际上实现了一种“服务心跳”,并让应用程序知道服务是否正在运行。显然,Silverlight 应用程序必须在开始受控打印操作之前检查服务的运行状况,并且如果服务未运行,则必须阻止该操作。当然,对于正常的开放打印操作,服务是否存在以及其运行状况并不重要。
我在实现这种通过隔离存储的通信时必须解决的一个问题是物理隔离存储位置本身;事实上,即使任何 Silverlight 应用程序的隔离存储的根路径很容易在用户配置文件子树中找到,路径为:Windows XP 为 SYSTEMDRIVE\Documents and Settings\USER\Local Settings\Application Data\Microsoft\Silverlight\is,Windows Vista 和 7 为 SYSTEMDRIVE\Users\USER\AppData\LocalLow\Microsoft\Silverlight\is,但事先无法确定特定 Silverlight 应用程序的确切物理路径。
为了解决这个问题,我决定让服务通过给定根路径和 Silverlight 应用程序的名称(以 XAP 文件 URL 的形式表示)自行查找隔离存储的特定路径。事实上,通过递归检查隔离存储的文件系统子树,并分析名为 id.dat 的文件的内容,可以找到特定 Silverlight 应用程序的确切隔离存储物理路径。要使此搜索正常工作,唯一条件是应用程序应该已经创建了自己的隔离存储存储库(即使 - 以防万一 - 在之前的会话中)。
因为在第一次启动时(或在用户故意删除隔离存储后),Silverlight 应用程序尚未创建其隔离存储存储库(因为这将在第一次读/写操作时发生),为了确保它尽早创建,我必须在应用程序启动时强制创建它。
当拦截应用程序知道隔离存储的特定路径后,就可以通过简单地读取和写入具有预定义、约定名称的简单文件来实现 Silverlight 应用程序和拦截应用程序之间的通信。
拦截服务发送的消息(即“服务心跳”)将简单地通过一个名为 ServiceHeartBeat.txt 的哨兵文本文件来实现,该文件包含一个时间戳(客户端 PC 上的当前日期和时间)。该文件将定期(例如,每 8 秒)用更新的日期和时间重写。应用程序发送的消息(我将称之为“应用程序信号”,因为它们信号了一个即将发生的打印操作)也将通过一个名为 ApplicationSignal.txt 的哨兵文本文件来实现,该文件包含一个时间戳(客户端 PC 上的当前日期和时间)。每次 Silverlight 应用程序的用户请求受控打印操作时,该文件将用更新的日期和时间重写。
以下时序/活动图(时间从上到下运行)应能阐明应用程序和拦截服务(即我们的本地可执行文件)之间的交互,前提是后者在 Silverlight 应用程序启动时已在运行。
如前所述,Silverlight 的任何受控打印操作都应仅在拦截服务实际运行时才发生。这就是为什么 Silverlight 应用程序必须在调用实际打印之前检查服务心跳是否存在,并且必须阻止打印操作,如果拦截代码未运行,如下图所示。
当然,如果 Silverlight 应用程序需要提供开放打印操作,则无需对拦截代码的运行状态进行任何检查(因为我们不想在任何情况下阻止打印操作,无论服务运行还是停止)。此外,应用程序不应向拦截可执行文件发送即将打印的信号(因为它不是受控打印操作,并且我们不希望打印对话框自动确认)。然后,拦截代码(如果正在运行)将不会自动确认打印对话框(即使它仍将其检测为刚打开并具有焦点的打印窗口)。
同样,如果用户从其他应用程序启动打印操作或使用浏览器打印功能,正如预期的那样,拦截服务(如果正在运行)将不会自动确认打印对话框(因为没有应用程序发送了要使用自动确认进行管理的即将发生的打印操作信号)。
实现细节
现在让我们看看我编写的代码来实现我上述的解释。
源代码下载由一个 Visual Studio 2010 解决方案组成,其中包含两个项目:
- PrintControlled:一个 Silverlight 项目,模拟了一个需要提供受控(即自动确认)打印操作的应用程序;
- ControlledPrintService:一个 Windows Forms 项目,将“拦截服务”实现为一个标准的 C# 应用程序。
Silverlight 应用程序(PrintControlled 项目)
如前所述,需要受控打印操作的 Silverlight 应用程序必须在启动时初始化其隔离存储,以便启用与拦截服务的通信。此步骤由 InitializeIsolatedStorage
方法执行,该方法又简单地调用 IsolatedStorageFile.GetUserStoreForApplication
方法。从此刻起,拦截服务将能够识别应用程序的确切隔离存储路径并与之交互。
当应用程序需要开始受控打印操作时,它将:
- 将自己的信号哨兵文件写入隔离存储,以指示它将调用打印操作(参见
WriteApplicationSignal
方法); - 检查 ControlledPrintService 应用程序是否已启动并正在运行,方法是验证服务心跳是否存在(参见
ControlledPrintServiceIsRunning
方法)。
如果服务心跳存在且“足够近期”(参见 ServiceHeartBeatTolerance
),则打印操作可以继续。
以下代码摘录实现了这些概念:
private void btnPrint_Click(object sender, RoutedEventArgs e)
{
// Write on the Isolated Storage the application
// signal sentinel file, in order to signal
// that the application is going to invoke a Print operation
WriteApplicationSignal();
// Check if the ControlledPrint service
// is up&running by verifying the presence
// of the service heart beat
// If the service heart beat is absent
// or not "recent" (see ServiceHeartBeatTolerance),
// then the print operation is prevented
if (!ControlledPrintServiceIsRunning())
{
MessageBox.Show("Printing is not enabled at this moment.\n
The ControlledPrint service is not running.");
return;
}
// Actually execute the print operation
pd = new PrintDocument();
...
pd.Print("");
}
// Execute the writing of the application signal
// in order to signal that the application
// is going to invoke a print operation
// The signal is a simple text file containing a timestamp
private void WriteApplicationSignal()
{
try
{
IsolatedStorageFile ISfile = IsolatedStorageFile.GetUserStoreForApplication();
IsolatedStorageFileStream fs =
ISfile.OpenFile(ApplicationSignalFilename, FileMode.Create);
StreamWriter sw = new StreamWriter(fs);
sw.Write(DateTime.Now.ToString("yyyyMMddHHmmss", new CultureInfo("en-us")));
sw.Close();
fs.Close();
}
catch { }
}
// Check if the ControlledPrint service heart beat is there, and if it is recent enough
// (see ServiceHeartBeatTolerance)
private bool ControlledPrintServiceIsRunning()
{
try
{
IsolatedStorageFile ISfile = IsolatedStorageFile.GetUserStoreForApplication();
if (!ISfile.FileExists(ServiceHeartBeatFilename))
return false;
IsolatedStorageFileStream fs =
ISfile.OpenFile(ServiceHeartBeatFilename, FileMode.Open);
StreamReader sr = new StreamReader(fs);
DateTime ServiceHeartBeatDateTime;
try
{
ServiceHeartBeatDateTime = DateTime.ParseExact(sr.ReadToEnd(), "yyyyMMddHHmmss",
new CultureInfo("en-us"));
}
catch
{
ServiceHeartBeatDateTime = DateTime.MinValue;
}
sr.Close();
fs.Close();
if (ServiceHeartBeatDateTime.AddSeconds(ServiceHeartBeatTolerance) >= DateTime.Now)
return true; // the ControlledPrint service is considered up & running
else
return false; // the ControlledPrint service seems to be asleep or dead
}
catch
{
return false;
}
}
Silverlight 应用程序将需要一些配置参数(目前硬编码),例如用于调整用于评估检测到的服务心跳是否“足够近期”的容差期。
// Filename for the application "signal"
// sentinel file, to be written in the Isolated Storage of the
// application in order to signal that
// the application is going to invoke a Print operation
string ApplicationSignalFilename = "ApplicationSignal.txt";
// Filename for the service "heart beat" sentinel file,
// to be looked for in the Isolated Storage of the
// application in order to check if the ControlledPrint service is up&running
string ServiceHeartBeatFilename = "ServiceHeartBeat.txt";
// Tolerance (in sec.) to be used in order to decide
// if the detected service heart beat is recent enough
// It should be set to a value greather than ServiceHeartBeatTimerInterval
// in the ControlledPrint service
int ServiceHeartBeatTolerance = 10;
拦截应用程序(ControlledPrintService 项目)
首先,拦截应用程序必须挂钩 WM_SETFOCUS
Windows 事件,以便准备拦截和识别任何焦点窗口。此外,在启动拦截代码时,还必须启动心跳机制(在这里,这很简单地使用了一个 Timer
):在心跳定时器的每次“Tick”时,服务将尝试在应用程序的隔离存储上写入哨兵心跳文本文件;如果应用程序的隔离存储路径尚不清楚,则会在写入服务心跳之前检查文件系统以查找它。
private void Form1_Load(object sender, EventArgs e)
{
// Hook the WM_SETFOCUS Windows event
Automation.AddAutomationFocusChangedEventHandler(
new AutomationFocusChangedEventHandler(Focus_Changed));
// Setup the timer for the ControlledPrint service heart beat
// This heart beat signals to a designated Silverlight
// application that the service is up&running
// (by writing a sentinel file to the Isolated Storage
// of that Silverlight application)
timServiceHeartBeat.Interval = ServiceHeartBeatTimerInterval * 1000;
timServiceHeartBeat_Tick(null, null); // Force a first service heart beat
timServiceHeartBeat.Enabled = true; // Start the service heart beat timer
}
// The following variable will hold the path of the Isolated Storage
// for the designated Silverlight application (detected at runtime
// by looking for the SilverlightApplicationURL in the general
// Isolation Storage, see ISrootPath)
string ISapplicationFullPath = "";
// Execute the writing of the service heart beat in order to give evidence
// to the fact that the ControlledPrint service is up&running
private void timServiceHeartBeat_Tick(object sender, EventArgs e)
{
if (ISapplicationFullPath == "")
{
// If the ISapplicationFullPath variable is still empty,
// discover the Isolated Storage full path
// for the designated Silverlight application
ISapplicationFullPath =
SearchISfullPath(ISrootPath, SilverlightApplicationURL);
}
if (ISapplicationFullPath != "")
WriteServiceHeartBeat();
}
// Execute the writing of the service heart beat in order to signal
// that the ControlledPrint service is up&running
// The heart beat is a simple text file containing a timestamp
private void WriteServiceHeartBeat()
{
try
{
FileStream fs = File.Open(ISapplicationFullPath + @"\" +
ServiceHeartBeatFilename, FileMode.Create);
StreamWriter sw = new StreamWriter(fs);
sw.Write(DateTime.Now.ToString("yyyyMMddHHmmss",
new CultureInfo("en-us")));
sw.Close();
fs.Close();
}
catch { }
}
SearchISfullPath
方法简单地执行以下操作:它从给定路径(通常是隔离存储根目录)开始递归遍历文件系统层次结构,以查找指定 Silverlight 应用程序的隔离存储完整路径(给定其 XAP 的 URL SilverlightApplicationURL
)。
private string SearchISfullPath(string path, string SilverlightApplicationURL)
{
string[] files = Directory.GetFiles(path);
foreach (string f in files)
if (Path.GetFileName(f) == "id.dat")
{
string SLapp = ReadSLappFromIdDatFile(f);
if (SLapp.ToUpper() == SilverlightApplicationURL.ToUpper())
return Path.GetDirectoryName(f) + @"\f";
}
string[] dirs = Directory.GetDirectories(path);
foreach (string d in dirs)
{
string res = SearchISfullPath(d, SilverlightApplicationURL);
if (res != "")
return res;
}
return "";
}
// Retrieve the Silverlight application URL
// from an "id.dat" Isolated Storage file
private string ReadSLappFromIdDatFile(string IdDatFilename)
{
FileStream fs = File.Open(IdDatFilename, FileMode.Open);
StreamReader sr = new StreamReader(fs);
string res = sr.ReadToEnd();
sr.Close();
fs.Close();
return res;
}
拦截服务其余的工作是在发生 FocusChanged
事件时完成的。拦截服务必须:检测焦点窗口是否为标题为“Print”的窗口;查找窗口上的“Print”按钮;检查应用程序信号,以查看它是否“足够近期”(参见 ApplicationSignalTolerance
);调用 Button
的“点击”。
// Manage the WM_SETFOCUS Windows event happened
private void Focus_Changed(object src, AutomationFocusChangedEventArgs e)
{
// Retrieve the control that currently has the focus
AutomationElement FocusedElement = src as AutomationElement;
try
{
// Traverse the control tree upwards in order to find a window titled "Print"
while (FocusedElement != null && FocusedElement != AutomationElement.RootElement)
{
// Check if the window title is "Print"
string WindowTitle = FocusedElement.Current.Name;
if (string.Equals("Print", WindowTitle, StringComparison.InvariantCultureIgnoreCase))
{
if ((IntPtr)FocusedElement.Current.NativeWindowHandle != IntPtr.Zero)
// A window titled "Print" has been found
PrintDialogReceivedFocus(FocusedElement);
break;
}
// Retrieve the parent control
FocusedElement = TreeWalker.ControlViewWalker.GetParent(FocusedElement);
}
}
catch
{ }
}
private void PrintDialogReceivedFocus(AutomationElement PrintWindow)
{
if (PrintWindow == null && PrintWindow == AutomationElement.RootElement)
return;
AutomationElement ParentWindow = TreeWalker.ControlViewWalker.GetParent(PrintWindow);
if (ParentWindow == null)
return;
// Identify the "Print" button,
// that will be used to auto-confirm the printing operation
AutomationElement PrintButton = null;
//PrintButton = FindButtonByName(PrintWindow, "Print");
// For testing/debugging purposes, use the "Cancel" button
// instead of the "Print" button
PrintButton = FindButtonByName(PrintWindow, "Cancel");
if (PrintButton != null)
{
object objPattern;
// Prepare the invocation for the button
if (PrintButton.TryGetCurrentPattern(InvokePattern.Pattern, out objPattern))
{
if (objPattern != null)
{
// Check if the application signal is recent enough;
// if it is "old", then do nothing
if (!CheckApplicationSignal())
return;
// Actually execute the invocation on the button
(objPattern as InvokePattern).Invoke();
//System.Diagnostics.Debug.WriteLine("PrintDialogOpened: Closed");
}
}
}
}
// Traverse the control tree downwards in order to find a button with the specified name
private AutomationElement FindButtonByName(AutomationElement StartingElement,
string ButtonName)
{
try
{
AutomationElement ElementNode =
TreeWalker.ControlViewWalker.GetFirstChild(StartingElement);
while (ElementNode != null)
{
try
{
if (ElementNode.Current.ControlType.ProgrammaticName == "ControlType.Button")
if (ElementNode.Current.Name == ButtonName)
return ElementNode;
}
catch { }
AutomationElement ButtonInChildren = FindButtonByName(ElementNode, ButtonName);
if (ButtonInChildren != null)
return ButtonInChildren;
ElementNode = TreeWalker.ControlViewWalker.GetNextSibling(ElementNode);
}
return null;
}
catch
{
return null;
}
}
// Check if the application signal is there, and if it is
// recent enough (see ApplicationSignalTolerance)
private bool CheckApplicationSignal()
{
try
{
if (ISapplicationFullPath == "")
return false;
if (!File.Exists(ISapplicationFullPath + @"\" + ApplicationSignalFilename))
return false;
FileStream fs = File.Open(ISapplicationFullPath + @"\" +
ApplicationSignalFilename, FileMode.Open);
StreamReader sr = new StreamReader(fs);
DateTime ApplicationSignalDateTime;
try
{
ApplicationSignalDateTime =
DateTime.ParseExact(sr.ReadToEnd(), "yyyyMMddHHmmss",
new CultureInfo("en-us"));
}
catch
{
ApplicationSignalDateTime = DateTime.MinValue;
}
sr.Close();
fs.Close();
if (ApplicationSignalDateTime.AddSeconds(ApplicationSignalTolerance) >= DateTime.Now)
return true; // the Silverlight application is considered up & running
else
return false; // the Silverlight application seems to be asleep, dead,
// or not having called the printing function recently
}
catch
{
return false;
}
}
拦截服务将需要一些配置参数(为简单起见,目前已硬编码),使其知道客户端机器上隔离存储的根路径,让它知道要用于通信的 Silverlight 应用程序的名称,调整服务心跳频率,调整用于评估检测到的应用程序信号是否“足够近期”的容差期。此外,还需要根据 Silverlight 应用程序的配置来配置通过隔离存储进行通信时使用的文件名。
// Root path of the general Silverlight Isolated Storage;
// it depends from the current user
// and could vary depending on the operating system.
// For Windows XP it is in the form of:
// <SYSTEMDRIVE>\Documents and Settings\<user>\Local
// Settings\Application Data\Microsoft\Silverlight\is
// For Windows Vista / Seven:
// <SYSTEMDRIVE>\Users\<user>\AppData\LocalLow\Microsoft\Silverlight\is
string ISrootPath =
@"C:\Users\alberto.venditti\AppData\LocalLow\Microsoft\Silverlight\is";
// URL of the XAP file of the Silverlight Application designated
// to interact with the ControlledPrint service
string SilverlightApplicationURL =
"https://:47787/CLIENTBIN/PRINTCONTROLLED.XAP";
// Filename for the service "heart beat" sentinel file, to be written
// in the Isolated Storage of the designated Silverlight application
// in order to signal that the ControlledPrint service is up&running
string ServiceHeartBeatFilename = "ServiceHeartBeat.txt";
// Interval (in seconds) for the service heart beat updates
int ServiceHeartBeatTimerInterval = 8;
// Filename for the application "signal" sentinel file,
// to be looked for in the Isolated Storage
// of the designated Silverlight application
// in order to check if the application recently invoked a Print operation
string ApplicationSignalFilename = "ApplicationSignal.txt";
// Tolerance (in seconds) to be used in order to decide
// if the detected application signal is recent enough
int ApplicationSignalTolerance = 2;
关注点
此解决方案探索了一种奇特的方式来实现标准本地 .NET 应用程序和浏览器中运行的 Silverlight 应用程序之间的通信,通过对隔离存储的原始使用。尽管这种机制在某种程度上可能被认为是不优雅的,但它确实有效,并且在我的场景中代表了一个具体的解决方案。
除了这种特殊的 Silverlight 隔离存储用法之外,我还发现使用 Microsoft UI Automation 框架来获取有关客户端计算机上发生的 UI 事件的信息并与该 UI 交互发送输入到控件很有趣。
警告
如导言中所述,这里提出的解决方案仅适用于某些特定场景(通常是:受控的业务线或企业环境):请注意这一点以及它暴露的后续限制。
如果在生产环境中使用,这里解释的握手通信(基于读写非常简单的文本文件)应该肯定会增加一些安全概念(例如:加密实际文本文件的内容),以使用户难以篡改 Silverlight 应用程序和拦截服务之间交换的信息。
正如一位读者及时指出的那样,拦截代码方法(目前在一个标准的 Windows Forms 应用程序中实现)如果在 Windows 服务中打包(由于 Windows 服务的非交互性),则可能不起作用。