f022bf6e246c57e613659f181671b2450499b5cb
Guides/LayeredSubtotaling.md
| ... | ... | @@ -10,216 +10,512 @@ Layered subtotaling is a customized way to display subtotals that will aggregate |
| 10 | 10 | |
| 11 | 11 |  |
| 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 |