Reporting Service 報表 List 區塊使用多資料表
10 | 14,240 |
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 欄位。
public static void MergeData(this DataTable table, IEnumerable<object> data, string segName)
{
var json = JsonConvert.SerializeObject(data);
var t = JsonConvert.DeserializeObject<DataTable>(json);
table.MergeData(t, segName);
}
public static void 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);
}
}
資料來源範例如下:
public class Skill
{
public string UserId { get; set; }
public string SkillName { get; set; }
public int Level { get; set; }
public Skill(string userId, string skillName, int level)
{
UserId = userId;
SkillName = skillName;
Level = level;
}
}
public class Language
{
public string UserId { get; set; }
public string Lang { get; set; }
public int Level { get; set; }
public Language(string userId, string lang, int level)
{
UserId = userId;
Lang = lang;
Level = level;
}
}
public static class SkillDataStore
{
public static List<Skill> GetSkillData()
{
return new List<Skill>()
{
new Skill("Jeffrey", "爆破", 5),
new Skill("Jeffrey", "嘴砲", 4),
new Skill("Jeffrey", "嘲諷", 3),
new Skill("Darkthread", "發廢文", 5),
};
}
public static List<Language> GetLangData()
{
return new 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>
<DataSet Name="DataSet1">
<Query>
<DataSourceName>RDLCTestModels</DataSourceName>
<CommandText>/* Local Query */</CommandText>
</Query>
<Fields>
<Field Name="UserId">
<DataField>UserId</DataField>
<rd:TypeName>System.String</rd:TypeName>
</Field>
<Field Name="SkillName">
<DataField>SkillName</DataField>
<rd:TypeName>System.String</rd:TypeName>
</Field>
<Field Name="Lang">
<DataField>Lang</DataField>
<rd:TypeName>System.String</rd:TypeName>
</Field>
<Field Name="Level">
<DataField>Level</DataField>
<rd:TypeName>System.Int32</rd:TypeName>
</Field>
<Field Name="IsSkill">
<DataField>IsSkill</DataField>
<rd:TypeName>System.Boolean</rd:TypeName>
</Field>
<Field Name="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 了事。
就醬,再度靠著奧步驚險過關~(煙)
2017-09-04 更新:補充實測數據,以奧步取代子報表後,同事的報表批次產生時間由近兩小時縮短成一分鐘完成,特此記錄。
Comments
# by Ken
對reporting service不熟,純就C#討論的話,結構有機會用同一個class打死,(如下) merge的動作應該會簡單多了。 -- public string UserId { get; set; } public string ContentName { get; set; } public int Level { get; set; } public bool filter {get; set;} // table1=true; table2=false; 日後還有其他種的話,再把filter的datatype換成int做分類用?
# by 資淺工程師
黑暗大, RDLC的Data Source不是可以用自己定義的Class https://stackoverflow.com/questions/27695864/using-net-class-as-the-datasource-with-ssrs-rdlc
# by Jeffrey
to 資淺工程師,是的,RDLC可以使用物件陣列當成資料來源(順便推一下舊文: http://blog.darkthread.net/post-2017-06-02-rdlc-with-objectdatasource.aspx ),此處支援 DataTable 是因為我們的專案仍大量使用 ADO.NET 讀取資料,支援 DateTable 可兩邊通吃。
# by Jeffrey
to Ken, 方法可行,但實務上資料物件常自其他程式庫、Web API,為此鋸箭做法修改資料物件介面怪怪的,故我仍傾向將新増識別欄位邏輯放在 RDLC 產生階段,更符合觀注點分離原則。
# by Ken
黑大,其實我的想法比較取巧,我並非想修改資料物件介面,而是類似把資料包裝成一個ViewModel的概念給報表用,那麼不論資料物件是從其他程式庫、Web API或其他未知的來源而來,只要最終都能化成同樣的ViewModel,報表都只要專注套用此model就好,除非該報表的樣式或呈現的資訊有異動才需要再修改此model。 這樣是否仍未臻理想?
# by Jeffrey
to Ken, 切出ViewModel可行。我自己這麼拿捏:若此ViewModel從頭到尾只用在單一報表,且資料來源固定不會切換,則花功夫建立專屬ViewModel的效益只有可讀性較高及方便產生欄位清單,感覺不夠划算,在這類情境下我傾向用DataTable打發較省事,不過這屬於個人偏好,僅供參考。除了要花功夫宣告、維護,我沒想到ViewModel有什麼缺點,若使用ViewModel還有其他優勢或考量,甚至就只是覺得順眼或開心,都是使用它的好理由。
# by 路人
我是菜鳥,想請問實作要新增什麼專案呢? 謝謝。
# by 路人
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)); 請問這一段是加在mail裡面嗎? 但加上去之後Models, rptViewer, 跟 WebForm 都有紅色 波浪 請問我是少加什麼library嗎? 謝謝。
# by Jeffrey
to 路人, 請先參考這篇入門 http://blog.darkthread.net/post-2017-05-28-rdlc-in-vs2017.aspx
# by ByTIM
最近用到這篇知識,感謝下!