E’ del luglio 2009 il post “Printing in Silverlight 3, yes we can!” in cui si spiega come poter stampare con silverlight 3. Il post sfrutta le potenziale della classe WriteableBitmap e l’Encoder PNG di Joe Stegman per generare un immagine al volo a partire da un elemento dell’interfaccia grafica per salvare poi l’immagine su disco che l’utente può poi stampare. Ovviamemte il post ora con SL4 è del tutto obsoleto in quanto è stato introdotto System.Windows.Printing in cui PrintDocument permette di inviare in stampa un qualsiasi oggetto dell’interfaccia.
Nel mio caso devo introdurre la stampa in un progetto Silverlight 3 per una soluzione Visual Studio 2008... ovviamente al momento la migrazione non è possibile e in attesa della migrazione ho trovato utile il post in questione anche se il risultato finale non sarebbe per nulla piaciuto all’utente finale (The result is a generated PNG file, ready to print!)... creare un immagine da stampare a parte non è facilmente accettabile da utenti non tecnici, e la cosa è facilmente capibile.
Ecco come mi sono mosso per migliorare la fase di stampa dell’immagine in modo da rendere l’operazione più integrata.
1) implemento un servizio web wcf per l’upload dell’immagine. L’immagine verrà mantenuta nella Cache per 5 minuti.
[OperationContract]
public string UploadImage(string id, byte[] data)
{
HttpContext.Current.Cache.Add(id,
data,
null,
DateTime.Now.AddMinutes(5),
Cache.NoSlidingExpiration,
CacheItemPriority.Normal,
null);
return id;
}
2) implemento un web service wcf REST per il download dell’immagine
[OperationContract]
[WebInvoke(Method = "GET", UriTemplate = "/GetImage/{id}")]
public Stream GetImage(string id)
{
var image = (byte[])HttpContext.Current.Cache[id];
if (image == null)
{
WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
WebOperationContext.Current.OutgoingResponse.SuppressEntityBody = true;
return null;
}
else
{
WebOperationContext.Current.OutgoingResponse.ContentType = "image/png";
return new MemoryStream(image);
}
}
3) definisco due div nella pagina: normalDiv in cui sposto tutto il contenuto non stampabile della pagina e printableDiv che di default sarà vuoto. Lo snippet di codice che segue è quello che ho applicato ad una webform classica senza MasterPage.
<form id="form1" runat="server" style="height: 100%">
<div id="printableDiv">
</div>
<div id="normalDiv">
//TODO: inserire qui il contenuto web form
</div>
</form>
4) lo style è ora fondamentale!
<style type="text/css">
...
#printableDiv
{
display: none;
}
@media print
{
#normalDiv
{
display: none;
}
#printableDiv
{
display: block;
}
}
</style>
5) In silverlight definisco un metodo che incapsula il codice descritto nel post “Printing in Silverlight 3, yes we can!” in modo da avere un metodo che mi permetta di traformare un qualsiasi FrameworkElement in un array di byte di immagine Png. La cosa renderà più semplice la lettura del codice.
class Helper
{
public static byte[] ToImagePng(FrameworkElement el)
{
WriteableBitmap bitmap = new WriteableBitmap(el, null);
EditableImage imageData = new EditableImage((int)el.ActualWidth, (int)el.ActualHeight);
for (int y = 0; y < bitmap.PixelHeight; ++y)
{
for (int x = 0; x < bitmap.PixelWidth; ++x)
{
int pixel = bitmap.Pixels[bitmap.PixelWidth * y + x];
imageData.SetPixel(x, y,
(byte)((pixel >> 16) & 0xFF),
(byte)((pixel >> 8) & 0xFF),
(byte)(pixel & 0xFF),
(byte)((pixel >> 24) & 0xFF)
);
}
}
MemoryStream mem = new MemoryStream();
Helper.CopyStreamContents(imageData.GetStream(), mem);
mem.Flush();
return mem.ToArray();
}
}
6) Tutti i mattoni sono pronti! Ecco come mi muovo per integrare il tutto. Nel mio caso decido di definire che il bottone di stampa sarà nella pagina html. Il bottone richiamerà la funzione javascript “printDiv();”. La funzione javascript richiamerà uno ScriptableMember definito nell’applicazione silverlight “BeginPrint”. Nel metodo l’oggetto “slobj” si intende l’object che include la nostra applicazione silverlight. Nessun problema se venisse usato un bottone nell’applicazione silverlight, nel caso l’evento di click implementerà quanto definito nel punto (7).
function printDiv() {
var control = document.getElementById("slobj");
control.Content.myObj.BeginPrint();
}
7) Implemento il metodo BeginPrint.
7.1) genero l’immagine a partire da se stesso (lo usercontrol) usando il metodo definito nel punto (5)
7.2) genero un identificativo univoco per l’immagine
7.3) invoco il metodo UploadImage, definito nel punto (1), per caricare l’immagine appena generata.
7.4) al termine del caricamento invoco la funzione javascript “beginPrintCallback” passando come parametro l’url del servizio REST per il download dell’immagine, definito nel punto (2)
[ScriptableMember]
public void BeginPrint()
{
byte[] image = Helper.ToImagePng(this);
string id = Guid.NewGuid().ToString("N");
RemoteServiceClient client = new RemoteServiceClient();
client.UploadImageCompleted+=(s, e) => {
string imageToPrintUrl = "http://localhost/myapp/myrestservice.srv/GetImage/"+id;
HtmlPage.Window.Invoke("beginPrintCallback", imageToPrintUrl);
};
client.UploadImageAsync(id, image);
}
8) Definisco la funzione javascript "beginPrintCallback". Il metodo riceve in input l’url dell’immagine remota appena caricata e inserisce nel contenuto html del div stampabile un oggetto img... infine invoca la stampa!
function beginPrintCallback(url) {
var printableDiv = document.getElementById("printableDiv");
printableDiv.innerHTML = "<img src='" + url + "' alt='Report'>";
window.print();
}
9) ... e voilà il gioco è fatto! Premo il bottone, il browser mi chiede di scegliere la stampate e su carta vedo stampato il contenuto della pagina della mia applicazione silverlight!
In teoria sarebbe possibile un secondo metodo che non coivolge web service. Le RFC2397 definisce lo schema "data" come sorgente dell’oggetto IMG che permette di incorporare l’immagine nella pagina usando una stringa base64. In questo caso l’immagine ricavata dall’applicazione silverlight verrebbe convertita in stringa base64 e utilizzata direttamente senza neccesità di upload sul server. Seguono il metodo da implementare nell'applicazione silverlight e il metodo javascript da implementare nella pagina e da richiamare nel bottone in sostituzione a quello definito nel punto (6).
[ScriptableMember]
public string PrintB64()
{
byte[] image = Helper.ToImagePng(this);
string b64 = Convert.ToBase64String(image);
return "data:image/png;base64," + b64;
}
function printDiv64() {
var control = document.getElementById("slobj");
var srcb64 = control.Content.myObj.PrintB64();
var printableDiv = document.getElementById("printableDiv");
printableDiv.innerHTML = "<img src='" + srcb64 + "' alt='Report'>";
window.print();
}
Purtroppo questa soluzione è limitata a immagini di dimensioni ridotte. Infatti nelle stesse RFC si legge. The "data:" URL scheme is only useful for short values. Note that some applications that use URLs may impose a length limit; for example, URLs embedded within <A> anchors in HTML have a length limit determined by the SGML declaration for HTML [RFC1866]. The LITLEN (1024) limits the number of characters which can appear in a single"
La soluzione è davvero totalmente obsoleta? Si, se si considera che il SL3 è destinato a essere velocemente rimpiazzato ovunque da SL4; No, se si considerano i casi di stampe miste in cui parte vuole essere generata da applicazione SL e parte da pagina web.
oO0( Printing in SL3, yes we can! )
posted @ lunedì 23 agosto 2010 01:52