2013年11月13日 星期三

[Crystal Report] 新手入門包 - 加參數

其實從第一集開始,應該可以看出一些端倪:我們會變動的資料都是從DataSet那裡來的。
不過有的時候我們的需求可沒有這麼簡單。

常見的情況還有……我們的標題會有條件式的變動、我們的報表頭有一些要從外部自由輸入的字串、報表尾巴有一些什麼統計數據會從外部輸入的數字來做加加減減的,甚至是報表中間的細目的某某欄位是從外部輸入的參數與其它欄位加加減減出來的。

所以我們會需要一個可以從外部輸入參數的方法。

--我們先來看報表--

在報表上其實很簡單:左邊那個欄位總管一點開,裡面有個參數欄位,在它上面點右鍵按新增,就可以加參數欄位啦。
通常我在操作這個動作的時候通常是做顯示用的而已,沒有運算,只會去改它的名稱。如果要拿它來加加減減做統計的話要把類型從字串改成數字。

然後就看你那個參數要加多少有多少。


做到這兒,再把參數欄位拉到報表就可以了。

--再來看看程式怎麼弄--


直接拿下面的程式去貼就好了。
下面的程式可以放在Utils裡面,需要的時候Call一下就有了。
其中:Report就是我們前一集提到的ReportDocument;ParameterName就是你在報表裡設定的參數名稱;Value就是這個參數的值。
    ''' 
    ''' 設定報表裡的靜態參數
    ''' 
    ''' ReportDocument
    ''' 參數名稱
    ''' 參數值
    ''' 
    Public Shared Sub SetParameter(ByRef Report As ReportDocument, ByVal ParameterName As String, ByVal Value As Object)
        '這段程式直接從其它地方原封不動抄過來。如果有可有效或可讀性更高的寫法,歡迎修改。
        'Add by Break 2013/09/18

        Dim crParameterFieldDefinitions As ParameterFieldDefinitions = Report.DataDefinition.ParameterFields
        Dim pfd As ParameterFieldDefinition = crParameterFieldDefinitions(ParameterName)
        Dim pdv As ParameterDiscreteValue = New ParameterDiscreteValue
        pdv.Value = Value
        Dim pv As ParameterValues = New ParameterValues
        pv.Add(pdv)
        pfd.ApplyCurrentValues(pv)
    End Sub

什麼時候呼叫它呢?我自己是在宣告ReportDocument、Set完DataSource之後和輸出報表之前。供大家參考:)。

2013年11月11日 星期一

[Crystal Report] 新手入門包 - 套資料

上一集講到Crystal Report的報表檔怎麼完成。

這一集就說說怎麼把正確的資料丟到報表檔呈現出來。
這是我自己常用的方式,也把它做成抽象類別,如果有人有更好或更方便的方法,也請不吝指教。

--套資料--

整體來說,我們可以把步驟分作幾個抽象動作:


  1. 我們要先從資料庫把資料找出來
  2. 把找出來的資料,依照自己的格式丟到自己在做的DataSet
  3. 宣告一個ReportDocument,路徑、DataSource設定好。
  4. 就可以輸出了。

1. 從資料庫裡撈資料

端看個人習慣使用什麼方式在做資料庫的存取。
我的考量是:報表通常都會輸出一些不同table在JOIN來JOIN去的結果,所以就算系統裡有使用Entity Framework之類的物件關聯式資料庫,在查報表的時候我還是習慣直接寫SQL Script。
不管怎麼樣,輸出是一個DataTable還是SqlDataReader都好,只要能正確把資料讀出來、正確了解各個欄位的定義就好。
Dim Command as SqlCommand = New SqlCommand()
Command.Connection = new SqlConnection(ConnectionString)
Command.CommandText = "SELECT * FROM [TableName] " + vbNewLine 'SQL Script
Command.Connection.Open()
Dim SearchResult as DataTable = CommandToDataTable(Command)

2. 整理格式,丟到DataSet

這裡指的DataSet是指在做報表時做的那個自訂DataSet。
有的時候我們從資料抓出來的資料會需要做一些格式的轉換才會顯示在頁面上,就在這個階段的來完成。
我個人在這裡常做的事是把存在DB裡的7碼民國年把年月日用斜線隔開,或是把代碼檔轉換成中文。
Dim dtReturn As dtDepartmentDocRegisteredList = New dtDepartmentDocRegisteredList()
For Each row As DataRow In SearchResult.Rows
    Dim dr As DataRow = dtReturn.DataTable1.NewRow()
    dr("ReceivedDate") = FormatMandarinDate(row("ReceivedDate").ToString(), "/")
    dr("DocumentType") = DictionaryManager.GetValue(GetType(CodeDocumentType), row("DocumentType"))
    dr("Organizer") = row("Organizer")
    dr("SendDate") = FormatMandarinDate(row("SendDate").ToString(), "/")
    dtReturn.DataTable1.Rows.Add(dr)
Next
Return dtReturn

3. 宣告、設定ReportDocument

ReportDocument是Crystal Report Runtime裡的元件,應該是早就安裝好了。要用的時候Include或Import一下應該就有了。
必備的設定是報表檔的路徑,還有用該元件的SetDataSource方法把整理好的DataSet設定給他。
如果要完成的報表比較複雑,你可能還會加參數、設定一些雜七雜八的東西。
Dim doc As ReportDocument = New ReportDocument()
doc.Load("C:\XXXXX\XXX.rpt") '磁碟路徑。我要用的話都會用MapPath來讓Framework自己轉。
doc.SetDataSource(dtReturn)  '整理出來的DataSet丟進去

4. 匯出


這裡介紹的匯出動作會把報表含你輸出的資料一起弄成特定格式的檔案,再存到主機裡。
在我近期所做的專案裡,他們匯出報表的方式統一採用先存成檔案,再供Client端做檔案下載。所以該方式還算堪用。
Dim oExDo As New DiskFileDestinationOptions
'輸出的檔案名稱,ExportFormatType.PortableDocFormat是指PDF。
Dim FileName As String = String.Format("{0}_{1}.{2}", ReportName, DateTime.Now.Ticks, GetReportExtension(ExportFormatType.PortableDocFormat))

oExDo.DiskFileName = "C:\XXXXX\" + FileName
doc.ExportOptions.ExportDestinationType = ExportDestinationType.DiskFile
doc.ExportOptions.ExportFormatType = FileType
doc.ExportOptions.DestinationOptions = oExDo

doc.Export()  'Export to PDF
doc.Close()
doc.Dispose()

中間有任何一個地方壞掉或不合Crystal Report的規則,會直接噴Exception出來。


--功德圓滿--

到後來,我都把一些常用的程式片段另外取出來放在Utils裡。
像報表匯出、加參數等常用的東西,要我直接寫出來我還不會寫,都直接從網路上咖過來整理一下就可以用了。

2013年11月8日 星期五

[Crystal Report] 新手入門包 - 從無到有

上一次有提到ReportService新手入門,這次就乾脆把Crystal Report的新手入門包也弄一弄,希望對以後有用。

開發環境:
Visual Studio 2010


--Let's Start--

1. 首先,我們要讓開發工具知道我們在幹嘛。

要先去下載Crystal Report的套件,安裝一下。沒有裝這個,就算新增了Crystal Report項目,也只會看到一大堆16進位碼。

要下載我用紅框框框起來的那部分,開發工具才有圖形介面可以讓你開發報表,不然只有runtime而已,開發工具不支援。


2. 安裝好了之後,就可以開始開發了。

在方案總管那裡,在你打算放報表檔案的資料夾裡新增Crystal Report項目,取個好名字給它。
我覺得"Crystal Report1.rpt"很好,就這麼取吧~

他又問我要不要用精靈。按照慣例,開個空白報表就好。

空白報表就長這個樣子啦。

報表在預設的時候分了5個Section:

  1. 報表首:整張報表的頭,如果你一份報表有好幾張紙的份量,放在這個Section裡面的內容只會顯示在第一頁。
  2. 頁首:每一頁都會顯示的內容。
  3. 細目:你要顯示一條一條列表的地方,概念像是DataTable裡的DataRow,或是資料表裡的Record。
  4. 報表尾:只會顯示在最後一頁的部分。通常會放什麼總計、一些註解或是簽名欄。
  5. 頁尾:都會顯示在每一頁的最尾巴。


3. 把報表的尺寸調整好!

如果你沒先做這一步,什麼都畫好了再來調報表,你會調到想哭。


4. 再來,把DataSet建出來。

在ReportingService那篇有提到一個重要的東西:DataSet,這裡也需要。
Crystal Report裡,他可以用微軟自己的DataSet。就和在專案裡加一個檔案一樣,新增一個「資料集」到一個適合的地方。


建好就長這樣



5. 報表樣式自己畫,有用過小畫家的應該都會,這裡就不細講了。

有機會再聊聊我自己常用的小技巧。


6. 用資料庫專家,把DataSet嵌進報表裡。




完事之後,就會看到資料庫專家的下面多一個DataTable,裡面有你弄好的欄位。



7. 把資料庫的欄位一個一個拉到報表裡適合的位子。

什麼叫適合的位子?就看你報表樣式要怎樣畫就怎樣畫。


8. 預覽一下。

你可以在開發工具上切換報表開發和預覽兩個頁籤。預覽一點下去,就會看到你的報表設定完之後,最後跑資料出來會是什麼德性。
裡面的資料都是Crystal Report自己亂數產出來的,和實際資料沒有關係,所以也不要慌,除了內容之外,最後讓使用者看到的大概就是那樣了。


如此一來,一張報表就開發完了。
至於要怎麼套正確的資料,乃至於實際應用在介面上,我們下回再分曉。









你說啥?斷在這裡很不負責任嗎?
沒辦法,上一集Reporting Service的內容也是斷在報表剛做好而已,再來我就沒試過了。
為了兩集的內容相對稱,停在這裡是很合理的。

2013年11月7日 星期四

[ReportingService] 新手入門包 - 從無到有

昨天在整理這半年多來的截圖,看到之前原本要寫的東西沒寫,真的是囧翻了。

這是我在上一個客戶那裡用到的東西:Reporting Service。整體來說好像比Crystal Report還簡單,但我想還是記一下這個新手入門款,免得七久八久沒碰就忘了,下回再摸一遍。

開發環境:
Visual Studio 2008




--開發分隔線--

1. 開始之前,我們會需要一個Reporting Server Project。


2. 接下來,我們會需要一個Shared Data Source。

設定好一下連線字串或帳號密碼就行了,不難。搞不定就多try幾次吧~
事成之後就會多一個剛建出來的DataSource。


3. 再來,就準備要開發新報表囉!


在圖裡,我個人習慣打開空報表再一點一點慢慢弄。Wizard這種東西讓我…嗯,不是很習慣,它通常…很肥。

個人在畫報表上的習慣,我會把尺規打開,在對線對刻度的時候比較方便,有刻度可以定位,可以減少畫歪的情況
留意一下報表的寬度高度,還有上下左右邊界留白,不然在產出報表(印出來或是產成PDF的時候會因為超過邊界而多印一頁。


4. 開發報表最重要的事,就是把DataSet嵌進報表裡。


pop出DataSet屬性視窗之後,就可以開始寫SQL在裡面、設定參數。
這個屬性視窗中間有Query的那一大空格,就是讓你寫SQL的地方,開發工具會根據你SELECT出來的欄位,顯示在左邊。


5. 最後,你應該會是需要把DataSet裡面一筆一筆的Row顯示在報表裡。

只要在報表本體裡按右鍵→插入→表格,再把左邊DataSet裡的欄位一格一格拖進去就行了。





這麼著,一張簡單的報表就這麼從無到有地製作出來了,感覺比Crystal Report簡單滿多的。
至少表格的線不用再一條一條畫。

2013年11月6日 星期三

[MSSQL] 備份組包含現有的xxx資料庫以外的資料庫

今天玩資料庫遇到比較特別的情況,卡了一下,還不小心把人家的db搞掛了,還好重開就好了。
需求很簡單:我要從A機器裡的db,複製到B機器裡。
其實就是備份/還原嘛~聽起來沒什麼了不起。

但是DBMS就是一種會把聽起來很簡單的事情搞得很複雜的東西。(好啦,我知道「備份/還原」都是被抽象化的結果,裡面藏著很複雜的邏輯。)



備份:
備份上自己沒遇到什麼問題。
在SQL Server上要備份下來的資料庫上面按右鍵→工作→備份。會跑出下面那個畫面。


備份路徑稍微注意一下,以自己方便(或習慣)為主。
出來的備份檔應該都會是.bak檔,然後是放在A機器裡。


接著,把這個.bak檔複製到B機器裡。這個別問我啊,隨身碟、檔案分享、FTP…什麼都好。




還原:
還原的部分就沒這麼順遂了。
同樣的,在B機器的SQL Server上要還原的資料庫上按右鍵→工作→還原→資料庫。會跑出下面的畫面。


設定一下.bak檔的路徑之後就開始了,期待它完成,我今天的工作就結束了。
問題就是他媽的沒這麼爽,才按下去它就跑出一個Exception,上面寫「備份組包含現有的xxx資料庫以外的資料庫備份問題」之類的訊息。



我太懶了,我懶得寫找解法碰釘子的過程,直接寫怎麼處理。
1. 先把DB弄離線。

ALTER DATABASE [dbname] SET offline

2. 執行下列指令:
restore database DBName
from disk = 'D:\DBName.bak'
with
move 'DBName' to 'C:\Program Files\...\xxx.mdf', 
move 'DBName_log' to 'C:\Program Files\...\xxx_log.LDF',
NoRecovery,
Replace
go

留意一下.mdf和.ldf檔路徑就好。
如果執行成功了,就會出現「幾秒內成功處理了 多少頁(0.000 MB/sec)。

3. 再把DB弄上線。
ALTER DATABASE [dbname] SET online

4. 還原!
資料庫上按右鍵→工作→還原→資料庫,.bak路徑設好、勾還原,按確定。

5. 忐忑不安地等待,搞定就開心~ 搞不定就GG。

2013年2月7日 星期四

[ .NET ] 讓DataTable自己過濾資料


前陣子接到一個需求,那個情境整個讓我整個大發火:允許使用者輸入查詢條件,針對已查詢出來的結果再進行查詢

聽起來是一個很好用的功能,如果查出來的資料有30頁分頁,用這個方式來過濾資料確實是對使用者來說相當方便。
真正讓我發火的是:那個頁面是40多種查詢項目共用的,不同的查詢項目,GridView上產生的欄位會不一樣,而聽到這個需求的時候,我還在考慮是要從資料庫重新拉資料,還是只是把已經抓出來的資料藏在什麼地方,要的時候再拿出來。更重要的是,我聽到這個需求的時候,隔天就要驗收,而我還一點頭緒也沒有。
順便宣達一下:在開發功能的時候,真的要有人隨時檢視合約和工作項目之間的關聯,真的不要到驗收前才發現有一些該完成的項目沒列在工作項目裡。

--該冷靜一下的分隔線--

時程的關係,當時已經沒有時間再去改寫DAO,讓使用者在操作介面的時候讓系統重新跟資料庫不斷地拉少量資料,所以決定在頁面上動手腳。
不知道從哪時候開始就有聽說過.NET的伺服器元件裡有一個地方可以讓人寫類似SQL的過濾句,又隱約看過DataTable裡有個Select之類的東西,就打算從這方面開始著手。

查了一下又試了一下,發現有點不太容易使用。
DataTable.Select(string expression)中,Select裡可以塞一段類似SQL裡的WHERE條件句的字串,把DataTable裡的資料再做一次過濾(參考expression的寫法)。壞就壞在他回傳的是DataRow陣列,而且還是這個DataTable自己的DataRow,不是另外Create一塊新的DataRow來放,所以也不能把這些DataRow再加到其它的DataTable裡,我要怎麼顯示在介面上都不太對,這個東西就變成練習用的玩具,不算產出。

後來survey各路名門文獻來尋找靈感,就這麼無意間讓我知道由DataTable.DefaultView回傳回來的一個DataView,裡面除了裝著DataTable的資料之外,官方還說可以進行資料的篩選和排序。

是我要的篩選

於是我就在這個DataView.RowFilter裡加那些原本寫在DataTable.Select裡的expression,再把GridView和DataTable重新Bind一下,就可以得到我要的結果了!

    private void SelectDataTable(string keyword) {
        DataTable dt = (DataTable)ViewState["dtSource"];

        if (keyword == "")
        {
            dt.DefaultView.RowFilter = "";
            ViewState["RowFilter"] = "";
        }
        else
        {
            StringBuilder expWhere = new StringBuilder();
            foreach (DataColumn col in dt.Columns)
            {
                if (col.ColumnName == "SeqID") { continue; }
                if (col.DataType != typeof(string)) { continue; }
                expWhere.Append(string.Format("{0} LIKE '%{1}%' OR ", col.ColumnName, keyword));
            }
            dt.DefaultView.RowFilter = ((ViewState["RowFilter"].ToString() != "") ? ViewState["RowFilter"] + " AND " : "") + "(" + expWhere.ToString().Remove(expWhere.Length - 3, 3) + ")";
            ViewState["RowFilter"] = dt.DefaultView.RowFilter;
        }
        ViewState["dtSource"] = dt;
        SetGridView(dt);
    }



老天保佑,It's work!


下面是畫面圖。為保護當事人已馬賽克(?)處理

圖1. 查出來的資料很多,有8頁

 圖2. 透過RowFilter來過濾資料

圖3. 再濾一次。這個功能會讓關鍵字查詢用AND的方式把前後的查詢一次一次濾掉,不過這裡看不出來....


Ref: