แสดงบทความที่มีป้ายกำกับ Tips แสดงบทความทั้งหมด
แสดงบทความที่มีป้ายกำกับ Tips แสดงบทความทั้งหมด

วันพฤหัสบดีที่ ๘ กรกฎาคม พ.ศ. ๒๕๕๓

Fix: IE8 Stuck on new tab

ตอนนี้ที่บริษัทเริ่มใช้ IE8 กันมากขึ้นแล้วครับ ทีนี้ก็เจอปัญหาว่าบางโปรแกรมที่เราเขียน เราสั่งให้มันเป็น Window ใหม่เช่นแสดงรายงาน หรือทำ Popup ปรากฏว่าอยู่ดีๆ ก็ไม่สามารถเปิด Window ใหม่ได้ครับ (มารู้ทีหลังว่าเป็นเพราะเราลงโปรแกรมบางตัว เช่น Image Viewer ครับ)

ทีแรกลองค้นใน google ดูก็พบว่าวิธีแก้ปัญหาคือต้องลงทะเบียน dll บางตัวใน registry ใหม่ ซึ่งมีหลายตัวมากครับ ให้เราลองสั่ง regsvr32 ทีละตัวดู ซึ่งก็เสียเวลามาก โชคดีที่เพื่อนที่ทำงานได้ bat file มา 1 ตัวซึ่งแก้ปัญหานี้ได้ พอไปดูในโค้ดก็เห็น link ไปเวบ http://iefaq.info ครับ พอเข้าไปดูปรากฏว่าไม่ใช่ภาษาอังกฤษซะงั้น

แต่สำหรับคนที่มีปัญหาเกี่ยวกับ IE ลองเข้าไปดูได้ครับ โดยเฉพาะ IE8 ถ้าสนใจตัว script สำหรับแก้ปัญหา registry ของ IE8 ให้ไปที่หน้า

http://iefaq.info/index.php?action=artikel&cat=47&id=136&artlang=de

สำหรับผมใช้ Windows7 32 bit เลยดาวน์โหลดตัว IE8-rereg.zip มาใช้ครับ สุดยอดมากๆๆ

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

แก้ปัญหา Export to Excel ด้วย GridView: Control 'GridView1' of type 'GridView' must be placed inside a form tag with runat=server.

วันก่อนผมเขียนโปรแกรมเพื่อ export ข้อมูลจาก GridView มาเป็น Excel ครับ (ก่อนหน้านี้ใช้ Datagrid ไม่เคยมีปัญหา) ก็เจอ error ดังนี้ครับ



นั่งงมอยู่ตั้งนาน ก็เลยเจอวิธีแก้ว่าให้เพิ่มโค้ดดังนี้ครับ


Public Overrides Sub VerifyRenderingInServerForm(ByVal control As System.Web.UI.Control)
'MyBase.VerifyRenderingInServerForm(control)

End Sub



แค่นี้ก็ใช้งานได้แล้วครับ

แต่ถ้าใครเจอปัญหาว่า "RegisterForEventValidation can only be called during Render();"



ให้เอา AllowSorting กับ AllowPaging ออกครับ

เท่านี้เราก็สามารถใช้ GridView ในการสร้าง Excel ได้แล้วครับ

วันศุกร์ที่ ๒ กรกฎาคม พ.ศ. ๒๕๕๓

ปัญหาเมื่อ export excel: The file you are trying to open, 'name.xls', is in a different format than specified by the file extension

ช่วงนี้หลังจากที่ทำงาน migrate Office จาก 2003 เป็น 2007 ก็เจอปัญหาว่าโปรแกรมที่เราสร้างรายงาน Excel ด้วยวิธีการกำหนด Response.ContentType = "application/vnd.ms-excel" ได้ผลลัพธ์ตามปกติ แต่ว่าจะมี alert ขึ้นมาว่า

The file you are trying to open, 'name.xls', is in a different format than specified by the file extension. Verify that the file is not corrupted and is from a trusted source before opening the file. Do you want to open the file now?

จริงๆก็ไม่ได้มีปัญหาอะไรมากหรอกครับ เพราะแค่กด OK ก็เปิดดู Excel ได้ตามปกติ แต่หลังๆเริ่มมี user บ่นขึ้นเรื่อยๆ เลยลอง search ดูก็พบ article ที่เวบไมโครซอฟท์ครับ

When you open a file in Excel 2007, you receive a warning that the ...
Article ID: 948615 - Last Review: March 4, 2008 - Revision: 1.1
support.microsoft.com/kb/948615


วิธีแก้ก็ง่ายๆคือไปกำหนด registry นั่นเอง ผมก็เลยทำเป็นไฟล์ reg จะได้ส่งให้ user เฉพาะรายที่มีปัญหารันได้เลย (จริงๆแล้วเราสามารถใช้ Group Policy ในการแก้ปัญหานี้ได้ครับ ลองดูใน article ละกัน)

ก่อนอื่นก็สร้างไฟล์ชื่อ ExcelFileFormat.reg สำคัญตรงนามสกุลไฟล์ให้เป็น .reg นะครับ แล้วก็ใส่เขียนโค้ดดังนี้

REGEDIT4
[HKEY_CURRENT_USER\Software\Microsoft\Office\12.0\Excel\Security]
"ExtensionHardening"=dword:0

จากนั้นก็ save ไฟล์

พอต้องการใช้ ก็ double click ที่ไฟล์นี้ได้เลยครับ

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

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 ก่อน แล้วค่อยเขียนไฟล์ ปรากฏว่าใช้เวลามากกว่าเปิดไฟล์แล้ววนลูปเขียนเข้าไปตรงๆเลยครับ

วันพุธที่ ๒๔ กุมภาพันธ์ พ.ศ. ๒๕๕๓

ส่ง URL Parameter ให้ ClickOnce Application

ผมเคยเขียน blog เรื่อง สั่งรัน Win App บนเครื่อง Client เมื่อเดือนธันวาคม 2009 แล้วก็ติดเรื่องส่ง paramenter ให้ ClickOnce application เอาไว้ครับ วันนี้มีโอกาสก็เลยมาเขียนเรื่องนี้ซะเลย

ก่อนอื่นเราก็มาสร้าง Console Application สำหรับการทดสอบซะก่อนครับ จากนั้นก็ Add .NET Reference เข้ามาในโปรเจคเรา 2 ตัว คือ System.Deployment กับ System.Web ครับ แล้วก็เขียนโค้ด Import NameSpace ที่จำเป็นเข้าในด้วย


Imports System.Collections.Specialized
Imports System.Deployment.Application
Imports System.Web

Module Module1

Sub Main()
Dim params = GetURLParameter()
If params.Count > 0 Then
For i As Integer = 0 To params.Count - 1
Console.WriteLine("Parameter {0} is {1}", params.Keys(i), params.Item(i))
Next
Else
Console.WriteLine("No URL Parameter")
End If
Console.WriteLine("Press any key to quit.")
Console.ReadLine()

End Sub

Private Function GetURLParameter() As NameValueCollection
Dim NameValueTable As New NameValueCollection

If (My.Application.IsNetworkDeployed) Then
Dim QueryString As String = ApplicationDeployment.CurrentDeployment.ActivationUri.Query
NameValueTable = HttpUtility.ParseQueryString(QueryString)
End If

Return NameValueTable
End Function

End Module


โค้ดที่ใช้ทดสอบมีเท่านี้ ไม่ยากใช่ไหมครับ แต่ที่สำคัญอีกอย่างหนึ่งก็คือ เราต้องกำหนดให้ ClickOnce Application ของเราสามารถรับ URL Parameters ได้ก่อน โดยไปที่ Property ของโปรเจค แล้วเลือก Publish Tab ครับ



ทีนี้เราจะเห็นปุ่ม OPTIONS กดเข้าไปได้เลยครับ



แล้วก็ทำการเลือก Manifests และคลิ๊กเครื่องหมายถูกตรง Allow URL parameters

เสร็จแล้วก็ทำการ Publish ครับ ทีนี้เรามาลองทดสอบเรียก ClickOnce Application แบบไม่ส่ง parameter ก่อน



โปรแกรมขึ้นว่า No parameter ถูกต้อง คราวนี้ลองทดสอบแบบส่ง QueryString ไปด้วย
(http://nithi/COParameter/CoParameter.application?param1=test1&param2=test2&name=nithi)



เรียบร้อยครับ คราวนี้เราก็สามารถส่ง URL Parameter ไปให้ Application ของเราได้แล้วครับ

Reference:
How to: Retrieve Query String Information in a ClickOnce Application

วันอาทิตย์ที่ ๒๑ กุมภาพันธ์ พ.ศ. ๒๕๕๓

ASP.NET สั่งเคลียร์ Session เมื่อเปลี่ยน Page

วันนี้เจอคำถามที่ greatfriends ว่าเมื่อ User ปิด Browser หรือเปลี่ยน Page เราต้องการไปเคลียร์ Session ของ Page เดิมจะทำยังไงดี

ถ้าเป็นเรื่องปิด Browser หรือว่า User ไปเปิดเวบอื่นคงไม่มีปัญหาครับ เพราะ ASP.NET มันจะเคลียร์ค่าให้เมื่อ Session Time Out อยู่แล้ว ซึ่งเราก็สามารถเขียนโค้ดใน Session_End ที่ Global.asax เพิ่มเติมก็ได้ แต่ปัญหาคือถ้า user เปลี่ยนหน้า เราจะทำยังไงดี สิ่งแรกที่ผมคิดคือ เรากำหนดให้มัน Post Back กลับมาหน้าเดิมก่อน เพื่อสั่ง Clear Session จากนั้นเราค่อยสั่ง Navigate ไปยังหน้าที่ต้องการอีกที

เช่นสมมติ เรามี User Control 1 ตัว เป็นเมนูสำหรับ Application เราใช้ <asp: Menu> และสร้าง <asp:MenuItem> เพื่อระบุเมนูย่อย แทนที่เราจะใช้ NavigateUrl Attribute เพื่อสั่ง Redirect ไปหน้าใหม่ทันที่ เราก็มาเขียนโค้ดที่ MenuItemClick Event แทนครับ

พอตอนเย็นก็นึกวิธีใหม่ออก ก็เลยมาลองดูครับ วิธีนี้คือ เราใช้ ASP.NET AJAX มาเรียก WebService เพื่อทำการ Clear Session นั่นเองครับ ลองมาดูโค้ดของ WebService กันก่อน


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

Namespace jNithi.WS
<System.Web.Script.Services.ScriptService()> _
<WebService(Namespace:="http://tempuri.org/")> _
<WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Public Class SessionManager
Inherits System.Web.Services.WebService

<Script.Services.ScriptMethod()> _
<WebMethod()> _
Public Function ClearSession(ByVal sessionName As String) As Boolean
Dim session As System.Web.SessionState.HttpSessionState
session = System.Web.HttpContext.Current.Session

If session(sessionName) IsNot Nothing Then
Select Case sessionName.ToUpper
Case "SESSION_RECEIPT_REPORT"
DirectCast(session(sessionName), CrystalDecisions.CrystalReports.Engine.ReportDocument).Dispose()
Case "SESSION_RECEIPT_DATA"
DirectCast(session(sessionName), IList).Clear()
Case Else
End Select
session(sessionName) = Nothing
session.Remove(sessionName)
End If

End Function

End Class

End Namespace


ทีนี้ในหน้า aspx เราก็เขียน javascript สำหรับมาเรียก Webservice ตัวนี้ครับ

<script type="text/javascript">
function Body_OnUnload() {
jNithi.WS.SessionManager.ClearSession('SESSION_RECEIPT_REPORT');
}

</script>
</head>
<body onunload="Body_OnUnload();">
<form id="form1" runat="server">

<asp:ScriptManager ID = "ScriptManager1" runat="server" >
<services>
<asp:ServiceReference Path="~/wsSession.asmx" />
</services>

</asp:ScriptManager>


เท่าที่ลองทดสอบดู ก็ใช้งานได้ผลโอเคนะครับ เมื่อเราเปลี่ยนหน้า หรือปิด Browser มันจะไปเรียก WebService เพื่อสั่ง Clear Session ให้ แต่มีข้อแม้นิดหนึ่งครับ คือ onunload event ของ Body นั้นมันจะทำงานทุกครั้งที่มีการเปลี่ยนหน้า นั่นคือ ถ้ามีการ PostBack กลับไป มันก็จะเกิด Event นี้ด้วยครับ แต่ถ้าเราออกแบบ ASP.NET AJAX Page ของเราดีๆ ไม่ให้มีการ Post Back กลับไป แนวคิดนี้ก็ทำงานได้โอเคครับ

เดี๋ยวคงต้องขอทดสอบอีกซักพัก ก่อนจะเอาไปใช้จริงครับ

วันอังคารที่ ๙ กุมภาพันธ์ พ.ศ. ๒๕๕๓

Crystal Report - สร้างรายงานให้ user กำหนดเงื่อนไข Group ได้เอง (Dynamic Group By)

วันนี้เรามาดูเทคนิคของ Crystal Report อีกครั้งครับ (เนื่องจากดูใน Google Analytics เห็นคนชอบบทความเกี่ยวกับ Crystal Report เป็นพิเศษ) คราวนี้เราจะมาสร้างรายงานที่สามารถให้ user กำหนด Group By ได้เองตามใจ Blog นี้จะทำแค่ Group by 1 ระดับให้ดู แต่เราสามารถประยุกต์ใช้ให้ user เลือก group ได้มากกว่า 1 ระดับ รวมถึงสามารถกำหนดเงื่อนไข sort by ได้ด้วย

ก่อนอื่นเลยสิ่งที่จำเป็นก็คือ เราต้องสร้าง Parameter Field ขึ้นมาก่อน เพื่อรับค่า parameter จาก user ว่าต้องการให้ group by column ไหนของข้อมูลครับ จากตัวอย่างผมสร้าง Parameter Field ชื่อ pGroupBy




จากนั้นก็สร้าง Formula Field เพื่อนำค่าที่ได้จาก Parameter Field มาคำนวนหา column ที่ต้องการ group ครับ
ในตัวอย่างผมสร้าง Formula Field ชื่อ GroupBy



จากโค้ดใน Formula Editor ผมใช้ IF THEN ELSE ซ้อนกัน (ที่จริงน่าใช้ Switch Case แต่ผมขี้เกียจแก้แล้ว) สังเกตุว่าถ้า user ส่ง parameter มาเป็น Dept ผมจะให้ Group By Formula Field อีกที นอกนั้นก็จะ Group By Database Field ตามปกติครับ

คราวนี้มาดูหน้าเวบกันบ้างครับ ผมก็สร้าง Drop-down List มา 1 ตัว ชื่อ ddlGroupBy และใส่ List ที่ต้องการไว้


</asp:DropDownList ID="ddlGroupBy" runat="server" style="position:absolute;left:100px;top:10px" CssClass="txtOptional" Width="110px" EnableTheming="False" EnableViewState="False" >
</asp:ListItem Text="Client" Value="Client" Selected="True"></asp:ListItem>
</asp:ListItem Text="Most Work Lawyer" Value="MostWorkLawyer"></asp:ListItem>
</asp:ListItem Text="Resp. Lawyer" Value = "RespStaff"></asp:ListItem>
</asp:ListItem Text="Department" Value="Dept" ></asp:ListItem>
</asp:DropDownList>


ตอนสร้างรายงานก็อย่าลืม SetParameterValue ด้วยครับ


Dim reportDoc As New ReportDocument
reportDoc.FileName = Server.MapPath("~/Reports/AR/rptAgingReport.rpt")
reportDoc.SetDataSource(ARReportResults)
reportDoc.SetParameterValue("pGroupBy", ddlGroupBy.SelectedValue)
reportDoc.SetParameterValue("AsOfDate", asOfDate)


เท่านี้ก็เรียบร้อยแล้วครับ ทีนี้ user ก็สามารถเลือก column ที่ต้องการ Group By ได้ตามใจครับ

วันศุกร์ที่ ๒๙ มกราคม พ.ศ. ๒๕๕๓

Crystal Report - แสดงข้อความหลายคอลัมภ์ด้วย RTF Text Interpretation

วันก่อนได้รับ requirement ว่าในรายงานตัวหนึ่ง ต้องการให้แสดง Report Footer เพื่อแสดงข้อมูลตามเงื่อนไข โดยข้อมูลดังกล่าวต้องสามารถ ทำตัวหนา ตัวเอียง สำหรับข้อมูลที่ต้องการเน้น และจัดรูปแบบเป็น 2 คอลัมภ์ ซึ่งใน Report Footer มีทั้งหมด 5 ส่วน ส่วนดังกล่าวเป็นส่วนที่ 4

เริ่มแรกผมสร้าง Parameter Field มา 1 ตัว ชื่อ PaymentAccount และสร้าง Text Object มา 1ตัว และลาก Payment Account มาใส่ใน Text Object เพื่อให้เป็น Embedded Field เราจะได้สามารถกำหนด Text Interpretation ได้ครับ (จากรูปสังเกตุว่าเป็น Report Footer d)



เสร็จแล้วคลิ๊กเมาส์ขวาที่ Text Object ที่สร้างขึ้น เลือก Property แล้วคลิ๊กเครื่องหมายถูกที่ Can Grow ใส่ตัวเลข 0 คือไม่จำกัดบรรทัดลงไป เพื่อให้ Object นี้มันยืดได้

ต่อไปก็กำหนด Text Interpretation โดยการ double click ที่ Text Object แล้วเลือก Embedded field คลิ๊กเมาส์ขวา เลือก Format Embedded Field



เสร็จแล้วก็เลือก Paragraph Tab เลือก Text Interpretation เป็น RTF Interpretation



เสร็จขั้นตอนของรายงานแล้ว ทีนี้ก็มาเขียนโค้ดสำหรับสร้างข้อมูล RTF ครับ


Public Shared Function GetAccount(ByVal coCode As String) As String
Dim sb As New Text.StringBuilder

'Set RTF1, ANSI Page and Font-family
sb.AppendLine("{\rtf1\ansi\ansicpg1252\deff0\deflang1033\deflangfe1033{\fonttbl{\f0\fswiss\fprq2\fcharset0 Arial;}}")

'Set Tab (at 0.5 in and 2.5 in) and Font-size
sb.AppendLine("\viewkind4\uc1\pard\nowidctlpar\sa200\sl276\slmult1\tx720\tx3600\f0\fs20")

'Insert first blank line
sb.AppendLine("\tab \par ")
Select Case coCode
Case "Nithi"
sb.AppendLine("\tab Account Name \tab Nithi Juabsami \par ")
sb.AppendLine("\tab Savings Account No.: \tab 1-1111-1111, USD account. \par")
sb.AppendLine("\tab Bank Name: \tab Citibank, N.A. \par ")
sb.AppendLine("\tab Bank Address: \tab 82 North Sathorn Road \par ")
sb.AppendLine("\tab \tab Silom, Bangrak, \par ")
sb.AppendLine("\tab \tab Bangkok 10500, Thailand \par ")
sb.AppendLine("\tab -or- \par ")
sb.AppendLine("\tab Account Name: \tab Nithi Juabsamai \par ")
sb.AppendLine("\tab Savings Account No.: \tab x-xxxx-xxxx \par ")
sb.AppendLine("\tab Bank Name: \tab The Siam Commercial Bank Plc., Thanon Witthayu Branch \par ")
sb.AppendLine("\tab Bank Address: \tab 132 Wireless Road, Lumpini, Bangkok 10330 \par ")
Case "jNithi"
sb.AppendLine("\tab Account Name: \tab jNithi O.P. \par ")
sb.AppendLine("\tab Bank Account No.: \tab xxx-xxxxxx-xxx (USD), Current Account \par ")
sb.AppendLine("\tab Name of Bank: \tab The Siam Commercial Bank Plc., Thanon Witthayu Branch. \par ")
sb.AppendLine("\tab Bank Address: \tab 132 Wireless Road, Lumpini, Bangkok 10330 \par ")
Case Else
End Select

' Insert bottom blank line
sb.AppendLine("\tab \par ")

' SET end of RTF
sb.AppendLine("}")
Return sb.ToString
End Function


มาดูรายละเอียดกันครับ
{\rtf1\ansi\ansicpg1252\deff0\deflang1033\deflangfe1033{\fonttbl{\f0\fswiss\fprq2\fcharset0 Arial;}}
บรรทัดนี้เรากำหนด RTF Format และกำหนดให้ใช้ ANSI PAGE 1252 และ font คือ Arial

\viewkind4\uc1\pard\nowidctlpar\sa200\sl276\slmult1\tx720\tx3600\f0\fs20
บรรทัดนี้เรากำหนดให้เอกสารเรามี tab 2 อัน อันแรกคือ tx720 แปลว่าอยู่ที่ 720 twips หรือ 0.5 นิ้ว (โดยปกติ 1นิ้ว จะเท่ากับ 1440 twips) และอันที่สองคือ tx3600 คือ 3600 twips หรือ 2.5นิ้ว ส่วน fs20 คือขนาดของ font เท่ากับ 10 pt ครับ

\tab Account Name \tab Nithi Juabsami \par
แปลว่าพิมพ์ tab 1 ครั้ง (คือเลื่อนไปตำแหน่งที่ 0.5 นิ้วที่กำหนด) พิมพ์คำว่า Account Name แล้วพิมพ์ tab (เลื่อนไปยังตำแหน่ง 2.5 นิ้ว พิมพ์ Nithi Juabsamai แล้วขึ้นบรรทัดใหม่ (\par)

ตอนเขียนโค้ดสั่ง Report ก็เรียกฟังก์ชันนี้เพื่อส่ง parameter ให้ Crystal Report

rpt = New rptTestReceipt
rpt.SetDataSource(_receipt)
rpt.SetParameterValue("AsOfDate", AsOfDate.ToString("dd MMM yyyy"))
rpt.SetParameterValue("PaymentAccount", PaymentAccount.GetAccount(_receipt(0).CoCode))


คราวนี้ลองรันดูผลลัพธ์ครับ



เรียบร้อยครับ ตอนนี้ข้อมูล Payment Account ของเราก็แสดงเป็น 2 column (ซึ่งเราแบ่งด้วย Tab) ถ้าต้องการเพิ่ม Column เราก็สามารถกำหนด tab ที่ตำแหน่งที่ต้องการเพิ่มได้ครับ

ที่จริงเราสามารถกำหนด HTML Interpretation ได้ครับ แต่ว่า Cystal Report รองรับแค่ subset ของ HTML ครับ คือรองรับได้แค่บาง tag ซึ่ง tag อย่าง table ไม่รองรับครับ ผมเลยเปลี่ยนมาใช้ RTF Interpretation แทน

หมายเหตุ
จริงๆแล้วเราสามารถใช้ Word Pad ที่มาพร้อม Windows ในการจัดเอกสารตามต้องการ สามารถกำหนดตัวหนา ตัวเอียงได้ตามสะดวก แล้ว save เป็น rtf ไฟล์ จากนั้นก็ใช้ Note Pad เปิดมา copy ใส่ได้เลยครับ แต่อย่าใช้ Microsoft Word นะครับ เพราะมันจะใส่ tag เพิ่มเติมมาเยอะแยะจนน่าเวียนหัว

วันพุธที่ ๑๓ มกราคม พ.ศ. ๒๕๕๓

SQL Server - เก็บผลลัพธ์จาก Stored Procedure ลงในตาราง

ปกติผมก็ใช้ Stored Procedure ใน SQL Server เสมอๆครับ ซึ่งพอเรา execute stored procedure ก็จะได้ result set มาใช้งาน แต่ทีนี้ถ้าเราต้องการทำ query result set ที่ได้ละจะทำยังไงดี ถ้าเป็นเมื่อก่อนผมก็ copy stored procedure มาสร้างใหม่แล้วแก้คำสั่งข้างในให้ทำ query เพิ่มไปเลย เช่นไป join object อื่น หรือสั่ง where สั่ง group by ฯลฯ แล้วแต่ต้องการ

หรืออีกวิธีก็สั่ง CREATE TABLE ก่อน แล้วก็ INSERT ข้อมูลที่ได้จาก Stored Procedure ซึ่งวิธีนี้ถ้ามันมีหลาย field ตอนสั่ง CREATE TABLE ก็เหนื่อยไม่ใช่เล่น ซึ่งวิธีนี้ใช้บ่อยโดยเฉพาะอย่างยิ่ง stored procedure ของระบบ เช่นพวก sp_who2, sp_lock เป็นต้น ลองดูตัวอย่างครับ


CREATE TABLE #locks (spid int, dbid int, objid int,
indid int, [type] varchar(4), resource varchar(50), mode varchar(2), status varchar(10));

INSERT INTO #locks (spid, dbid, objid, indid, [type], resource, mode, status)
EXEC dbo.sp_lock;

SELECT * FROM #locks;

DROP TABLE #locks;


อันนี้ตัวอย่างแค่ 8 fields ยังเหงื่อตก เพราะเทสกันหลายรอบครับ ทีแรกผมกำหนด datatype ไม่ถูกมันก็ error ใส่ ขนาดน้อยไปเช่น resource varchar(10) ก็ error ครับ

วันนี้ผมมีวิธีใหม่มาเสนอครับ ลองดูโค้ดละกัน


SELECT * INTO #tmpWho FROM OPENROWSET('SQLNCLI', 'Server=testSQLServer;Trusted_Connection=yes;', 'EXEC sp_who') ;

SELECT * FROM #tmpWho Where dbname='master' and loginame = 'sa';

DROP TABLE #tmpWho;

ครับ โค้ดนี้เราใช้คำสั่ง OPENROWSET ร่วมกับ SELECT INTO นั่นเอง สะดวกดีมากทีเดียว 555

คราวนี้มาดูตัวอย่างการใช้งานจริงบ้าง


SELECT * INTO tempTestOutput FROM OPENROWSET ('SQLNCLI', 'Server=testSQLServer;Trusted_Connection=yes;', 'EXEC testDB.dbo.spTestOutput ''testParameter1''');

SELECT t.*, e.Department, e.HiredDate
FROM tempTestOutput t INNER JOIN Employee e ON t.EmployeeId = e.EmployeeId
WHERE e.ResignedDate IS NULL
ORDER BY t.EmployeeId


คราวนี้ผมก็ได้ temp table เอาไว้ยำข้อมูลแล้วครับ เสร็จแล้วก็อย่าลืม DROP TABLE ด้วยนะ

Referrence
www.stackoverflow.com - How to SELECT * INTO [temp table] FROM [Stored Procedure]

วันพุธที่ ๖ มกราคม พ.ศ. ๒๕๕๓

VB.NET เขียนโค้ดควบคุม Object ของ Crystal Report

หลายๆคนที่พัฒนาซอฟท์แวร์ทางธุรกิจก็หนีไม่พ้นที่จะต้องทำรายงาน และที่นิยมมากสำหรับนักพัฒนา .NET ก็คงหนีไม่พ้น Crystal Report นั่นเองครับ ซึ่งใช้งานไม่ยาก ยืดหยุ่น และมีฟังก์ชันมากมายให้ใช้งาน และความยอดเยียมอีกอย่างก็คือ เราสามารถเขียนโค้ดเพื่อควบคุม Crystal Report ได้ด้วย

ลองมาดูตัวอย่างจากคำถามที่ greatfriends ละกันครับ

How to Return total page in crystal report to VB -- HELP ME!!
http://greatfriends.biz/?118103

ปัญหามีอยู่ว่า ผมมี crstal report ที่สร้างออกมาเป็น PDF และก็ต้องเอามก Merge กับ File Attach ที่เป็น PDF ให้เป็น ไฟล์เดียว โดย File ที่ Attach นั้นจะต้องอยู่ตรงกลางระหว่างไฟล์ PDF แรก ซึ่งเลขหน้าเวลา Count จะไม่ถูกต้อง เพราะผม แบ่งไฟล์ PDF แรกเป็นสองส่วน โดยเป็นส่วนหัวและท้าย ซึ่งเวลามัน count page ก็จะแยกกันซึ่งไม่ถูกต้อง คือสมมติ ส่วนหัวมี 10 หน้า และท้าย 5 หน้ามันแสดงเป็น 1 of 10 และ ส่วนท้ายแสดงเป็น 1 of 5 ซึ่งส่วนท้ายต้องเป็น 11 of 15

ผมจึงคิดว่าให้ ส่วนหัว ส่งค่ากลับมาที่ vb ว่ามีจำหน้าทั้งหมดเท่าไหร่ และผมจะส่ง เป็น Parameter ให้กับส่วนท้าย
นะครับ แต่วิธีส่งค่ากลับมาวีบี ทำไงอะครับ ผมรู้สึกว่ามันไม่มีทาง แต่ท่านใดมีแนวทางอื่น ช่วยผมทีครับ


ผมตอบไว้ดังนี้ครับ
สมมติว่าผมสร้าง ITextObject 1 ตัวเอาไว้ที่ Report Footer ของ Crystal Report ตั้งชื่อ txtTotalPage แล้วเอาค่าของ PageNo มาใส่ไว้


ในโค้ด VB

Dim rptDoc As New ReportDocument
rptDoc.FileName = Server.MapPath("~/Reports/Test.rpt")
rptDoc.SetDataSource(DataSet1)
rptDoc.SetParameterValue(0, CDate(Me.txtAsOfDate.Text).ToString("dd MMMM yyyy"))

Dim txtTotalPage As TextObject = TryCast(rptDoc.ReportDefinition.ReportObjects("txtTotalPage"), CrystalDecisions.CrystalReports.Engine.TextObject)

Dim TotalPage As Integer = 0
If IsNumeric(txtUSAmount.Text) Then
TotalPage = CInt(txtUsAmount.Text)
End If


เสร็จแล้วก็เอา TotalPage ไปเป็น Parameter ของ report ตัวที่สอง จากนั้นก็ Export รายงานทั้ง 2 ตัว เป็น pdf แล้วเอามา merge กัน

ทีนี้นอกจาก เราจะอ่านค่าจาก TextObject เรายังสามารถใส่ค่าเข้าไปให้ TextObject ก็ได้ หรือว่าจะกำหนด property อื่นๆ เช่น Top, Width, EnableSuppress ....

ลองดูตัวอย่างครับ


Dim pos As Integer
Dim iCurrency As Integer
Dim txtEUAmount As TextObject = rptDoc.ReportDefinition.ReportObjects("txtEUAmount")
Dim txtEUSum As ReportObject = rptDoc.ReportDefinition.ReportObjects("txtEUSum")

'เรียก function เพื่อตรวจสอบเงื่อนไขสกุลเงิน
If IsContainCurrency(ARStatementOfAccounts, "EUR") Then
iCurrency += 1
pos = CalculateObjectPosition(iCurrency) ' เรียกฟังก์ชันสำหรับคำนวณตำแหน่งของ Object ใน Detail
txtEUAmount.Left = pos
pos = CalculateSumObjectPosition(iCurrency) 'เรียกฟังก์ชันสำหรับคำนวนตำแหน่งของ Object ใน Group Footer
txtEUSum.Top = pos
Else
txtEUAmount.Text = ""
txtEUSum.ObjectFormat.EnableSuppress = True
End If

pos = 920 + (iCurrency - 1) * 280
DirectCast(rptDoc.ReportDefinition.ReportObjects("LineFV0"), LineObject).Bottom = pos


จะสังเกตุว่ามีการประกาศตัวแปร TextObject และ ReportObject ครับ ซึ่ง ReportObject นั้นเป็น Base Class ของ Object ต่างๆ เช่น LineObject, TextObject ใน Crystal นั่นเอง ดังนั้นถ้าเรารู้ Type แน่นอน เราสามารถทำการ Casting เพื่อกำหนด property เฉพาะของ Type นั้นๆได้ครับ

ก็ไม่ยากใช่ไหมครับ ตอนนี้เราสามารถเขียนโปรแกรมเพื่อทำ Dynamic Layout ของ Crystal Report ได้แล้ว

วันพฤหัสบดีที่ ๑๐ ธันวาคม พ.ศ. ๒๕๕๒

SQLServer ดึงข้อมูลจาก dbf

จริงๆผมต้องเขียนโปรแกรมเพื่อดึงข้อมูลจาก dbf เข้า SQLServer บ่อยๆ ถ้า dbf ตัวไหนที่ต้องใช้ประจำก็จะสร้าง Linked Server เก็บไว้ แต่ถ้าทำเป็น ad hoc ก็จะใช้คำสั่ง OPENROWSET แทน แต่ก็ลืมวิธีทุกครั้ง ต้องเสียเวลาไป search ใน google ทุกที คราวนี้เลยมาเขียนไว้ใน blog ดีกว่า ถ้าลืมอีกคราวหน้าก็มาหาที่นี่ได้เลย 555

สำหรับการสร้างใช้ OPENROWSET ก็ไม่ยากครับ เขียนแบบนี้

SELECT * FROM OPENROWSET('MICROSOFT.JET.OLEDB.4.0','dBase 5.0;HDR=NO;IMEX=2;DATABASE={path to dbf}','select * from {filename}.dbf')

แต่ที่สำคัญคือต้องปิดโปรแกรม foxpro หรือโปรแกรมที่กำลังเปิดไฟล์ dbf นั้นๆไปก่อนครับ ไม่งั้นมันจะขึ้นว่าติดปัญหาเรื่อง permission ไปนั่ง search หาสาเหตุตั้งนาน

ส่วนสร้าง Linked Server นั้นก็ทำดังนี้ครับ
1. ไปที่ Linked Server คลิ๊กเมาส์ขวาเลือก New Linked Server
2. ใส่ชื่อ Linked Server ที่ต้องการครับ สมมติชื่อ DBLink
3. Provider ให้เลือกเป็น Microsoft Jet 4.0 ครับ (จริงๆมันมี Visual Fox Pro ให้เลือกด้วย และในบอร์ดต่างๆเค้าว่ากันว่าจะทำให้ตอน select ข้อมูล มันเร็วกว่า Jet แต่ผมก็ยังไม่ได้ลองครับ)
4. Product Name ใส่อะไรก็ได้ครับ แต่อย่าทิ้งว่าง สมมติใส่เป็น Microsoft Jet
5. Data source ใส่ path ที่เก็บ dbf ครับ เช่น c:\dbfFiles
6. Provider String ใส่ dBase 5.0
7. เปลี่ยนมาที่ Security Page ครับ ตรง option สำหรับ log in เลือกตัวล่างสุด ที่เขียนว่า Be made using this security context เสร็จแล้วตรง Remote Login ให้ใส่ Admin ส่วน With Password ให้เว้นว่างไว้ครับ
8. กด OK เป็นอันเสร็จพิธี

ทีนี้เวลาเขียนคำสั่ง SQL ก็เขียนประมาณนี้ครับ สมมติว่าต้องการดูข้อมูลจาก testdata.dbf

SELECT * FROM DBLink...testdata

ลองเปรียบเทียบกับการใช้ OPENROWSET

SELECT * FROM OPENROWSET('MICROSOFT.JET.OLEDB.4.0','dBase 5.0;HDR=NO;IMEX=2;DATABASE=c:\dbfFiles', 'select * from testdata.dbf')

ไม่ยากใช่ไหมครับ แต่ทำไมผมลืมทุกทีก็ไม่รู้สิ

วันพฤหัสบดีที่ ๒๒ ตุลาคม พ.ศ. ๒๕๕๒

ASP.NET รวม Crystal Report 2 รายงานเป็น pdf ด้วย iTexSharp

คราวนี้เป็นตอนที่ 3 สำหรับ iTextSharp ครับ เราจะสร้าง pdf file จาก Crystal Report (ต่อจากนี้จะเรียกย่อๆว่า CR) 2 ไฟล์ มารวมกันให้เป็นไฟล์เดียว ในการทดสอบผมใช้ CR ตัวเดียว แต่สร้างเป็น 2 report สำหรับเอามารวมกันเป็น pdf ลองดูโค้ดกันครับ


Imports CrystalDecisions.CrystalReports.Engine
Imports XXEntity = jnithi.CLASS.XX.Entity
Imports XXManager = jnithi.CLASS.XXManager

Partial Class frmTestITextSharp
Inherits System.Web.UI.Page

Private Sub CreateReport()
'สร้าง CR1
Dim myReport1 As New ReportDocument()
Dim rpt1Data = XXManager.XXOutStandingManager.GetXXOutStandings(0, Nothing, Nothing, "R00939", "")
myReport1.FileName = Server.MapPath("~/Reports/XX/rptXXOutstanding.rpt")
myReport1.SetDataSource(rpt1Data)
myReport1.SetParameterValue(0, Date.Today.ToString("dd-MMM-yyyy"))
myReport1.SetParameterValue(1, "")
myReport1.SetParameterValue(2, "Client")

' สร้าง CR2
Dim myReport2 As New ReportDocument
Dim rpt2Data = XXManager.XXOutStandingManager.GetXXOutStandings(0, Nothing, Nothing, "A01032", "")
myReport2.FileName = Server.MapPath("~/Reports/XX/rptXXOutstanding.rpt")
myReport2.SetDataSource(rpt2Data)
myReport2.SetParameterValue(0, Date.Today.ToString("dd-MMM-yyyy"))
myReport2.SetParameterValue(1, "")
myReport2.SetParameterValue(2, "Client")

' สร้าง pdfReader1 ชื่อ reader1 โดย export stream จาก CR1
Dim s1 As System.IO.Stream = myReport1.ExportToStream(CrystalDecisions.Shared.ExportFormatType.PortableDocFormat)
Dim reader1 As New iTextSharp.text.pdf.PdfReader(s1)

' สร้าง pdfReader2 ชื่อ reader2 โดย export stream จาก CR2
Dim s2 As System.IO.Stream = myReport2.ExportToStream(CrystalDecisions.Shared.ExportFormatType.PortableDocFormat)
Dim reader2 As New iTextSharp.text.pdf.PdfReader(s2)

' สร้าง pdfCopy โดยกำหนด os เป็น output FileStream เพื่อสร้าง File ชื่อ rptMerge.pdf
Dim pdfDoc As New iTextSharp.text.Document()
Dim os As New System.IO.FileStream(Server.MapPath("~/rptMerge.pdf"), IO.FileMode.Create)
Dim pdfCopy As New iTextSharp.text.pdf.PdfCopy(pdfDoc, os)

pdfDoc.Open() ' เปิด pdf Document เพื่อเริ่มการ copy

' วนลูปอ่าน reader1 เพื่อ copy ทีละหน้า
For iPage As Integer = 1 To reader1.NumberOfPages
pdfCopy.AddPage(pdfCopy.GetImportedPage(reader1, iPage))
Next

' วนลูปอ่าน reader2 เพื่อ copy ทีละหน้า
For iPage As Integer = 1 To reader2.NumberOfPages
pdfCopy.AddPage(pdfCopy.GetImportedPage(reader2, iPage))
Next

' Clear Memory
pdfDoc.Close()
pdfCopy.Close()
reader1.Close()
reader2.Close()
os.Close()
os.Dispose()
s1.Close()
s1.Dispose()
s2.Close()
s2.Dispose()

Response.Redirect("~/rptMerge.pdf")

End Sub

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
CreateReport()
End Sub
End Class


เมื่อเราใช้ iTextSharp สร้าง pdf ไฟล์เสร็จก็สั่ง redirect เพื่อเปิดไฟล์ที่ browser ครับ ทีแรกผมทดสอบให้ iTextSharp สร้าง pdf เป็น MemoryStream แล้วลองใช้ Response.BinaryWrite กับ Response.OutputStream.Write เพื่อสร้าง HTTPResponse แต่ไม่สามารถทำได้ครับ พอเปิดไฟล์ Browser จะฟ้องว่าไฟล์เสีย ลอง search ใน google ดูหลายๆคนพบปัญหาเดียวกัน เลยแก้ด้วยการใช้ FileStream เพื่อสร้าง temp file แทนตามตัวอย่างด้านบนครับ

ลองดูโค้ด MemoryStream ที่มีปัญหาดู เผื่อมีคนแก้ได้ครับ

' โค้ดด้านบนเหมือนเดิม เริ่มเปลี่ยนตั้งแต่ Dim os ลงมา
' เปลี่ยนจาก FileStream เป็น MemoryStream
'Dim os As New System.IO.FileStream(Server.MapPath("~/rptMerge.pdf"), IO.FileMode.Create)
Dim os As New System.IO.MemoryStream()

Dim pdfCopy As New iTextSharp.text.pdf.PdfCopy(pdfDoc, os)
pdfDoc.Open() ' เปิด pdf Document เพื่อเริ่มการ copy

' วนลูปอ่าน reader1 เพื่อ copy ทีละหน้า
For iPage As Integer = 1 To reader1.NumberOfPages
pdfCopy.AddPage(pdfCopy.GetImportedPage(reader1, iPage))
Next

' วนลูปอ่าน reader2 เพื่อ copy ทีละหน้า
For iPage As Integer = 1 To reader2.NumberOfPages
pdfCopy.AddPage(pdfCopy.GetImportedPage(reader2, iPage))
Next

Response.Clear()
Response.ContentType = "application/pdf"
Response.AddHeader("content-disposition", "attachment;filename=Export.pdf")
Response.BinaryWrite(os.ToArray)

' Clear Memory
pdfDoc.Close()
pdfCopy.Close()
reader1.Close()
reader2.Close()
os.Close()
os.Dispose()
s1.Close()
s1.Dispose()
s2.Close()
s2.Dispose()
Response.End()


blog ก่อนหน้านี้มีคนบอกอยากให้ comment ด้วย คราวนี้ก็เลยใส่ comment มา แต่ไม่แน่ใจว่าจะละเอียดพอหรือเปล่าครับ

บทความก่อนหน้า
เขียนโปรแกรมจัดการเอกสาร pdf ด้วย iTextSharp
การจัดการเอกสาร pdf ด้วย iTextSharp(2)

วันจันทร์ที่ ๑๒ ตุลาคม พ.ศ. ๒๕๕๒

ลองสร้าง Extension Method ให้ Enum

คราวก่อนตอนที่ผมต้องการหาชื่อของ Enum ได้ลองทำการสร้าง Extension Method ชื่อ ToDictionary ไว้ ตามชื่อเลยครับ Method นี้จะไปสร้าง Dictionary สำหรับเก็บชื่อ (Key) และค่า (Value) ของ Enum ที่ต้องการ


Public Module EnumExtension
<System.Runtime.CompilerServices.Extension()> _
Public Function ToDictionary(ByVal en As System.Enum) As Dictionary(Of String, Integer)
Dim result As New Dictionary(Of String, Integer)
For Each v As Integer In System.Enum.GetValues(en.GetType)
result.Add(System.Enum.GetName(en.GetType, v), v)
Next
Return result
End Function

End Module


เนื่องจากผมใช้กับ ASP.NET ดังนั้น Module นี้ ผมเอาไปไว้ที่ App_Code folder ครับ ทีแรกเอาไปใส่ใน BLL ซึ่งอยู่คนละ project ก็เลยเรียกใช้ใน ASP.NET ไม่ได้ งงตั้งนานเหมือนกัน ลองทดสอบดูก็น่าพอใจครับ ถือว่าเป็นการทดสอบการสร้าง Extension Method ละกัน

วันอังคารที่ ๑๕ กันยายน พ.ศ. ๒๕๕๒

VB.NET ICloneable

วันนี้ (15 ก.ย. 2552) ได้เข้าไปอ่านกระทู้ที่ GreatFriends เจอคำถามหนึ่งน่าสนใจครับ

การให้ค่ากับ object
http://greatfriends.biz/?113627

คำถามมีดังนี้ครับ
ผมสร้าง class Node กับ Variable

ซึ่ง class Node ก็ประกอบไปด้วยตัวแปรต่าง
และก็มี Variable
ส่วน class Variable ก็ประกอบไปด้วยตัวแปรต่าง

เมื่อผมต้องการกำหนดค่าให้กับ Node ก็สร้าง Node node = new Node;
Variable variable = new Variable;
variable. = .....;ก็กำหนดค่าให้

แล้วก็ node.Variable = variable;

ที่นี้พอผมเปลี่ยนค่า ใน
variable variable. = .....;

ที่นี้node.Variable ก็เลยเปลียนตามไปด้วย
แต่ผมไม่ต้องการให้เปลี่ยน
ผมเลยอยากทราบว่าต้องเซตค่าnode.Variable =
variableยังไงถึงค่าในnode.Variable กับ variable
แยกออกจากกันไม่สัมพันธ์กันอ่ะครับ


ลักษณะนี้ก็คือ เราต้องการให้ Variable Object ที่ถูกกำหนดค่าให้ Node Object เป็นคนละตัวกับ Variable Object ที่เราสร้างไว้แต่แรก ผมเลยเสนอให้ใช้ ICloneable Interface ครับ ก็เลยคิดว่าน่าจะลองเขียนทดสอบเล่นๆดู มาลองดูโค้ดกันครับ

ส่วนของ Variable Class ที่เราทำการ Implement ICloneable ครับ

Public Class Variable
Implements ICloneable

Private _variableName As String
Private _value As String

Public Property Value() As String
Get
Return _value
End Get
Set(ByVal value As String)
_value = value
End Set
End Property

Public Property VariableName() As String
Get
Return _variableName
End Get
Set(ByVal value As String)
_variableName = value
End Set
End Property

Public Function Clone() As Object Implements System.ICloneable.Clone
Return TryCast(MemberwiseClone(), Variable)
End Function

End Class


ส่วนของ Node Class

Public Class Node

Private _nodeId As Integer
Private _nodeName As String
Private _variable1 As Variable
Private _variable2 As Variable

Public Property Variable2() As Variable
Get
Return _variable2
End Get
Set(ByVal value As Variable)
_variable2 = value
End Set
End Property

Public Property Variable1() As Variable
Get
Return _variable1
End Get
Set(ByVal value As Variable)
_variable1 = value
End Set
End Property

Public Property NodeName() As String
Get
Return _nodeName
End Get
Set(ByVal value As String)
_nodeName = value
End Set
End Property

Public Property NodeId() As Integer
Get
Return _nodeId
End Get
Set(ByVal value As Integer)
_nodeId = value
End Set
End Property

End Class


และทีนี้ก็มาทดสอบกัน

Dim myNode As New Node
Dim myVar1 As New Variable
Dim myVar2 As New Variable

myNode.NodeId = 1
myNode.NodeName = "Test"
myVar1.VariableName = "EmployeeName"
myVar1.Value = "Nithi"

myVar2.Value = "EmployeeSurname"
myVar2.Value = "Juabsamai"

myNode.Variable1 = myVar1.Clone
myNode.Variable2 = myVar2

Debug.Print("{0} - {1} {2}", myNode.NodeName, myNode.Variable1.Value, myNode.Variable2.Value)

myVar1.Value = "Wanwimon"
myVar2.Value = "Permpanit"

Debug.Print("{0} - {1} {2}", myNode.NodeName, myNode.Variable1.Value, myNode.Variable2.Value)

myNode.Variable1.Value = "Nithi"
myNode.Variable2.Value = "Juabsamai"
Debug.Print("{0} - {1} {2}", myNode.NodeName, myNode.Variable1.Value, myNode.Variable2.Value)
Debug.Print("Variable2 = " & myVar2.Value)


ที่น่าสนใจคือ .NET ได้เตรียม MemberwiseClone Function มาให้เราเรียบร้อยแล้ว ไม่ต้องเขียนเอง สะดวกดีครับ หลังจากทดสอบดูแล้ว คิดว่าน่าจะตรงกับที่เจ้าของกระทู้ที่ GreatFriends ต้องการนะครับ

วันอาทิตย์ที่ ๑๓ กันยายน พ.ศ. ๒๕๕๒

VB.NET กับ ENUM

วันก่อนเขียนโปรแกรมเพื่อไป Execute StoreProcedure ตัวหนึ่ง ซึ่งใน SP ตัวนี้จะมีการ Return Value กลับมาให้ด้วยเพื่อบอกสถานะของการทำงาน ดังนั้นในส่วนของ VB.NET ผมก็เลยสร้าง Enum ขึ้นมาเพื่อใช้ระบุสถานะดังกล่าว


Public Class abcManager

Public Enum xxxResult
Success = 0
BillNotFound = 100
NoOutstandingAmount = 200
SQLException = 300
UnknownException = 900
End Enum

Public Shared Function xyz() as xxxResult
Public Shared Function .......

End Class



ทีนี้ตอนฝั่ง UI (Presentation Layer) เรียก xyz function มาใช้งานก็ได้ xxxResult กลับมา ทีนี้ตอนจะแสดงผลลัพธ์ให้ user เห็น ถ้าเป็นตัวเลขก็คงไม่ได้ ก็ต้องแสดงเป็นข้อความ ความคิดแรกคือ ใช้ Select Case ในการตรวจสอบ xxxResult เพื่อแสดงข้อความ แต่คิดอีกทีมันไม่ยืดหยุ่นแฮะ ดังนั้นคิดอีกที เอาชื่อของ Enum มาโชว์เลยก็น่าจะได้ ทีแรกผมเขียนแบบนี้ครับ


Dim result As xyzBLL.abcManager.xxxResult
result = xyzBLL.abcManager.xyz()
MsgBox("Error - " & [Enum].GetName(result.GetType, result), MsgBoxStyle.Exclamation + MsgBoxStyle.OkOnly, My.Application.Info.ProductName)


ซักพักนึกขึ้นมาได้เลยว่าน่าจะทำ Overrides Function ToString ของ Enum แต่อยากรู้ว่า default มันได้ค่าอะไร เลยลองทดสอบ result.ToString เอ่อ ปรากฎได้ผลเหมือนกัน เพิ่งรู้นะเนียว่า ToString ของ Enum มันคืนค่าเป็น Name อุตสาห์ไปใช้ GetName Function เฮ้อ เข้าใจคำว่า ฉลาดเรื่องโง่ๆ ละ

-*-"

เคยได้ยินมาว่าคนเก่งมักเรียนรู้จากประสบการณ์ตนเอง ส่วนคนฉลาด มักเรียนรู้จากประสบการณ์คนอื่น
เลยเอาเรื่องนี้มาเขียน blog จะได้มีคนฉลาดเยอะๆ ^_^

วันพฤหัสบดีที่ ๑๐ กันยายน พ.ศ. ๒๕๕๒

VB.NET จัด format ตัวอักษรใน PrintDocument

ช่วง 1-2 ปีมานี้ จะมีน้องๆนักศึกษาเข้ามาถามเรื่อง PrintDocument ใน VB.NET หลายคนทีเดียวครับ สังเกตุอย่างหนึ่งคือการเขียนใช้สไตล์เดียวกัน ดังนั้นเลยเดาว่าคงใช้หนังสือภาษาไทยเล่มเดียวกันในการศึกษา คำถามหนึ่งที่พบบ่อยคืออยากจัดตัวอักษรชิดขวาหรือกึ่งกลางทำอย่างไรดี

ในโค้ดที่แนบมาให้ดู จะมี AnyString Sub ในการวาดตัวอักษรลงบน PrintDocument ครับ ซึ่ง Sub นี้มันจะวาดตัวอักษรชิดซ้ายตลอด ดังนั้นผมเลยเสนอให้สร้าง Sub ที่สามารถกำหนด StringFormat ได้ว่าจะให้ชิดซ้าย ชิดขวา หรือกึ่งกลาง


Public Sub FormatString(ByVal g As Graphics, ByVal printString As String, ByVal xPos As Integer, ByVal yPos As Integer, ByVal align As StringAlignment)

Dim f As New StringFormat
f.Alignment = align
g.DrawString(printString, useFont, Brushes.Black, xPos, yPos, f)

End Sub


สังเกตุว่าจะมีการใช้ตัวแปรชื่อ useFont ด้วยครับ ซึ่ง useFont ก็ต้องถูกประกาศในระดับ Page และกำหนด Font ที่ต้องการใช้วาด PrintDocument ที่ต้องการ

เวลาเรียกใช้ให้จัดชิดขวาก็

FormatString(e.Graphics, "test123", 100, currentYPosition, StringAlignment.Far)

ถ้าต้องการให้อยู่ตรงกลางก็

FormatString(e.Graphics, "test123", 100, currentYPosition, StringAlignment.Center)

วันอังคารที่ ๘ กันยายน พ.ศ. ๒๕๕๒

VB.NET เคลียร์ Excel ไม่ให้เหลือซาก

ปกติเวลาผมเขียนโปรแกรมเพื่อออกรายงานมักใช้ Crystal Report เป็นหลัก เพราะว่าสามารถให้ user preview ได้ รวมถึง export เป็น PDF, Excel, word ได้สะดวกดีครับ โดยเฉพาะเมื่อ Export เป็น Excel ผมพบว่ามันทำงานได้เร็วกว่าการสั่งเปิด Excel เพื่อใส่ข้อมูลตรงๆมากครับ แต่บางครั้งก็จำเป็นจริงๆที่ต้องใช้ Excel ในการออกรายงาน

เวลาออกรายงานบน Excel ผมมักจะเขียนโค้ดประมาณนี้ครับ (ใช้ BackgroundWorker ด้วย)

Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim wk As BackgroundWorker = DirectCast(sender, BackgroundWorker)
Try
isSuccess = False
CreateExcel(wk, e)
CreateReportByLocation(wk, e)
CreateReportByTitle(wk, e)
CreateReportList(wk, e)
isSuccess = True
Catch ex As Exception
isSuccess = False
Finally
FinishCreatingReport(wk, e)
End Try
End Sub


คำสั่ง CreateExcel จะเป็นการ New Excel.Application รวมถึงสร้าง WorkBook สำหรับเริ่มต้นการออกรายงาน
ส่วน CreatReportXXX จะเป็นการสร้างรายงานแต่ละ WorkSheet ตามต้องการครับ สุดท้ายก็ FinishCreatingReport จะเป็นการคืน memory ให้กับระบบ เราจะมาลองดู function นี้กันครับ


Private Sub FinishCreatingReport(ByVal wk As BackgroundWorker, ByRef e As DoWorkEventArgs)
Try
xlBook.SaveAs(fileName)
Catch ex As Exception
MessageBox.Show(ex.Message & vbCrLf & "Please contact ITD.", My.Application.Info.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
Finally
'// Release memory
wk.ReportProgress(95)
xlApp.Quit()
GC.SuppressFinalize(xlSheet)
GC.SuppressFinalize(xlBook)
GC.SuppressFinalize(xlApp)
GC.Collect()
xlSheet = Nothing
xlBook = Nothing
xlApp = Nothing
End Try
End Sub


พอรันโปรแกรมและเปิด Task Manager ดูพบว่า
1. พอ BackgroundWorker1_RunWorkerCompleted ทำงานเสร็จ (แน่นอนว่า เรียก FinishCreatingReport ซึ่งอยู่ใน BackgroundWorker1_DoWork แล้ว) ใน Task Manager ยังมี Excel.exe ค้างอยู่ครับ
2. ถ้ากดปุ่มเพื่อสั่งออกรายงานใหม่อีกครั้ง Excel.exe จะหายไปจาก Task Manager พอโปรแกรมสั่ง CreateExcel ถึงจะสร้างขึ้นมาใหม่
3. เมื่อ BackgroundWorker ทำงานเสร็จ ออกรายงานเรียบร้อย Excel.exe ยังคงอยู่ ถ้าผมปิด form นี้ Excel.exe จะหายไปจาก Task Manager แต่ถ้าผม stop debugging ใน Visual Studio หรือโปรแกรมที่เขียนมี bug และ shutdown ไป Excel.exe จะยังคงค้างอยู่ใน Task Manager

ทีนี้ผมลองเพิ่มคำสั่ง ReleaseComObject ครับ


Private Sub FinishCreatingReport(ByVal wk As BackgroundWorker, ByRef e As DoWorkEventArgs)
Try
xlBook.SaveAs(fileName)
Catch ex As Exception
MessageBox.Show(ex.Message & vbCrLf & "Please contact ITD.", My.Application.Info.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
Finally
'// Release memory
wk.ReportProgress(95)
xlApp.Quit()
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlSheet)
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlBook)
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp)
GC.SuppressFinalize(xlSheet)
GC.SuppressFinalize(xlBook)
GC.SuppressFinalize(xlApp)
GC.Collect()
xlSheet = Nothing
xlBook = Nothing
xlApp = Nothing
End Try
End Sub


ลองรันโปรแกรมดู พบว่าเมื่อ BackgroundWorker ทำงานเสร็จ Excel.exe จะถูกลบทิ้งไปทันที ไม่เหลือใน Task Manager แล้ว (พอไล่ดูอย่างละเอียด พบว่า Excel.exe จะหายไป ก่อน BackgroundWorker1_RunWorkerCompleted จะทำงานเสร็จครับ)

ดังนั้นนอกจากจะใช้งาน Excel แล้ว ถ้าเรามีการทำ COM Marshal (InterOp) กรุณาเรียกใช้ System.Runtime.InteropServices.Marshal.ReleaseComObject เพื่อจะได้มั่นใจว่า Unmanaged Memory ได้ถูก Release จริงๆครับ

วันศุกร์ที่ ๔ กันยายน พ.ศ. ๒๕๕๒

BlueprintCSS

วันนี้ได้อ่าน forum ต่างประเทศ เจอคำถามโลกแตกที่ว่าทำไมตอน design เวบ แล้วทดสอบใน IE7 ก็ดูดีแต่พอดูด้วย browser อื่นมันเละไปหมด ซึ่งปัญหานี้คาดว่าคงเจอกันหลายคน แต่โชคดีครับที่ผมทำ Web Application สำหรับใช้ภายในบริษัท ดังนั้นจึงสามารถบอกให้พนักงานใช้เฉพาะ IE เท่านั้นได้ เลยไม่ค่อยเจอปัญหานี้ มีบ้างที่ IE6 กับ IE7 ไม่เหมือนกัน เป็นเพราะว่า IE7 นั้นทาง Microsoft เริ่มกลับมาใช้มาตรฐานสากลแล้ว ซึ่ง IE ก่อนหน้านี้ใช้มาตรฐานของ Microsoft (คือเมื่อก่อนใช้กติกูเป็นหลัก)

หลายๆคนแก้ปัญหานี้โดยการใช้เทคนิคที่เรียกว่า CSS Hacked (สำหรับภาษาไทยศึกษาเพิ่มเติมได้ที่ www.divland.com ครับ) หรืออาจจะเขียน javascript สำหรับโหลด CSS โดยตรวจสอบ browser ก่อนว่าเป็นเวอร์ชันไหน แล้วก็ไปอ่านข้อมูล CSS ที่เขียนสำหรับเวอร์ชันนั้นๆมาใส่ใน webpage ซึ่งแน่นอนว่ามันต้องถึกพอดู และก็ต้องทดสอบหลายๆครั้ง เพื่อให้มั่นใจว่าการแสดงผลถูกต้องตามต้องการ

พอดีเจอคำตอบหนึ่งแนะนำให้ลอง BlueprintCSS ซึ๋งเป็น CSS Framework ตัวหนึ่งที่นอกจากจะช่วยแก้ปัญหานี้ยังมีความสามารถอื่นๆอีกเพียบ ผมก็ลองดาวน์โหลดมาทดสอบตามระเบียบครับ ไว้มีโอกาสจะมาลองเขียน blog เพิ่ม สำหรับผู้สนใจลองไปศึกษาหรือดาวน์โหลดได้ที่ http://www.blueprintcss.org/
มีทั้ง Demo, Wiki, Discussion และอื่นๆครบถ้วน น่าสนใจจริงๆครับ

วันอาทิตย์ที่ ๑๖ สิงหาคม พ.ศ. ๒๕๕๒

VB.NET สร้างรายงานด้วย .rdlc (Client Report Definition File)

ช่วงนี้ดึกๆหามรุ่งหามค่ำนั่งโม่โปรแกรมอยู่ (เล่นปังยา 2 ชั่วโมง เขียนโปรแกรม ครึ่งชั่วโมง) พอจะสร้างรายงาน ปรากฏว่าตัว Visual Studio 2008 ที่ลงไว้ไม่มี Crystal Report มาด้วย (กรรม หาแผ่นก็ไม่เจอ) ก่อนหน้านี้เคยใช้ Print Document รู้สึกว่ามันถึกมาก (เหมือนใช้ VSPrint ใน VB6 เลย) มานั่งทบทวนจำได้ว่าเคยอ่านบทความเรื่อง rdlc กับ Report Viewer ผ่านตา เลยไปลองใช้บริการอากู๋ดูครับ

ได้ข้อมูลมาเยอะมาก อ่านผ่านๆไม่ค่อยรู้เรื่อง ก็ลงมือลองเขียนเลยครับ ปรากฏว่าไม่ยากเท่าไหร่ ติดที่ว่าไม่ถนัดครับ แต่ก็พอถูๆไถๆไปได้ ลองเขียน Daily Report ใช้เวลาคลำประมาณครึ่งชั่วโมงก็เสร็จครับ ไว้ถ้ามีเวลาจะศึกษาเพิ่ม แล้วจะมาลงวิธีการสร้างรายงานอีกที

พอสร้างรายงานเสร็จก็เขียนโค้ดสำหรับ Preview Report ครับ ก็ใช้ ReportViewer Control นั่นแหละ ก่อนอื่นสร้าง form สำหรับ query ข้อมูลก่อน แล้วพอ user กด print ก็ไปเปิด form ที่มี ReportViewer ขึ้นมาแสดง


Private Sub btnPreview_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnPreview.Click

Dim reports As List(Of Entity.ReportDailySummary) = ReportManager.GetDailySummaryReport(Me.dtpWorkingDate.Value)

If reports Is Nothing OrElse reports.Count = 0 Then
MsgBox("ไม่พบข้อมูล", MsgBoxStyle.Information + MsgBoxStyle.OkOnly, My.Application.Info.Description)
Else
Dim ds As New Microsoft.Reporting.WinForms.ReportDataSource("ReportDailySummary", reports)
Dim params As New List(Of Microsoft.Reporting.WinForms.ReportParameter)
params.Add(New Microsoft.Reporting.WinForms.ReportParameter("WorkingDate", Me.dtpWorkingDate.Value.ToString("dd MMMM yyyy")))
frmPreview.ReportViewer1.LocalReport.ReportPath = "rptDailySummary.rdlc"
frmPreview.ReportViewer1.LocalReport.DataSources.Add(ds)
frmPreview.ReportViewer1.LocalReport.SetParameters(params)
frmPreview.ReportViewer1.RefreshReport()
frmPreview.Text = "รายงานสรุปยอดขายประจำวัน"
frmPreview.ShowDialog()
End If

End Sub


เนื่องจากขี้เกียจ และต้องรีบไปออกรอบ ดังนั้นลงแค่ code พอ เผื่อว่าวันหลังลืมวิธี Preview Report จะได้มาค้นดูได้

ปล. วันนี้ตอนอยู่ที่ทำงานจะเขียนคำสั่งสำหรับ sort list ว่าจะใช้ Lamda Expression แต่นึกไม่ออก ลองเขียนแล้วก็รันไม่ผ่าน ยังดีที่ได้เอาโค้ดมาลง blog ไว้ เลยมาเปิดหาดู สะดวกดีครับ ดีกว่าไปเปิดโปรเจคเก่าๆมาหามากมายก่ายกอง

วันศุกร์ที่ ๑๔ สิงหาคม พ.ศ. ๒๕๕๒

VB.NET อ่านข้อมูลภาษาไทยจากไฟล์

วันก่อนมีโอกาสเขียนโปรแกรมสำหรับทำการอ่านข้อมูลจาก text file แล้วตัดเอาข้อมูลไปเก็บใน database เพื่อประมวลผล จริงๆก็ไม่ยากครับ แต่ปรากฏว่าข้อมูลภาษาไทยมันกลายเป็นอักษรต่างดาว สาเหตุก็คือ text file ที่ว่าเป็น daily report ที่ได้จากการ export มาจากโปรแกรม POS (Point of Sale) และมันถูก encode ด้วย ANSI (ASCII) ไม่ใช่เป็น UNICODE พอเขียน VB.NET สั่งอ่านไฟล์ตรงๆมันเลย encode ไม่ถูกครับ เพราะว่า .NET ใช้ Unicode (UTF-16) เป็น default encoder

ดังนั้นตอนใช้ StreamReader ก็กำหนด Encode ไปด้วยแค่นี้เอง (งมตั้งนาน - จริงๆแล้วพอคิดไม่ออก เลยไปเล่นเกมส์ พอเล่นเสร็จ ง่วงแล้วก็เลยรีบนอน 5555) ทีนี้ก็มีให้เลือกว่าจะใช้ Windows-874 หรือว่า TIS-620 ผมก็เลือกใช้ Windows-874 เพราะว่า text file โดน export มาจากโปรแกรมที่เขียนบน Windows ซึ่งเขียนมานานพอสมควร ดังนั้นก็เลยเลือกใช้ Windows-874 ซะเลย (ยังไม่ได้ลอง TIS-620 เลย เพราะขี้เกียจ)

ลองดูโค้ดกัน ผมสร้าง Singleton Class ชื่อ DBImportManager ขึ้นมา มี Sub อยู่ 1 ตัว สังเกตุว่ามี BackGroundWorker เป็นพารามิเตอร์ด้วย


Public Sub ImportData(ByVal bk As System.ComponentModel.BackgroundWorker, ByVal fileName As String, ByVal fileImporter As IFileImporter)

If Not FileIO.FileSystem.FileExists(fileName) Then Exit Sub
Dim iLines As Integer
Try
Dim fs As New System.IO.StreamReader(fileName, System.Text.Encoding.GetEncoding(874))
Do While Not fs.EndOfStream
fileImporter.ParseLine(fs.ReadLine)
bk.ReportProgress(10, "Reading: " & iLines)
Loop
fileImporter.Update()
Catch exIO As IO.IOException
bk.ReportProgress(0, exIO.Message)
Catch ex As Exception
bk.ReportProgress(0, ex.Message)
End Try

End Sub


และพารามิเตอร์ตัวสุดท้ายก็เป็น IFlieImporter เนื่องจากว่าเรามีไฟล์หลายตัวที่ต้องทำการ import ข้อมูล ซึ่งแต่ละไฟล์ก็จะมีเงื่อนไขในการตัดไม่เหมือนกัน ผมเลยสร้าง Interface ตัวนี้ขึ้นมาครับ และก็ไปสร้าง FileImporter สำหรับไฟล์แต่ละแบบ เช่น รายงานประจำวัน รายงานลูกค้าใหม่ รายงานรับสินค้าเข้า ฯลฯ ดังนั้นเวลาเปลี่ยน logic ในการตัดไฟล์ เราก็ไปทำการแก้ไขเฉพาะ FileImporter Class ที่เราสร้างขึ้น โดยจะไม่กระทบกับ Class ตัวนี้เลย

ลองรันทดสอบดู เรียบร้อยครับ ผลลัพธ์ถูกต้อง