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. :-)

12 comments:

  1. That's great..
    I was thinking to use Microsoft.mshtml assembly reference to locate all img tags in exported html string and replace their source property one by one. But for this, I first had to save report to a temp directory so that I can locate report files. Also I had to distribute the dll to all my customers. So this would be an inefficient solution.
    Your solution is the one what I need. Thanks..:)
    Melike

    ReplyDelete
  2. I'm happy that this has helped at least one other person :-)

    ReplyDelete
  3. What about pictures with an URL?

    ReplyDelete
  4. You would use the ImageURL property of the XRPicturebox control if you wanted to link to images on a web site instead of embedding them into the email.

    ReplyDelete
  5. Thanks for your solution, it works perfect - except the images are always embedded full size. Therefore I extended your code with a scaling snipped grabbed from http://www.vb-helper.com/howto_net_image_resize.html. Then you can even specify the size of the images to send.

    ReplyDelete
  6. Nice! 5 minutes and I got it running with XAF!

    ReplyDelete
  7. Is there a way to allow XRShape images as well?

    ReplyDelete
  8. This is great was just looking if this was possible. Hopefully it will be build in at some point.

    ReplyDelete
  9. Hi,

    I tried this solution and converted the code to C#, however the report always shows up in plain text format, no images or formatting appear, can you help me with this.

    Thanks

    ReplyDelete
  10. thanks a lot for your helpful code
    I did convert to C# and use Get methods to my mailing classes

    it is working great

    for converting I used http://converter.telerik.com/
    it is simple and good site

    ReplyDelete
  11. You saved my ass today :). Thank you for this wonderful code. It did Work immidiately without any problems. I also converted it to C# using http://converter.telerik.com/.
    Thank you.

    ReplyDelete
  12. Thanks. Your article is very helpful. Nice sharing!
    I need to printing barcodes in vb.net, hope to learn more from your experience,
    Best regards.

    ReplyDelete