Reporting Service RDLC 報表設計進階議題一枚。

先說情境,假設有技能專長與擅長語言兩個資料表,其中有每個人的資料,想在 RDLC 報表採以下形式呈現:先印出姓名,接著以表格形式分別列出技能清單與語言清單:

這類需求,最直覺有效的做法是使用子報表!很不幸,同事嘗試用子報表解決卻踼到鐵板:明細資料總筆數約 2000 筆,拆成 500 個子報表,產生報表耗時七分鐘,志玲姐姐都護完一生了報表還出不來,想當然爾被使用者狠狠打槍!

查了文獻,有文章指出包太多 SubReport 註定快不起來:(但資料都在記憶體, 500 個子報表慢到七八分鐘讓人意外)

總而言之,得想想繞路的方法。我優先想到的武器是 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

最近用到這篇知識,感謝下!

Post a comment