Tuesday, 4 August 2009

Creating a LiveZilla Button Server Control

Due to a customers request for a specific feature, I’ve just discovered LiveZilla. It’s a really good and FREE live chat program for your website.

It basically requires putting a small piece of JavaScript into your page wherever you want the Live Chat button to appear. This code can contain “variables” to help identify the person browsing your site and what part of the site they are on.

The problem I had was updating this code. The LiveZilla Server comes with a nifty program to generate this code and initially I started by placing the code at runtime into an ASP.NET Literal Control. This soon became a pain whenever I wanted to update the code and had to manually double-quote all of the Javascript so I could place it in a string variable, replace the variables in the code (allowing for HTML encoding) with the values to identify the user browsing the site and then writing it out to the Literal control.

Another spanner was thrown in the works because the site(s) I was using this on are sold to multiple customers (hosted separately) and each “version” of the site needed to use code specific to the customer. Well not wanting to have to put customer specific code into my pages, I needed a way to populate the Chat buttons easily and dynamically from data retrieved from a database.

What I wanted to achieve was:

  • Chat buttons populated from database.
  • Ability to use different Chat buttons on different pages
  • Each button should populate with customer specific code.

Here’s what I did:-

Create a new ASP.Net Server Control project called “LiveZillaButton”. This creates a project and gives you the following code to get you started (I’m using VS2008):

Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.Text
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

<DefaultProperty("Text"), ToolboxData("<{0}:ServerControl1 runat=server></{0}:ServerControl1>")> _
Public Class ServerControl1
    Inherits WebControl

    <Bindable(True), Category("Appearance"), DefaultValue(""), Localizable(True)> Property Text() As String
            Dim s As String = CStr(ViewState("Text"))
            If s Is Nothing Then
                Return "[" + Me.ID + "]"
                Return s
            End If
        End Get

        Set(ByVal Value As String)
            ViewState("Text") = Value
        End Set
    End Property

    Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
    End Sub

End Class

I replaced the default “Text” property with my own “Tag” property. This property is going to be used to identify the database record that the button should get it’s code from. I’ve altered the attributes so the property appears in it’s own “Livezilla” section in the property grid instead of the “Appearance” section.

<Bindable(True), Category("Livezilla"), DefaultValue("")> _
Property Tag() As String
        Dim s As String = CStr(ViewState("Tag"))
        Return If(String.IsNullOrEmpty(s), String.Empty, s)
    End Get

    Set(ByVal Value As String)
        ViewState("Tag") = Value
    End Set
End Property
I now add a TrackingCode Property. This will hold the Javascript code if I decide to populate the button manually instead of using the Tag property.
<Bindable(True), Category("Livezilla"), DefaultValue("")> _
Property TrackingCode() As String
        Dim s As String = CStr(ViewState("TrackingCode"))
        Return If(String.IsNullOrEmpty(s), String.Empty, s)
    End Get

    Set(ByVal Value As String)
        ViewState("TrackingCode") = Value
    End Set
End Property
I need to raise an event before the control outputs it’s HTML that will allow me to customise it to my requirements. To do this I need some way of passing data to and from the event handler. So I created a new Class in the project with the following code:-
Public Class LivezillaEventargs
    Inherits EventArgs

    Public Sub New(ByVal tag As String, ByVal ButtonContent As String)
        _tag = tag
        _buttonContent = ButtonContent
    End Sub

    Private _tag As String
    Public ReadOnly Property Tag() As String
            Return _tag
        End Get
    End Property

    Private _buttonContent As String
    Public Property ButtonContent() As String
            Return _buttonContent
        End Get

        Set(ByVal Value As String)
            _buttonContent = Value
        End Set
    End Property

End Class

Back to the main control, I need an Event and also some default HTML that will be used when the none of the controls properties are populated. Lets add the following to our control.

Friend Const EmptyButton = "<div style=""border: thin groove #000080; margin: 0px; padding: 0px; width: 150px; height: 80px""><table style=""width: 100%; height: 100%""><tr><td style=""height: 30px; font-weight: bold; font-family: Arial, Helvetica, sans-serif; text-align: center; color: #000080;"">Livezilla</td></tr><tr><td style=""font-size: 11px; vertical-align: middle; text-align: center; font-family: Arial, Helvetica, sans-serif;"">Populate the TrackingCode property or handle the BeforeRender event</td></tr></table></div>"

Public Event BeforeRender(ByVal sender As Object, ByRef e As LivezillaEventargs)

Remember we still have a “RenderContents” Method that has been complaining about it’s lack of a “Text” property since we replaced it with a Tag property. Modify this method so it now contains:-

Protected Overrides Sub RenderContents(ByVal output As HtmlTextWriter)
    Dim TC As String = TrackingCode
    Dim EV As New LivezillaEventargs(Tag, TC)

    RaiseEvent BeforeRender(Me, EV)

    If String.IsNullOrEmpty(EV.ButtonContent) Then
        EV.ButtonContent = EmptyButton
    End If

End Sub

This raises our event and allows us to manipulate the HTML before it is output.

The button now is fully functional and you can build the project and add it to the toolbox. However, you may want to make a few more changes first for better design time support.

The control attributes still have their default values so let’s change them to :-

<DefaultEvent("BeforeRender"), DefaultProperty("TrackingCode"), ToolboxData("<{0}:LivezillaButton runat=server></{0}:LivezillaButton>")> _
Double clicking the Livezilla button on your web form will now automatically create the BeforeRender Event Handler and dropping the Button onto your page will highlight the TrackingCode property by default. I also renamed the control from it’s default of “ServerControl1”.
Add a new file to your project called “AssemblyInfo.vb” and type the following into it:-
Imports System.Web.UI

<Assembly: TagPrefix("LiveZillaButton", "LZ")> 
This gives our control a custom Tag prefix otherwise our button would be created on the web form with the default prefix of cc1. The next part is totally optional. We don’t necessarily want the control to render the button at design time so we need to tell it to do something different when we are working on the design surface. Add a reference to System.Design to the project and then create a new class containing:-
Friend Class LivezillaControlDesigner
    Inherits System.Web.UI.Design.ControlDesigner

    Protected Button As LivezillaButton

    Public Overrides Sub Initialize(ByVal component As System.ComponentModel.IComponent)
        If TypeOf component Is LivezillaButton Then
            Button = DirectCast(component, LivezillaButton)
        End If
    End Sub

    Public Overrides Function GetDesignTimeHtml() As String
        Return If(String.IsNullOrEmpty(Button.TrackingCode), LivezillaButton.EmptyButton, Button.TrackingCode)
    End Function

End Class

This is the designer that will handle our design time HTML. As it stands, it just outputs our standard button HTML but you can modify the GetDesignTimeHtml method to return whatever you want to use instead. I’ll leave this part up to you.

The final step is to tell our button to use our new ControlDesigner so add the following attribute to the LiveZillaButton Class.

And that’s it. The control is finished. To use it, just drop the button onto your web form and either:-
  1. Populate the TrackingCode property if you want a static button with specific markup.
  2. Populate the Tag property and handle the BeforeRender Event to dynamically change the markup.

Protected Sub LivezillaButton1_BeforeRender(ByVal sender As Object, ByRef e As LiveZillaButton.LivezillaEventargs) Handles LivezillaButton1.BeforeRender
    If e.Tag = "LoginPage" Then
        e.ButtonContent = "Our Button Content - Possibly from a database"
        e.ButtonContent = "Our other Button content"
    End If
End Sub

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")

' Add the HTML portion of the message

Return MM
End Function

''' <summary>
''' Export the current report as text
''' </summary>
Private Function GetTextString() As String
Using MS As New IO.MemoryStream
Dim B As Byte() = MS.ToArray()
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
' 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)

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

' Export to the stream

' 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
_Images = Nothing

' Remove any event handlers we added earlier
For Each B As Band In Bands
SetEvents(B, False)
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
RemoveHandler a.HtmlItemCreated, AddressOf HTMLImageCreated
End If
SetEvents(a, Attach)
End If
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

' 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"

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

' Remove any existing IRMark
Dim nodeIr = xmlBody.SelectSingleNode("//tax:IRmark", ns)
If Not nodeIr Is Nothing Then
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