วันเสาร์ที่ ๒๗ มีนาคม พ.ศ. ๒๕๕๓

VB.NET ออกรายงาน Excel แบบรวดเร็ว

โดยปกติเมื่อเราพัฒนาโปรแกรมทางธุรกิจ ที่หนีไม่พ้นก็คือต้องออกรายงานครับ และโดยส่วนใหญ่เรามักจะใช้ Crystal Report เพราะสะดวก ง่าย และรวดเร็วครับ ที่สำคัญเรายังสามารถ Export ข้อมูลออกเป็น Excel, Word หรือ PDF ก็ยังได้

แต่ถ้าเราไม่สามารถใช้ Crystal Report ได้ละ เช่นลูกค้าไม่มี License เป็นต้น ทางเลือกหนึ่งก็คือออกรายงานเป็น Excel ครับ ถ้าลูกค้ามี Licese ของ Microsoft Excel อยู่แล้วก็ไม่ยาก หรือเราอาจใช้ OWC (Office Web Componet) แทนก็ได้ ซึ่ง OWC สามารถดาวน์โหลดได้จากเวบของ Microsoft ครับ (ปัจจุบันผมใช้ OWC11) ซึ่งสามารถออกรายงาน รวมถึงสร้างกราฟได้ด้วย

แต่ข้อเสียของการใช้ Excel หรือ OWC คือ ช้าครับ แถมกิน CPU สูงมากโดยเฉพาะถ้าต้องวนลูปเพื่อใส่ข้อมูล วันนี้ผมจะนำเสนออีกวิธีในการออกรายงาน Excel ครับ นั่นคือเรานำข้อมูลที่ต้องการไปใส่ใน delimited file ก่อนเช่น .csv (CSV file), .txt (Tab delimited file), .prn (Spaced delimited, Formatted Text) หรือ .txt ที่เรากำหนด delimeter เอง เช่น ; หรือ : เป็นต้นครับ จากนั้นค่อยใช้ Excel หรือ OWC เปิดไฟล์พวกนี้ขึ้นมาเพื่อทำการตกแต่งรายงานให้สวยงาม เช่น ใส่ Header, กำหนดขนาด หรือ layout, ตีเส้นตาราง เป็นต้นครับ

ทีนี้มาลองดูการออกรายงานด้วย Tab Delimited ละกันครับ ผมเริ่มจากสร้าง Console Application มาตัวหนึ่ง แล้วก็ Add Reference ของ Microsoft Excel 12.0 Object Library ครับ



จากนั้นก็สร้างข้อมูลสำหรับทดสอบ โดยสร้าง Employee Class ขึนมา และ EmployeeManager Class สำหรับ สร้าง List ของ Employee ขนาด 100,000 ข้อมูลครับ


Public Class Employee
Public EmployeeID As String
Public FirstName As String
Public LastName As String
Public BirthDay As Date
Public Salary As Double
End Class

Public Class EmployeeManager
Public Shared Function GetEmployees() As IList(Of Employee)
Dim employees As New List(Of Employee)
Dim emp As Employee = Nothing
For iRecord As Integer = 0 To 100000
emp = New Employee With {.EmployeeID = iRecord, .FirstName = "Test" & iRecord, .LastName = "LName", .BirthDay = DateSerial(1990, 1, 1), .Salary = 2000.0 + CDbl(iRecord)}
employees.Add(emp)
Next
Return employees
End Function
End Class


ทีนี้ก็มาเขียนโค้ดสำหรับการทดสอบกันครับ

Module Module1
Const EU_DATE_FORMAT As String = "dd-MM-yyyy hh:mm:ss"
Dim employees As IList(Of Employee)
Dim startDate As Date = Now
Dim endDate As Date = Nothing

Private Sub DecorateExcel(ByVal xlsBook As Microsoft.Office.Interop.Excel.Workbook)

Dim xlsSheet As Microsoft.Office.Interop.Excel.Worksheet = Nothing
xlsSheet = xlsBook.ActiveSheet
xlsSheet.Range("A:E").EntireColumn.AutoFit()

With xlsSheet.Range("A1").Font
.Bold = True
.Size = "14"
End With

With xlsSheet.Range("A2").Font
.Bold = True
.Size = "12"
End With

With xlsSheet.Range("A3:E3")
.Interior.Color = System.Drawing.Color.Silver.ToArgb
.Font.Bold = True
End With

xlsSheet.Range("E:E").NumberFormat = "#,##0.00"

Dim r As Microsoft.Office.Interop.Excel.Range
r = xlsSheet.Range("A3:E20004")

With r.Borders
.LineStyle = Microsoft.Office.Interop.Excel.XlLineStyle.xlContinuous
.Weight = Microsoft.Office.Interop.Excel.XlBorderWeight.xlThin
.ColorIndex = 0
End With

End Sub

Private Sub ExportByTAB()
startDate = Now
Console.WriteLine("Export by Tab delimited")
Console.WriteLine("Start at " & startDate.ToString(EU_DATE_FORMAT))

Dim _fileNameTAB As String = "C:\test.txt"
Dim _fileName As String = "c:\testTAB.xlsx"

Dim fs As System.IO.FileStream = System.IO.File.Create(_fileNameTAB)
Dim writer As New System.IO.StreamWriter(fs)

writer.WriteLine("List of Employee")
writer.WriteLine("As of " & Now.ToString("dd MMM yyyy"))
writer.WriteLine("Employee ID" & vbTab & "First Name" & vbTab & "Last Name" & vbTab & "Birth Date" & vbTab & "Salary")

For Each emp As Employee In employees
writer.WriteLine(String.Concat(emp.EmployeeID, vbTab, emp.FirstName, vbTab, emp.LastName, vbTab, emp.BirthDay.ToString("yyyy-MM-dd"), vbTab, emp.Salary))
Next

writer.Close()
writer.Dispose()
fs.Close()
fs.Dispose()

' Open txt file with Excel for decoration
Dim xlsAPP As New Microsoft.Office.Interop.Excel.Application
Dim xlsBook As Microsoft.Office.Interop.Excel.Workbook
xlsAPP.Visible = False
xlsBook = xlsAPP.Workbooks.Open(_fileNameTAB)
DecorateExcel(xlsBook)

' Save as Excel2007
If System.IO.File.Exists(_fileName) Then System.IO.File.Delete(_fileName)
xlsBook.SaveAs(_fileName, Microsoft.Office.Interop.Excel.XlFileFormat.xlOpenXMLWorkbook)
xlsBook.Close(SaveChanges:=False)

xlsAPP.Quit()

System.Runtime.InteropServices.Marshal.ReleaseComObject(xlsAPP)
GC.SuppressFinalize(xlsAPP)
GC.Collect()
xlsBook = Nothing
xlsAPP = Nothing

System.IO.File.Delete(_fileNameTAB)

endDate = Now
Console.WriteLine("Finish at " & endDate.ToString(EU_DATE_FORMAT))
Console.WriteLine("Total " & (endDate - startDate).TotalMilliseconds & " milliseconds")
Console.WriteLine("***********************")

End Sub

Sub Main()

employees = EmployeeManager.GetEmployees()
Console.WriteLine("Testing for " & employees.Count - 1 & " records.")
Console.WriteLine("")
ExportByTAB()
Console.WriteLine("Finish test.")
Console.ReadLine()

End Sub

End Module


ลองรันผลทดสอบดูครับ เครื่องผม Pentium 4 2.66GHz RAM 2 GB ข้อมูลขนาด 100,000 เรคคอร์ดใช้เวลา 7 วินาทีกว่าๆ



ข้อมูลขนาด 5,000 เรคอร์ด ใช้เวลา 2.1 วินาที


ลองดูผลลัพธ์ครับ


เรียบร้อยครับ ทีนี้ถ้าเราอยากจัด Layout สวยๆ เราสามารถเปิด Excel แล้วสั่งบันทึก Macro แล้วค่อยเอา source code ที่ได้ไปใส่เพิ่มใน DecorateExcel Function ครับ

ปล.
1. ผมลอง export โดยใช้ CSV ก็ได้ความเร็วไม่ต่างกันมากครับ
2. ผมลองวนลูปเก็บข้อมูลเข้า StringBuilder ก่อน แล้วค่อยเขียนไฟล์ ปรากฏว่าใช้เวลามากกว่าเปิดไฟล์แล้ววนลูปเขียนเข้าไปตรงๆเลยครับ

วันอังคารที่ ๒๓ มีนาคม พ.ศ. ๒๕๕๓

VB6 ติดต่อ ASP.NET WebService

ช่วงนี้มีโอกาสกลับไปแก้โปรแกรมเก่าที่เขียนด้วย VB6 ครับ และแน่นอนงานที่แก้ก็คือให้โปรแกรมมันทำงานกับ WebServices ได้นั่นเอง หลังจากลองๆค้นไปค้นมา ก็เลยทำโปรแกรมทดสอบมาก่อนครับ

เริ่มต้นที่ ASP.NET WebServices กันก่อน แรกสุดก็สร้าง Employee Class ครับ

<Serializable()> _
<System.ComponentModel.DefaultProperty("EmployeeID")> _
Public Class Employee

Private _employeeId As String
Private _employeeName As String
Private _employeeSurnam As String

Public Property EmployeeSurname() As String
Get
Return _employeeSurnam
End Get
Set(ByVal value As String)
_employeeSurnam = value
End Set
End Property

Public Property EmployeeName() As String
Get
Return _employeeName
End Get
Set(ByVal value As String)
_employeeName = value
End Set
End Property

Public Property EmployeeId() As String
Get
Return _employeeId
End Get
Set(ByVal value As String)
_employeeId = value
End Set
End Property

Public Overrides Function ToString() As String
Return EmployeeId
End Function

Public Function Save() As Boolean
Return True
End Function

End Class


ที่จริงมี EmployeeManager Class ด้วย แต่ว่าขี้เกียจเอามาลงให้ดู ทีนี้ก็มาสร้าง WebServices กันครับ


Imports System.Web
Imports System.Web.Services
Imports System.Web.Services.Protocols

<WebService(Namespace:="http://jnithi/")> _
<WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Public Class EmployeeWS
Inherits System.Web.Services.WebService

<WebMethod()> _
Public Function GetEmployee(ByVal empID As String) As Employee
Dim emp As Employee = Nothing
emp = EmployeeManager.GetEmployeeByID(empID)
Return emp
End Function

<WebMethod()> _
Public Function UpdateEmployee(ByVal empID As String, ByVal empName As String, ByVal empSurname As String) As Boolean
Dim emp As Employee = Nothing
Dim result As Boolean = False
emp = EmployeeManager.GetEmployeeByID(empID)
emp.EmployeeName = empName
emp.EmployeeSurname = empSurname
result = emp.Save
Return result
End Function

End Class


ลองรัน WebServices ดูครับ ไปที่ http://nithi/TestWeb/EmployeeWS.asmx จะเห็นว่ามี Service 2 ตัว ตามที่เราสร้างไว้ครับ กดเลือก GetEmployee เพื่อดู Service Description

ดูที่ SOAP 1.1 ครับ เดี๋ยวเราจะนำข้อมูลตรงนี้ไปใช้ในการเรียก WebService

ส่วนของ SOAP Request


ส่วนของ SOAP Response


คราวนี้มาที่ VB6 บ้าง เริ่มจากสร้าง Project ใหม่ จากนั้นก็สร้าง Class ชื่อ clsEmployee ครับ ให้มี Property 3 ตัวเหมือนฝั่ง .NET ทีนี้ก็มาสร้าง Class สำหรับจัดการ WebService ครับ ตั้งชื่อ clsEmployeeWS ก่อนอื่นก็ Add Reference ก่อนครับ เลือก Microsoft XML, v4.0 (ผมลองตั้งแต่ v 2.6 ก็ใช้ได้หมดครับ เลยเลือกอันกลางๆ)แล้วก็เขียนโค้ดได้เลย


Dim asmxURL As String
Dim soapAction As String
Dim soapEnvelop As String
Dim domResult As MSXML2.DOMDocument

Private Function GetResponse(URL As String, action As String, envelop As String) As MSXML2.DOMDocument
Dim xmlHTTP As New MSXML2.xmlHTTP
Dim domReturn As New MSXML2.DOMDocument
Dim domDOC As New MSXML2.DOMDocument

domDOC.loadXML envelop

xmlHTTP.Open "POST", URL, False

' Create headings
xmlHTTP.setRequestHeader "Content-Type", "text/xml; charset=utf-8"
xmlHTTP.setRequestHeader "SOAPAction", action

' Send
xmlHTTP.send domDOC.xml
domReturn.loadXML xmlHTTP.responseText


Set xmlHTTP = Nothing
Set domDOC = Nothing
Set GetResponse = domReturn
End Function


ผมสร้าง Function ชื่อ GetResponse ไว้สำหรับติดต่อกับ WebServices ครับ Function นี้จะคืนค่าเป็น XMLDOMDocument มาให้ครับ ถ้าเราเรียก WebServices สำเร็จ ลอง debug ดูค่า domReturn.xml เพื่อดูผลลัพธ์ได้ครับ

เสร็จแล้วก็สร้าง Function สำหรับเรียกใช้งาน WebServices ครับ โดยใน GetEmployee Function เราจะระบุ parameter ที่ต้องการได้แก่ asmxURL, soapAction (ดูจาก SOAP Request ในรูปด้านบนครับ) แล้วก็ Envelop (copy มาจาก SOAP Request เหมือนกัน) แต่อย่าลืมเปลี่ยน parameter นะครับ ตรงบรรทัด " & empID & "


Public Function GetEmployee(empID) As clsEmployee
asmxURL = "http://nithi/testweb/EmployeeWS.asmx"
soapAction = "http://jnithi/GetEmployee"
soapEnvelop = " " & _
" " & _
" " & _
" " & _
" " & empID & " " & _
"
" & _
"
" & _
"
"
Set domResult = GetResponse(asmxURL, soapAction, soapEnvelop)

Dim oNode As MSXML2.IXMLDOMNode
Dim oNodes As IXMLDOMNodeList

'ดึง ... ผลลัพธ์ได้เป็น List (XMLDOMNodeList)
Set oNodes = domResult.getElementsByTagName("GetEmployeeResult")
Dim employee As clsEmployee

If oNodes.length > 0 Then
Set oNode = oNodes.Item(0)
Set employee = New clsEmployee
employee.EmployeeID = oNode.selectSingleNode("EmployeeId").Text
employee.EmployeeName = oNode.selectSingleNode("EmployeeName").Text
employee.EmployeeSurname = oNode.selectSingleNode("EmployeeSurname").Text
End If
Set GetEmployee = employee
End Function


คราวนี้มาทำหน้าจอทดสอบครับ




Private Sub ClearEmployee()
Me.txtEmpID.Text = ""
Me.txtEmpName.Text = ""
Me.txtEmpSurname.Text = ""
End Sub

Private Sub cmdClear_Click()
ClearEmployee
Me.txtSearchID.Text = ""
End Sub

Private Sub cmdSearch_Click()
Dim ws As New clsEmployeeWS
Dim employee As clsEmployee

'เรียก clsEmployeeWS ให้ติดต่อ WebServices
Set employee = ws.GetEmployee(Me.txtSearchID.Text)
If employee Is Nothing = False Then
Me.txtEmpID.Text = employee.EmployeeID
Me.txtEmpName.Text = employee.EmployeeName
Me.txtEmpSurname.Text = employee.EmployeeSurname
Else
ClearEmployee
MsgBox "Cannot find employee.", vbInformation + vbOKOnly, App.Title
End If
End Sub


ลองรันดูผลลัพธ์ครับ



เรียบร้อย ไม่ยากครับ

จริงๆตอนทดสอบ ผมยังลองทำการ Insert, Update, Delete แล้วก็ SELECT ข้อมูลเป็น Array มาด้วย แต่คงไม่ได้เอามาลง Blog ให้ดูครับ เพราะถ้ารู้ concept แล้วที่เหลือก็ไม่ยากแล้วครับ

วันอังคารที่ ๙ มีนาคม พ.ศ. ๒๕๕๓

ลอง Chrome และ FireFox 3.6

ผมเคย download หมาย่างมาลองใช้ตั้งแต่เวอร์ชัน 1.x จนล่าสุดในเครื่องเป็นเวอร์ชัน 2.x แต่แค่ลองคลำๆดูก็เลิกกลับมาใช้ IE เหมือนเดิม เพราะว่าใช้ FF แล้วเข้าไป Pantip แล้ว font มันไม่สวย ตัวเล็ก อ่านยากกกกก และเวบไทยหลายๆเวบแนะนำให้ใช้ IE มากกว่า

จนเมื่อวานนี้ได้ลองดาวน์โหลด Chrome มาใช้ มันเยี่ยมมาก เร็วและดี เลยลอง search ใน google เรื่อง Browser ต่างๆเพิ่มเติม จนเจอว่า FF มันมี option ให้ set font ได้ (จริงๆถ้าตั้งใจเล่นก็น่าจะ set เองได้ตั้งนานแล้ว มัวแต่ติด IE) พอลอง set ตามและเข้า Pantip และเวบอื่นๆที่เล่นบ่อยๆดู โอ้ว จอร์จ ไม่น่าโง่ตั้งนาน ก็เลยรีบไปดาวน์โหลด FF3.6 ภาษาไทยที่ http://www.mozilla.com/th/ และก็ติดตั้ง Extension พวก FireBug, WebDeveloper มาลองเล่นดู เพราะเคยอ่านเจอใน Textbook ครับ อยากลองเล่นมานานแล้วแต่ขี้เกียจลง FF

เมื่อคืนก็เลยลอง Chrome กับ FF อยู่ 3-4 ชั่วโมง สรุปว่าส่วนตัวผมชอบ Chrome มากกว่า ตอนนี้เลยใช้ Chrome เป็นหลัก ยกเว้นเข้าบางเวบก็จะใช้ IE ครับ ส่วน FF ก็ชอบอยู่นะก็จะลองใช้สลับกับ Chrome ดูครับ

ว่างๆจะลองโหลด IE8 กับ Opera มาเล่นดูบ้าง ไม่ได้เล่น Opera มานานมากแล้ว

ปล. วันนี้ป่วยลางานอยู่บ้านครับ เพราะเป็นฝีฝักบัวโดนหมอผ่าก้อนฝีออกเท่ามะนาวลูกย่อมๆ (หมอบอก เพราะผมหลับตาปี๋ไม่กล้ามอง) หมอยังไม่เย็บแผลเลย ตอนนี้เอาผ้าก็อซใหญ่กว่าหัวแม่โป้ง (อันนี้เห็นตอนทำแผล) ยัดเข้าไปแล้วปิดแผลไว้ รอดูอาการถ้าดีขึ้นถึงจะเย็บแผล ระหว่างพิมพ์ blog ยังมีรูโบ๋อยู่ที่หน้าท้องอยู่เลย 555

ปล.2 ตอนทำความสะอาดแผล เจ็บโครตๆๆๆ แต่ยังน่าจะน้อยกว่าผู้หญิงคลอดลูกเยอะ T_T ดังนั้นชาติหน้าขอเกิดเป็นผู้ชายอีกนะคร้าบบบบ