Reporting Service RDLC 報表設計進階議題一枚。
先說情境,假設有技能專長與擅長語言兩個資料表,其中有每個人的資料,想在 RDLC 報表採以下形式呈現:先印出姓名,接著以表格形式分別列出技能清單與語言清單:
這類需求,最直覺有效的做法是使用子報表!很不幸,同事嘗試用子報表解決卻踼到鐵板:明細資料總筆數約 2000 筆,拆成 500 個子報表,產生報表耗時七分鐘,志玲姐姐都護完一生了報表還出不來,想當然爾被使用者狠狠打槍!
查了文獻,有文章指出包太多 SubReport 註定快不起來:(但資料都在記憶體, 500 個子報表慢到七八分鐘讓人意外)
- https://www.mssqltips.com/sqlservertip/3659/sql-server-reporting-services-best-practices-for-perform...
19: Avoid sub reports in Reporting Services
Sub reports are convenient for reuse, but don't perform well when there are many sub report instances during the runtime especially inside a Tablix. Try to avoid the use of sub reports if possible. If drill down reports are needed, consider linked reports to fulfill your requirement. - https://technet.microsoft.com/en-us/library/bb522806(v=sql.105).aspx
Many Instances of Subreports in a Tablix Data Region Slow Report Performance
Understand the advantages and disadvantages of using subreports. Each subreport instance is a separate query execution and a separate report processing task.
總而言之,得想想繞路的方法。我優先想到的武器是 List,以使用者分群,在方格中放入兩個資料表格,一個顯示技能,一個顯示語言,像這樣:
不幸地,再踼到 List 限制的鐵板!List 資料區只能套用單一資料來源:
Because the List contains a grouping level, you can use the List data region only with a single dataset. 參考
最後,想到一個很可恥卻有用的方法-把兩個資料表合併成一個,額外加入 IsSkill 及 IsLang 布林欄位區隔是那一種資料(或用TableSrc string 識別也成),把兩種資料合併在同一個資料表中:
接著,在 Table1 套用 「IsSkill = True」Filter、Table2 套用 「IsLang = true」Filter,就成功在一個 List 顯示兩種資料!(灑花)
最後補上一些實作細節。
首先,將兩個 DataTable 合併成一個應該難不倒大家,但若資料來源是物件陣列,要將多個物件陣列合併成一個 DataTable 就要靠點技巧。分享一則密技:將物件陣列先轉成 JSON 字串,再用 Json.NET JsonConvert.DeserializeObject<DataTable>() 就能瞬間轉成 DataTable。我將合併邏輯寫成 DataTable 型別的擴充方式,合併資料來源則 IEnumerable<object> 與 DataTable 通吃,合併時還要傳入segName 方便加入 IsXXX 欄位。
publicstaticvoid MergeData(this DataTable table, IEnumerable<object> data, string segName)
{
var json = JsonConvert.SerializeObject(data);
var t = JsonConvert.DeserializeObject<DataTable>(json);
table.MergeData(t, segName);
}
publicstaticvoid MergeData(this DataTable table, DataTable toAdd, string segName)
{
var segFldName = $"Is{segName}";
toAdd.Columns.Add(segFldName, typeof(bool));
foreach (DataColumn c in toAdd.Columns)
{
if (!table.Columns.Contains(c.ColumnName))
table.Columns.Add(c.ColumnName, c.DataType);
}
foreach (DataRow row in toAdd.Rows)
{
row[segFldName] = true;
var newRow = table.NewRow();
foreach (DataColumn c in toAdd.Columns)
newRow[c.ColumnName] = row[c.ColumnName];
table.Rows.Add(newRow);
}
}
資料來源範例如下:
publicclass Skill
{
publicstring UserId { get; set; }
publicstring SkillName { get; set; }
publicint Level { get; set; }
public Skill(string userId, string skillName, int level)
{
UserId = userId;
SkillName = skillName;
Level = level;
}
}
publicclass Language
{
publicstring UserId { get; set; }
publicstring Lang { get; set; }
publicint Level { get; set; }
public Language(string userId, string lang, int level)
{
UserId = userId;
Lang = lang;
Level = level;
}
}
publicstaticclass SkillDataStore
{
publicstatic List<Skill> GetSkillData()
{
returnnew List<Skill>()
{
new Skill("Jeffrey", "爆破", 5),
new Skill("Jeffrey", "嘴砲", 4),
new Skill("Jeffrey", "嘲諷", 3),
new Skill("Darkthread", "發廢文", 5),
};
}
publicstatic List<Language> GetLangData()
{
returnnew List<Language>()
{
new Language("Jeffrey", "C#", 4),
new Language("Jeffrey", "JavaScript", 3),
new Language("Darkthread", "T-SQL", 3),
new Language("Darkthread", "PL/SQL", 2)
};
}
}
處理資料時,先建立空白 DataTable,再使用 MergeData() 合併 List<Skill> 及 List<Language>:
DataTable t = new DataTable("Data");
t.MergeData(Models.SkillDataStore.GetSkillData(), "Skill");
t.MergeData(Models.SkillDataStore.GetLangData(), "Lang");
rptViewer.LocalReport.DataSources.Add(
new Microsoft.Reporting.WebForms.ReportDataSource("DataSet1", t));
由於 DataSet 是動態組裝的 DataTable,沒有現成的資料模型範本,設計報表時看不到可用欄位無法拖拉設定。
有兩種解決方法,第一種是手動修改 RDLC XML 加上欄位:
<DataSets>
<DataSetName="DataSet1">
<Query>
<DataSourceName>RDLCTestModels</DataSourceName>
<CommandText>/* Local Query */</CommandText>
</Query>
<Fields>
<FieldName="UserId">
<DataField>UserId</DataField>
<rd:TypeName>System.String</rd:TypeName>
</Field>
<FieldName="SkillName">
<DataField>SkillName</DataField>
<rd:TypeName>System.String</rd:TypeName>
</Field>
<FieldName="Lang">
<DataField>Lang</DataField>
<rd:TypeName>System.String</rd:TypeName>
</Field>
<FieldName="Level">
<DataField>Level</DataField>
<rd:TypeName>System.Int32</rd:TypeName>
</Field>
<FieldName="IsSkill">
<DataField>IsSkill</DataField>
<rd:TypeName>System.Boolean</rd:TypeName>
</Field>
<FieldName="IsLang">
<DataField>IsLang</DataField>
<rd:TypeName>System.Boolean</rd:TypeName>
</Field>
</Fields>
<rd:DataSetInfo>
<rd:DataSetName>DynamicDataTable</rd:DataSetName>
<rd:TableName>Data</rd:TableName>
</rd:DataSetInfo>
</DataSet>
</DataSets>
或者先跑程式取得 DataTable 再匯出 XSD 也成,我選擇手工修改 RDLC XML 了事。
就醬,再度靠著奧步驚險過關~(煙)