Guides/LayeredSubtotaling.md
... ...
@@ -10,216 +10,512 @@ Layered subtotaling is a customized way to display subtotals that will aggregate
10 10
11 11
![Custom Layered Subtotaling](http://wiki.izenda.us/Guides/Developer-Links-and-Guides/custom_layered_subtotaling.png)
12 12
13
-In this example, the data would be aggregated based on [ShipCountry] normally. The subtotal of all countries' freight would be totaled up. With layered subtotaling, we can get subtotals for drilldowns (such as the subtotal of [ShipRegion] ([ShipCountry] drills down to [ShipRegion])) in one report. This allows not only each [ShipCountry] field value to be totaled up, but it also allows us to see each [ShipRegion]'s total.
13
+In this example, the data would be aggregated based on [ShipCountry] normally. The subtotal of all countries' freight would be totaled up. With layered subtotaling, we can get subtotals for drilldowns (such as the subtotal of [ShipRegion] ([ShipCountry] drills down to [ShipRegion])) in one report. This allows not only each [ShipCountry] field value to be totaled up, but it also allows us to see each [ShipRegion]'s total.
14 14
15 15
##Implementation
16 16
17
-Here is a sample global.asax to implement layered subtotaling in your own application. This example behavior modifies the "Line Delimited With Labels" option in the Style tab of the Report Designer. However, you can customize which of the visual grouping styles this behavior modifies, if any, by modifying the following line:
17
+Here is a sample global.asax to implement layered subtotaling in your own application. This example behavior modifies the "Line Delimited With Labels" option in the Style tab of the Report Designer. However, you can customize which of the visual grouping styles this behavior modifies, if any, by modifying the following lines:
18 18
19 19
```csharp
20
-isTargetVGStyle = AdHocContext.CurrentReportSet.VisualGroupStyle == VisualGroupStyle.LineDelimitedWithLabels;
20
+LayeredSubtotaling myLayeredSubtotaling = new LayeredSubtotaling(VisualGroupStyle.LineDelimitedWithLabels);
21 21
```
22 22
23
+###C♯
24
+
23 25
```csharp
26
+using System;
27
+using System.Collections.Generic;
28
+using HtmlAgilityPack;
29
+using Izenda.AdHoc;
30
+
31
+[Serializable]
32
+public class CustomAdHocConfig : FileSystemAdHocConfig
33
+{
34
+ LayeredSubtotaling myLayeredSubtotaling = new LayeredSubtotaling(VisualGroupStyle.LineDelimitedWithLabels);
35
+
36
+ public override void PreExecuteReportSet(ReportSet reportSet)
37
+ {
38
+ myLayeredSubtotaling.hasSubtotals = reportSet.Detail.AddGrandTotals;
39
+ }
40
+
41
+ public override void ProcessDataSet(DataSet ds, string reportPart)
42
+ {
43
+ myLayeredSubtotaling.DoSubtotalingForVG(ds, reportPart);
44
+ }
45
+
46
+ public override string PerformCustomRendering(string initialHtml)
47
+ {
48
+ return myLayeredSubtotaling.PerformCustomRendering(initialHtml);
49
+ }
50
+}
51
+
52
+/// <summary>
53
+/// Summary description for LayeredSubtotaling
54
+/// </summary>
55
+public class LayeredSubtotaling
56
+{
57
+ public bool hasSubtotals = false;
58
+ static Dictionary<string, List<object>> deltas = new Dictionary<string, List<object>>();
59
+ bool firstPass = true;
60
+ VisualGroupStyle targetVGStyle;
61
+
62
+ public LayeredSubtotaling(VisualGroupStyle targetVGStyle)
63
+ {
64
+ _targetVGStyle = targetVGStyle;
65
+ }
66
+
24 67
/// <summary>
25 68
/// Overrides base version to support custom totaling of various deltas in the report.
26 69
/// </summary>
27
-
28
- public override void ProcessDataSet(System.Data.DataSet ds, string reportPart) {
29
- if (firstPass) {
30
- string header, where;
31
- System.Collections.Generic.List<object> dataColumns = new System.Collections.Generic.List<object>();
32
- System.Collections.Generic.Dictionary<string, string> dataHeaders = new System.Collections.Generic.Dictionary<string, string>();
33
-
34
- deltas = new System.Collections.Generic.Dictionary<string, System.Collections.Generic.List<object>>();
35
- isTargetVGStyle = AdHocContext.CurrentReportSet.VisualGroupStyle == VisualGroupStyle.LineDelimitedWithLabels;
36
-
37
- if (!String.IsNullOrEmpty(reportPart) && isTargetVGStyle) {
38
- foreach (System.Data.DataTable table in ds.Tables) {
39
- foreach (System.Data.DataRow row in table.Rows) {
40
- header = String.Empty;
41
- where = String.Empty;
42
-
43
- foreach (System.Data.DataColumn column in table.Columns) {
44
- // If the value in this column is a string, it means the column is a header, and not a data column.
45
- if (row[column].GetType() == typeof(string)) {
46
- // If the header is empty, add the first value formatted as follows.
47
- if (header.Length == 0) {
48
- header = row[column].ToString();
49
- where = "[" + column + "] = '" + row[column] + "'";
50
- }
51
- else {
52
- // Otherwise, add the item, separated by a comma.
53
- header += ", " + row[column].ToString();
54
- where += " AND [" + column + "] = '" + row[column] + "'";
55
- }
56
-
57
- // If the header is already in the list, ignore it and move on.
58
- if (!dataHeaders.ContainsKey(header)) {
59
- dataHeaders.Add(header, where);
60
- }
70
+ public void DoSubtotalingForVG(System.Data.DataSet ds, string reportPart)
71
+ {
72
+ if (firstPass && !String.IsNullOrEmpty(reportPart))
73
+ {
74
+ string header, where;
75
+ List<object> dataColumns = new List<object>();
76
+ Dictionary<string, string> dataHeaders = new Dictionary<string, string>();
77
+
78
+ bool isTargetVGStyle = AdHocContext.CurrentReportSet.VisualGroupStyle == _targetVGStyle;
79
+
80
+ if (isTargetVGStyle)
81
+ {
82
+ foreach (System.Data.DataTable table in ds.Tables)
83
+ {
84
+ foreach (System.Data.DataRow row in table.Rows)
85
+ {
86
+ header = String.Empty;
87
+ where = String.Empty;
88
+
89
+ foreach (System.Data.DataColumn column in table.Columns)
90
+ {
91
+ // If the value in this column is a string, it means the column is a header, and not a data column.
92
+ if (row[column].GetType() == typeof(string))
93
+ {
94
+ // If the header is empty, add the first value formatted as follows.
95
+ if (header.Length == 0)
96
+ {
97
+ header = row[column].ToString();
98
+ where = "[" + column + "] = '" + row[column] + "'";
99
+ }
100
+ else
101
+ {
102
+ // Otherwise, add the item, separated by a comma.
103
+ header += ", " + row[column].ToString();
104
+ where += " AND [" + column + "] = '" + row[column] + "'";
105
+ }
106
+
107
+ // If the header is already in the list, ignore it and move on.
108
+ if (!dataHeaders.ContainsKey(header))
109
+ {
110
+ dataHeaders.Add(header, where);
111
+ }
112
+ }
113
+
114
+ // Otherwise this is a data column and add it to that data structure.
115
+ else if (!dataColumns.Contains(column) && row[column].GetType() != typeof(DBNull))
116
+ {
117
+ dataColumns.Add(column);
118
+ }
119
+ }
120
+ }
121
+
122
+ // Iterate over the gathered headers, computing the sum for each data column associated with it.
123
+ foreach (string head in dataHeaders.Keys)
124
+ {
125
+ List<object> data = new List<object>();
126
+ foreach (object column in dataColumns)
127
+ {
128
+ data.Add(table.Compute("Sum(" + column.ToString() + ")", dataHeaders[head]));
129
+ }
130
+
131
+ if (data.Count > 0)
132
+ {
133
+ // add the header and all the sums for the data columns to the deltas structure.
134
+ if(!deltas.ContainsKey(head))
135
+ deltas.Add(head, data);
136
+ }
137
+ }
61 138
}
62
-
63
- // Otherwise this is a data column and add it to that data structure.
64
- else if (!dataColumns.Contains(column) && row[column].GetType() != typeof(DBNull)) {
65
- dataColumns.Add(column);
66
- }
67
- }
68
- }
69
-
70
- // Iterate over the gathered headers, computing the sum for each data column associated with it.
71
- foreach (string head in dataHeaders.Keys) {
72
- System.Collections.Generic.List<object> data = new System.Collections.Generic.List<object>();
73
- int i = 0;
74
- foreach (object column in dataColumns) {
75
- data.Add(table.Compute("Sum(" + column.ToString() + ")", dataHeaders[head]));
76
- }
77
-
78
- if (data.Count > 0) {
79
- // add the header and all the sums for the data columns to the deltas structure.
80
- deltas.Add(head, data);
81
- }
82 139
}
83
- }
84 140
}
85
- }
86 141
87
- // If subtotaling is enabled, toggle first pass so the subtotal loop does wipe out custom totals.
88
- if (hasSubtotals) {
89
- firstPass = !firstPass;
90
- }
91
- }
92
-
93
- public override void PreExecuteReportSet(ReportSet reportSet) {
94
- //Inspect the actual report object to determing if subtotaling is used.
95
- hasSubtotals = reportSet.Detail.AddGrandTotals;
96
- base.PreExecuteReportSet(reportSet);
142
+ // If subtotaling is enabled, toggle first pass so the subtotal loop does wipe out custom totals.
143
+ if (hasSubtotals)
144
+ {
145
+ firstPass = !firstPass;
146
+ }
97 147
}
98 148
99 149
/// <summary>
100 150
/// Override this to implement your own custom report rendering. This requires the
101 151
/// html agility pack NuGet package to work.
102 152
/// </summary>
153
+ public string PerformCustomRendering(string initialHtml)
154
+ {
155
+ bool isTargetVGStyle = AdHocContext.CurrentReportSet.VisualGroupStyle == _targetVGStyle;
156
+ if (isTargetVGStyle)
157
+ {
158
+ HtmlDocument doc = new HtmlDocument();
159
+ doc.LoadHtml(initialHtml);
160
+
161
+ // This uses an Xpath declaration to get all rows that are of class Visual group. This is done to match
162
+ // the visual grouping to the deltas retreived from ProcessDataSet.
163
+ HtmlNodeCollection highestLevelDeltas = doc.DocumentNode.SelectNodes("//tr[@class='VisualGroup']");
164
+
165
+ if (highestLevelDeltas == null)
166
+ {
167
+ return initialHtml;
168
+ }
103 169
104
- public override string PerformCustomRendering(string initialHtml) {
105
- if (isTargetVGStyle) {
106
- HtmlDocument doc = new HtmlDocument();
107
- doc.LoadHtml(initialHtml);
108
-
109
- // This uses an Xpath declaration to get all rows that are of class Visual group. This is done to match
110
- // the visual grouping to the deltas retreived from ProcessDataSet.
111
- HtmlNodeCollection highestLevelDeltas = doc.DocumentNode.SelectNodes("//tr[@class='VisualGroup']");
170
+ //The report table is passed to several functions as a reference to the current report.
171
+ HtmlNode reportTable = doc.DocumentNode.SelectSingleNode("//table[@class='ReportTable']");
172
+
173
+ // Splits the string into an array for looping and comparison.
174
+ string[] lastDelta = SanitizeDelta(highestLevelDeltas.FindFirst("tr").FirstChild.InnerHtml.Split(new string[] { "<br>" }, StringSplitOptions.RemoveEmptyEntries));
175
+
176
+ // Loops through all the deltas extracted from the report, node by node.
177
+ foreach (HtmlNode delta in highestLevelDeltas)
178
+ {
179
+ // Splits the string into an array for looping and comparison.
180
+ string[] currentDelta = SanitizeDelta(delta.FirstChild.InnerHtml.Split(new string[] { "<br>" }, StringSplitOptions.RemoveEmptyEntries));
181
+
182
+ string deltaString = string.Empty;
183
+
184
+ // Loops through, comparing the arrays index by index. Ignores the last index since this
185
+ // delta is handled by the standard subtotaling.
186
+ for (int i = 0; i < currentDelta.Length - 1; i++)
187
+ {
188
+ // Adds the current index from the array to a string for later use.
189
+ deltaString += lastDelta[i];
190
+
191
+ if (lastDelta[i] != currentDelta[i])
192
+ {
193
+ // This check is needed to handle 1st level delta changes. In these cases, there can be
194
+ // more than just one delta to output, and this method will print them all.
195
+ if (i == 0)
196
+ {
197
+ PrintFinalDeltas(reportTable, delta, lastDelta);
198
+ }
199
+ // Otherwise, add the delta to report and remove it from the list of deltas.
200
+ else
201
+ {
202
+ InjectHtml(reportTable, delta, deltaString);
203
+ deltas.Remove(deltaString);
204
+ }
205
+ break;
206
+ }
207
+ // If the values at the index are the same, add a , and move to the next index.
208
+ else
209
+ {
210
+ deltaString += ", ";
211
+ }
212
+ }
112 213
113
- if (highestLevelDeltas == null) {
114
- return initialHtml;
115
- }
214
+ lastDelta = currentDelta;
116 215
117
- //The report table is passed to several functions as a reference to the current report.
118
- HtmlNode reportTable = doc.DocumentNode.SelectSingleNode("//table[@class='ReportTable']");
119
-
120
- // Splits the string into an array for looping and comparison.
121
- string[] lastDelta = SanitizeDelta(highestLevelDeltas.FindFirst("tr").FirstChild.InnerHtml.Split(new string[] { "<br>" }, StringSplitOptions.RemoveEmptyEntries));
122
-
123
- // Loops through all the deltas extracted from the report, node by node.
124
- foreach (HtmlNode delta in highestLevelDeltas) {
125
- // Splits the string into an array for looping and comparison.
126
- string[] currentDelta = SanitizeDelta(delta.FirstChild.InnerHtml.Split(new string[] { "<br>" }, StringSplitOptions.RemoveEmptyEntries));
127
-
128
- string deltaString = string.Empty;
129
-
130
- // Loops through, comparing the arrays index by index. Ignores the last index since this
131
- // delta is handled by the standard subtotaling.
132
- for (int i = 0; i < currentDelta.Length - 1; i++) {
133
- // Adds the current index from the array to a string for later use.
134
- deltaString += lastDelta[i];
135
-
136
- if (lastDelta[i] != currentDelta[i]) {
137
- // This check is needed to handle 1st level delta changes. In these cases, there can be
138
- // more than just one delta to output, and this method will print them all.
139
- if (i == 0) {
140
- PrintFinalDeltas(reportTable, delta, lastDelta);
141
- }
142
- // Otherwise, add the delta to report and remove it from the list of deltas.
143
- else {
144
- InjectHtml(reportTable, delta, deltaString);
145
- deltas.Remove(deltaString);
146
- }
147
- break;
148
- }
149
- // If the values at the index are the same, add a , and move to the next index.
150
- else {
151
- deltaString += ", ";
152
- }
153
- }
216
+ // This sanitizes the string in the case of a DBNull value.
217
+ string deltaHead = string.Join(",", currentDelta).Replace(", ,", ", ");
154 218
155
- lastDelta = currentDelta;
156 219
157
- // This sanitizes the string in the case of a DBNull value.
158
- string deltaHead = string.Join(",", currentDelta).Replace(", ,", ", ");
220
+ // Remove the delta from the list.
221
+ if (deltas.ContainsKey(deltaHead))
222
+ {
223
+ deltas.Remove(deltaHead);
224
+ }
225
+ }
159 226
227
+ // This final loop writes out the remaining deltas
228
+ PrintFinalDeltas(reportTable, reportTable.LastChild, lastDelta);
160 229
161
- // Remove the delta from the list.
162
- if (deltas.ContainsKey(deltaHead)) {
163
- deltas.Remove(deltaHead);
164
- }
230
+ return doc.DocumentNode.OuterHtml;
165 231
}
166
-
167
- // This final loop writes out the remaining deltas
168
- PrintFinalDeltas(reportTable, reportTable.LastChild, lastDelta);
169
-
170
- return doc.DocumentNode.OuterHtml;
171
- }
172
- return initialHtml;
232
+ return initialHtml;
173 233
}
174 234
175 235
/// <summary>
176 236
/// PrintFinalDeltas is called to in the case of level 1 delta changes. It makes sure all deltas get printed out,
177 237
/// not just the level 1.
178 238
/// </summary>
179
- private static void PrintFinalDeltas(HtmlNode reportTable, HtmlNode previousNode, string[] lastDelta) {
180
- // delta takes the lastDelta array and converts it back into a string.
181
- string delta = string.Join(", ", lastDelta);
182
-
183
- // This loop keeps running until all of the pertinent information has been pulled from the string.
184
- while (delta.LastIndexOf(',') > 0) {
185
- //Since standard subtotals print off the low level delta, this puuls that one out and discards it.
186
- delta = delta.Substring(0, delta.LastIndexOf(','));
187
-
188
- // If the string is a valid delta, print it out and remove it from the dictionary.
189
- if (deltas.ContainsKey(delta)) {
190
- InjectHtml(reportTable, previousNode, delta);
191
- deltas.Remove(delta);
239
+ private static void PrintFinalDeltas(HtmlNode reportTable, HtmlNode previousNode, string[] lastDelta)
240
+ {
241
+ // delta takes the lastDelta array and converts it back into a string.
242
+ string delta = string.Join(", ", lastDelta);
243
+
244
+ // This loop keeps running until all of the pertinent information has been pulled from the string.
245
+ while (delta.LastIndexOf(',') > 0)
246
+ {
247
+ //Since standard subtotals print off the low level delta, this puuls that one out and discards it.
248
+ delta = delta.Substring(0, delta.LastIndexOf(','));
249
+
250
+ // If the string is a valid delta, print it out and remove it from the dictionary.
251
+ if (deltas.ContainsKey(delta))
252
+ {
253
+ InjectHtml(reportTable, previousNode, delta);
254
+ deltas.Remove(delta);
255
+ }
192 256
}
193
- }
194 257
}
195 258
196 259
/// <summary>
197 260
/// InjectHtml actually alters the html string, injecting the deltas into it.
198 261
/// </summary>
262
+ private static void InjectHtml(HtmlNode reportTable, HtmlNode currentNode, string deltaString)
263
+ {
264
+ // Build up the table row to inject.
265
+ HtmlNode deltaRow = HtmlNode.CreateNode("<tr class=ReportFooter></tr>");
266
+ deltaRow.AppendChild(HtmlNode.CreateNode("<td>" + deltaString + "</td>"));
267
+
268
+ // This will loop through the various delta columns for a particular row, adding each as a td to the row.
269
+ if (deltas.ContainsKey(deltaString))
270
+ {
271
+ foreach (object column in deltas[deltaString])
272
+ {
273
+ deltaRow.AppendChild(HtmlNode.CreateNode("<td>" + column + "</td>"));
274
+ }
199 275
200
- private static void InjectHtml(HtmlNode reportTable, HtmlNode currentNode, string deltaString) {
201
- // Build up the table row to inject.
202
- HtmlNode deltaRow = HtmlNode.CreateNode("<tr class=ReportFooter></tr>");
203
- deltaRow.AppendChild(HtmlNode.CreateNode("<td>" + deltaString + "</td>"));
204
-
205
- // This will loop through the various delta columns for a particular row, adding each as a td to the row.
206
- if (deltas.ContainsKey(deltaString)) {
207
- foreach (object column in deltas[deltaString]) {
208
- deltaRow.AppendChild(HtmlNode.CreateNode("<td>" + column + "</td>"));
276
+ // This is where the row actually gets inserted.
277
+ reportTable.InsertBefore(deltaRow, currentNode);
209 278
}
210
-
211
- // This is where the row actually gets inserted.
212
- reportTable.InsertBefore(deltaRow, currentNode);
213
- }
214 279
}
215 280
216
- private static string[] SanitizeDelta(string[] delta) {
217
- string[] sanitizedDelta = new string[delta.Length];
218
- for (int i = 0; i < delta.Length; i++) {
219
- if (!string.IsNullOrEmpty(delta[i].Substring(delta[i].IndexOf(':') + 1).Trim()))
220
- sanitizedDelta[i] = delta[i].Substring(delta[i].IndexOf(':') + 2);
221
- }
281
+ private static string[] SanitizeDelta(string[] delta)
282
+ {
283
+ string[] sanitizedDelta = new string[delta.Length];
284
+ for (int i = 0; i < delta.Length; i++)
285
+ {
286
+ if (!string.IsNullOrEmpty(delta[i].Substring(delta[i].IndexOf(':') + 1).Trim()))
287
+ sanitizedDelta[i] = delta[i].Substring(delta[i].IndexOf(':') + 2);
288
+ }
222 289
223
- return sanitizedDelta;
290
+ return sanitizedDelta;
224 291
}
292
+}
293
+```
294
+
295
+###VB.NET
296
+
297
+```visualbasic
298
+Imports System.Data
299
+Imports HtmlAgilityPack
300
+Imports Izenda.AdHoc
301
+
302
+<Serializable()>
303
+Public Class CustomAdHocConfig
304
+ Inherits FileSystemAdHocConfig
305
+
306
+ Dim myLayeredSubtotaling As New LayeredSubtotaling(VisualGroupStyle.LineDelimitedWithLabels)
307
+
308
+ Public Overrides Sub PreExecuteReportSet(ByVal reportSet As ReportSet)
309
+ myLayeredSubtotaling.hasSubtotals = reportSet.Detail.AddGrandTotals
310
+ End Sub
311
+
312
+ Public Overrides Sub ProcessDataSet(ByVal ds As DataSet, ByVal reportPart As String)
313
+ myLayeredSubtotaling.DoSubtotalingForVG(ds, reportPart, VisualGroupStyle.LineDelimitedWithLabels)
314
+ End Sub
315
+
316
+ Public Overrides Function PerformCustomRendering(initialHtml As String) As String
317
+ Return myLayeredSubtotaling.PerformCustomRendering(initialHtml, VisualGroupStyle.LineDelimitedWithLabels)
318
+ End Function
319
+End Class
320
+
321
+' <summary>
322
+' Summary description for LayeredSubtotaling
323
+' </summary>
324
+Public Class LayeredSubtotaling
325
+ Public hasSubtotals As Boolean = False
326
+ Shared deltas As New Dictionary(Of String, List(Of Object))()
327
+ Dim firstPass As Boolean = True
328
+ Dim _targetVGStyle As VisualGroupStyle
329
+
330
+ Sub New(targetVGStyle As VisualGroupStyle)
331
+ _targetVGStyle = targetVGStyle
332
+ End Sub
333
+
334
+ ' <summary>
335
+ ' Overrides base version to support custom totaling of various deltas in the report.
336
+ ' </summary>
337
+ Public Sub DoSubtotalingForVG(ds As DataSet, reportPart As String)
338
+ If firstPass AndAlso Not String.IsNullOrEmpty(reportPart) Then
339
+ Dim header, where As String
340
+ Dim dataColumns As New List(Of Object)()
341
+ Dim dataHeaders As New Dictionary(Of String, String)()
342
+
343
+ Dim isTargetVGStyle As Boolean = AdHocContext.CurrentReportSet.VisualGroupStyle = _targetVGStyle
344
+
345
+ If isTargetVGStyle Then
346
+ For Each table As DataTable In ds.Tables
347
+ For Each row As DataRow In table.Rows
348
+ header = String.Empty
349
+ where = String.Empty
350
+
351
+ For Each column As DataColumn In table.Columns
352
+ ' If the value in this column Is a string, it means the column Is a header, And Not a data column.
353
+ If row(column).GetType() Is GetType(String) Then
354
+ 'If the header Is empty, add the first value formatted as follows.
355
+ If header.Length = 0 Then
356
+ header = row(column).ToString()
357
+ where = "[" & column.ToString() & "] = '" & row(column) & "'"
358
+ Else
359
+ 'Otherwise, add the item, separated by a comma.
360
+ header += ", " & row(column).ToString()
361
+ where += " AND [" & column.ToString() & "] = '" & row(column) & "'"
362
+
363
+ 'If the header Is already in the list, ignore it And move on.
364
+ If (Not dataHeaders.ContainsKey(header)) Then
365
+ dataHeaders.Add(header, where)
366
+ End If
367
+ End If
368
+ 'Otherwise this Is a data column And add it to that data structure.
369
+ ElseIf Not dataColumns.Contains(column) AndAlso row(column).GetType() IsNot GetType(DBNull) Then
370
+ dataColumns.Add(column)
371
+ End If
372
+ Next
373
+ Next
374
+
375
+ 'Iterate over the gathered headers, computing the sum for each data column associated with it.
376
+ For Each head As String In dataHeaders.Keys
377
+ Dim data As New List(Of Object)()
378
+ For Each column As Object In dataColumns
379
+ data.Add(table.Compute("Sum(" + column.ToString() + ")", dataHeaders(head)))
380
+ Next
381
+
382
+ If data.Count > 0 Then
383
+ ' add the header And all the sums for the data columns to the deltas structure.
384
+ If (Not deltas.ContainsKey(head)) Then
385
+ deltas.Add(head, data)
386
+ End If
387
+ End If
388
+ Next
389
+ Next
390
+ End If
391
+ End If
392
+ ' If subtotaling Is enabled, toggle first pass so the subtotal loop does wipe out custom totals.
393
+ If hasSubtotals Then
394
+ firstPass = Not firstPass
395
+ End If
396
+ End Sub
397
+
398
+ ' <summary>
399
+ ' Override this to implement your own custom report rendering. This requires the
400
+ ' html agility pack NuGet package to work.
401
+ ' </summary>
402
+ Public Function PerformCustomRendering(initialHtml As String) As String
403
+ Dim isTargetVGStyle As Boolean = AdHocContext.CurrentReportSet.VisualGroupStyle = _targetVGStyle
404
+ If isTargetVGStyle Then
405
+ Dim doc As New HtmlDocument()
406
+ doc.LoadHtml(initialHtml)
407
+
408
+ ' This uses an Xpath declaration to get all rows that are of class Visual group. This Is done to match
409
+ ' the visual grouping to the deltas retreived from ProcessDataSet.
410
+ Dim highestLevelDeltas = doc.DocumentNode.SelectNodes("//tr[@class='VisualGroup']")
411
+
412
+ If highestLevelDeltas Is Nothing Then
413
+ Return initialHtml
414
+ End If
415
+
416
+ 'The report table Is passed to several functions as a reference to the current report.
417
+ Dim reportTable = doc.DocumentNode.SelectSingleNode("//table[@class='ReportTable']")
418
+
419
+ ' Splits the string into an array for looping And comparison.
420
+ Dim lastDelta = SanitizeDelta(highestLevelDeltas.FindFirst("tr").FirstChild.InnerHtml.Split(New String() {"<br>"}, StringSplitOptions.RemoveEmptyEntries))
421
+
422
+ 'Loops through all the deltas extracted from the report, node by node.
423
+ For Each delta As HtmlNode In highestLevelDeltas
424
+ ' Splits the string into an array for looping And comparison.
425
+ Dim currentDelta = SanitizeDelta(delta.FirstChild.InnerHtml.Split(New String() {"<br>"}, StringSplitOptions.RemoveEmptyEntries))
426
+
427
+ Dim deltaString = String.Empty
428
+
429
+ ' Loops through, comparing the arrays index by index. Ignores the last index since this
430
+ ' delta Is handled by the standard subtotaling.
431
+ For i As Integer = 0 To currentDelta.Length - 1
432
+ 'Adds the current index from the array to a string for later use.
433
+ deltaString += lastDelta(i)
434
+
435
+ If Not lastDelta(i) = currentDelta(i) Then
436
+ ' This check Is needed to handle 1st level delta changes. In these cases, there can be
437
+ ' more than just one delta to output, And this method will print them all.
438
+ If i = 0 Then
439
+ PrintFinalDeltas(reportTable, delta, lastDelta)
440
+ 'Otherwise, add the delta to report And remove it from the list of deltas.
441
+ Else
442
+ InjectHtml(reportTable, delta, deltaString)
443
+ deltas.Remove(deltaString)
444
+ End If
445
+ Exit For
446
+ 'If the values at the index are the same, add a , And move to the next index.
447
+ Else
448
+ deltaString += ", "
449
+ End If
450
+ Next
451
+
452
+ lastDelta = currentDelta
453
+
454
+ 'This sanitizes the string in the case of a DBNull value.
455
+ Dim deltaHead = String.Join(",", currentDelta).Replace(", ,", ", ")
456
+
457
+
458
+ 'Remove the delta from the list.
459
+ If deltas.ContainsKey(deltaHead) Then
460
+ deltas.Remove(deltaHead)
461
+ End If
462
+ Next
463
+
464
+ ' This final loop writes out the remaining deltas
465
+ PrintFinalDeltas(reportTable, reportTable.LastChild, lastDelta)
466
+
467
+ Return doc.DocumentNode.OuterHtml
468
+ End If
469
+ Return initialHtml
470
+ End Function
471
+
472
+ ' <summary>
473
+ ' PrintFinalDeltas Is called to in the case of level 1 delta changes. It makes sure all deltas get printed out,
474
+ ' Not just the level 1.
475
+ ' </summary>
476
+ Private Shared Sub PrintFinalDeltas(reportTable As HtmlNode, previousNode As HtmlNode, lastDelta As String())
477
+ 'delta takes the lastDelta array And converts it back into a string.
478
+ Dim delta = String.Join(", ", lastDelta)
479
+
480
+ ' This loop keeps running until all of the pertinent information has been pulled from the string.
481
+ While (delta.LastIndexOf(",") > 0)
482
+ 'Since standard subtotals print off the low level delta, this puuls that one out And discards it.
483
+ delta = delta.Substring(0, delta.LastIndexOf(","))
484
+
485
+ 'If the string Is a valid delta, print it out And remove it from the dictionary.
486
+ If (deltas.ContainsKey(delta)) Then
487
+ InjectHtml(reportTable, previousNode, delta)
488
+ deltas.Remove(delta)
489
+ End If
490
+ End While
491
+ End Sub
492
+
493
+ ' <summary>
494
+ ' InjectHtml actually alters the html string, injecting the deltas into it.
495
+ ' </summary>
496
+ Private Shared Sub InjectHtml(reportTable As HtmlNode, currentNode As HtmlNode, deltaString As String)
497
+ 'Build up the table row to inject.
498
+ Dim deltaRow = HtmlNode.CreateNode("<tr class=ReportFooter></tr>")
499
+ deltaRow.AppendChild(HtmlNode.CreateNode("<td>" + deltaString + "</td>"))
500
+
501
+ 'This will loop through the various delta columns for a particular row, adding each as a td to the row.
502
+ If deltas.ContainsKey(deltaString) Then
503
+ For Each column As Object In deltas(deltaString)
504
+ deltaRow.AppendChild(HtmlNode.CreateNode("<td>" + column.ToString() + "</td>"))
505
+ Next
506
+ 'This Is where the row actually gets inserted.
507
+ reportTable.InsertBefore(deltaRow, currentNode)
508
+ End If
509
+ End Sub
510
+
511
+ Private Shared Function SanitizeDelta(delta As String()) As String()
512
+ Dim sanitizedDelta(delta.Length - 1) As String
513
+ For i As Integer = 0 To delta.Length - 1
514
+ If Not String.IsNullOrEmpty(delta(i).Substring(delta(i).IndexOf(":") + 1).Trim()) Then
515
+ sanitizedDelta(i) = delta(i).Substring(delta(i).IndexOf(":") + 2)
516
+ End If
517
+ Next
518
+ Return sanitizedDelta
519
+ End Function
520
+End Class
225 521
```
... ...
\ No newline at end of file