Categories: Printing in Silverlight Posted on 6/23/2009 4:29 PM by Ryan Shelby  Feedback (7)

I have been working on a Line of Business application over the past few months.  Like many other Silverlight Developers, I didn’t find out about the lack of printing support in Silverlight 2 until I pressed the print button inside the browser. 

 

I googled for an easy solution, and found an article that inspired me called Printing in Silverlight 2 using CSS and ASP.NET AJAX 4.  I also read about XPS being used, but I couldn't find a successful example anywhere.  When I heard Silverlight 3 wouldn't have printing support, I had to make plans now to handle printing in my LOB application. 

 

My original goal was to create a Silverlight Datagrid Control that would automatically generate a print friendly view of the data; without having to change or customize code or templates from datagrid to datagrid. 

 

In addition, since all the information is already at client side inside the datagrid, I wanted to avoid making unnecessary trips back to the server in order to serve the print friendly version of the datagrid to the user.

 

The image below is the Print Friendly Silverlight Control Demo.  Click on it and then select print view to see the control in action.


Print Friendly Silverlight 2 Datagrid Control

 

The image below shows the HTML content automatically generated in the print view whenever the ItemsSource is set or changed.  To save ink I currently have simple black and white, but you can change the table style in the CSS if you want it to look more colorful.

 

How does it work?

There are 2 HTML DIV elements inside the ASPX page [one to hold the Silverlight control and the other to hold the HTML generated by the Silverlight Datagrid Control].

 

Dividers for both Silverlight Content and HTML Content

 

There are also 2 style sheets.  Style.css shows the Silverlight DIV and hides the HTML DIV during normal screen viewing.  The Print.css hides the Silverlight DIV and shows the HTML DIV in print view.

 

Style Sheets for both Silverlight Content and HTML Content

 

When the Silverlight Datagrid's ItemsSource is set or changed, the content | text is extracted from the Datagrid control itself, and then written to the Print DIV element.  Because the style sheets hide the HTML during normal screen view, the user cannot see it unless they click the print preview button in the browser. 

 

Iterating throught the actual datagrid control instead of the ItemsSource makes it possible to simply retrieve the header and row information contained inside the controls.  Because the datagridrow cell can contain many different kinds of controls [TextBox, TextBlock, DataGridTextColumn, Grids and StackPanels], determining the control type is necessary in order to extract the text or content values during the LoadingRow and the UnloadingRow events.  In addition, if the control inside the datagridcell is a Grid or StackPanel, then we to take care of child controls with multiple values.

 

The challenge with this approach however is that datagridrows are only loaded when they are actually visible on the screen.  For instance, if 10 rows are visible on the screen, then only 10 rows will be visible in the Print View.  Therefore, in order to make sure all the HTML information is created in the Print View, we need to make all datgridrows visible by forcing the scrollviewer of the Datagrid to move down the grid and back.  We are able to do this through code, and because the scrolling is so fast, the user never sees it.

 

Below is the code to the Print Friendly Datagrid:

    1 Imports System.Collections
    2 Imports System.Windows.Browser
    3 
    4 Namespace HTML
    5 
    6     Public Class DataGrid
    7         Inherits System.Windows.Controls.DataGrid
    8 
    9         Private m_Doc As HtmlDocument               'Main HTML Document.
   10         Private m_PrintContainer As HtmlElement     'DIV element [already exists on main HTML Document].
   11         Private m_Table As HtmlElement              'TABLE element [added to printContainer element].
   12         Private m_Caption As HtmlElement            'CAPTION element [added to TABLE element].
   13         Private m_THead As HtmlElement              'THEAD element [added to TABLE element].
   14         Private m_TR As HtmlElement                 'TR element [added to THEAD element].
   15         Private m_TD As HtmlElement                 'TD element [added to TR element].
   16 
   17 #Region "PUBLIC PROPERTIES"
   18 
   19         Public Shadows Property ItemsSource() As IEnumerable
   20             Get
   21                 Return MyBase.ItemsSource
   22             End Get
   23             Set(ByVal value As IEnumerable)
   24                 MyBase.ItemsSource = value
   25                 Me.UpdateLayout()
   26                 ScrollThroughGrid()
   27             End Set
   28         End Property
   29 
   30         Public WriteOnly Property Caption() As String
   31             Set(ByVal value As String)
   32                 m_Caption.SetProperty("innerHTML", value)
   33             End Set
   34         End Property
   35 
   36 #End Region
   37 
   38         Public Sub New()
   39 
   40             m_Doc = HtmlPage.Document
   41             m_PrintContainer = m_Doc.GetElementById("printContainer")   'DIV element inside Default.aspx page.
   42 
   43             'Remove any HTML elements previously created inside DIV element.
   44             While m_PrintContainer.Children.Count > 0
   45                 m_PrintContainer.RemoveChild(m_PrintContainer.Children(0))
   46             End While
   47 
   48             'Add new table and caption elements.
   49             m_Table = m_Doc.CreateElement("TABLE")
   50             m_PrintContainer.AppendChild(m_Table)
   51             m_Caption = m_Doc.CreateElement("CAPTION")
   52             m_Table.AppendChild(m_Caption)
   53 
   54         End Sub
   55 
   56         Private Sub ScrollThroughGrid()
   57 
   58             AddHandler Me.UnloadingRow, AddressOf Me.Unloading_Row  'Enable event only during forced scroll.
   59 
   60             ClearRowsFromTable()    'Clear any row data from table.
   61 
   62             'Force incremental scroll to last datarow in datagrid.
   63             'Unloading_Row | Loading_Row events triggered as each row comes into view | out of view.
   64             Dim I As Integer = 0
   65             Dim myEnumerator As IEnumerator = Me.ItemsSource.GetEnumerator
   66             While myEnumerator.MoveNext()
   67                 Me.ScrollIntoView(Me.ItemsSource(I), Me.Columns(0))
   68                 Me.UpdateLayout()
   69                 I += 1
   70             End While
   71 
   72             'Scroll back to first datarow.
   73             If I > 0 Then
   74                 Me.ScrollIntoView(Me.ItemsSource(0), Me.Columns(0))
   75                 Me.UpdateLayout()
   76             End If
   77 
   78             RemoveHandler Me.UnloadingRow, AddressOf Me.Unloading_Row
   79 
   80         End Sub
   81 
   82 #Region "GRID / ROW EVENTS"
   83 
   84         Private Sub Grid_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
   85             AddHeaderToTable()
   86         End Sub
   87 
   88         Private Sub Unloading_Row(ByVal sender As System.Object, ByVal e As System.Windows.Controls.DataGridRowEventArgs)
   89             AddRowToTable(e.Row)
   90         End Sub
   91 
   92         Private Sub Loading_Row(ByVal sender As System.Object, ByVal e As System.Windows.Controls.DataGridRowEventArgs) Handles Me.LoadingRow
   93             AddHandler e.Row.Loaded, AddressOf Row_Loaded
   94         End Sub
   95 
   96         Private Sub Row_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
   97             AddRowToTable(sender)
   98         End Sub
   99 
  100 #End Region
  101 
  102 #Region "TABLE PROCEDURES"
  103 
  104         Private Sub AddHeaderToTable()  'Called from Grid_Loaded event.
  105 
  106             'Add Header Information:
  107             If m_THead Is Nothing Then
  108 
  109                 m_THead = m_Doc.CreateElement("THEAD")
  110                 m_TR = m_Doc.CreateElement("TR")
  111 
  112                 'Grab header information from each data column in datagrid.
  113                 For Each dc As DataGridColumn In Me.Columns
  114 
  115                     Dim myHeader As String = dc.Header
  116                     Dim myTH As HtmlElement = m_Doc.CreateElement("TH")
  117                     myTH.SetProperty("innerHTML", myHeader)
  118                     m_TR.AppendChild(myTH)
  119 
  120                 Next dc
  121 
  122                 m_THead.AppendChild(m_TR)
  123                 m_Table.AppendChild(m_THead)
  124 
  125             End If
  126 
  127         End Sub
  128 
  129         Private Sub AddRowToTable(ByVal _dgr As DataGridRow)    'Called from Unloading_Row and Row_Loaded events.
  130 
  131             'Assumes first column contains unique value - otherwise the same row will be added during load | unload events.
  132             Dim myID As String = GetContentsInsideControls(Me.Columns(0).GetCellContent(_dgr))
  133             If myID = "" Then Exit Sub
  134 
  135             Dim existingTR As HtmlElement = m_Doc.GetElementById(myID)
  136 
  137             If existingTR Is Nothing Then
  138 
  139                 m_TR = m_Doc.CreateElement("TR")
  140                 m_TR.SetProperty("id", myID)
  141 
  142                 'Loop through each column and grab content value.
  143                 For Each dgc As DataGridColumn In Me.Columns
  144 
  145                     Dim myControl As FrameworkElement = dgc.GetCellContent(_dgr)
  146                     m_TD = m_Doc.CreateElement("TD")
  147                     m_TD.SetProperty("innerHTML", GetContentsInsideControls(myControl))   'Value of content in control.
  148                     m_TR.AppendChild(m_TD)
  149 
  150                 Next dgc
  151 
  152                 m_Table.AppendChild(m_TR)
  153 
  154             End If
  155 
  156         End Sub
  157 
  158         Private Sub ClearRowsFromTable()    'Called from ScrollThroughGrid sub.
  159 
  160             For I As Integer = (m_Table.Children.Count - 1) To 0 Step -1
  161                 m_TR = m_Table.Children(I)
  162                 If m_TR.TagName.ToUpper = "TR" Then
  163                     m_Table.RemoveChild(m_TR)
  164                 End If
  165             Next I
  166 
  167         End Sub
  168 
  169 #End Region
  170 
  171 #Region "PRIVATE FUNCTIONS"
  172 
  173         Private Function GetContentsInsideControls(ByVal _ControlsInsideRow As FrameworkElement) As String
  174 
  175             'Called from AddRowToTable Sub.  Returns the text value contained inside the control passed.
  176             'Currently handles TextBlock | TextBox | HyperlinkButton | StackPanel | Grid.
  177 
  178             If TypeOf (_ControlsInsideRow) Is TextBlock Then
  179 
  180                 Dim myTextBlock As TextBlock = _ControlsInsideRow
  181 
  182                 Return myTextBlock.Text
  183 
  184             ElseIf TypeOf (_ControlsInsideRow) Is TextBox Then
  185 
  186                 Dim myTextBox As TextBox = _ControlsInsideRow
  187                 Return myTextBox.Text
  188 
  189             ElseIf TypeOf (_ControlsInsideRow) Is HyperlinkButton Then
  190 
  191                 Dim myHyperLinkButton As HyperlinkButton = _ControlsInsideRow
  192                 Return myHyperLinkButton.Content
  193 
  194             ElseIf TypeOf (_ControlsInsideRow) Is StackPanel Then
  195 
  196                 Dim subContentValue As String = ""
  197                 Dim myStackPanel As StackPanel = _ControlsInsideRow
  198 
  199                 'Loop through each child control in stackpanel and return string.
  200                 For Each mySubControl As FrameworkElement In myStackPanel.Children
  201                     subContentValue = subContentValue & GetContentsInsideControls(mySubControl) & "<br />"
  202                 Next mySubControl
  203 
  204                 Return subContentValue
  205 
  206             ElseIf TypeOf (_ControlsInsideRow) Is Grid Then
  207 
  208                 Dim subContentValue As String = ""
  209                 Dim myGrid As Grid = _ControlsInsideRow
  210 
  211                 'Loop through each child control in stackpanel and return string.
  212                 For Each mySubControl As FrameworkElement In myGrid.Children
  213                     subContentValue = subContentValue & GetContentsInsideControls(mySubControl) & "<br />"
  214                 Next mySubControl
  215 
  216                 Return subContentValue
  217 
  218             Else
  219 
  220                 Return ""
  221 
  222             End If
  223 
  224         End Function
  225 
  226 #End Region
  227 
  228     End Class
  229 
  230 End Namespace

 

Below is the minimal code required to setup and reference the Printer Friendly Datagrid in the XAML.  The example below only uses DataGridTextColumns, but you can also use template columns containing TextBlocks, TextBoxes, HyperLinkButtons, and Grids or StackPanels containing child controls.

 

    1 <UserControl x:Class="PrintFriendly.Page"
    2     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    3     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    4     xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" 
    5     xmlns:print="clr-namespace:PrintFriendly.HTML">
    6 
    7         <print:DataGrid x:Name="dgDemo" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="40,0,40,40" Grid.Row="2">
    8             <data:DataGrid.Columns>
    9                 <data:DataGridTextColumn Header="ID" Binding="{Binding ID}" IsReadOnly="True" />
   10                 <data:DataGridTextColumn Header="Question" Binding="{Binding Question}" IsReadOnly="True" />
   11                 <data:DataGridTextColumn Header="Posted" Binding="{Binding Date}" IsReadOnly="True" />
   12             </data:DataGrid.Columns>
   13         </print:DataGrid>
   14 
   15 </UserControl>

 

That's pretty much it.  It isn't native printing in Silverlight, but at least it allows users to print contents from basic Silverlight 2 Datagrids in an LOB application without having to create templates or write code for each different datagrid. Even if Microsoft added printing support to Silverlight tomorrow, I think I would still keep using this feature since it might end up saving printer ink.

 

By the way, there are a few limitations I should mention:

  1. The first datacolumn in the Silverlight Datagrid needs to contain values that are unique.  Since the HTML table rows are added during the LoadingRow and Unloading Row events, the first column value is checked to prevent the same row from being added multiple times.
  2. The text in the print view isn't showing up in Firefox for some reason.  If somebody knows why, please let me know!  NOW WORKS IN FIREFOX AND SAFARI BROWSERS TOO!  Click here to learn why it was only working for Intenet Explorer...

 


Source Code [For Silverlight 3]:PrintFriendlyDataGrid3.zip (1.50 mb)   [Added 8/1/2009]
Demo: Print Friendly Silverlight Datagrid Control


Comments

trackback
DotNetShoutout on 7/22/2009 10:14 PM rshelby.com | Printer Friendly Silverlight Datagrid Control

Thank you for submitting this cool story - Trackback from DotNetShoutout
pingback
stevepietrek.com on 7/23/2009 8:18 PM Pingback from stevepietrek.com

Links (7/23/2009) « Steve Pietrek – Everything SharePoint
trackback
anme on 7/25/2009 5:37 AM SilverLight资源收集

一、来自Codeplex网站1、patterns
CyclingFoodmanPA
CyclingFoodmanPA on 8/1/2009 8:33 PM Awesome job Ryan.  However, I am working in Silverlight 3 and I cannot seem to get it to work.  Has anyone else gotten it to work in SL3 and if so, what did you do?  In the Default.aspx file, all of the .js files have warnings indicating that they are not found.  I am thinking that that has something to do with it.  Any ideas to get this working would be awesome as I am dying to get it to work.

Keith aka CyclingFoodmanPA
Ryan Shelby
Ryan Shelby on 8/1/2009 10:05 PM Keith.  I just updated the source code above to Silverlight 3 and removed the script references.  So you should have no problem getting it to work now.  Sorry for the issue and please let me know if you have any problem with the new one.

Thanks!
Annuities
Annuities on 9/27/2009 2:03 PM 1k thanks for the code.
Greg Salzman
Greg Salzman on 10/20/2009 9:54 AM Take a look at http://silverpdf.codeplex.com Not a perfect solution for Silverlight printing, but might be helpful for some scenarios.

Send Feedback





biuquote
  • Comment
  • Preview
Loading