發佈日期: 11/24/2004 | 更新日期: 11/24/2004
Teodor Lachev
適用於: SQL Server 2000 Reporting Services
摘要:本文簡要介紹了由 Teodor Lachev 所著的 Microsoft Reporting Services in Action 一書。瞭解如何使用自定義代碼實現高級報表功能。
下載 Code.zip 示例代碼以獲取本文的示例代碼。
本頁內容
用自定義代碼擴展 Microsoft SQL Server 2000 Reporting Services | |
編寫嵌入式代碼 | |
使用外部程序集 | |
運轉中的自定義代碼:實現報表預測 | |
遷移 OpenForecast | |
小結 |
用自定義代碼擴展 Microsoft SQL Server 2000 Reporting Services
Microsoft 在 2004 年初發布了 Microsoft SQL Server 2000 Reporting Services (Reporting Services),以便爲開發人員提供一個完整的報表平臺,無論目標平臺或開發語言是什麼,它都可以輕鬆地與所有類型的應用程序集成。Reporting Services 最顯著的功能之一是它的可擴展特性,這是包括我在內的許多開發人員所欣賞的。您可以擴展或替換 Reporting Services 的幾乎任何方面,包括數據、傳遞、安全性以及報表呈現功能。例如,擴展您的報表功能的一個方法是將它們與您或其他人編寫的自定義 .NET 代碼集成在一起。
在本文中,我將爲您展示如何利用 Reporting Services 獨特的可擴展體系結構來增強您的報表功能。首先,我將說明嵌入式和自定義代碼選項是如何工作的。其次,我將爲您展示您可以如何利用自定義代碼來編寫帶有銷售預測功能的高級報表。
我將假定您已經具有關於 Reporting Services 的基礎知識,並且知道如何用表達式編寫報表。如果您還不熟悉 Reporting Services,請訪問其官方站點。本文中討論的代碼示例和示例報表均包含在文章源代碼中。示例報表將 AdventureWorks2000 數據庫用作其數據源,該數據庫可以從 Reporting Services 安裝程序中安裝。
編寫嵌入式代碼
顧名思義,嵌入式代碼保存在報表定義 (RDL) 文件中;它的作用範圍在報表層。您只能在 Microsoft Visual Basic .NET 中編寫嵌入式代碼。一旦代碼準備好,您就可以使用全局定義的 Code 成員在報表表達式中調用它。例如,如果您編寫了一個名爲 GetValue 的嵌入式代碼函數,就可以使用下列語法從您的表達式中調用它:
=Code.GetValue()
除了共享方法外,您的嵌入式代碼可以包含任何與 Visual Basic .NET 兼容的代碼。實際上,如果您將嵌入式代碼視爲項目中的私有類,就差不多了。您可以聲明類級別的成員和常數、私有或公共方法等。
您可以編寫嵌入式代碼來創建可重用的實用函數,它們可以從報表的幾個表達式中調用。例如,請考慮圖 1 中所示的 Territory Sales Crosstab 報表。
圖 1. 您可以使用嵌入式代碼來實現作用範圍在報表層的有用實用函數。
當數據缺失時(給定的行列組合中沒有報表數據),該報表會使用一個名爲 GetValue 的嵌入式函數來顯示 “N/A”。此外,GetValue 還將缺失數據與 NULL 值區分開來。當基礎值爲 NULL 時,嵌入式代碼會將其轉換爲零。
使用代碼編輯器
要編寫自定義嵌入式代碼,您可以使用報表設計器代碼編輯器 — 您可以在 Report Properties 對話框的 Code 選項卡上找到它,如圖 2 所示。
圖 2. 使用用於編寫嵌入式代碼的代碼編輯器。編輯器中所示的 GetValue 函數可確定某個值是缺失還是 NULL。
誠然,上述函數可以很容易地由一個基於 Iif 的表達式替換。但是,將邏輯封裝在嵌入式函數中有兩點優勢。首先,它將表達式的邏輯集中在一個地方,而不是在報表中的每個字段都使用 Iif 函數。其次,它使報表具有更好的可維護性,這是因爲如果您決定對函數進行邏輯更改,將不必跟蹤並更改報表中的每個 Iif 函數。
報表設計器將嵌入式代碼保存在報表定義文件的<Code> 元素下。執行此操作時,報表設計器將對文本進行 URL 編碼。如果您出於某些原因決定直接更改 Code 元素,就需要注意這一點。
處理缺失值
一旦 GetValue 函數可以區分報表中的 NULL 和缺失數據,我們就能以下列表達式作爲交叉表報表的 txtSales 和 txtNoOrders 數據字段的基礎:分別爲
=Iif(CountRows()=0, "N/A", Code.GetValue(Sum(Fields!Sales.Value)))
和
=Iif(CountRows()=0, "N/A", Code.GetValue(Sum(Fields!NoOrders.Value)))
。
CountRows 函數是 Reporting Services 所提供的幾個原生函數之一,它可以返回指定範圍內的行數。如果沒有指定範圍,它將默認爲最裏面的範圍,這在我們的示例中解析爲在數據單元格中定義值的靜態組。兩個表達式都會先使用 CountRows 來檢查缺失數據(沒有行),如果未發現缺失數據,將顯示 “N/A”。否則,它們將調用 GetValue 嵌入式函數來轉換 NULL 值。
我推薦您使用嵌入式代碼來編寫簡單的報表專用且類似於實用工具的函數。當您的編程邏輯變得越來越複雜時,請考慮將您的代碼移到外部程序集,我們將在下一步討論這一點。
使用外部程序集
以編程方式擴展報表的第二種方法是使用外部 .NET 程序集中的預打包邏輯,這些程序集可以使用任何 .NET 支持的語言編寫。這種將報表與外部程序集中的自定義代碼集成在一起的能力顯著提高了編程的選擇餘地。例如,通過使用自定義代碼,您可以:
• | 利用 .NET framework 豐富的功能集 — 或示例,比如說您需要一個集合來存儲某一矩陣區域的交叉表數據以執行一些計算。您可以“借用”.NET 隨附的任何集合類,如 Array、ArrayList、Hashtable 等。 |
• | 將您的報表與您或第三方供應商編寫的自定義 .NET 程序集集成在一起。例如,爲了向第二部分中的 Sales by Product Category 報表添加預測功能,我使用了開放源碼的 OpenForecast 包。 |
• | 通過使用功能強大的 Visual Studio .NET IDE 而不是原始的代碼編輯器,編寫代碼變得更加簡單了。 |
引用外部程序集
要使用位於外部程序集中的類型,您必須先使用 Report Properties 對話框中的 References 選項卡讓報表設計器知道它,如圖 3 所示。
圖 3. 使用“Report Properties”對話框來引用外部程序集。
假定我的報表需要使用自定義的 AWC.RS.Library 程序集(包含在文章的源代碼中),我必須首先使用 References 選項卡引用它。雖然這個選項卡允許您瀏覽並引用任意文件夾中的程序集,但請注意,當報表被執行時,.NET 公共語言運行庫 (CLR) 將根據 CLR 探測規則嘗試定位程序集。簡單地說,這些規則爲您提供了兩個部署自定義程序集的選項:
• | 將程序集部署爲私有程序集。 |
• | 將程序集部署爲 .NET 全局程序集緩存 (GAC) 中的共享程序集。作爲一個先決條件,您必須強命名您的程序集。有關如何執行此操作的詳細信息,請參閱 .NET 文檔。 |
如果您選擇了第一個選項,則需要將程序集同時部署到報表設計器和報表服務器中,這樣引用該程序集的報表將在測試期間順利執行,而且是作爲託管報表分別執行。假定您已經接受了默認的安裝設置,那麼如果要將程序集部署到報表設計器二進制文件夾中,請將程序集複製到 C:/Program Files/Microsoft SQL Server/80/Tools/Report Designer 中。一旦您完成此操作,就可以構建報表並在 Visual Studio .NET 的預覽模式中呈現它。
作爲將報表部署到報表目錄的一部分,請確保您將程序集複製到報表服務器二進制文件夾中,其默認位置是 C:/Program Files/Microsoft SQL Server/MSSQL/Reporting Services/ReportServer/bin。
請注意,將自定義程序集複製到正確位置只是部署過程的一部分。根據代碼的任務,您可能還需要調整代碼訪問安全策略,以便程序集代碼可以順利執行。如果您需要有關部署自定義程序集的詳細信息,請參閱 Reporting Services 文檔中的“Using Custom Assemblies with Reports”部分。
調用共享方法
如果您只需要調用程序集中的共享方法(在 C# 中也稱爲靜態方法),那麼就可以這麼做了,這是因爲共享方法在報表中全局可用。
您可以通過下列語法,用完全限定的類型名稱來調用共享方法:
<Namespace>.<Type>.<Method>(argument1, argument2, ..., argumentN)
例如,如果我需要從一個表達式或嵌入式代碼中調用 RsLibrary 類(AWC.RS.Library 程序集)中的 GetForecastedSet 共享方法,我會使用下列語法:
=AWC.Reporting Services.Library.RsLibrary.GetForecastedSet(forecastedSet, forecastedMonths)
其中,AWC.RS.Library 是命名空間,RsLibrary 是類型,GetForecastedSet 是方法,還有 forecastedSet 和 forecastedMonths 是參數。
調用實例方法
要調用實例方法,您還有一些額外的工作要做。首先,您必須枚舉需要在 Classes 網格中實例化的所有實例類(類型)。對於每個類,您都必須指定一個實例名稱。在後臺,Reporting Services 將創建一個具有該名稱的變量,以保存對此類型實例的引用。
當您在 Classes 網格中指定類名稱時,請確保您輸入的是完全限定的類型名稱(包含命名空間)。在我的示例中(圖 3),命名空間是 AWC.RS.Library,而類名稱是 RsLibrary。如果您不確定完全限定類名稱是什麼,請使用 Visual Studio .NET Object Browser 或其他實用工具(如出色的 Lutz Roeder 的 .NET Reflector)來定位類名稱並查找其命名空間。
例如,假定我需要調用 AWC.RS.Library 程序集中的一個實例方法,那麼現在我必須聲明一個實例變量 m_Library,如圖 3 所示。在我的示例中,這個變量將保存對 RsLibrary 類的引用。
如果您要聲明多個指向同一類型的變量,則每個變量都需要引用一個該類型的單獨實例。在後臺,當報表被處理時,Reporting Services 將實例化和實例變量數量一樣多的所引用類型的實例。
一旦完成引用設置,您就可以通過所指定的實例類型名稱來調用實例方法。就像使用嵌入式代碼一樣,您可以使用 Code 關鍵字來調用實例方法。共享方法和實例方法之間的區別是您使用變量名稱來調用方法,而不是使用類名稱。
例如,如果 RsLibrary 類型具有一個實例方法 DummyMethod(),我就能從一個表達式或嵌入式代碼中調用它,如下所示:
Code.m_Library.DummyMethod()
瞭解了我們作爲開發人員,以編程方式擴展報表功能時所使用的選擇之後,下面看一下如何將其付諸實踐。在下一部分中,我們將瞭解如何使用嵌入式代碼和外部代碼向我們的報表中添加高級功能。
運轉中的自定義代碼:實現報表預測
在本部分中,我將向您展示如何在我們的報表中植入預測功能。下面是我們將要創建的示例報表的設計目標:
• | 讓用戶可以生成任意階段的銷售數據交叉表報表。 |
• | 讓用戶可以指定預測列的數量。 |
• | 使用數據外推法來預測銷售數據。 |
以下是我們的虛擬案例。假設您的用戶請求了一個報表,以顯示按產品類別分組的 Adventure Works 月度預測銷售數據。爲了讓事情變得更加有趣,我們將允許報表用戶指定一個數據範圍來篩選銷售數據以及預測月份的數量。爲了實現上述要求,我們將編寫一個交叉表報表 Sales by Product Category,如圖 4 所示。
圖 4. Sales by Product Category 使用嵌入式代碼和外部自定義代碼進行預測。
用戶可以輸入起止日期來篩選銷售數據。此外,用戶可以指定報表上將顯示多少個月的預測數據。報表以交叉表方式顯示數據,在行上顯示產品類別,在列上顯示時間。報表的數據部分首先顯示所請求時間段內的實際銷售額,後面以粗體顯示預測銷售額。
例如,如果用戶輸入 4/30/2003 作爲起始日期,輸入 3/31/2004 作爲截止日期,然後請求查看三個預測月,那麼報表將顯示 2004 年 4 月、5 月和 6 月的預測數據(爲節省空間,圖 4 只顯示了一個月的預測數據)。
您可能也承認,獨自實現預測功能並不是一件簡單的任務。但如果已經有了爲我們執行此操作的預打包代碼,又會怎麼樣呢?如果這個代碼可以運行在 .NET 上,我們的報表就可以將其作爲自定義代碼來訪問。輸入 OpenForecast。
用 OpenForecast 來預測
預測本身就是一門科學。一般來說,預測關心的是用於預言未知的過程。預測專業人員使用數學模型來分析數據、發現趨勢並作出有根據的推斷,而不是去看水晶球。在我們的示例中,Sales by Product Category 報表將通過數據外推方法來預測未來的銷售數據。
用來外推一組數據的衆所周知的數學模型有很多,如多項式迴歸、簡單指數平滑法等。但是,實現這些模型並不是一件簡單的任務。相反,爲了我們的銷售預測示例,我們將使用由 Steven Gould 編寫的出色的開放源碼 OpenForecast 包。OpenForecast 是一個包含基於 Java 的預測模型的通用軟件包,這些模型可以應用於任何數據系列。這個軟件包不要求您瞭解任何預測知識,並且支持幾個數學預測模型,包括單變量線性迴歸、多變量線性迴歸等。要了解有關 OpenForecast 的詳細信息,請訪問它的主頁 http://OpenForecast.sourceforge.net/。
現在,讓我們看一下如何實現預測示例,並通過編寫一些嵌入式代碼和外部代碼與 OpenForecast 集成。
實現報表預測功能
創建一個具有預測功能的交叉表報表需要以下幾個實現步驟。讓我們從一個預想方法的高級視圖開始,然後向下追溯到實現細節。
選擇一種實現方法
圖 5 顯示瞭解決方案的邏輯體系結構視圖。
圖 5. Sales by Product Category 報表使用嵌入式代碼來調用 AwRsLibrary 程序集,接着調用 J# OpenForecast 包。
我們的報表將使用嵌入式代碼來調用一個自定義程序集 (AwRsLibrary) 中的共享方法,並獲得預測數據。AwRsLibrary 會將現有的銷售數據加載到一個 OpenForecast 數據集中,並從 OpenForecast 獲得預測模型。然後,它將調用 OpenForecast 來獲取所請求月份的預測值。AwRsLibrary 將預測數據返回給報表,然後再顯示它。
我們至少有兩種實現選擇可以將交叉表銷售數據傳遞到 AwRsLibrary。
• | 再次從數據庫中獲取銷售數據。要實現這一點,報表可以按行傳遞選定的產品類別和月銷售額。然後,AwRsLibrary 可以進行數據庫調用以檢索匹配的銷售數據。 |
• | 使用報表內部的嵌入式代碼將現有銷售數據加載到某種類型的結構中,並將該結構傳遞到 AwRsLibrary。 |
後一種方法的優點是:
• | 自定義代碼邏輯是獨立的。我們不必再次查詢數據庫。 |
• | 使用默認的自定義代碼安全策略。我們不必爲 AwRsLibrary 程序集提高默認的代碼訪問安全策略。如果我們選擇了第一個選項,就不能略過默認代碼訪問安全設置,這是因爲 Reporting Services 將只授予自定義程序集“執行”的權力,而這對於進行數據庫調用是不夠的。實際上,對於 OpenForecast,我必須對兩個程序集都授予完全信任的權力,這是因爲任何 J# 代碼都需要完全信任權力來順利執行。但是,如果我選擇 C# 作爲編程語言,就不必再執行此操作了。 |
• | 無需數據同步。我們不必考慮同步兩個數據容器、矩陣區域和 AwRsLibrary 數據集。 |
出於上述原因,我選擇了第二種方法。爲了實現此方法,我們將使用一個表達式來填充矩陣區域數據值。該表達式將調用我們的嵌入式代碼以加載一個數組結構,該數組結構是在嵌入式代碼中按行來維護的。一旦給定的行被加載,我們就將該數組傳遞到 AwRsLibrary 以獲得預測數據。
現在,讓我們從將 OpenForecast 轉換到 .NET 開始來討論實現細節。
遷移 OpenForecast
OpenForecast 是用 Java 編寫的,因此我必須克服的第一個障礙就是將其與 .NET 集成在一起。我有兩個選擇:
• | 我可以使用一個第三方的 Java 到 .NET 網關來集成兩個平臺。由於這種方法的複雜性,我很快就放棄了它。 |
• | 將 OpenForecast 轉換到支持 .NET 的語言之一。Microsoft 爲此提供了兩個選擇。第一,您可以使用 Microsoft Java Language Conversion Assistant 將 Java 語言代碼轉換到 C#。第二,我可以將 OpenForecast 轉換到 J#。這會保留 Java 語法,儘管該代碼將在 .NET 公共語言運行庫(而不是 Java 虛擬機)的控制下執行。 |
我決定將 OpenForecast 轉換到 J#。這種方法附帶的好處是,開放源碼開發人員可以只維護一個基於 Java 的 OpenForecast 版本。
將 OpenForecast 轉換到 J# 比我想象的要簡單。我創建了一個新的 J# 庫項目,將其命名爲 OpenForecast,然後在其中加載了所有 *.java 源文件。我在源代碼中包含了 .NET 版本的 OpenForecast,本文隨附有該源代碼。我只須顧及幾個 MultipleLinearRegression 中的編譯錯誤,實際結果是,有幾個 Java 哈希表方法在 J# 中不受支持,如 keySet()、entries() 以及哈希表克隆。我還包含了一個 WinForm 應用程序 (TestHarness),您可以使用它來測試轉換後的 OpenForecast。
實現 AwRsLibrary 程序集
下一步是創建自定義的 .NET 程序集 AwRsLibrary,它將跨接報表嵌入式代碼和 OpenForecast。我將 AwRsLibrary 作爲一個 C# 類庫項目來實現。在其中,我創建了一個類 RsLibrary,它公開了一個靜態(共享)方法 GetForecastedSet。該方法的 AwRsLibrary 代碼包含在本文的示例代碼中。
GetForecastedSet 方法以數據集數組的形式接收給定產品類別的現有銷售數據,以及對預測數據請求的月數。接着,集成 OpenForecast 有五個步驟:
步驟 1:首先,我們創建一個新的 OpenForecast 數據集,並用來自矩陣行數組的現有數據加載它。
步驟 2:接下來,我們獲得一個給定的預測模式。OpenForecast 可讓開發人員通過調用 getBestForecast 方法,來根據給定數據系列獲得最佳的預測數學模型。該方法將檢查數據集並嘗試幾個預測模型,以便選擇最理想的一個。如果返回的模型不是很合適,您可以通過實例化在模型項目文件夾下找到的任何類來顯式地請求一個預測模型。
步驟 3:接下來,我們準備另一個數據集來保存預測數據,並使用與預測月數一樣多的元素來初始化該數據集。
步驟 4:最後,我們調用 forecast 方法來外推數據並返回預測結果。
步驟 5:剩下的最後一件事是將預測數據加載回數據集數組中,以便我們能夠將其傳回報表嵌入式代碼。
在我們完成了 AwRsLibrary 和 OpenForecast 兩個 .NET 程序集之後,就需要部署它們。
部署自定義程序集
我們需要將自定義程序集同時部署到報表設計器和報表服務器的二進制文件夾中。自定義程序集的部署過程由下列步驟組成:
• | 將程序集複製到報表設計器和報表服務器的二進制文件夾中。 |
• | 如果自定義代碼需要一個提高的代碼訪問安全權限集,則需要調整基於代碼的安全性。 |
要讓 AwRsLibrary 和 OpenForecast 兩個程序集在設計時都可用,我們必須將 AWC.RS.Library.dll 和 OpenForecast.dll 複製到報表設計器文件夾中,其默認位置是 C:/Program Files/Microsoft SQL Server/80/Tools/Report Designer。
同樣,要在報表服務器下順利呈現已部署的報表,我們必須將兩個程序集都部署到報表服務器的二進制文件夾中,其默認位置是 C:/Program Files/Microsoft SQL Server/MSSQL/Reporting Services/ReportServer/bin。事實上,如果所引用的自定義程序集尚未全部部署好,報表服務器將不會讓您從 Visual Studio .NET IDE 中部署報表。
默認的 Reporting Services 代碼訪問安全策略在默認情況下對所有自定義程序集授予執行權限。但是,J# 程序集需要完全信任的代碼訪問權限。因爲 .NET 公共語言運行庫沿調用堆棧向上遍歷來驗證所有調用方都具有必要的權限集,所以我們需要將兩個程序集的代碼訪問安全策略提升到完全信任級別。這將需要對報表設計器和報表服務器的安全配置文件進行更改。
爲了幫助您設置代碼訪問安全策略,我提供了我的 Config 文件夾中的 rssrvpolicy.config 的副本。在接近文件末尾的地方,您將看到兩個 CodeGroup XML 元素,分別指向 AwRsLibrary 和 OpenForecast 文件。您需要將這些元素複製到報表服務器的安全配置文件 (rssrvpolicy.config) 中。
此外,如果您希望在報表設計器的預覽窗口中預覽(運行)報表,那麼還需要將這些更改傳播到報表設計器的安全配置文件 (rspreviewpolicy.config) 中。
在自定義程序集部署之後,我們需要在報表中編寫一些 Visual Basic .NET 嵌入式代碼來調用 AwRsLibrary 程序集,此內容將在下一部分中進行討論。
編寫報表嵌入式代碼
要將報表與 AwRsLibrary 集成,我編寫了 GetValue 函數,如清單 2 所示。
清單 2. 嵌入式 GetValue 函數調用 AwRsLibrary 程序集
Dim forecastedSet() As Double ' array with sales data Dim productCategoryID As Integer = -1 Dim bNewSeries As Boolean = False Public Dim m_ExString = String.Empty ' holds the error message, if any Function GetValue(productCategoryID As Integer, orderDate As DateTime, sales As Double, reportParameters as Parameters, txtRange as TextBox) As Double Dim startDate as DateTime = reportParameters!StartDate.Value Dim endDate as DateTime = reportParameters!EndDate.Value Dim forecastedMonths as Integer = reportParameters!ForecastedMonths.Value If (forecastedSet Is Nothing) Then ReDim forecastedSet(DateDiff(DateInterval.Month, startDate, endDate) + forecastedMonths) #1 End If If Me.productCategoryID <> productCategoryID Then #2 Me.productCategoryID = productCategoryID bNewSeries = True Array.Clear(forecastedSet, 0, forecastedSet.Length - 1) End If Dim i = DateDiff(DateInterval.Month, startDate , orderDate) ' Is this a forecasted value? If orderDate <= endDate Then ' No, just load the value in the array forecastedSet(i) = sales Else If bNewSeries Then Try AWC.RS.Library.RsLibrary.GetForecastedSet(forecastedSet, forecastedMonths) #3 bNewSeries = False Catch ex As Exception m_ExString = "Exception: " & ex.Message System.Diagnostics.Trace.WriteLine(ex.ToString()) throw ex End Try End If End If Return forecastedSet(i) End Function
因爲矩陣區域數據單元格使用了一個引用 GetValue 函數的表達式,所以這個函數可由每個數據單元格調用。表 1 列出了 GetValue 函數所採用的輸入參數。
表 1. 矩陣區域中的每個數據單元格都將調用 GetValue 嵌入式函數,並傳遞下列輸入參數。 | |
參數 | 用途 |
productCategoryID | 與單元格相對應的 rowProductCategory 行分組的 ProductCategoryID 值。 |
orderDate | 與單元格相對應的 colMonth 列分組的 OrderDate 值。 |
sales | 該單元格的合計銷售總數。 |
reportParameters | 爲了計算數組維度,GetValue 需要報表參數的值。我傳遞了一個對報表參數集合的引用,而不是使用 Parameters!ParameterName.Value 個別地傳遞參數。 |
txtRange | 保存錯誤消息的變量,以防在獲取預測數據時發生異常。 |
要了解 GetValue 如何工作,請注意矩陣區域內的每個數據單元格都是來自 forecastedSet 數組的。如果單元格不需要預測(它的相應日期處於請求日期範圍內),則我們只需在數組中加載單元格的值,並將其傳回以便在矩陣區域中顯示它。爲了實現此操作,我們需要初始化該數組以獲得一個等於請求月數加預測月數的秩。一旦矩陣區域移動到一個新的行並調用我們的函數後,我們就可以通過調用 AwRsLibrary:GetForecastedSet 方法來預測數據了。
實現 Sales by Product Category 交叉表報表
編寫報表本身最困難的部分就是設置它的數據,以確保我們在矩陣區域中始終通過正確的列數來顯示預測的列。默認情況下,矩陣區域將不顯示沒有數據的列。這會擾亂從數組傳送到單元格的正確偏移量的計算。
因此,我們必須確保數據庫返回請求數據範圍內所有月份的記錄。要實現這一點,我們需要在數據庫中預處理銷售數據。這正是 spGetForecastedData 存儲過程的任務。在這個存儲過程中,我用請求數據範圍內的所有月度週期預填充了一個自定義表,如清單 3 所示。
清單 3. spGetForecastedData 存儲過程確保返回的行集合具有正確的列數
CREATE PROCEDURE spGetForecastedData ( @StartDate smalldatetime, @EndDate smalldatetime ) AS DECLARE @tempDate smalldatetime DECLARE @dateSet TABLE ( #1 ProductCategoryID tinyint, OrderDate smalldatetime ) SET @tempDate = @EndDate WHILE (@StartDate <= @tempDate) #2 BEGIN INSERT INTO @dateSet SELECT ProductCategoryID, @tempDate FROM ProductCategory SET @tempDate = DATEADD(mm, -1, @tempDate) END SELECT DS.ProductCategoryID, PC.Name as ProductCategory, OrderDate AS Date, NULL AS Sales FROM @dateSet DS INNER JOIN ProductCategory PC ON DS.ProductCategoryID=PC.ProductCategoryID UNION ALL #3 SELECT PC.ProductCategoryID, PC.Name AS ProductCategory, SOH.OrderDate AS Date, SUM(SOD.UnitPrice * SOD.OrderQty) AS Sales FROM ProductSubCategory PSC INNER JOIN ProductCategory PC ON PSC.ProductCategoryID = PC.ProductCategoryID INNER JOIN Product P ON PSC.ProductSubCategoryID = P.ProductSubCategoryID INNER JOIN SalesOrderHeader SOH INNER JOIN SalesOrderDetail SOD ON SOH.SalesOrderID = SOD.SalesOrderID ON P.ProductID = SOD.ProductID WHERE (SOH.OrderDate BETWEEN @StartDate AND @EndDate) GROUP BY SOH.OrderDate, PC.Name, PC.ProductCategoryID ORDER BY PC.Name, OrderDate
最後,我用可獲取銷售數據的實際 Transact-SQL 語句聯合了表 @dateSet(其 Sales 列值設置爲 NULL)的所有記錄。
設置數據集之後,編寫報表的其餘部分就簡單多了。我們對報表的交叉表部分使用了一個矩陣區域。要了解矩陣區域魔法是如何工作並調用嵌入式 GetValue 函數的,您可能要用下列表達式替換 txtSales 文本框的表達式:
圖 6 顯示了在應用該表達式時,Sales by Product Category 的外觀。
圖 6. 矩陣區域如何聚合數據。
如您所見,我們可以輕鬆地獲得相應的行和列組值,矩陣區域將使用這些值來計算區域數據單元格中的聚合值。現在,我們有一種方法來標識每個數據單元格。矩陣區域的設置如表 2 所示。
表 2. 用預測值填充矩陣區域的竅門是讓其數據單元格以表達式爲基礎。 | ||
矩陣區域 | 名稱 | 表達式 |
行 | rowProductGroup | =Fields!ProductCategory.Value |
列 | colYear colMonth | =Fields!Date.Value.Year =Fields!Date.Value.Month |
數據 | txtSales | =Code.GetValue(Fields!ProductCategoryID.Value, Fields!Date.Value, Sum(Fields!Sales.Value), Parameters, ReportItems!txtRange) |
要實現預測列(以粗體顯示)的條件格式,我對 txtSales 文本框的字體屬性使用了下列表達式:
=Iif(Code.IsForecasted(Fields!Date.Value, Parameters!EndDate.Value), "Bold", "Normal")
該表達式調用了報表嵌入式代碼中的 IsForecasted 函數。該函數僅僅將銷售月度日期與所請求的截止日期相比較,如果銷售日期在截止日期之前,則返回 false。
最後,剩下的最後一件事情就是使用報表的 References 選項卡來引用 AwRsLibrary 程序集,如我們在圖 3 中看到的那樣。請注意,對於該報表,我們不需要設置實例名稱(不需要在 Classes 網格中輸入任何內容),這是因爲我們不調用任何實例方法。
調試自定義代碼
您可能發現調試自定義代碼比較有挑戰性。因爲這個原因,我想與您分享幾個對於調試自定義代碼比較有用的技術。
對於調試嵌入式代碼來說,沒有太多的選擇。到目前爲止,我所發現的唯一方法就是:當報表在報表設計器中呈現時,使用 MsgBox 函數來輸出消息和變量值。確保在將報表部署到報表服務器之前刪除對 MsgBox 的調用。如果您沒有刪除,則所有 MsgBox 調用都將導致異常。因爲某些原因,在嵌入式代碼內使用 System.Diagnostics.Trace (OutputDebugString API) 跟蹤消息會導致“被吞沒”,並且既不會在 Visual Studio .NET 輸出窗口中顯示,也不會在使用一個外部跟蹤工具時顯示。
在使用外部程序集時,您至少有兩個調試選擇:
• | 輸出跟蹤消息。 |
• | 使用 Visual Studio .NET 調試器來逐句通過自定義代碼。 |
跟蹤
例如,在 AwRsLibrary.GetForecastedSet 方法中,我正在使用 System.Dianogistics.Trace.WriteLine 輸出跟蹤消息,以顯示觀察值和預測值。要想在 Visual Studio .NET 或報表服務器中運行報表時查看這些消息,您可以使用 Mark Russinovich 開發的出色的 DebugView 工具,如圖 7 所示。
圖 7. 在 DebugView 中輸出來自外部程序集的跟蹤消息。
調試自定義代碼
您還可以通過將 Visual Studio .NET 調試器附加到報表設計器進程,來逐句通過自定義程序集代碼,如下所示:
• | 在 Visual Studio .NET 的一個新實例中打開您要調試的自定義程序集。像往常一樣在您的代碼中設置斷點。 |
• | 在您的自定義程序集項目屬性中,選擇 Configuration Properties->Debugging,然後將 Debug Mode 設置爲 Wait to Attach to an External Process。 |
• | 在 Visual Studio .NET 的另一個實例中打開您的商業智能項目。 |
• | 返回到自定義程序集項目,單擊 Debug 菜單,然後單擊 Processes...。找到承載商業智能項目的 devevn 進程,並附加到它上面。在 Attach To Process 對話框中,確保 Common Language Runtime 複選框已選中,然後單擊 Attach。此時,您的 Processes 對話框應該類似於圖 8 中所示的對話框。 圖 8. 要調試自定義程序集,應連接到承載商業智能項目的 Visual Studio 實例。 |
在我的示例中,我想在 Sales by Product Category 報表調用 AwRsLibrary 程序集時,調試其中的代碼。出於這個原因,我在 AwRsLibrary 項目中連接到 AWReporter devenv 進程。
• | 在商業智能項目中,預覽調用自定義程序集的報表。或者,如果您已經在預覽報表,則單擊 Preview Tab 工具欄上的 Refresh Report 按鈕。此時,您的斷點應該被 Visual Studio .NET 調試器發現。 |
您很快就會發現,如果需要更改代碼並重新編譯自定義程序集,則在嘗試將其重新部署到報表設計器文件夾時,會導致下列異常:
Cannot copy <assembly name>: It is being used by another person or program.
問題在於 Visual Studio .NET IDE 保留了對自定義程序集的引用。您需要關閉 Visual Studio .NET,然後重新部署新的程序集。要避免此問題,您可以使用 Report Host(預覽窗口)來調試自定義程序集代碼。要進行此操作,請執行以下步驟:
• | 將自定義程序集添加到包含商業智能項目的 Visual Studio .NET 解決方案中。 |
• | 將商業智能項目的起始項更改爲調用自定義代碼的報表,如圖 9 所示。 圖 9. 使用 Report Host 調試選項來避免鎖定程序集。 |
• | 按下 F5 在預覽窗口中運行報表。當報表調用自定義代碼時,您的斷點將被捕獲。 |
在使用預覽窗口方法時,Visual Studio .NET 不會鎖定自定義程序集。這可讓您將程序集的生成位置更改到報表設計器文件夾,以便在您重新生成程序集時,它始終包含最新的副本。在預覽窗口中運行項目,是報表設計器配置文件 (rspreviewpolicy.config) 中指定的代碼訪問安全策略設置的一個主題。
小結
在本文中,我們瞭解到如何將報表與我們或其他人編寫的自定義代碼集成在一起。
對於簡單的報表專用編程邏輯,可以使用嵌入式 Visual Basic .NET 代碼。當代碼的複雜性增加或者您希望使用 Visual Basic .NET 以外的編程語言時,可以將您的代碼移到外部程序集中。
使用自定義代碼只是開發人員擴展 Reporting Services 的幾種方法之一。要了解有關 Reporting Services 擴展性的詳細信息,請參閱 Reporting Services 聯機叢書的“Extending Reporting Services”部分。