Sunday 26 July 2009

Embedded images when exporting Devexpress XtraReport to HTML

I use Devexpress controls with my development projects and came across an issue in Xtrareports whereby when exporting a report to HTML, all the images are referenced as local file system resources which makes it impossible to assign the resulting HTML to the body of an email if it contains images.

The original issue is here if you are interested: http://www.devexpress.com/Support/Center/p/CS17889.aspx

Well I've needed to do this again so thought I'd look for a workaround. Here's what I came up with.

Add this code to the code behind file of the report you want to send as a HTML email.

Private _ImageNumber As Integer ' Counter for the images
Private _HTML As AlternateView ' Holds the View that will contain the completed HTML portion of the message
Private _Images As List(Of LinkedResource) ' Container for the embedded images

Public Function GetMail(ByVal ToAddress As String, ByVal FromAddress As String) As MailMessage

Dim MM As New MailMessage(FromAddress, ToAddress)

' Create the plain text portion of the mail for those who can't view HTML
Dim PlainView As AlternateView = AlternateView.CreateAlternateViewFromString(GetTextString, Nothing, "text/plain")
MM.AlternateViews.Add(PlainView)

' Add the HTML portion of the message
MM.AlternateViews.Add(GetHTMLMailView)

Return MM
End Function

''' <summary>
''' Export the current report as text
''' </summary>
Private Function GetTextString() As String
Using MS As New IO.MemoryStream
ExportToText(MS)
Dim B As Byte() = MS.ToArray()
MS.Flush()
Return System.Text.Encoding.UTF8.GetString(B)
End Using
End Function



''' <summary>
''' Get an alternateview object that contains the HTML portion of the mail (including embedded images)
''' </summary>
Private Function GetHTMLMailView() As AlternateView
Try
' Initialise variables to default
_ImageNumber = 1
_Images = New List(Of LinkedResource)

' Add event handlers that will be used to modify the HTML produced by the export
For Each B As Band In Bands
SetEvents(B, True)
Next

' Create a stream to export into
Using MS As New IO.MemoryStream

' Export to the stream
ExportToHtml(MS)

' Create the HTML view of the message
_HTML = AlternateView.CreateAlternateViewFromString(System.Text.Encoding.UTF8.GetString(MS.ToArray), Nothing, "text/html")

' Add the embedded images
_Images.ForEach(AddressOf _HTML.LinkedResources.Add)

Return _HTML
End Using
Finally
_Images = Nothing

' Remove any event handlers we added earlier
For Each B As Band In Bands
SetEvents(B, False)
Next
End Try
End Function



''' <summary>
''' Recursive function to find all XRPicturebox controls
''' </summary>
Private Sub SetEvents(ByVal C As XRControl, ByVal Attach As Boolean)
If C.HasChildren Then
For Each a As XRControl In C.Controls
If TypeOf (a) Is XRPictureBox Then
If Attach Then
AddHandler a.HtmlItemCreated, AddressOf HTMLImageCreated
Else
RemoveHandler a.HtmlItemCreated, AddressOf HTMLImageCreated
End If
Else
SetEvents(a, Attach)
End If
Next
End If
End Sub



''' <summary>
''' The event handler that will handle the custom HTML generation
''' </summary>
Private Sub HTMLImageCreated(ByVal sender As Object, ByVal e As HtmlEventArgs)
Dim Img = CType(sender, XRPictureBox)

' Exit if the XRPicturebox does not contain an image
If Img.Image Is Nothing Then Return


' Set the custom HTML
' Embedded images are referenced using 'cid:<ImageID>'
e.ContentCell.InnerHtml = String.Format("<img src='cid:img{0}'>", _ImageNumber)

' Write the current image to a stream
Dim MS = New MemoryStream
Img.Image.Save(MS, Drawing.Imaging.ImageFormat.Png)
MS.Position = 0

' Create an embedded image from the memorystream
Dim R As New LinkedResource(MS, "image/png")

' Set the image identifier to the Value we supplied in the custom HTML
R.ContentId = String.Format("img{0}", _ImageNumber)

' Add the embedded image into our placeholder collection
' We will add it to the message later as we haven't created that yet
_Images.Add(R)

' Increment the counter ready for the next image
_ImageNumber += 1
End Sub



Use the following code when you want to send the mail (including embedded images)

Dim rpt As New MyReport
Dim SC = New SmtpClient(Server, 25)
Dim MM = rpt.GetMail(ToAddress, FromAddress)
MM.Subject = "Test Mail"
SC.Send(MM) 

If you need to do this for more than one report then just create a base report containing the code and inherit any reports that require the functionality.

You can use a similar method if you want to do the same for barcodes or charts within the report.

Complete demo project can be downloaded here. Don't forget to use the ProjectConverter if you are using a different version of the Devexpress Suite.

Leave me a comment if you find this useful. :-)

Friday 10 July 2009

Calculating an IRMark for the HMRC Gateway using VB.NET

This took ages to finally work out so I thought I'd share the function. Just pass in the XML file as a Byte Array and the IRMark is returned.

Friend Shared Function GetIRMark(ByVal Xml As Byte()) As String

' Convert Byte array to string
Dim text As String = Encoding.UTF8.GetString(Xml)
Dim Doc As New XmlDocument Doc.PreserveWhitespace = True Doc.LoadXml(text)
Dim ns As New XmlNamespaceManager(Doc.NameTable) ns.AddNamespace("env", Doc.DocumentElement.NamespaceURI)
Dim Body = Doc.SelectSingleNode("//env:Body", ns) ns.AddNamespace("tax", Body.FirstChild.NextSibling.NamespaceURI)

Create an XML document of just the body section
Dim xmlBody = New XmlDocument
xmlBody.PreserveWhitespace = True
xmlBody.LoadXml(Body.OuterXml)

' Remove any existing IRMark
Dim nodeIr = xmlBody.SelectSingleNode("//tax:IRmark", ns)
If Not nodeIr Is Nothing Then
nodeIr.ParentNode.RemoveChild(nodeIr)
End If

' Normalise the document using C14N (Canonicalisation)
Dim c14n = New XmlDsigC14NTransform c14n.LoadInput(xmlBody)

Using S As Stream = c14n.GetOutput()
Dim Buffer(S.Length - 1) As Byte
S.Read(Buffer, 0, S.Length)

' Convert to string and normalise line endings
text = Encoding.UTF8.GetString(Buffer)
text = text.Replace("&#xD;", "")
text = text.Replace(vbCrLf, vbLf)
text = text.Replace(vbCr, vbLf)

' Convert the final document back into a byte array
Dim b = Encoding.UTF8.GetBytes(text)

' Create the SHA-1 hash from the final document
Dim SHA = SHA1.Create
Dim hash = SHA.ComputeHash(b)
Return Convert.ToBase64String(hash)
End Using

End Function