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.
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.
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.
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].
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.
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.
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.