三個單引號,讓你的程式碼更優

我們在「Visual Studio 2008中的JavaScript IntelliSense」裡有介紹一個「XML Code Comment, XML程式碼註解」的方式,而且如果你是使用C#(C Shap)在開發應用程式,那你會發現,C#也是使用此種方式在註解程式碼(或說Sub/Function/Class...比較準),而且C#是當檔案一產生時就產生預設空白註解內容,但使用VB是不會。

其實我們VB也是使用「類似」的方式,而VB是使用連續三個「'」(單引號)來產生XML程式碼註解,開啟任一*.vb檔案,在任一Sub/Function上方打上連續三個「'''」,你會發現Visual Studio 2008開發工具,自動幫你補上預設空白的註解內容:

1''' <summary>
2'''
3''' </summary>
4''' <param name="sender"></param>
5''' <param name="e"></param>
6''' <remarks></remarks>

而且param的部份,Visual Studio 2008還會自動去抓取你Sub/Function裡的參數,來產生。這些註解對程式維護的幫助必要性,不用再提,而且在使用工具產生程式碼說明文件時,會有很大的幫忙。三個單引號,讓你的程式碼可看性更優,學起來。

SqlCommand的非同步行程

我們在前一篇提到MARS的功能,讓我們在進一步來談談ASP.NET裡SqlCommand類別的非同步功能。在MARS我們說明在一個資料連線裡同時存取多個結果集。而非同步功能能讓使用者同時對資料庫送出數個各不相關的命令。如果兩者結合,那更是強大。

首先使用MARS我們必須先在連線字串裡啟用「MultipleActiveResultSets=True」,再來,我們要啟用ADO.NET的非同步功能,我們也必須在連線字串裡加入「Asynchronous Processing=true」。

ADO.NET-Asyn.aspx

以下我們將介紹三種非同步的方式。

01<form id="form1" runat="server">
02    <div>
03        <asp:Button ID="Poll" runat="server" Text="輪詢(Poll)" /> 
04        <asp:Button ID="bntWait" runat="server" Text="等待-單一表(WaitHandle)" />
05         
06 
07        <asp:Button ID="bntHandles" runat="server" Text="等待-兩資料表(WaitHandle) + MARS" />
08         
09 
10         
11 
12        <asp:GridView ID="gvOrders" runat="server" Caption="Orders">
13        </asp:GridView>
14         
15 
16        <asp:GridView ID="gvOrderDetails" runat="server" Caption="Order Details">
17        </asp:GridView>
18    </div>
19    </form>

ADO.NET-Asyn.aspx.vb


001Imports System.Data
002Imports System.Data.SqlClient
003Imports System.Configuration
004 
005Partial Class ADONET_Asyn
006    Inherits System.Web.UI.Page
007 
008    '使用MARS
009    Dim conn As New SqlConnection(ConfigurationManager.ConnectionStrings("NorthwindConnectionString").ConnectionString)
010    Dim strSQL As String = "select top 5 Customers.CompanyName , Customers.ContactName , Orders.OrderID ,Orders.OrderDate , Orders.RequiredDate, Orders.ShippedDate from Orders, Customers where (Orders.CustomerID = Customers.CustomerID) order by Customers.CompanyName, Customers.ContactName"
011 
012    Protected Sub Poll_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Poll.Click
013        Dim cmd As New SqlCommand
014        cmd.CommandText = strSQL
015        cmd.CommandType = CommandType.Text
016        cmd.Connection = conn
017 
018        conn.Open()
019 
020        '非同步結果
021        Dim asyncResult As IAsyncResult
022        '執行非同步
023        asyncResult = cmd.BeginExecuteReader
024 
025        '非同步是否完成,不然停止1秒
026        While Not asyncResult.IsCompleted
027            System.Threading.Thread.Sleep(1000)
028        End While
029 
030        Dim dr As SqlDataReader
031        '取回非同步資料
032        dr = cmd.EndExecuteReader(asyncResult)
033 
034        gvOrders.DataSource = dr
035        gvOrders.DataBind()
036 
037        conn.Close()
038    End Sub
039 
040    Protected Sub bntWait_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles bntWait.Click
041        Dim cmd As New SqlCommand
042        cmd.CommandText = strSQL
043        cmd.CommandType = CommandType.Text
044        cmd.Connection = conn
045        conn.Open()
046 
047        Dim asyncResult As IAsyncResult
048        '等待碼
049        Dim WHandle As System.Threading.WaitHandle
050        Dim dr As SqlDataReader
051 
052        asyncResult = cmd.BeginExecuteReader
053        '取得非同步等待碼
054        WHandle = asyncResult.AsyncWaitHandle
055 
056        'WaitOne,等待完成
057        If WHandle.WaitOne = True Then
058            dr = cmd.EndExecuteReader(asyncResult)
059            gvOrders.DataSource = dr
060            gvOrders.DataBind()
061            conn.Close()
062        End If
063    End Sub
064 
065    Protected Sub bntHandles_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles bntHandles.Click
066        Dim cmd As New SqlCommand
067        cmd.CommandText = strSQL
068        cmd.CommandType = CommandType.Text
069        cmd.Connection = conn 'MARS
070 
071        Dim odcmd As New SqlCommand
072        odcmd.CommandText = "select * from customers where companyName = 'Alfreds Futterkiste'"
073        odcmd.CommandType = CommandType.Text
074        odcmd.Connection = conn 'MARS
075 
076        conn.Open() 'MARS: cmd, odcmd
077 
078        '非同步處理
079        Dim orAsyncResult As IAsyncResult
080        Dim odAsyncResult As IAsyncResult
081        '等待碼陣列
082        Dim WHandles(1) As System.Threading.WaitHandle
083        Dim orDr As SqlDataReader
084        Dim odDr As SqlDataReader
085 
086        '取得非同步結果
087        orAsyncResult = cmd.BeginExecuteReader
088        odAsyncResult = odcmd.BeginExecuteReader
089 
090        '取得等待碼(WaitHandle)
091        WHandles(0) = orAsyncResult.AsyncWaitHandle
092        WHandles(1) = odAsyncResult.AsyncWaitHandle
093 
094        '第一種:使用WaitAll
095        ''等待全部「等待碼」完成
096        'System.Threading.WaitHandle.WaitAll(WHandles)
097 
098        'orDr = cmd.EndExecuteReader(orAsyncResult)
099        'odDr = odcmd.EndExecuteReader(odAsyncResult)
100 
101        'gvOrders.DataSource = orDr
102        'gvOrders.DataBind()
103 
104        'gvOrderDetails.DataSource = odDr
105        'gvOrderDetails.DataBind()
106 
107        '第二種:使用WaitAny
108 
109        For i As Integer = 0 To 1
110            Dim WHIndex As Integer
111            WHIndex = System.Threading.WaitHandle.WaitAny(WHandles)
112 
113            Select Case WHIndex
114                Case 0
115                    orDr = cmd.EndExecuteReader(orAsyncResult)
116                    gvOrders.DataSource = orDr
117                    gvOrders.DataBind()
118                Case 1
119                    odDr = odcmd.EndExecuteReader(odAsyncResult)
120                    gvOrderDetails.DataSource = odDr
121                    gvOrderDetails.DataBind()
122            End Select
123        Next
124 
125        conn.Close()
126 
127    End Sub
128End Class

其實用心看,程式不難,而且與我們之前寫的ADO.NET程式還有點像。

輪詢(Poll)

我們先由輪詢(Poll)來看,由BeginExecuteReader來啟動非同步的動作,BeginExecuteReader會傳回一個IAsyncResult,然後判斷非同步是否完成(asyncResult.IsCompleted),如已經完成,使用EndExecuteReader(asyncResult)取回DataReader資料。

等待(WaitHandle)

在等待方法裡,過程差不多,只是多了一個「等待碼(asyncResult.AsyncWaitHandle),而且改由這個等待碼來判斷非同步是否完成,再接下去完成後面的動作。而WaitHandle.WaitOne()會等待單一非同步行程完成或逾時,WaitAll()會等待所有非同步過程結束,WaitAny()需要以陣列管理一個以上的等待碼,而且你必須為每個想要處理的等待碼個別叫用

在第三個按鈕裡我們看出MARS加上非同步的實用性,你可以同時發出「多個」Query而不用擔心。

ASP.NET中MARS(多重作用結果集)

MARS:Multiple Active Result Sets,多重作用結果集。

MARS能在同一條資料連線(SqlConnection)開啟一個以上的結果集,並讓你同時存取這些結果集。

讓我們回憶一下,我們撰寫資料庫的流程,

  1. 連線字串(SqlConnection)
  2. SQL定義(SqlCommand)
  3. 取回資料(DataReader, DataSet)
  4. 處理(...DataBind...)
  5. 關閉連線(Close)

不管怎麼寫,大多跳不出這幾個步驟,而如果同一個畫面,同時需要存取幾個DataReader,DataSet,同樣的東西你就要寫兩次,寫三次,這樣是非常沒有效率,這時就非常合適使用MARS技巧。也就是說,步驟1裡的SqlConnection可以同時間給多個SqlCommand來使用,或是說,你需要同時存取同一台Database Server裡的不同資料庫,那也非常合適使用這個MARS技巧。

MARS的設定也異常簡單,只需要在設定連線字串時加入「MultipleActiveResultSets=True」來啟用MARS功能即可。

我們來看一個實例,我們要輸出一個Orders資料表,然後在Orders裡又抓取Orders Details的資料來一起輸出。


無MARS的GridView查詢


以下範例我使用一個下拉式選單,來查詢北風Customers的資料,同時顯示兩個GridView。

GridView.aspx

01Imports System.Data
02Imports System.Data.SqlClient
03Imports System.Configuration
04 
05Partial Class GridView
06    Inherits System.Web.UI.Page
07 
08    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
09        If Not Page.IsPostBack Then
10            Dim ddlconn As New SqlConnection(ConfigurationManager.ConnectionStrings("NorthwindConnectionString").ConnectionString)
11 
12            Dim ddlcmd As New SqlCommand
13            ddlcmd.CommandText = "select distinct ContactTitle from customers"
14            ddlcmd.CommandType = CommandType.Text
15            ddlcmd.Connection = ddlconn
16            ddlcmd.Connection.Open()
17 
18            Dim ddldr As SqlDataReader
19            ddldr = ddlcmd.ExecuteReader
20 
21            ddlTitle.DataSource = ddldr
22            '顯示文字所對應資料庫的欄位
23            ddlTitle.DataTextField = "ContactTitle"
24            '欄位值所對應資料庫的欄位
25            ddlTitle.DataValueField = "ContactTitle"
26            ddlTitle.DataBind()
27 
28            ddlcmd.Dispose()
29            ddlconn.Dispose()
30        End If
31    End Sub
32 
33    Protected Sub ddlTitle_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ddlTitle.SelectedIndexChanged
34        '使用DataReader
35        Dim conn As New SqlConnection(ConfigurationManager.ConnectionStrings("NorthwindConnectionString").ConnectionString)
36 
37        Dim cmd As New SqlCommand
38        cmd.CommandText = "select top 5 * from customers where ContactTitle=@ContactTitle"
39        cmd.CommandType = CommandType.Text
40        cmd.Connection = conn
41 
42        '參數設定
43        Dim ContactParam As New SqlParameter
44        ContactParam.ParameterName = "@ContactTitle"
45        '需得選取的值
46        ContactParam.Value = ddlTitle.SelectedValue
47        cmd.Parameters.Add(ContactParam)
48        cmd.Connection.Open()
49 
50        Dim dr As SqlDataReader
51        'CommandBehavior.CloseConnection,DataReader自動Close
52        dr = cmd.ExecuteReader(CommandBehavior.CloseConnection)
53 
54        'DataTable可把DataReader當成資料來源
55        'Dim drTodt As New DataTable
56        'drTodt.Load(dr)
57 
58        '由DataTable傳回DataTableReader物件
59        'Dim dtTodr As DataTableReader
60        'dtTodr = drTodt.CreateDataReader
61 
62        'gvDR.DataSource = drTodt
63        'gvDR.DataSource = dtTodr
64        gvDR.DataSource = dr
65        gvDR.DataBind()
66 
67        'drTodt.Dispose()
68        'dtTodr.Close
69        cmd.Dispose()
70        conn.Dispose()
71 
72        '使用DataTable
73        Dim dtconn As New SqlConnection(ConfigurationManager.ConnectionStrings("NorthwindConnectionString").ConnectionString)
74 
75        Dim dtcmd As New SqlCommand
76        dtcmd.CommandText = "select top 5 * from customers  where ContactTitle=@dtContactTitle order by customerid desc"
77        dtcmd.CommandType = CommandType.Text
78        dtcmd.Connection = dtconn
79 
80        '參數設定
81        Dim dtContactParam As New SqlParameter
82        dtContactParam.ParameterName = "@dtContactTitle"
83        '取得選取值
84        dtContactParam.Value = ddlTitle.SelectedValue
85        dtcmd.Parameters.Add(dtContactParam)
86 
87        Dim dap As New SqlDataAdapter
88        Dim dt As New DataTable
89        dap.SelectCommand = dtcmd
90        dap.Fill(dt)
91 
92        gvDT.DataSource = dt.DefaultView
93        gvDT.DataBind()
94 
95        dap.Dispose()
96        dtcmd.Dispose()
97        dtconn.Close()
98    End Sub
99End Class

你可以看到在SelectedIndexChanged事件裡,我為了要讓兩個GridView顯示不同的資料,所以我必須開關三次SqlConnection(含下拉式控制項的那一次),但其實這是不必要的,接下來我們來看看MARS的使用。

Multiple Active Result Sets


MARS.aspx

1<%@ Page Language="VB" %>
2<%@ Import Namespace="System.Data" %>
3<%@ Import Namespace="System.Data.SqlClient" %>
4<%@ Import Namespace="System.Configuration" %>
5<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
6 
7<script runat="server">
8    '全域連線物件
9    '在連線字串中必須設定「MultipleActiveResultSets=True」
01Dim conn As New SqlConnection(ConfigurationManager.ConnectionStrings("NorthwindConnectionString").ConnectionString)
02     
03    Protected Sub Page_Load(ByVal sender As Object, _
04                    ByVal e As System.EventArgs)
05 
06        Dim cmd As New SqlCommand
07        Dim OrdersReader As SqlDataReader
08        
09        cmd.CommandText = _
10                " SELECT TOP 5 Customers.CompanyName, Customers.ContactName, " & _
11                " Orders.OrderID, Orders.OrderDate, " & _
12                " Orders.RequiredDate, Orders.ShippedDate " & _
13                " FROM Orders, Customers " & _
14                " WHERE Orders.CustomerID = Customers.CustomerID " & _
15                " ORDER BY Customers.CompanyName, Customers.ContactName "
16 
17        cmd.CommandType = CommandType.Text
18        cmd.Connection = conn
19 
20        conn.Open()
21        OrdersReader = cmd.ExecuteReader()
22 
23        gvOrders.DataSource = OrdersReader
24        gvOrders.DataBind()
25 
26        conn.Close()
27    End Sub
28 
29    ' RowDataBound事件處理常式
30    Protected Sub gvOrders_RowDataBound(ByVal sender As Object, _
31                 ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs)
32 
33        Dim OrderRecord As IDataRecord
34        Dim lblOrderDetail As Label
35 
36        '從e(DataReader)取得現在連繫的記鍵
37        '轉換為IDataRecord interface
38        OrderRecord = CType(e.Row.DataItem, IDataRecord)
39 
40        '從GridView物件找到Label控制項
41        'Label控制項在Order Details裡面
42        lblOrderDetail = CType(e.Row.FindControl("lblOrderDetail"), Label)
43 
44        If OrderRecord Is Nothing Or lblOrderDetail Is Nothing Then
45            Return
46        End If
47 
48        Dim cmd As New SqlCommand
49        Dim OrderDetailReader As SqlDataReader
50        cmd.CommandText = _
51                "SELECT Products.ProductName, [Order Details].UnitPrice, " & _
52                " [Order Details].Quantity, [Order Details].Discount " & _
53                " FROM [Order Details], Products " & _
54                " WHERE [Order Details].ProductID = Products.ProductID " & _
55                " AND [Order Details].OrderID = " + _
56                Convert.ToString(OrderRecord("OrderID"))
57        cmd.CommandType = CommandType.Text
58 
59        '在連線字串中必須設定「MultipleActiveResultSets=True」
60        'MARS:Multiple Active Result Sets,多重作用結果集
61        '注意這裡,我們使用的是同一條SqlConnection來連線,也就是MARS作用所在
62        cmd.Connection = conn
63 
64        OrderDetailReader = cmd.ExecuteReader()
65 
66        While OrderDetailReader.Read()
67            lblOrderDetail.Text += OrderDetailReader(0).ToString() + "
68"
69        End While
70 
71    End Sub
72</script>
73 
75<head runat="server">
76    <title>MARS範例</title>
77</head>
78<body>
79    <form id="form1" runat="server">
80    <div>
81        <asp:Label ID="lblCounter" runat="server">
82        
01     
02        <asp:GridView ID="gvOrders" runat="server" AutoGenerateColumns="False" OnRowDataBound="gvOrders_RowDataBound" Width="100%">
03            <Columns>
04                <asp:BoundField HeaderText="Company Name" DataField="CompanyName"></asp:BoundField>
05                <asp:BoundField HeaderText="Contact Name" DataField="ContactName"></asp:BoundField>
06                <asp:TemplateField>
07                    <HeaderTemplate>
08                        Order Detail
09                    </HeaderTemplate>
10                    <ItemTemplate>
11                        <asp:Label ID="lblOrderDetail" runat="server">
12                    </ItemTemplate>
13                </asp:TemplateField>
14                <asp:BoundField HeaderText="Order Date" DataField="orderdate" DataFormatString="{0:d}">
15                </asp:BoundField>
16                <asp:BoundField HeaderText="Required Date" DataField="requireddate" DataFormatString="{0:d}">
17                </asp:BoundField>
18                <asp:BoundField HeaderText="Shipped Date" DataField="shippeddate" DataFormatString="{0:d}">
19                </asp:BoundField>
20            </Columns>
21        </asp:GridView>
22         
23 
24         
25 
26    </div>
27    </form>
28</body>
29</html>

範例的重點在71裡RowDataBound事件裡使用同一個SqlConnection物件來連接。MARS在你需要同一Web Form裡需要使用越來越多結果集時,你就可以發現MARS的效果,讓我們的程式更有彈性,更靈活。

警告! www.seednet.net.tw 並不是合法主機!

看圖說故事。我的眼睛應該沒有問題?
如果「它」不合法,那我上的是什麼網站?


SqlDataAdapter類別,批次更新屬性

SqlDataAdapter類別,是我們在學習ASP.NET資料庫裡兩大支柱的其中之一,會使用到它,也是因為要對Data進行操作,才會考慮使用到它,不然如果是單純取得資料,那一定是考慮SqlDataReader

但我們在使用SqlDataAdapter類別更新」資料時,有一個屬性非常重要,但也是大多沒有介紹,「UpdateBatchSize 屬性」,也是就是,我們原先如果更新了100筆資料,那就會一筆Update Query,一筆Update Query來對資料庫做更新資料的動作,這樣的方式不但沒有效率,又浪費資源,但這個UpdateBatchSize 屬性,能讓我們改變SqlDataAdapter類別的行為,讓我們使用「批次」的方來更新資料,但如我們有一個dap的SqlDataAdapter實體,所以我們可以設定「dap.UpdateBatchSize=10」,以10筆資料為一個單位來更新,經過這樣設定後,原本需要100次Query,只批次簡化為10次Query就結束了。

而相較於單一Query的更新方式,UpdateBatchSize當然可以大幅改善效能。而UpdateBatchSize的預設值正是1,而如果我們設定為0,那SqlDataAdapter物件會一次處理所有的Update命令

也請小心,不要將UpdateBatchSize設的太大,每種資料庫批次處理的能力不同,上線前可多多測試,找出一個合理值,如果超出資料庫處理能力,SqlDataAdapter會拋出例外。

SqlBulkCopy類別,大量複製資料從A到B

昨天已經說了一個豬頭的故事,那今天就來寫個不豬頭的程式碼。

Default.aspx
1<form id="form1" runat="server">
2<div>
3<asp:button id="btnBulkCopy" runat="server" text="Start Bulk Copy"></asp:button>
4<asp:label id="lblResult" runat="server">
5<asp:label id="lblCounter" runat="server">
6</div>
7</form>

Default.aspx.vb

程式目的:將程式本機資料庫中Share的資料大量複製到遠端Northwind資料庫中。

01Imports System.Data
02Imports System.Data.SqlClient
03 
04Partial Class _Default
05    Inherits System.Web.UI.Page
06 
07    Protected Sub btnBulkCopy_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnBulkCopy.Click
08        '程式本機 Database Server Connection String
09        Dim shcs As New SqlConnectionStringBuilder
10        shcs.DataSource = ".\SQLEXPRESS"
11        shcs.InitialCatalog = "Share"
12        shcs.UserID = "bruce"
13        shcs.Password = "123456789"
14 
15        '遠端Database Server Connection String
16        Dim ntcs As New SqlConnectionStringBuilder
17        ntcs.DataSource = "192.168.3.12\SQLEXPRESS"
18        ntcs.InitialCatalog = "Northwind"
19        ntcs.UserID = "bruce"
20        ntcs.Password = "987654321"
21 
22        Dim shcmd As New SqlCommand
23        Dim shdr As SqlDataReader
24 
25        Dim shconn As New SqlConnection(shcs.ConnectionString)
26        shcmd.CommandText = " SELECT ID, First_Name, Last_Name, 'CEO' as Source FROM MailingList"
27        shcmd.CommandType = CommandType.Text
28        shcmd.Connection = shconn
29        shcmd.Connection.Open()
30 
31        '與遠端建立SqlBulkCopy實體
32        Dim ntbcp As New SqlBulkCopy(New SqlConnection(ntcs.ConnectionString))
33        '指定遠端的資料表名稱
34        ntbcp.DestinationTableName = "Employees"
35        '本機與遠端的Column對應,如本機與遠端的Schema一樣,可省略ColumnMappings
36        ntbcp.ColumnMappings.Add("ID", "EmployeeID")
37        ntbcp.ColumnMappings.Add("First_Name", "FirstName")
38        ntbcp.ColumnMappings.Add("Last_Name", "LastName")
39        '自定義欄位,把Source對應到Employees的Title欄位,所有欄位都會等於CEO
40        Dim TitleMapping As New SqlBulkCopyColumnMapping("Source", "Title")
41        ntbcp.ColumnMappings.Add(TitleMapping)
42        'TimeOut,單位秒
43        ntbcp.BulkCopyTimeout = 360
44        '多少筆發一次通知
45        ntbcp.NotifyAfter = 1000
46 
47        '委派
48        AddHandler ntbcp.SqlRowsCopied, AddressOf OnSqlRowsCopied
49        'AddHandler ntbcp.SqlRowsCopied, New SqlRowsCopiedEventHandler(AddressOf OnSqlRowsCopied)
50 
51        shdr = shcmd.ExecuteReader
52 
53        Try
54      '執行WriteToServer,把本機資料(DataReader)大量複製到遠端資料庫
55            ntbcp.WriteToServer(shdr)
56        Catch ex As Exception
57            lblResult.Text = ex.Message
58        Finally
59            shdr.Close()
60        End Try
61    End Sub
62 
63    Private Sub OnSqlRowsCopied(ByVal sender As Object, ByVal args As SqlRowsCopiedEventArgs)
64     '由於ntbcp.NotifyAfter的關係,每1000筆觸發一次
65        lblCounter.Text += args.RowsCopied.ToString() + " rows are copied.
66"
67    End Sub
68End Class

程式流程不會很難,

  1. 建立本機SqlConnection實體
  2. 建立遠端SqlBulkCopy實體(使用遠端Sqlconnection實體)
  3. 執行本機ExecuteReader方法,取得資料
  4. 執行SqlBulkCopy實體的WriteToServer方法,將DataReader寫到遠端Server去

委派那一段可寫可不寫,在複製過程會觸發OnSqlRowsCopied事件,主要目的是顯示訊息讓使用者了解目前的進度。

如果兩端網路連線速度沒問題的話,十萬筆資料,都能在幾秒之內「秒殺~秒殺~秒殺~秒殺~秒殺~秒殺~秒殺~」,寫十萬筆Insert,有SqlBulkCopy不用,發瘋嗎。

ASP.NET WebForm命名注意

昨天在寫一支SqlBulkCopy類別的程式,說實在,這個類別可以寫的很簡單,也可以寫的很複雜。(廢話)

先簡單介紹一下,這個類別是.NET Framework 2.0所提供的一個類別,看命名就知道是在做「大量複製」,一般而言,例如,我們每一筆Insert就會對資料庫發出一筆Query,所以如我之前不是使用匯入方式來新增資料,而是使用複製、貼上就會浪費許多時間。

你簡單想像,SqlBulkCopy類別,就是「程式版的匯入功能」,讓你可以從多樣的資料來源,將你需要的大量資料批次匯入目的地資料庫

但今天我不是要介紹SqlBulkCopy類別的功能,而且我在寫的這支程式,非常奇怪,不論我怎麼宣告,Dim ntbcp As New sqlBulkCopy(ConnectionString),就是會跑出來「Public Sub New()的引數太多」的錯誤訊息,查了許久,實在想不出什麼原因,上網也找不到什麼可參考的資料,所以就上MSDN請教大內高手。

在來回幾次後,終於發現問題,豬頭是自己產生的。我是豬頭。

我新增的網頁名稱為「sqlBulkCopy.aspx」,所以產生了「Partial Class sqlBulkCopy」(s小寫),所以在程式中引用到自己(Web Form)的Class,而不是System.Data.SqlClient中SqlBulkCopy(S大寫)類別,所以當然有問題。

這也給自己一個經驗,
  1. .NET Framework類別,最好不要(一定不可以)拿來命名為網頁名稱
  2. 當局者迷。

使用SqlConnectionStringBuilder類別快速建立資料庫連線字串

我們ASP.NET網頁時,很少不用寫資料庫程式,而除了使用DataSource控制項外,如需要「靈活彈性」的話,那就不是DataSource控制項能提供了,所以等玩DataSource控制項一段時間後,還是不免要自行撰寫程式碼,而在寫資料庫連線程式時,常常會找筆記或參考程式複製、貼上那一段長長長長的ConnectionString,因為那一段ConectionString是字串,包在""之中,所以IntelliSense不會出現任何提示,你少打了一個字,打錯了一個字,也只能在執行網頁時才能除錯。也因為太長了,除非你天天打,打到都會背了,不然我看很少有人會去背那堆的參數。

其實在ASP.NET 2.0時就有提供一個非常好用的SqlConnectionStringBuilder類別,光看名子就知道,這是讓你拿來建立SQL連線字串,使用起來非常直覺,而且又提供IntelliSense,相信我,這可是能減輕腦負擔的好東西,如果你使用過一次SqlConnectionStringBuilder類別,大概就不會想回頭了,再去愛別人了。

01Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
02    '以下範例程式,未使用import System.Data.SqlClient
03        '原始:Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;User ID=bruce;Password=123456789
04        '以下使用SqlConnectionStringBuilder類別來建立ConnectionString
05        Dim CSBuilder As New System.Data.SqlClient.SqlConnectionStringBuilder
06        CSBuilder.DataSource = ".\SQLEXPRESS"
07        CSBuilder.InitialCatalog = "Northwind"
08        CSBuilder.UserID = "bruce"
09        CSBuilder.Password = "123456789"
10        CSBuilder.PersistSecurityInfo = True
11 
12    'CSBuilder.ConnectionString取得連線字串,建立SqlConnection
13    Dim conn As New System.Data.SqlClient.SqlConnection(CSBuilder.ConnectionString)
14    'Other Code
15    End Sub

這麼好用的東西,你在市面上一般的書本中是看不到哦,學起來,根本就不用背任何東西,IntelliSense選一選就完成了,不過,必要的資訊(就是後面的字串內容)還是要記一下。

Microsoft Visual Studio International Feature Pack 2.0--在ASP.NET實作繁簡轉換

先看微軟的介紹:

Visual Studio International Feature Pack 2.0 擴充了先前 1.0 版的功能, 它提供了一組控制項和類別庫以幫助.NET開發人員建立符合國際化需求的應用程式。
跟我們之前介紹使用資源檔的方式不同,它提供的是控制項(Controls)和類別庫(Class Library)來幫助.NET開發人員處理各種亞洲語系上的的文字、字串處理工作。

而Visual Studio International Feature Pack 2.0是「擴充」之前1.0的功能,所以本身不含1.0的功能,如果想使用1.0提供的功能,可下載1.0進行安裝。

Microsoft Visual Studio International Pack 1.0 SR1
Microsoft Visual Studio International Feature Pack 2.0

如果你的網頁需要處理多國語言的問題,尤其是亞洲這個雙字元文字的問題,可一定要好好玩玩這個佛心級工具。想想,以前光一個「繁體轉簡體」的問題,就不知要花多少力,現在簡單幾行程式就可以完成了,.NET Framework是的越來越強大,真的是越來越愛了。

在Visual Studio International Pack 1.0 SR1的版本裡,我比較有興趣的是,CHTCHSConv.msi及EANumFormat.msi,

CHTCHSConv.msi
Traditional Chinese to Simplified Chinese Conversion Library and Add-In Tool (中文繁簡轉換類別庫 及Visual Studio Add-In Tool工具):提供了一組類別庫以幫助程式開發人員在應用程式中轉換將中文繁體至中文簡體(或是中文簡體至中文繁體). 此轉換機制使用的內建於 Microsoft office 2007 的程式, 它的以進行詞對詞的轉換, 讓轉換的品質更好. 若使用者的系統中沒有安裝Microsoft office 2007 , 此類別庫將使用傳統的字對字轉換. 除此之外, 此元件也包括了一個 Visual Studio Add-In Tool 工具可以讓使用都直接轉換存在於資源檔中的中文字串.
安裝後,將「C:\Program Files\Microsoft Visual Studio International Pack\Traditional Chinese to Simplified Chinese Conversion Library and Add-In Tool」之下的「ChineseConverter.dll」參考或複製到「Bin」目錄之下即可。

實作ChineseConverter

TCtoSC.aspx
01<form id="form1" runat="server">
02<div>
03<asp:textbox id="tbxTCtoSC" runat="server"></asp:textbox>
04<asp:button id="btnTCtoSC" runat="server" text="繁體轉簡體">
05<asp:label id="lblTCtoSC" runat="server"></asp:label>   
06<asp:textbox id="tbxSCtoTC" runat="server"></asp:textbox>
07<asp:button id="btnSCtoTC" runat="server" text="簡體轉繁體">
08<asp:label id="lblSCtoTC" runat="server"></asp:label>
09</asp:button></asp:button>
10</div>
11</form>

TCtoSC.aspx.vb
01'繁簡轉換命名空間
02Imports Microsoft.International.Converters.TraditionalChineseToSimplifiedConverter
03Partial Class Test_TCtoSC
04    Inherits System.Web.UI.Page
05 
06    Protected Sub btnTCtoSC_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnTCtoSC.Click
07        If Me.tbxTCtoSC.Text.Length > 0 Then
08            Dim TWString As String = Me.tbxTCtoSC.Text
09            '使用ChineseConverter.Convert方法
10            '第一個參數是你要轉換的字串
11            '第二個參考是你要轉換目的語言
12            'TraditionalToSimplified是繁體轉換成簡體
13            Me.lblTCtoSC.Text = ChineseConverter.Convert(TWString, ChineseConversionDirection.TraditionalToSimplified)
14        End If
15    End Sub
16 
17    Protected Sub btnSCtoTC_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnSCtoTC.Click
18        If Me.tbxSCtoTC.Text.Length > 0 Then
19            Dim SCString As String = Me.tbxSCtoTC.Text
20            '使用ChineseConverter.Convert方法
21            '第一個參數是你要轉換的字串
22            '第二個參考是你要轉換目的語言
23            'SimplifiedToTraditional是簡體轉換成繁體
24            Me.lblSCtoTC.Text = ChineseConverter.Convert(SCString, ChineseConversionDirection.SimplifiedToTraditional)
25        End If
26    End Sub
27End Class

以上這支程式還可以用我們之前介紹使用Button裡的CommandName及CommandArgument來改寫。

EANumFormat.msi
East Asia Numeric Formatting Library (亞洲語系的數值字串格式化類別庫):East Asia Numeric Formatting Library 提供了一組類別庫以幫助程式開發人員將數值資料格式化成亞洲語系的數值字串. 支援的亞洲語系包括了繁體中文, 簡體中文, 日文以及韓文.

將「C:\Program Files\Microsoft Visual Studio International Pack\East Asia Numeric Formatting Library」目錄之下的「EastAsiaNumericFormatter.dll」參考或複製到「Bin」之下。

實作EastAsiaNumericFormatter

EastAsiaNumericFormatting.aspx
1<form id="form1" runat="server">
2<div>
3<asp:textbox id="tbxNumber" runat="server" tooltip="請輸入整數,例如:12345"></asp:textbox>
4<asp:button id="btnNumToChinese" runat="server" text="數字轉中文">   
5<asp:label id="lblNumtoChinese" runat="server"></asp:label>
6<asp:label id="lblNumtoJP" runat="server"></asp:label>
7</asp:button>
8</div>
9</form>

EastAsiaNumericFormatting.aspx.vb
01'亞洲數字格式命名空間
02Imports Microsoft.International.Formatters
03'為了設定日文需要使用CultureInfo("ja")
04Imports System.Globalization
05Partial Class Test_EastAsiaNumericFormatting
06    Inherits System.Web.UI.Page
07 
08    Protected Sub btnTCtoNum_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnNumToChinese.Click
09        If Me.tbxNumber.Text.Length > 0 Then
10            Dim ChangeNumber As Integer = Convert.ToInt32(Me.tbxNumber.Text)
11            Try
12                '會依瀏灠器做CultureInfo的選擇,如簡體瀏灠器則出現簡體中文的數字
13                Me.lblNumtoChinese.Text = String.Format(New EastAsiaNumericFormatter(), "數字的中文是:{0:L}", ChangeNumber)
14                '指定轉換的文字
15                'Me.lblNumtoJP.Text = "數字的日文是:" & EastAsiaNumericFormatter.FormatWithCulture("L", ChangeNumber, Nothing, New CultureInfo("ja"))
16                'FormatWithCulture(String, Object, IFormatProvider, CultureInfo)
17                Me.lblNumtoJP.Text = String.Format("數字的日文是:{0}", EastAsiaNumericFormatter.FormatWithCulture("L", ChangeNumber, Nothing, New CultureInfo("ja")))
18            Catch ex As Exception
19                Me.lblNumtoChinese.Text = "錯誤:" & ex.Message
20            End Try
21        End If
22    End Sub
23End Class

在Microsoft Visual Studio International Feature Pack 2.0的NumericConversion,新增對阿拉伯文的支援,安裝到可到「C:\Program Files\Microsoft Visual Studio International Feature Pack 2.0\NumericConversion」之下將「InternationalNumericFormatter.dll」參考或複製到「Bin」目錄之下,新增「InternationalNumericFormatter」類別,可進行對阿拉伯數字字串的設定。

舉個例子,以下是我的想法,我還沒有實作,但應該可行。
使用WebRequest類別將網頁資料讀進來,然後透過上述方法去轉換內容,然後再輸出,看你是要繁轉簡或簡轉繁,應該都不成問題才是。


工具給各位,怎麼用就看各位的功力了,拿別人的努力讓自己更上一層樓。加油。

FileUpload中MIME

以下為MSDN中的MIME

Known MIME Types

text/richtext
text/html
audio/x-aiff
audio/basic
audio/wav
image/gif
image/jpeg
image/pjpeg
image/tiff
image/x-png
image/x-xbitmap
image/bmp
image/x-jg
image/x-emf
image/x-wmf
video/avi
video/mpeg
application/postscript
application/base64
application/macbinhex40
application/pdf
application/x-compressed
application/x-zip-compressed
application/x-gzip-compressed
application/java
application/x-msdownload

MIME常用在檔案上傳(FileUpload控制項),我們需要判斷檔案是否是我們要的類型,例如,我們希望使用者只能上傳zip檔:

1IF FileUpload1.HasFile Then
2  try
3    IF FileUpload1.PostedFile.ContentType = "application/x-zip-compressed" Then
4    'Code
5    End IF
6  '...
7End IF

或是在傳送Mail(System.Net)時的附加檔案,總不希望使用者亂上傳資料檔案,所以就需要MIME來判斷,總之如果你需要資料使用者上傳了什麼東東,就需要使用MIME就對了。

參考:

ASP.NET控制Meta Tags

你或許使用過ASP.NET的Page.Title來修改網頁的標題,但你有沒有試過使用ASP.NET來修改Meta,你或許會說,那還不簡單,使用Page.Meta就好了,嗯!你打打看。

其實沒有那怎難,在ASP.NET中,除了控制項,我們還可以把一般的HTML轉換為HTML控制項,在ASP.NET中每一個HTML控制項都有對應的類別,例如,在Visual Studio中打「Dim meta As New html」你就可以看到所以html對應的類別,如HtmlAnchor對應的就是&lt;a>。

但如果我們想在程式碼中操控這些HTML,那就先必須把這些HTML轉換為ASP.NET的控制項,轉換的過程異常簡單,只需要在HTML Tags裡加上「runat="server"」屬性馬上就能將HTML轉換為ASP.NET的控制項。就我們的需求,我想要操控Meta所以我幫Meta加上兩個屬性:

1<meta id="metaName" runat="server" />

這樣就馬上讓meta成為ASP.NET的控制項,而id是為了讓我們可以在程式碼中進行設定。

1Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
2        metaName.Attributes("name") = "author"
3        metaName.Attributes("Content") = "Bruce"
4    End Sub

這樣讓meta可進行程式碼操作後,就可以進行許多事,例如我們的動態頁面裡的meta可以與資料庫互動,動態產生meta,來進行簡易meta的SEO動作。

一樣的原理,可以用於其他HTML中,重點只有在「runat="server"」,把HTML轉換為HTML控制項,成為控制項後,ASP.NET才能進行程式碼的操作。

使用Calendar控制項+jQuery實作DatePicker

我們在讓使用者輸入資料時,常有機會需要讓使用者選擇日期,所以而這種選擇日期的功能我們稱「Date Picker」,你上網搜尋,可以找到一大堆資源,但我想要使用ASP.NET裡的Calendar加jQuery來實作看看,動手做,樂趣無窮。

做法很簡單,我們先新增一default.aspx網頁,然後在網頁上放上一個TextBox及一個<img>,TextBox是讓使用者選擇完日期後,把日期放入TextBox之中,而<img>是顯示及隱藏Calendar控制項。

Default.aspx

01<form id="form1" runat="server">
02<div>
03<asp:label associatedcontrolid="tbxDate" id="lblBirthday" runat="server" text="生日:">
04        <asp:textbox id="tbxDate" runat="server">
05        <img alt="選擇日期" id="ibnt" src="Calendar.png" />
06</div>
07<asp:calendar backcolor="White" bordercolor="#999999" cellpadding="4" daynameformat="Shortest" font-names="Verdana" font-size="8pt" forecolor="Black" height="180px" id="cal" runat="server" width="200px">
08            <selecteddaystyle backcolor="#666666" font-bold="True" forecolor="White">
09            <selectorstyle backcolor="#CCCCCC">
10            <weekenddaystyle backcolor="#FFFFCC">
11            <todaydaystyle backcolor="#CCCCCC" forecolor="Black">
12            <othermonthdaystyle forecolor="#808080">
13            <nextprevstyle verticalalign="Bottom">
14            <dayheaderstyle backcolor="#CCCCCC" font-bold="True" font-size="7pt">
15            <titlestyle backcolor="#999999" bordercolor="Black" font-bold="True">  </titlestyle></dayheaderstyle></nextprevstyle></othermonthdaystyle></todaydaystyle></weekenddaystyle></selectorstyle></selecteddaystyle></asp:calendar>
16</form>

我們先在Defuault.asp.vb中處理ASP.NET的相關事件。

Defuault.asp.vb

01Protected Sub cal_DayRender(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.DayRenderEventArgs) Handles cal.DayRender
02      '生日不會大於今天,所以把大於今天的日期選取功能取消
03      If e.Day.Date > DateTime.Now Then
04          e.Day.IsSelectable = False
05      End If
06  End Sub
07 
08  Protected Sub cal_SelectionChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles cal.SelectionChanged
09      Me.tbxDate.Text = Me.cal.SelectedDate.ToShortDateString
10  End Sub

最後就是處理Calendar顯示及隱藏的jQuery:

jQuery - position

01$(function() {
02    //先隱藏Calendar
03    $("#cal").hide();
04    //<img>的click事件
05    $("#ibnt").click(function() {
06        var p = $("#ibnt");
07        //可使用position或offset來取得<img>的top及left座標
08        //var position = p.position();
09        //$("#cal").css({ "position": "relative", "top": position.top, "left": position.left });
10        var offset = p.offset();
11        //設定相對位置
12        $("#cal").css({ "position": "relative", "top": offset.top, "left": offset.left });
13        $("#cal").toggle();
14    });
15 
16    //讓TextBox也能有<img>一樣的效果
17    $("#tbxDate").click(function() {
18        var p = $("#tbxDate");
19        var position = p.position();
20        $("#cal").css({ "position": "relative", "top": position.top, "left": position.left });
21        //var offset = p.offset();
22        //$("#cal").css({ "position": "relative", "top": offset.top, "left": offset.left });
23        $("#cal").toggle();
24    });
25 });

簡單吧,不過由於我們是使用ASP.NET的Calendar控制項,所以在點選時,會產生Postback,才有辦法把值寫入TextBox中,不像一般純JavaScript那樣單純,把值直接寫入TextBox中。控制項有控制項的好處,或許我們還能用CallBack來改寫,或使用Ajax實作來減少Postback,但我的重點是在jQuery,簡單又實用。

當業績與良心對上時

我的Focus已經過保固有一段時間了,因為習慣,雖然有認識保養廠的朋友,但我還是都回原廠保養。但近來我的Focus出現冷車異音的問題,但發生的頻率還不到100%,所以每次回原廠保養也沒查出什麼所以然?要嘛就不出聲,要嘛就咻咻咻的叫。

但這都不是問題,一般的保養是沒有什麼問題,但最近幾次進廠,都會有意外出現,我所謂的意外就是會發生:「先生,我們保養的過程發現你的愛車xxx有問題,這可能會有安全性上的問題,請問你是否要做更換?」

通常我們又不懂這些專有名詞,懂的只是皮毛,又聽到「安全性」上的問題,通常都會花錢了事,不過幾次下來,總覺得不對,而且每次都是壞那種我認為很奇怪的地方,例如有一次,Ford跟我說我Focus有一隻避振器漏油,問我是否要更換?我心想,我的Focus大多只跑高速公路,我們台灣的高速公路能「振」到Focus避振器漏油,也太不容易了吧,原本我是不想換,但剛好假日又要回宜蘭,心想,又是長途、又是安全性問題,而且重點是,我現在車上有寶寶,花錢了事吧。

重要的是,Ford每次這種「換東西的問題」產生時,你要換也可以,不換也可以,不換的話Ford是不會也不會跟你說什麼,好像出事也不關他們的事一樣,這樣的態度讓我有了疑心。照理說,如果真的會影響到安全(因為可能會死人),說什麼也要跟顧客解譯清楚,就算不在原廠維修,也要提醒顧客這個問題一定要去處理好,不要去影響到行車安全。

所以這一次送去保養後,真的電話又來了,又說我xxx有問題,是否要更換零件。這一次我就直接說不用了。我將車開回來了直接去找我保養廠的朋友,請他幫我看原廠說的那個問題,第二天我朋友打電話來,說原廠在放屁,他們要做業績,你不用理他們。

我說,做業績是什麼意思?我朋友說,你的零件根本沒問題。朋友還說,他學弟就在Ford原廠,他學弟跟他說,Ford原廠會要求業績,一台車進廠,沒花到5000以上他們會被「釘」,而且每個月還有60萬的業績要求,所以像我們這些過保的車,就是他們下手的目標。反正我們什麼也不懂,隨便唬唬可以達到要求了。

我朋友的解譯跟我的想法match,這樣的說法,請各位去求證,我也只是聽說,但我是相信的那一個。如果有問題,還是到現場請師博說明給你看,他們知道你是會到現場看的人,或許比較不敢這用電話唬人。

即時先行編譯(in-place precompilation)

我們都知道,在ASP.NET中,第一次被瀏覽的網頁會比較慢才會做出反應,原因是第一次被瀏覽的網頁需要complier,第二次就不用再complier,但如果我們想一開始就把全部的網頁都complier好,那可以使用即時先行編譯(in-place precomplation)技術。

如果網站剛上線,或Server維護重開機,可以使用這個方便的小工具,幫我們先把全部的網頁「點一遍」,也因為每一頁都被點過了,也代表每一頁都被complier過了,所以第一位進來的仁兄,不用等那麼久。

通常使用下面範例二即可。

C:\Windows\Microsoft.NET\Framework\v2.0.50727>aspnet_compiler.exe /?

先行編譯 ASP.NET 應用程式的公用程式
Copyright (C) Microsoft Corporation. All rights reserved.

使用方式:
aspnet_compiler [-?] [-m metabasePath | -v virtualPath [-p physicalDir]]
                [[-u] [-f] [-d] [-fixednames] targetDir] [-c]
                [[-keyfile file | -keycontainer container]
                     [-aptca] [-delaySign]]
                [-errorstack]

-?            列印這段說明文字。
-m            應用程式的完整 IIS Metabase 路徑。這個參數不能和 -v 或 -p 參數結合。
-v            要編譯的應用程式的虛擬路徑 (例如 "/MyApp")。如果已指定 -p,就會使用實體路徑尋找應用程式,否則會使用 IIS Me
tabase,並假設應用程式位於預設網站 (在
              "/LM/W3SVC/1/Root" 下)。這個參數不能和 -m 參數結合。
-p            要編譯的應用程式的實體路徑。如果遺漏 -p,就會使用 IIS Metabase 尋找應用程式。這個參數必須和 -v 結合。
-u            如果已指定這個路徑,就可以更新先行編譯的應用程式。
-f            如果目標目錄已存在則覆寫。現有的內容將遺失。
-d            如果已指定,會在編譯時發出偵錯資訊。
targetDir     編譯應用程式的實體路徑。如果未指定這個路徑,則會就地先行編譯應用程式。
-c            如果已指定,先行編譯的應用程式便會完整重建。任何之前編譯過的元件都會重新編譯。已指定 targetDir 時,永遠啟
用此選項。
-keyfile      強式名稱金鑰檔的實體路徑。
-keycontainer 指定強式名稱金鑰容器。
-aptca        如果已指定,強式名稱的組件將會允許部分信任的呼叫端。
-delaysign    如果已指定,建立時組件不會完整簽署。
-fixednames   如果已指定,編譯的組件將會獲得固定的名稱。
-nologo       隱藏編譯器著作權訊息。
-errorstack   顯示可協助進行特定條件偵錯的額外偵錯資訊。

範例:

下列兩個命令功能相同,而且都倚賴 IIS Metabase。編譯完成的應用程式會部署到 c:\MyTarget:

    aspnet_compiler -m /LM/W3SVC/1/Root/MyApp c:\MyTarget
    aspnet_compiler -v /MyApp c:\MyTarget



下列命令會就地編譯應用程式 /MyApp。如此一來,當 HTTP 要求傳送至這個應用程式時,就不再需要進行編譯:

    aspnet_compiler -v /MyApp



下列命令「不」倚賴 IIS Metabase,因為它會明確指定應用程式的實體來源目錄:

    aspnet_compiler -v /MyApp -p c:\myapp c:\MyTarget