Introduction
As you may know, Silverlight 4 introduced the support for a basic printing functionality through its Printing API (based on the PrintDocument
class). This API allows you to send to a printer (in a bitmap-based way) your application screen, a portion of it, or an alternative custom visual-tree properly constructed.
In Silverlight, the printing support is limited in some ways for security reasons; for example:
- the print operation must be user-initiated (that is: it is only permitted in the context of a handler that handles a user event);
- the "Print" dialog box (that is the window depicted below, where the user can select the printer's settings and then click "Print" to continue, or "Cancel" to cancel the print operation) is always shown.
Especially the latter limitation is quite annoying, specifically when the user is expected to confirm all the default settings proposed by the Print dialog and to simply initiate the printing operation by clicking on the "Print" button. Not only could this represent a click-once-more bother, but in some situations, it could represent a real issue.
For example, think about a controlled line-of-business scenario where the Silverlight application is used for a ticket counter: in order to deliver a ticket to the buyer, the user is expected to type in some data and then to print a ticket that should be printed once (and only once) to the default printer. Of course, in this scenario, giving the user the ability to print more copies or to change the target printer is unwanted and to be avoided. Then the mandatory appearance of the Print dialog (as forced by Silverlight) looks like a big limitation. Moreover, as a programmer, you have no ways to know if the user changed some details in the Print dialog's options before confirming the print operation; so not only are you unable to prevent him/her from making those modifications but also you can't actually know if some modifications occurred (in fact, neither the BeginPrintEventArgs
nor the PrintPageEventArgs
do carry this information).
Because I found myself exactly in this situation, I tried to find out a solution to guarantee the printing operation from Silverlight occurs in a silent way, that is: having the user initiating the print operation from the application without the need for him/her to interact with the Print dialog, and basically having the default Print dialog's options all auto-confirmed without the user intervention.
This way:
- I'm sure that the application will print on the printer currently configured as the default printer;
- I'm sure that only one copy will be printed after the user initiates the print operation;
- further reprinting (in case needed because of paper jams or other issues) will be driven completely by the application logic (for example, asking for further confirmations or further passwords and privileges for reprinting) and properly logged at the application level.
The solution I'm proposing here (be aware of that!) is suitable only in some specific scenarios (typically: controlled line-of-business or enterprise situations) because:
- it is subject to some limitations (it runs only on Windows-based PCs, even if Silverlight has a wider compatibility);
- it leverages the Silverlight Isolated Storage, so the user must not disable it on his client;
- it requires control over the final user's machine (where a custom executable has to be running to enable such kind of silent printing from Silverlight).
The basic idea
To achieve silent printing from Silverlight, on the assumption that the Print dialog showing is unavoidable, my basic idea consists in intercepting that dialog appearance and immediately simulating a user confirmation of the proposed default settings by a mimic click on the "Print" button of the same dialog.
In order to intercept the opening of a particular window, we have to work at the Operating System level, interacting with the messages and events the Windows OS manages when dealing with windows. This requires running a fully trusted code on the client machine, something you can achieve by running a native .NET executable or by leveraging COM Interop with Silverlight (but only if you are choosing a solution involving a fully trusted out-of-browser Silverlight application).
For the specific scenario I had to deal with (where I had full control over the installation and setup of the PCs targeted to operate at the ticket counter desks), I chose the option to have a custom executable installed on the client machine, acting as an "intercepting service". It performs the task of monitoring any window being opened, checks if it is a Print dialog and - if it actually is - automatically confirms the proposed options by simulating a click on the "Print" button.
Intercepting the Print dialog
In order to monitor any window being opened, I decided to hook into the system event WM_SETFOCUS
(fired when any window receives the user input focus). To accomplish this, I used the Microsoft UI Automation framework (available from .NET Framework 3.5 onwards) and specifically the UIAutomationClient.dll and UIAutomationTypes.dll assemblies (containing a set of types for managed code that enables UI Automation client applications to obtain information about the UI and to send input to controls).
The code I prepared for the intercepting service (actually implemented in a simple Windows Forms application in the source code download), basically does the following:
- it hooks into the
WM_SETFOCUS
Windows event;
- when managing the
WM_SETFOCUS
Windows event happens:
- it retrieves the control that currently has the focus;
- it traverses the controls tree upwards in order to find a window titled "Print";
- when a window titled "Print" has been found:
- it identifies the "Print" button;
- it invokes a "click" on it in order to auto-confirm the printing operation.
In fact, while this intercepting code is running on the client machine, if I navigate to my Silverlight application and I use it to print something, the Print dialog appears for a very short moment (it just "blinks") and it is immediately closed by the auto-confirmation code, without giving me any chance to interact with it or to modify any of the proposed settings. So, this seems to work! But we are only half way, because of these main problems:
- the intercepting code just looks for a focused window titled "Print", but currently there is no check about the owner of the Print dialog; so, the auto-confirmation will work for the Print dialog initiated by any caller (not only by the designated Silverlight application); and this is not good, because if I'm going to print a web page through the standard Print facility of the browser or I want to print a document through any application like Microsoft Word, if the intercepting code is running, then the auto-confirmation occurs, but I don't want it;
- if I want, in the same Silverlight application, to provide some printing features "controlled" in the described way (i.e., with a Print dialog suppressed by auto-confirmation) and other printing features "left open" to the normal user intervention through the standard Print dialog, at the moment, I can't achieve that;
- if the intercepting code is not running (because it crashed or didn't start), the Silverlight application behaves normally, showing me the Print dialog and allowing me to change everything in it; I'd like, instead, to have the print functionality prevented in such a situation, then forcing the intercepting code to be up'n'running as a condition to have the Silverlight application be able to execute the printing operations I want to keep "controlled".
If problem #1 could be solved by implementing a check on the identity of the application currently owning the Print dialog window detected as just opened and focused, it's clear that problems #2 and #3 require a sort of communication between the intercepting application and the Silverlight application. In fact, in order to distinguish printing operations that should be auto-confirmed from printing operations that shouldn't, the Silverlight application should in some way differentiate the calls to the printing API, and the intercepting code should be able to operate this distinction consequently. Moreover, it should signal in some way to the Silverlight application the fact it is running, in order to let the Silverlight application decide if a controlled printing operation has to be initiated (because the intercepting code is running and able to manage the auto-confirmation) or if it has to be prevented (because the intercepting code appears not to be running, so it wouldn't able to manage the auto-confirmation - and in this case, we prefer to inhibit the printing operation at all).
Letting Silverlight communicate with the intercepting application
Is a browser-hosted Silverlight application and a local executable able to communicate? My first answer to this question has been: "of course not, because of the Silverlight's sandbox and the limitations of a browser-hosted application, that (in Silverlight 4) cannot be fully trusted!"
But an easy way to let them communicate does exist, and I found it in the Silverlight Isolated Storage. Think about this: the Isolated Storage for a Silverlight application is a files structure hosted on the client PC's file system, inside the user's profile folder subtree; if a Windows executable is executed with proper permissions and proper file system access, it is certainly able to read and write on the same files and folders used by the Silverlight application's Isolated Storage... or not?
So I decided to have the Silverlight application and the local executable implementing the "intercepting service" communicate through Isolated Storage. Basically, I considered Isolated Storage as a common place to deliver and pick-up messages (a sort of shared mailbox):
- Messages delivered by the Silverlight application are actually carrying this conceptual content: "My dear intercepting service, I'm about to start a controlled printing operation and I would like you to manage it by issuing an auto-confirmation on the Print dialog that is going to appear". Such kind of messages (sent immediately before invoking a
PrintDocument.Print
) are to be used only for controlled printing operations and obviously are to be omitted for left-open printing operations (then solving problem #2 above, that was: differentiating and distinguishing between controlled and left-open print operations). Moreover, because it is the Silverlight application that is in charge of signaling its intention to have a print operation managed by the intercepting service, this also solves problem #1; in fact, any other print operation (for example, initiated by another application) will generate a Print dialog window that - even if detected by the service as a just opened and focused "Print" window - will not be auto-confirmed at all.
- The conceptual content carried by messages delivered by the intercepting service is: "My dear Silverlight application, I'm up, running, and in good health: if you want, you can initiate a controlled printing operation, because I'm ready to manage it by issuing an auto-confirmation on the Print dialog that will appear if you do so; but - please - just before initiating a printing operation that you want me to manage as controlled, tell me about your intention". Such kind of messages (sent on a recurring basis, typically more times in a minute) actually implement a sort of "service heart beat" and let the application know if the service is running or not. Obviously, the Silverlight application has to check the service's health before starting a controlled printing operation, and it has to prevent the operation if the service is not found alive. Of course for normal, left-open printing operations, the existence and health of the service are not significant.
A problem I had to solve while implementing this communication through Isolated Storage was about the physical Isolated Storage location itself; in fact, even if the root path for the Isolated Storage of any Silverlight application can be easily found in the user's profile subtree in a directory path like: SYSTEMDRIVE\Documents and Settings\USER\Local Settings\Application Data\Microsoft\Silverlight\is for Windows XP and SYSTEMDRIVE\Users\USER\AppData\LocalLow\Microsoft\Silverlight\is for Windows Vista and 7, it is not easy to say in advance the exact physical path of a specific Silverlight application.
In order to solve this issue, I decided to let the service find by itself the Isolated Storage specific path, given the root path and the name of the Silverlight application (expressed in the form of a URL of the XAP file). In fact, by recursively inspecting the Isolated Storage file system subtree, and analyzing the content of the file named id.dat, it is possible to find the exact Isolated Storage physical path for a specific Silverlight application. The only condition to have this search working is that the application should have already created its own Isolated Storage repository (even if - in case - during a preceding working session).
Because at its very first startup (or after the user deleted the Isolated Storage deliberately) the Silverlight application has not yet created its Isolated Storage repository (because this will happen upon the first read/write operation on it), to be sure it is created early, I have to force its creation just when the application starts up.
When the Isolated Storage specific path is known by the intercepting application, the communication between the Silverlight application and the intercepting application can occur, by simply reading and writing simple files having predefined, conventional names.
The messages sent by the intercepting service (namely "service heart beats") will be implemented simply by a sentinel text file named ServiceHeartBeat.txt containing a timestamp (the current date and time on the client PC). This file will be rewritten with the updated date and time on a recurring basis (let's say, each 8 seconds). The messages sent by the application (that I'll call "application signals", because they signal for an imminent printing operation) will also be implemented by a sentinel text file named ApplicationSignal.txt containing a timestamp (the current date and time on the client PC). This file will be rewritten with the updated date and time each time a controlled print operation is requested by the Silverlight application's user.
The following time/activity diagram (time running up-down) should clarify the interaction between the application and the intercepting service (that is: our local executable) in the hypothesis that the latter is already running when the Silverlight application starts:
As stated before, any controlled printing operation from Silverlight should occur if and only if the intercepting service is actually running. That's why the Silverlight application must check for the presence of the service heart beat before invoking the actual print, and must prevent the printing operation if the intercepting code is not running, as shown in the following diagram:
Of course, if the Silverlight application has to provide a left-open print operation, no checks are to be done on the intercepting code running status (because we don't want to prevent the printing operation in any case, both being the service running or stopped). Besides, the application shouldn't signal the imminent print operation to the intercepting executable (because it's not a controlled print operation, and we don't want the Print dialog auto-confirmed). Then the intercepting code (if running) will not auto-confirm the Print dialog (even if it still detects it as a just opened and focused Print window):
And also if the user starts a printing operation from other applications or uses the browser print facility, as anticipated, the intercepting service (if running) will not auto-confirm the Print dialog (because no application signaled an imminent printing operation to be managed with auto-confirmation):
Implementation details
Let's now have a look at the code I wrote in order to implement what I explained above.
The source code download is made up of a Visual Studio 2010 solution containing two projects:
- PrintControlled: a Silverlight project simulating an application that needs to provide a controlled (that is: auto-confirmed) printing operation;
- ControlledPrintService: a Windows Forms project implementing the "intercepting service" as a standard C# application.
The Silverlight application (PrintControlled project)
As stated before, the Silverlight application needing a controlled printing operation has to initialize its Isolated Storage on startup, in order to have the communication with the intercepting service enabled. This step is performed by the InitializeIsolatedStorage
method, that in turn simply invokes the IsolatedStorageFile.GetUserStoreForApplication
method. From this moment on, the intercepting service will be able to identify the exact Isolated Storage path of the application and interact with it.
When the application needs to start a controlled printing operation, it will:
- write on the Isolated Storage its own signal sentinel file, in order to signal that it is going to invoke a Print operation (see the
WriteApplicationSignal
method);
- check if the ControlledPrintService application is up'n'running, by verifying the presence of the service heart beat (see the
ControlledPrintServiceIsRunning
method).
If the service heart beat is present and "recent enough" (see ServiceHeartBeatTolerance
), then the printing operation can proceed.
The following code excerpt implements these concepts:
private void btnPrint_Click(object sender, RoutedEventArgs e)
{
WriteApplicationSignal();
if (!ControlledPrintServiceIsRunning())
{
MessageBox.Show("Printing is not enabled at this moment.\n
The ControlledPrint service is not running.");
return;
}
pd = new PrintDocument();
...
pd.Print("");
}
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 { }
}
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; else
return false; }
catch
{
return false;
}
}
The Silverlight application will need some configuration parameters (currently hard-coded), for example to tune the tolerance period to be used in evaluating if a detected service heart beat is "recent enough":
string ApplicationSignalFilename = "ApplicationSignal.txt";
string ServiceHeartBeatFilename = "ServiceHeartBeat.txt";
int ServiceHeartBeatTolerance = 10;
The intercepting application (ControlledPrintService project)
First of all, the intercepting application must hook the WM_SETFOCUS
Windows event, in order to be prepared to intercept and identify any focused window. Also, while starting up the intercepting code, the heart beat mechanism has to be started too (here, this is simply done with a Timer
): at each "Tick" of the heart beat timer, the service will try to write the sentinel heart beat text file on the application's Isolated Storage; if the application's Isolated Storage path is not known yet, the file system is inspected in order to find it before writing the service heart beat.
private void Form1_Load(object sender, EventArgs e)
{
Automation.AddAutomationFocusChangedEventHandler(
new AutomationFocusChangedEventHandler(Focus_Changed));
timServiceHeartBeat.Interval = ServiceHeartBeatTimerInterval * 1000;
timServiceHeartBeat_Tick(null, null); timServiceHeartBeat.Enabled = true; }
string ISapplicationFullPath = "";
private void timServiceHeartBeat_Tick(object sender, EventArgs e)
{
if (ISapplicationFullPath == "")
{
ISapplicationFullPath =
SearchISfullPath(ISrootPath, SilverlightApplicationURL);
}
if (ISapplicationFullPath != "")
WriteServiceHeartBeat();
}
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 { }
}
The SearchISfullPath
method simply does the following: it recursively traverses the file system hierarchy starting from a given path (typically, the Isolated Storage root) to find out the Isolated Storage full path for a designated Silverlight application (given the URL of its XAP in 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 "";
}
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;
}
The remaining work for the intercepting service is done when a FocusChanged
event occurs. The intercepting service has to: detect if the focused window is a window titled "Print"; find the "Print" button on the window; check the application signal to see if it is "recent enough" (see ApplicationSignalTolerance
); invoke the "click" on the Button
:
private void Focus_Changed(object src, AutomationFocusChangedEventArgs e)
{
AutomationElement FocusedElement = src as AutomationElement;
try
{
while (FocusedElement != null && FocusedElement != AutomationElement.RootElement)
{
string WindowTitle = FocusedElement.Current.Name;
if (string.Equals("Print", WindowTitle, StringComparison.InvariantCultureIgnoreCase))
{
if ((IntPtr)FocusedElement.Current.NativeWindowHandle != IntPtr.Zero)
PrintDialogReceivedFocus(FocusedElement);
break;
}
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;
AutomationElement PrintButton = null;
PrintButton = FindButtonByName(PrintWindow, "Cancel");
if (PrintButton != null)
{
object objPattern;
if (PrintButton.TryGetCurrentPattern(InvokePattern.Pattern, out objPattern))
{
if (objPattern != null)
{
if (!CheckApplicationSignal())
return;
(objPattern as InvokePattern).Invoke();
}
}
}
}
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;
}
}
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; else
return false; }
catch
{
return false;
}
}
The intercepting service will need some configuration parameters (currently hard-coded for simplicity), to let it know the root path of the Isolated Storage on the client machine, to let it know the name of the Silverlight application to be elected for communication, to tune the service heart beat frequency, to tune the tolerance period to be used in evaluating if a detected application signal is "recent enough". Also, the file names to be used in the communication through Isolated Storage will need to be configured according to the Silverlight application's configuration.
string ISrootPath =
@"C:\Users\alberto.venditti\AppData\LocalLow\Microsoft\Silverlight\is";
string SilverlightApplicationURL =
"HTTP://LOCALHOST:47787/CLIENTBIN/PRINTCONTROLLED.XAP";
string ServiceHeartBeatFilename = "ServiceHeartBeat.txt";
int ServiceHeartBeatTimerInterval = 8;
string ApplicationSignalFilename = "ApplicationSignal.txt";
int ApplicationSignalTolerance = 2;
Points of interest
This solution explores a queer way to achieve the communication between a standard local .NET application and a Silverlight application running in the browser, through an original use of the Isolated Storage. Even if this mechanism could be considered in some way inelegant, it does actually work and it did represent a concrete solution in my scenario.
Apart from that peculiar use of the Silverlight's Isolated Storage, I found interesting the use of the Microsoft UI Automation framework to obtain information about the UI events occurring on the client machine and to interact with that UI sending input to controls.
Warnings
As stated in the introductory paragraph, the solution presented here is suitable only in some specific scenarios (typically: controlled line-of-business or enterprise situations): be aware of that and of the subsequent limitations it exposes.
If used in a production environment, the hand-shake communication explained here (based on reading and writing very simple text files) should certainly be enriched with some security concepts (such as: encrypting the actual text file's content) in order to make it difficult for the user to tamper the information exchanged by the Silverlight application and the intercepting service.
As promptly noted by a reader, the intercepting code approach (currently implemented in a standard Windows Forms application) could not work if packaged inside a Windows Service (due to the non-interactive nature of Windows services).