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

เปรียบเทียบ DataTable ด้วย LINQ

วันก่อนได้มีโอกาสลองใช้ LINQ กับ DataTable ดูครับ นั่นคือถ้าเรามี data table 2 ตัวที่มี schema เหมือนกัน อยากรู้ว่าข้อมูลใน data table ตัวแรก กับ ตัวที่สองมี row ไหน ที่เหมือนหรือต่างกันบ้าง วิธีเดิมๆคือเราเขียนลูปไล่ตรวจสอบที่ละ row เอง ซึ่งจริงๆก็ไม่ยากครับ แต่สำหรับ .NET Framework 3.5 นั้นง่ายกว่าเดิม เพราะเราสามารถใช้ LINQ ทำงานแทนได้ครับ ลองดูโค้ดกัน ก่อนอื่นเรามาเตรียม DataTable ทั้ง 2 ตัวก่อน


'เตรียมข้อมูล Data1 ให้มี 7 row ตั้งแต่ 0-6
Dim dt1 As New Data.DataTable("Data1")
Dim dr1 As DataRow = Nothing
dt1.Columns.Add(New Data.DataColumn("ID"))
dt1.Columns.Add(New Data.DataColumn("Name"))
For iRecord As Integer = 0 To 6
dr1 = dt1.NewRow()
dr1.Item("ID") = iRecord
dr1.Item("Name") = "Test" & iRecord
dt1.Rows.Add(dr1)
Next

'เตรียมข้อมูล Data2 ให้มี 7 rows ตั้งแต่ 4 - 10
Dim dt2 As New Data.DataTable("Data2")
Dim dr2 As DataRow = Nothing
dt2.Columns.Add(New Data.DataColumn("ID"))
dt2.Columns.Add(New Data.DataColumn("Name"))
For iRecord As Integer = 4 To 10
dr2 = dt2.NewRow()
dr2.Item("ID") = iRecord
dr2.Item("Name") = "Test" & iRecord
dt2.Rows.Add(dr2)
Next


ทีนี้ถ้าเราอยากจะหาเฉพาะ DataRow ของ Data1 ที่ไม่ซ้ำกับใน Data2 เราสามารถสั่ง Data1 ExceptRow Data2 ได้ครับ แต่ก่อนอื่นเราต้องแปลง DataTable ให้เป็น Enumerable ก่อนเพราะว่าคำสั่ง Except ไม่มีใน Extension Method ของ DataTable ครับ แต่มีใน System.Linq.Enumerable


Dim r1 = dt1.AsEnumerable()
Dim r2 = dt2.AsEnumerable()


ตัว r1 และ r2 จะมี type เป็น System.Data.EnumerableRowCollection(Of System.Data.DataRow) ครับ ทีนี้เราจะเห็นมี Extension Method ชื่อ Except ให้เรียกใช้ได้แล้ว


Dim ExceptResult = r1.Except(r2, DataRowComparer.Default)
Dim IntersectResult = r2.Intersect(r1, DataRowComparer.Default)


จะสังเกตุว่าผมใส่ DataRowComparer.Default เป็น parameter ด้วย สำหรับ Comparer นั้นเราอาจจะเขียนเองก็ได้ครับ ถ้าเรามีเงื่อนไขในการเปรียบเทียบ DataRow ของเราเอง เช่นต้องการเทียบเฉพาะ Primary Key หรือเฉพาะบาง field เป็นต้นครับ ทีนี้ type ของ ExceptResult จะเป็น
System.Collections.Generic.IEnumerable(Of System.Data.DataRow) ครับ จากตัวอย่างข้างต้น ค่าที่ได้ของ ExceptResult จะมี 4 DataRows นั่นคือ ID ตั้งแต่ 0-3

ส่วนบรรทัดต่อมา ผมลองคำสั่ง Intersect นั่นคือ เอาเฉพาะข้อมูลที่เหมือนกันระหว่าง 2 EnumerableRowCollection ผลลัพธ์ก็จะได้ 3 DataRows คือ ID ตั้งแต่ 4-6 นั่นเอง

ถ้าผมแก้จาก
dr2.Item("Name") = "Test" & iRecord
เป็น
dr2.Item("Name") = "Test2" & iRecord

แล้วลองรันใหม่ จะพบว่า ExceptResult.Count = 7 นั่นคือข้อมูลทั้งหมดของ dt1 และ IntersectResult.Count = 0 เพราะว่าเราใช้ DataRowComparer.Default อย่างที่บอกครับ ดังนั้นแต่ละ row ถึงจะมี ID เดียวกัน เป็นข้อมูลใน field อื่นไม่เหมือนกัน ก็จะถือว่าเป็น row นั้น ไม่เหมือนกันครับ

คราวนี้มาลองเล่นกันต่อครับ กำลังสนุกเลย


Dim dtResult = ExceptResult.CopyToDataTable()
dtResult.TableName = "dtResult"
dt1.Rows(0).Delete()
dt2.Rows(1).Delete()


บรรทัดแรกผมใช้ Extension Method ชื่อ CopyToDataTable ก็ตรงตัวครับ คือมันจะ copy DataRows ไปสร้าง DataTable ใหม่ สำหรับคำส่งนี้ถ้าเราส่ง DataTable ไปเป็น parameter มันจะ copy DataRows ไปยัง DataTable ตัวนั้นๆแทนครับ

ต่อมาผมลองสั่งลบแถวแรก ของ dt1 และลบแถวที่สองของ dt2 ทีนี้ลองมา debug ดูข้อมูลใน dt1, dt2, r1, r2, ExceptResult, IntersectResult จะพบว่าเมื่อเราสั่งลบ Datarows แล้ว มันจะมีผลต่อ r1, r2, ExceptResult, IntersectResult ครับ แต่มันไม่มีผลต่อ dtResult ครับ ดังนั้นตอนนี้ค่าของ dtResult จะไม่เท่ากับ ExceptResult แล้ว

ที่เป็นแบบนี้เพราะว่า Except() กับ Intersect() นั้นใช้ความสามารถของ Yield Return นั่นเองครับ ไอ้เจ้า Yield Return นี่มันสุดยอดจริงๆ เสียดายที่ VB2008 ไม่สามารถใช้ Yield Return ได้ครับ สำหรับผู้ที่อยากรู้เรื่อง Yield Return ลอง search ใน google ดูครับ ผมเคยอ่านที่ www.coredeveloper.net แต่วันนี้มันเข้าไปเวบนี้ไม่ได้ซะละ

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

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')

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

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

เรื่องประหลาดใจ

วันนี้มีเรื่องน่าประหลาดใจเรื่องหนึ่งสำหรับผมครับ คือผมได้รับอีเมลล์มาแสดงความขอบคุณและบอกว่าแก้ปัญหาเรื่องการเขียนโปรแกรมได้แล้ว ที่น่าประหลาดใจคือผมได้รับเมลล์มาขอความช่วยเหลือเรื่องเขียนโปรแกรมมากกว่า 5 ปีแล้ว ตั้งแต่เริ่มตอบกระทู้ที่พันธ์ทิพย์และเขียนบทความที่ expert2you เท่าที่จำได้ครั้งนี้เป็นครั้งแรกครับที่พอผมตอบเมลล์ไป แล้ววันรุ่งขึ้นมีเมลล์มาแจ้งว่าทำได้แล้วพร้อมกับขอบคุณ ปกติถ้าได้รับเมลล์จะบอกว่ายังไม่ได้ ช่วยแก้โค้ดหน่อย หรือไม่ก็หายไปเลยครับ จะมีเมลล์มาขอบตคุณบ้างเมื่อมีคำถามใหม่ -*-

ที่ประหลาดใจกว่าก็คือในหลายๆเวบบอร์ดก็มีการบ่นเรื่องนี้ครับ คือคนตอบกระทู้แล้ว พอคนได้คำตอบก็หายไปเลย อย่างน้อยมาบอกว่าทำได้แล้ว คนอื่นๆที่ search เจอภายหลังจะได้มั่นใจว่า เจอคำตอบที่ถูกต้องก็ยังดี ถ้าคำตอบในกระทู้ยังไม่ถูก แต่ไปหาวิธีแก้ปัญหามาได้แล้ว น่าจะมีอัพเดทในกระทู้นั้นๆบ้าง จะได้มีประโยชน์กับคนอื่นต่อไปครับ

ก็อยากชักชวนให้เพื่อนๆพี่ๆน้องๆ ที่ได้คำตอบแล้วช่วยมาอัพเดทในกระทู้ที่ตัวเองตั้งด้วยนะครับ

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

ASP.NET สั่งรัน Win App บนเครื่อง Client

ผมเคยสร้าง Web Application ตัวหนึ่ง เพื่อทำเป็น Application Portal คือถ้า user sign in เข้ามาใน Web นี้ จะมองเห็น link ของโปรแกรมทั้ง Win App และ Web App ที่มีสิทธิ์เปิดใช้งาน รวมถึงทำ Single Sign On ด้วย นั่นคือจะส่ง User Name + Password เพื่อไป log in ที่ app นั้นๆทันทีครับ แต่ใน App นั้นๆก็ต้องรองรับการส่ง Parameter ๆไปด้วยนะ

ทีนี้ถ้าเป็น Web App ด้วยกันก็ไม่มีปัญหาครับ แค่สร้าง link เท่านั้นเองและก็ส่ง parameter ที่เข้ารหัสไปด้วย แต่ถ้าเป็น Win App นี่แหละที่เป็นเรื่องใหญ่ เพราะว่า ASP.NET นั้นทำงานเป็น Server side script นั่นคือคำสั่งพวก Shell จะทำให้เกิดการรันโปรแกรมที่ฝั่ง Server ครับ เหมือนกรณีเราเขียนคำสั่ง MessageBox.Show นั่นแหละ มันจะขึ้น message box ที่ server เสมอ เราต้องใช้ Client side script แทน ถ้าเป็น javascript ก็คือ alert() นั่นเอง

ทีนี้ Win App ที่เราต้องการเรียกนั้น ถ้าเราพัฒนาด้วย .NET 2.0 ขึ้นไป สามารถใช้ ClickOnce ในการ Deploy ได้ ถ้าเป็นแบบนี้ก็ง่ายขึ้นครับ เพราะเราจะมี URI ให้เลือกใช้ดังนี้
  • http://ClickOnceServer/TestApplication ตัวนี้จะลิงค์ไปหน้าหลักของโปรแกรม ซึ่งจะมีปุ่ม Install และ ปุ่ม Install Prerequisites

  • http://ClickOnceServer/TestApplication/TestApplication.Application ตัวนี้จะไปรัน Application ทันทีครับ

ดังนั้นเราก็เขียน javascript เรียก .Application เพื่อสั่งรันโปรแกรมได้เลย



function btnAttn_OnClick() {
window.open("http://ClickOnceServer/TestApplication/TestApplication.Application", "Application");
intervalId = setInterval(UpdateInfo, 10000);
event.returnValue = false;
}

ในโค้ดผมมีการใช้ setInverval ด้วย เพราะต้องการไปเรียก UpdateInfo function ทุก 10 วินาที function ตัวนี้จะไปเรียก WebService เพื่อทำการ update สถานะบน label ทุก 10 วินาทีครับ

ตัว ClickOnce เราสามารถส่ง Commandline argument (parameter) ไปได้ด้วยครับ แต่ขอข้ามไปก่อนละกัน

ทีนี้ถ้าเป็น Win App ที่ไม่ใช่ ClickOnce ก่อนอื่นเลยจะมีข้อจำกัดดังนี้

  1. Win App นั้นต้อง Install ที่เครื่อง Client แล้ว และเราต้องรู้ Path ที่แน่นอน
  2. ต้องกำหนด Security Option ของ IE ในเครื่อง Client ด้วย ที่ผมทำคือไปกำหนด Trust Site ครับ เพราะเป็น Intranet อยู่แล้วก็ไม่มีปัญหา เพราะเราต้องใช้ WScript ในการสั่ง Shell application ที่ต้องการนั่นเอง

ดังนั้นวิธีนี้เหมาะกับเครื่องภายในเท่านั้นครับ

โค้ดส่วน HTML ซึ่งเก็บ hidden field ไว้สองตัวนั่นคือ User name กับ password อย่าลืมเข้ารหัสด้วยนะครับ



<body>
<form id="form1">
<div>
<input type=hidden id='hdnExeCommand' name='hdnExeCommand' value='<%=strExeCommand%>'>
<input type=hidden id='hdnWinAppLocation' name='hdnWinAppLocation' value='<%=strWinAppLocation %>'>
</div>
</form>
</body>

จะเห็นว่ามีการเอาค่าตัวแปรมาใส่ใน hidden field ด้วย ค่าตัวแปรที่ได้นี้มาจากคำสั่งถอดรหัสในฝั่ง Code Behind ครับ ทีนี้มาดู vbscript กัน (จริงๆจะใช้ javascript ก็ได้และดีกว่าด้วย แต่ผมอยากลอง vbscript ครับ)



<script language="vbscript">
dim WshShell, strExe, fso, strCommand
set WshShell = CreateObject("WScript.Shell")
set fso = CreateObject("Scripting.FileSystemObject")
strExe = document.all("hdnWinAppLocation").value
strCommand = document.all("hdnExeCommand").value
if fso.FileExists(strexe) then
WshShell.Run strCommand ,1, false
else
msgbox "File not found.",vbOKOnly+vbInformation,"TG Application Portal"
end if
set fso = nothing
set WshShell = nothing
window.close()
<script>


ส่วนเรื่องการส่ง command line argument กับ Encrypt&Decrpyt ถ้ามีเวลาจะกลับมาเขียนเพิ่มครับ

วันจันทร์ที่ ๑๖ พฤศจิกายน พ.ศ. ๒๕๕๒

.NET Framework 4.0

วันก่อนเจอคุณมี่ (MVP, GreatFriends Leader) ที่งานอบรมที่ไมโครซอฟท์ เลยมีโอกาสถามเรื่อง .NET Framework 4.0 ครับ เพราะผมไม่ค่อยได้ติดตาม .NET Framework 4.0 และ VS 2010 เลย (ทั้งที่มีแต่คนบอกว่า VS2010 สุดยอดให้ลองโหลดมาเล่น)

คำถามแรกคือ .NET Framework 4.0 ยังใช้ Core เดียวกันกับ .NET 2.0, 3.0 และ 3.5 หรือไม่
คำตอบคือ ทาง MS พัฒนาขึ้นมาใหม่เป็น Core ตัวใหม่เลย แต่ไม่ต้องห่วงครับ (ยังไม่ได้ถาม และยังไม่ได้ห่วงเลย คุณมี่รีบตอบให้เสร็จ - สงสัยกลัว VS2010 จะขายได้น้อย 555) เพราะ VS2010 สนับสนุนเรื่อง Target Framework เหมือนเดิม ดังนั้นเราสามารถเลือกพัฒนา .NET เวอร์ชัน 2.0,3.0, 3.5 หรือ 4.0 ก็ได้

อะ ก็ฟังดูดี แต่ว่าพอผมเอา VS2008 ไปเปิด Solution ที่พัฒนาด้วย VS2005 มันจะบังคับให้ Convert ยันเตเลย มันเปิดแล้วเลือก Target Framework เป็น 2.0 อัตโนมัติไม่ได้เหรอ แล้ว VS2010 จะทำได้มั๊ยเนี่ย ไม่อยาก Convert น่ะ (ตอนนั้นยังไม่ได้ห่วง ก็เลยลืมถามคำถามนี้ เซ็งเลยตรู แต่ตอนนี้ยังเป็น Beta ดังนั้นก็ต้องรอดูเวอร์ชันจริงอีกที)

คำถามที่สองก็คือ แล้ว .NET Framework 4.0 มันใหญ่มั๊ย เพราะว่า .NET Framework 2.0 Redis. มันแค่ 22.4 MB เอง พอใช้ Click Once แล้วต้อง Deploy ตัว .NET Framework ไปด้วย มันไม่ใหญ่เลยทำได้สะดวกครับ แต่ว่าพอมาใช้ .NET Framework 3.5 SP1 มันตั้ง 231 MB แนะวุ้ย ใหญ่มากกกกกกกกกกกกกกก

คำตอบคือไม่ใหญ่ครับ เพราะพัฒนาขึ้นใหม่ (จบ สั้นๆได้ใจความ)

ผมเลยลองไปดูในหน้าดาวน์โหลดของ MSDN ถ้าเลือกเป็น .NET Framework 4.0 X86 มันแค่ 37.7 MB เอง ถือว่าขนาดโอเคครับ ถ้าเป็น x64 ก็ 54.6 MB

ในหน้า MSDN มี Visual Studio Express 2010 Beta ให้โหลดมาลองด้วยครับ ถ้าใครกลัวโหลด VS2010 ตัว Profession หรือ Team Suit มาใช้แล้วติดใจต้องเสียเงินซื้อ ลองใช้ Express เวอร์ชันก็ได้นะครับ พอใช้รับงาน freelance หากินได้ ยกเว้นเรืองออกรายงานที่ต้องใช้ rdlc หรือไม่ก็ PrintDocument ที่ต้องถึกหน่อยครับ

อีกเรื่องที่สำคัญ คนที่มีลิขสิทธิ์ VS2008 แล้วอยากจะ upgrade ไปใช้ VS2010 ลองติดต่อคุณมี่ดูครับ เพราะเหมือนว่าจะมีโปรโมชันสำหรับคนต้องการ Upgrade ในราคาพิเศษนะ แต่ผมฟังไม่ถนัดเพราะว่าที่ทำงานผมมี Enterprise Agreement อยู่แล้ว มัน upgrade ฟรี (เพราะเสียเงินค่า EA ไปแล้ว)เลยไม่ได้สนใจมาก ที่สำคัญอีกอย่างคือผมกำลังกินขนมกับกาแฟอยู่น่ะครับ ถ้ายังไงคนที่สนใจ VS2010 ลองติดต่อคุณมี่ดูครับ ผมไม่ได้ขออนุญาตเลยไม่กล้าลงอีเมลล์ไว้ ลอง search หาดู หรือไม่ก็แวะไปคุยกับคุณมี่ที่ GreatFriends ครับ

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

Javascript - สร้าง TextBox เพื่อ filter รายการใน DropDownList

ผมได้รับ requirement ใหม่จาก user เจ้ากรรมอีกแล้ว นั่นคือมี DropDownList ตัวหนึ่งใน ASP.NET ที่มีรายการมากกว่า 100 รายการ (อันนี้กรองมาแล้วนะครับ) ซึ่ง option มันเยอะมาก ดังนั้น user ต้องการให้โปรแกรมสามารถ filter รายการใน DropDownList นี้ได้ครับ ทีนี้จะเขียนโปรแกรมที่ฝั่ง server ก็สามารถทำได้ แต่มันจะทำให้มีการ postback ไปหา ซึ่งไม่ค่อยดีเท่าไหร่ ดังนั้นผมเลยใช้ javascript สำหรับ filter แทนครับ

วิธีก็คือผมสร้าง Textbox ขึ้นมาหนึ่งตัว สำหรับให้ user พิมพ์ข้อความที่ต้องการกรอง และสร้าง drop-down list อีกหนึ่งตัวโดยให้ visibility=hidden คือซ่อนไม่ให้ user เห็น สำหรับเก็บ option ทั้งหมดที่ไม่ได้ทำการกรองครับ ลองมาดูโค้ดกัน

เริ่มจาก WebControl ก่อน มี DropDownList 2 ตัว และ TextBox ครับ


<asp:DropDownList ID="ddlWorkCode" runat="server" CssClass="txtRequired" Width="540px" AutoPostBack="True" style="height:24px"></asp:DropDownList"> 
<asp:TextBox ID="txtSearchWorkCode" runat="server" Width="421px" style="color: #000099; background-color: #afeeee;""></asp:TextBox">
<asp:DropDownList ID="ddlCache" runat="server" Width="0px" style="visibility:hidden" "></asp:DropDownList"> 


ในหน้า Code Behind ก็ทำการ Add Attribute ให้ txtSearchWorkCode

txtSearchWorkCode.Attributes.Add("onkeyup", "txtSearchWorkCode_OnKeyUp(this);")
txtSearchWorkCode.Attributes.Add("onblur", "txtSearchWorkCode_OnBlur();")



คราวนี้มาถึง javascript บ้าง คราวนี้เขียนสดๆไม่ได้ใช้ jQuery ครับ

function txtSearchWorkCode_OnKeyUp(obj) {
if (event.keyCode != 13 || event.keyCode != 9) {
var str = obj.value.toUpperCase();
var ddlWorkCode = $get("ddlWorkCode");
var ddlCache = $get("ddlCache");
var listWorkCode = ddlCache.getElementsByTagName("OPTION");

// ถ้าไม่มี OPTION ใน cache ให้ออกจาก function
if(listWorkCode.length==0) return false;

//clear option ใน DropDownList
ddlWorkCode.length = 0;
for (i = 0; i < listWorkCode.length; i++) {
if (listWorkCode[i].text.toUpperCase().indexOf(str) > -1) {
var o = listWorkCode[i].cloneNode(true);
ddlWorkCode.appendChild(o);
}
}
ddlWorkCode.selectedIndex = 0;
}
}

function txtSearchWorkCode_OnBlur() {
if ($get("txtSearchWorkCode").value == "") {
// ถ้าไม่มีคำสั่งกรอง ให้ไปดึง OPTION ทั้งหมดจาก DropDownList2 มาใส่คืน
var ddlWorkCode = $get("ddlWorkCode");
var ddlCache = $get("ddlCache");

ddlWorkCode.lenght = 0;
for (i = 0; i < ddlWorkCode2.options.length; i++) {
var o = ddlWorkCode2.options[i].cloneNode(true);
ddlWorkCode.appendChild(o);
}
ddlWorkCode.selectedIndex = 0;
}
}


ในส่วนคำสั่ง javascript มีสองจุดที่น่าสนใจครับ นั่นคือ
1. ผมใช้ ddlWorkCode.length=0; ในการเคลียร์ option ของ drop-down list ทิ้งครับ ถือว่าง่ายและสะดวกมาก ทีแรกผมใช้ for วนลูปเพื่อ remove element ออก ซึ่งแน่นอนว่าช้ากว่าครับ

2. ผมใช้ cloneNode method ในการ copy OPTION จาก drop-down list ตัวที่สอง (ที่เก็บข้อมูลทั้งหมดไว้) แล้วก็เอา OPTION ที่ clone ได้ไปใส่ใน drop-down list ตัวแรกด้วยคำสั่ง appendChild ทีแรกผมใช้วิธีสร้าง new Option ครับ แต่เจอคำสั่ง cloneNode ที่ www.java2s.com ใช้ได้สะดวกดีครับ และที่สำคัญพอลอง search ใน google พบว่าคำสั่ง cloneNode ทำได้เร็วกว่า New element ซะอีก

ตอนนี้ลองทดสอบโปรแกรมดู ก็ใช้งานได้ดียังไม่เจอ bug แต่เพิ่งทดสอบได้นิดเดียวคงต้องลองเล่นดูอีกสักพักครับ

ปล. สำหรับ $get() เป็น shortcut ของ ASP.NET AJAX ครับ มีค่าเท่ากับ document.getElementById()

วันอังคารที่ ๒๗ ตุลาคม พ.ศ. ๒๕๕๒

แนะนำ jnithi.bloggang.com

วันนี้กลับไปอ่าน blog เดิม (โปรแกรมมั่ว พ่อลูกอ่อน) ที่เขียนไว้ที่ jnithi.bloggang.com ครับ ก็เลยนึกขึ้นได้ว่ายังไม่ได้ทำ link ที่ bloggang มาที่ blogger นี่เลย จริงๆแล้วตอนที่เริ่มกลับมาเขียน blog ที่ blogger นี่ก็ไปอัพเดทที่ bloggang แต่ว่าพอกด save มันดันมี error ขึ้นมา เลยเซ็งอารมณ์ปล่อยทิ้งไว้อย่างนั้น จากนั้นก็เลยลืมไปเลยครับ 5555

ทีแรกตั้งใจว่าจะเขียนเรื่องเกี่ยวกับการพัฒนาโปรแกรมที่ blogger ส่วนที่ bloggang จะเขียนเรื่องลูกสาวอย่างเดียว ปรากฏว่าเพื่อนๆบอกว่าอยากให้ไปเขียนเรื่องน้องแพร์ที่ hi5 กับ facebook แทน แต่ด้วยความขี้เกียจก็เลยไม่ได้อัพเดททั้ง bloggang, hi5 และ facebook (จริงๆแล้วผมเล่น hi5 กับ facebook ไม่เป็นอะ เข้าไปแล้วมันงงๆๆๆ)

พอกลับไปอ่าน โปรแกรมมั่ว พ่อลูกอ่อน blog ก็เลยนึกขึ้นได้ว่าอาจจะมีประโยชน์สำหรับชาวโปรแกรมมั่วอีกหลายท่านครับ ก็เลยมาเขียน blog แนะนำที่นี่อีกทีครับ

รู้สึกว่าที่ bloggang คนจะเข้ามา comment เยอะกว่านะ ทั้งๆที่ดูจาก PageView แล้วยังน้อยกว่าคนที่เข้า blog นี้ซะอีก สงสัยว่าคนเข้า blog นี้จะรีบเข้ามาหาวิธีเขียนโปรแกรมเพราะส่วนมากมาจาก Search Engine เลยมาเร็ว อ่านเร็ว ไปเร็ว เพราะต้องรีบกลับไปปั่นงาน 5555 (เป็นเหมือนกัน) แต่ตอนนี้คนที่เข้าซ้ำประจำก็เริ่มมีหลายคนนะครับ

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

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)

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

VBScript อ่านค่า User Account เพื่อเปิด Website อัตโนมัติ

ผมได้รับคำสั่งให้ทำอย่างไรก็ได้ เมื่อ user ทำการ log in เข้า Windows แล้วให้ตรวจสอบว่าเป็น user ที่อยู่ในกลุ่มพิเศษหรือไม่ ถ้าใช่ให้เปิด Web Application ขี้นมา เพื่อแสดงสถานะงานคงค้าง (เรื่องสำคัญมากคือการทวงหนี้ลูกค้า)

ไปนั่งคิดนอนคิดก็เลยคิดว่าน่าจะเขียน Log on script ด้วย VBScript มาทำงานนี้ครับ เริ่มแรกผมเก็บข้อมูลรายชื่อ user ไว้ใน database ก่อน แล้วพอ Log on script ทำงานจะไปตรวจสอบว่า User นั้นตรงกับที่อยู่ใน database หรือไม่ ทีนี้เราต้องใช้ Windows Scripting Host (WScript) มาร่วมด้วยครับ


ON ERROR RESUME NEXT

'*******************************************************
'// GET LOGON USER ACCOUNT
'*******************************************************
Dim WSHNetwork
Set WSHNetwork = CreateObject("Wscript.Network")

Dim userAccount
userAccount = WSHNetwork.UserDomain & "\" & WSHNetwork.UserName
Set WSHNetwork = nothing

'*******************************************************
'// VALIDATE USER
'*******************************************************

Set cnn = CreateObject("adodb.connection")
Set rstUser = CreateObject("adodb.recordset")

cnn.connectionString = "Provider=SQLOLEDB.1;Persist Security Info=False;User ID=xxxx;Password=yyyy;Initial Catalog=UserList;Data Source=xxSQLServer"

cnn.open

Dim strSQL
strSQL = "select employeeid from dbo.tblUser a where aduser = '" & userAccount "'"
rstUser.open strsql, cnn

If not rstUser.eof Then
'// If valid user, execute IE
Dim WSHShell
Set WSHShell = CreateObject("WScript.Shell")
WSHShell.Run "iexplore.exe -new http://myHost/myApplication"
Set WSHShell = nothing
End If


rstUser.close
cnn.close

Set rstUser = Nothing
Set cnn = Nothing



ทดสอบการทำงานก็ได้ผลโอเคครับ แต่โดน Network Admin ติงมานิดหน่อยว่าไม่อยากให้ไปติดต่อ database เพราะมันช้า และต้องใช้ traffic ของ network เนื่องจาก SQLServer กับ AD อยู่คนละ Server กัน ดังนั้นเลยเปลี่ยนเอา list ของ user มาเก็บเป็น Text file แทนครับ ลองดูโค้ดเฉพาะตรงนี้กัน ส่วนหา user log on ก็เหมือนโค้ดด้านบน


const ForReading = 1, TristateTrue = -1

Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile("\\myserver\netlogon\UserList.txt", ForReading, false, TristateTrue)

Dim user
Dim isFound
isFound = 0

Do Until objFile.AtEndOfStream
user = trim(objFile.ReadLine)
If lcase(user) = lcase(useraccount) Then
isFound = 1
'msgbox isFound
Exit Do
End If
Loop

objFile.Close
Set objFile = Nothing
Set objFSO = Nothing

If isFound = 1 Then
'// If valid user, execute IE
Dim WSHShell
Set WSHShell = CreateObject("WScript.Shell")
WSHShell.Run "iexplore.exe -new http://myHost/myApplication"
Set WSHShell = nothing
End If

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

ลองสร้าง 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 ละกัน

วันพุธที่ ๗ ตุลาคม พ.ศ. ๒๕๕๒

ASP.NET ส่ง array จาก Code Behind Page ไปที่ javascript

ปกติเวลาเราเขียน javascript ที่หน้า aspx บางครั้งเราอาจจะต้องการข้อมูลที่อยู่ใน Code Behind Page เช่น clientId (ค่อนข้างใช้บ่อย ถ้ามีการใช้ MasterPage) เช่น

<script type="text/javascript" id="MainScript">
// <![CDATA[
var employeeTextBoxId = "<%=txtEmployee.ClientId%>";
var movementCount = "<%=EmploteeMovements.Count%>";
// ]]>
</script>

แต่ทีนี้ถ้าต้องการข้อมูลที่เป็น Array ละ ก็ไม่ยากครับ สร้าง Function ที่ฝั่ง Code Behind Page ขึ้นมาเพื่อเอา array มาต่อกันเป็น string โดยให้มีตัว separate (ในตัวอย่างใช้ ;) จากนั้นฝั่ง javascript ก็ไปสั่ง split อีกที

Public employeeName(5) As String

Protected Function GetEmployees() As String

Dim sb As New StringBuilder

For i As Integer = 0 To employeeName.Length - 1
sb.Append(employeeName(i) & ";")
Next

sb.Remove(sb.Length - 1, 1)
Return sb.ToString

End Function


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

employeeName(0) = "Nithi"
employeeName(1) = "Paetree"
employeeName(2) = "Ple"
employeeName(3) = "Suvimol"
employeeName(4) = "Weekit"
employeeName(5) = "Sasawin"

End Sub



<script type="text/javascript" id="MainScript">

// <![CDATA[
var a = "<%=GetEmployees()%>";
var empArray = a.split(";");
// ]]>

</script>



เท่าที่ลองดูหลายๆแบบ วิธีนี้ง่ายสุดครับ (แต่อาจไม่ดีสุดนะ)

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

ASP.NET AJAX 4.0 Preview 5

ตอนนี้ที่ Codeplex ได้มีการ release ASP.NET AJAX 4.0 Preview 5 แล้วครับ (อัพเดทล่าสุดวันที่ 14 กันยายน แต่ผมเพิ่งเห็น) ที่จริงว่าจะลองโหลดมาเล่นตั้งแต่ Preview ก่อนหน้านี้แล้ว เพราะว่า banpote_tt MVP หนุ่มหล่อจาก GreatFriends ได้ MSN มาคุย (โม้ติเวท) บรรยายถึงสรรพคุณถึง Microsoft Ajax Template ให้ฟังจนเคลิ้ม แต่ติดว่าช่วงนี้ยุ่งมากเลยไม่มีเวลาโหลดมาลองเล่นครับ คิดว่าอีก 2 อาทิตย์น่าจะว่างแล้ว คงได้ลองดาวน์โหลดมาเล่นดูบ้าง

วันก่อนเข้าไปอ่าน ScottGu's Blog
พบ entry หนึ่งน่าสนใจ (จริงๆก็น่าสนใจทุกอันแหละครับ) นั่นคือ Announcing the Microsoft AJAX CDN ถือว่าเป็นไอเดียที่ดีมากครับ

แต่สงสัยอย่างหนึ่ง ถ้า MS ทำการ update libraries ใน CDN ใหม่ จะมั่นใจได้ไงว่าจะไม่กระทบกับ App เดิมที่เราใช้งานอยู่เนี่ย เช่นอาจเกิด link ตาย หรือว่า library ตัวใหม่ไม่ compat กับของเก่า เราจะเจอ library hell หรือเปล่านะ

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

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 และอื่นๆครบถ้วน น่าสนใจจริงๆครับ

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

LINQ To SQL Group by multiple columns

วันนี้จะเขียน Linq To SQL เพื่อให้ทำคำสั่งเหมือน

SELECT cocode, arbook, batchdate, batchno,count(*)
FROM arpendingbill
WHERE cocode = 'TGIP' AND arbook = 'NEWAR'
GROUP BY cocode, arbook, batchdate, batchno

โดยให้นำข้อมูลไปใส่ Class ชื่อ ARPendingBatch เขียนเป็น LINQ ดังนี้ครับ


Dim b = From pb In dc.ARPendingBills _
Where pb.COCode = coCode AndAlso pb.ARBook = arBook _
Group By key = New With {pb.COCode, pb.ARBook, pb.BatchDate, pb.BatchNo} _
Into pbGroup = Group _
Select New ARPendingBatch With {.ARBook = key.ARBook, _
.CoCode = key.COCode, .BatchDate = key.BatchDate, _
.BatchNo = key.BatchNo, .BillCount = pbGroup.Count( _
Function(c) c.BatchDate = key.BatchDate AndAlso _
c.BatchNo = key.BatchNo)}


พอลอง debug เพื่อดู SQLCommand ที่มันสร้างขึ้นมาได้แบบนี้ครับ

SELECT ( SELECT COUNT(*) FROM [dbo].[ARPendingBill] AS [t2] WHERE ([t2].[BatchDate] = [t1].[BatchDate]) AND ([t2].[BatchNo] = [t1].[BatchNo]) AND ([t1].[COCode] = [t2].[COCode]) AND ([t1].[ARBook] = [t2].[ARBook]) AND ([t1].[BatchDate] = [t2].[BatchDate]) AND ([t1].[BatchNo] = [t2].[BatchNo]) AND ([t2].[COCode] = @p0) AND ([t2].[ARBook] = @p1) ) AS [BillCount], [t1].[BatchDate], [t1].[BatchNo], [t1].[ARBook], [t1].[COCode] AS [CoCode] FROM ( SELECT [t0].[COCode], [t0].[ARBook], [t0].[BatchDate], [t0].[BatchNo] FROM [dbo].[ARPendingBill] AS [t0] WHERE ([t0].[COCode] = @p0) AND ([t0].[ARBook] = @p1) GROUP BY [t0].[COCode], [t0].[ARBook], [t0].[BatchDate], [t0].[BatchNo] ) AS [t1]

รู้สึกว่ามันช่างเข้าใจยากพิกล ส่วนหนึ่งเป็นเพราะยังไม่ชินด้วยครับ และควรจะต้องไปจัด Layout ใหม่ให้อ่านง่ายๆก่อน แต่ผลลัพท์ได้ออกมาเหมือนกัน

ลองเขียนโดยใช้ Lambda Expression ครับ

Dim b = dc.ARPendingBills.Where( _
Function(a) a.COCode = coCode AndAlso a.ARBook = arBook).GroupBy( _
Function(g) New With {.coCode = g.COCode, .arBook = g.ARBook, _
.batchDate = g.BatchDate, .batchNo = g.BatchNo}).Select( _
Function(c) New ARPendingBatch With {.ARBook = c.Key.arBook, _
.CoCode = c.Key.coCode, .BatchDate = c.Key.batchDate, _
.BatchNo = c.Key.batchNo, .BillCount = c.Count( _
Function(k) k.BatchDate = c.Key.batchDate AndAlso _
k.BatchNo = c.Key.batchNo)})


ได้ผลลัพท์เหมือนกันครับ (ก็แหงละ ลอง debug ดู SQLCommand ก็เหมือนกับที่เขียนด้านบน)

รู้สึกขัดใจกับ SQLCommand ที่ Linq To SQL สร้างให้ ดังนั้นเลยเปลี่ยนมา ExecuteQuery ตรงๆแทน


Dim b = dc.ExecuteQuery(Of ARPendingBatch)( _
"SELECT cocode, arbook, batchdate, batchno, " & _
"BillCount=count(billRecId) " & _
"FROM arpendingbill & _
"WHERE cocode = '" & coCode & "' and arbook = '" & arBook & "' " & _
"GROUP BY cocode, arbook, batchdate, batchno")


มันไม่ Typed Save ครับ แต่ก็มีข้อดีคือ ผมไม่ต้องใช้เวลาเป็นชั่วโมงในการลองเขียน LINQ To SQl แล้ว debug ได้ SQLCommand ที่อ่านยาก และก็ต้อง Test เยอะเพื่อความมั่นใจ ดังนั้นช่วงนี้ถ้าเจอกรณีแบบนี้ก็จะใช้ ExecuteQuery ไปก่อน แล้วพอมีเวลาหัดให้คล่องๆแล้วค่อยกลับมาแก้ครับ (แต่มันก็อ่านยากเหมือนกันนะ programmer ที่ต้องมาแก้โค้ดนี้ภายหลัง ไม่แน่ใจว่าจะชอบแบบไหน ดังนั้นผมเลย Comment เก็บไว้ทั้ง 3 แบบเลย)

ว่าแต่โค้ด Linq To SQL ด้านบน สามารถเขียนได้ง่ายกว่านี้มั๊ยนะ

วันพุธที่ ๑๙ สิงหาคม พ.ศ. ๒๕๕๒

สร้าง DIV เพื่อบัง WebPage ขณะใช้งาน AJAX

ปกติเวลาพัฒนา Web Application ด้วย ASP.NET ผมมักจะใช้ ASP.NET AJAX ร่วมด้วยเสมอ และเมื่อมีการ PostBack หรือเรียกใช้ WebServices ก็จำเป็นต้องเขียน javascript เพื่อโชว์รูปว่ากำลัง Update และ disable controls ต่างๆ

พอดีได้เข้าไปดูเวบไหนจำไม่ได้แล้วครับ เค้าน่าจะสร้าง Div ที่กำหนด OPACITY ไว้เพื่อให้เป็นกรอบจางๆสำหรับบังหน้าเวบขณะ process งาน ก็สะดวกดีนะครับ เพราะว่าถ้าในหน้ามี control หลายๆตัว จะไปนั่งไล่ disable ทีละตัวก็จะถึกไปหน่อย ยิ่งถ้าใช้พวก asp:menu หรือ Master Page ด้วยแล้ว เสียเวลาน่าดูชม สร้าง Div มาบังมันซะเลย เร็วและได้ผลลัพท์ไม่แตกต่างกัน (อาจจะดีกว่าด้วยครับ) เลยมาลองทำดูบ้างครับ


<div id="divProgress">
<div id="divBackground"style="z-index:999; position:absolute;top:0px;left:0px; background-color:#eeeeee;filter:alpha(opacity=40); opacity:0.4;-moz-opacity:0.4"></div>
<div id="divImg" style="z-index:1000;position:absolute;border: 1px solid #800000; top:340px;left:200px;width:300px;text-align:center;background-color:#FFDECE">
<img alt="Processing" src="../Images/loading.gif" /><asp:label ID="lblLoading" CssClass ="textNormal" Text="Preparing reports..." runat="server"></asp:label>
</div>
<div>


โดยปกติก็จะกำหนด visibility เป็น hidden ไว้ก่อน พอมีการเรียกใช้ AJAX ก็จะเปลี่ยนเป็น visible มาดูโค้ดกันครับ


<script type="text/javascript" id="MSAJAX">
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_beginRequest(onBeginRequest);
prm.add_endRequest(onEndRequest);

function onBeginRequest(sender, e) {
ShowProgress(true);
}

function onEndRequest(sender, e) {
ShowProgress(false);
}
function ShowProgress(isShow) {
if(isShow == true){
$get("divProgress").style.visibility = "visible";
var dv = $get("divBackground")
dv.style.width = "1200px";
dv.style.height = "1000px";
}else{
$get("divProgress").style.visibility = "hidden";
}
}
</script>


เนื่องจากผมใช้ PageRequestManager ของ ASP.NET AJAX เป็นตัวสั่ง ShowProgress ดังนั้น Script นี้ต้องวางไว้ข้างล่าง ScriptManager control นะครับ

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

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 ตัวนี้เลย

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

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

วิเคราะห์ WebSite ด้วย Google Analytics

หลังจากเขียน Blog มาซักพัก เริ่มอยากรู้ว่ามีคนเข้าชมเยอะสักแค่ไหนครับ ก่อนหน้านี้เขียนที่ Bloggang.com จะมีตัวนับสถิติมาให้ (แต่ข้อมูลผิดบ่อยๆนะ) เลยลอง search ในเนตหาตัวนับสถิติฟรี (เน้น) มาลองใช้ครับ ก็เลยมาสะดุดที่ Google Analytics ครับ รู้สึกว่าเครื่องมือทันสมัยแถมมีเยอะมากมายครับ

ลองเข้าไปลงทะเบียนที่ http://www.google.co.th/analytics

ข้อมูลเพิ่มเติม ละเอียดน่าสนใจครับ (ภาษาไทย)
Google Analytics คืออะไร, Tung's Blog
มารู้จัก Google Analytics กันเถิด [ตอนที่ 1], โลกของคนไอที

ตอนนี้ยังใช้งานไม่ค่อยเป็น ต้องลองศึกษาอีกซักพักครับ หวังว่าคงมีคนเข้ามาชม blog เยอะๆ จะได้มีข้อมูลมาลองศึกษาเยอะๆตามไปด้วยครับ

วันพฤหัสบดีที่ ๖ สิงหาคม พ.ศ. ๒๕๕๒

jQuery ลอง Selectors หลายๆแบบ

วันก่อนคุณ Nine ได้ post ถามเรื่องการใช้ jQuery สำหรับค้นหาข้อมูลเพื่อซ่อน table โดยหน้า html ได้มาจาก MS SharePoint ครับ ทีแรกผมเข้าใจว่า html จะมี form เป็น pattern เดียวกัน คือมีจำนวนแถวและคอลัมภ์เท่ากันเสมอ ดังนั้นผมจึงเขียนโค้ดดังนี้ครับ


var isCompleted = $("table.ms-formtable tr:eq(11) td:eq(1)").html();
if (isCompleted!=null&&isCompleted.indexOf("1 ")>-1) {
$('table.ms-toolbar').hide();
}


คือใช้ Selectors แบบ ancestor descendant ร่วมกับ Filters หรืออธิบายคือ สั่งให้ jQuery ไปหา element ที่ class =table.ms-formtable (ซึ่งเป็น table) จากนั้นก็ไปที่ row ที่ 12 (index=11) ของตารางนี้ และไปที่ column ที่ 2 (index=1) แล้วไปดึงเอา innerHTML ออกมาซะ เพื่อตรวจสอบเงื่อนไขสำหรับ hide ตาราง

แต่ปรากฎว่าแต่ละหน้า อาจมี row ไม่เท่ากันก็ได้ ดังนั้นคุณ Nine จึงสั่งไปที่ table เดียวกัน (class =table.ms-formtable) แล้วใช้ jQuery Object Accessors ที่ชื่อว่า each ในการวนลูปทีละแถว เพื่อตรวจสอบข้อมูลทุกแถวครับ


$(document).ready(function($) {
var rows = $('table.ms-formtable tr').each(function() {
var row = $(this);
var columns = row.children('td');
if(columns.eq(1).html().indexOf('ProcessCompleted')>-1 && columns.eq(1).html().indexOf('1 ')>-1)
$('table.ms-toolbar').hide();
});
});


จริงๆแล้ว เราควรจะใช้ ID เป็น Selector จะได้ง่าย แต่เพราะว่า MS SharePoint มันดันสร้าง ID ซ้ำกันเยอะแยะนั่นสิ ทำให้เวลาเราใช้ ID Selector แล้วมันจะได้เฉพาะ element แรกเท่านั้นนั่นเอง (เพราะ ID ควรมีแค่ 1 ID ต่อ 1 page)
ใครสนใจจะดูโค้ดเต็มๆลองไปดูที่

jQuery ค้นหาค่า และซ่อน table ช่วยหน่อยคร้าบ
http://greatfriends.biz/?110667

วันจันทร์ที่ ๓ สิงหาคม พ.ศ. ๒๕๕๒

VB.NET Coding Convention

ผมได้เข้าไปค้นหาข้อมูลใน msdn ก็เลยเจอหัวข้อ VB.NET Coding Convention อยู่ภายใน VS2008 พออ่านๆดูก็พบว่ามีหลายอย่างที่ไม่ได้ทำตาม Convention ของ MS ครับ ก่อนอื่นมาดูก่อนว่าถ้าเราเขียนโค้ดตาม guide line นี้จะมีข้อดีอะไรบ้าง

  • Coding conventions create a consistent look to the code, so that readers can focus on content, not layout.

  • Conventions let the readers understand the code more quickly, because it allows them to make assumptions based on previous experience.

  • Conventions make copying, changing, and maintaining the code easier.

  • Conventions demonstrate Visual Basic "best practices."


  • ทีนี้มาลองดูหัวข้อที่น่าสนใจครับ

    Naming Convention

  • Do not use "My" or "my" as part of a variable name. This creates confusion with the My objects. ข้อนี้ผิดบ่อยครับ บางทีคิดไม่ออกชอบตั้งชื่อตัวแปรมี My นำหน้า


  • Layout Convention
  • Use only one statement per line. Do not use the Visual Basic line continuation character (:). ข้อนี้ก็ทำบ้าง แต่ส่วนใหญ่จะใช้กับ 2 statements สั้นๆ โดยเฉพาะอย่างยิ่งเขียนใน SELECT CASE หรือ IF THEN STATEMENT และพอทำแล้วผมว่าโค้ดอ่านง่ายนะ

  • Use only one declaration per line.

  • Add at least one blank line between method and property definitions. ผมทำตามนี้อยู่แล้ว เห็นโค้ดบางคนไม่เว้นบรรทัด อ่านแล้วลายตาครับ


  • Language Guidelines
    String Data Type
  • Use & to concatenate strings: เมื่อก่อนก็เขียนแบบนี้ แต่ตอนหลังเปลี่ยนมาใช้ String.Concat() นะ ว่าแต่ใช้แบบไหนดีเนี่ย

  • For appending strings in loops, use the StringBuilder object: อันนี้แน่นอน ใช้ StringBuilder ดีกว่าเห็นๆ


  • Type Inference
  • Take advantage of type inference for local variables: สำหรับผมกำหนด Type ไปเลยอ่านง่ายกว่า Type inference นะ อันนี้ขอไม่เปลี่ยน

  • Use Type Inference for Loop Variables in For or For Each Statements
    Allow type inference to determine the type of the loop range variable. เหมือนข้อเมื่อกี้ ส่วนตัวไม่ชอบ Type inference (แต่ก็ใช้บ้าง) ถ้าเป็นไปได้จะหลีกเลี่ยง มันเหมือนคนพูดสั้นๆให้เข้าใจเอาเอง


  • Unsigned Data Type
  • Use Integer rather than unsigned types unless memory is at a premium. อันนี้รวมถึงไม่ควรใช้ Int16, Int64, Short, Long ด้วย


  • Use the MsgBox Function
  • Use MsgBox instead of MessageBox.Show or Console.WriteLine. In environments that do not support the MsgBox function, such as Silverlight, use an appropriate alternative. อืม เคยอ่านมาว่าให้ใช้ MessageBox.Show แต่จริงๆชอบ MsgBox มากกว่า แบบนี้ก็กลับมาใช้แบบเดิมดีกว่าครับ


  • New Keyword
  • Use short instantiation:

  • Use object initializers for new objects instead of the parameterless constructor:


  • ยังมีอีกพอสมควรครับ สำหรับคนที่สนใจเข้าไปศึกษาได้ที่ http://msdn.microsoft.com/en-us/library/h63fsef3.aspx ครับ

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

    jQuery กับการกำหนด Mouse Hover ใน GridView

    อ้างอิงจากคำถามที่ GreatFriends ครับ
    ขอคำแนะนำเรื่อง GridView กับ Jquey จ้า
    http://greatfriends.biz?110946

    เจ้าของกระทู้ต้องการทำ Highlight สีของแถวใน GridView เมื่อเอา mouse ไปชี้ที่แถวนั้นๆครับ และพอกด Select ใน GridView ก็ให้นำข้อมูลมาใส่ textbox

    จริงๆแล้วถ้าข้อมูลที่จะใส่ TextBox อยู่ใน GridView ทั้งหมด เราสามารถใช้ jQuery ดึงข้อมูลของแถวมาใช้ได้เลย แต่ผมสมมติว่าเราต้องการข้อมูลอื่นๆ โดยอ้างจาก Id ของข้อมูลที่เลือกละกันครับ ดังนั้นผมก็ต้อง Post back กลับไปเพื่อหาค่ามาใส่ใน textbox ก็เลยทำหน้า page มาลองทดสอบดูครับ

    โค้ดส่วน Head ครับ Style ผม copy มาจากเจ้าของกระทู้เลย ส่วน jQuery ผมใช้ที่อาจารย์สุเทพแก้ไขให้ นำมาแยกออกจาก $document.ready() เป็น LoadHandle function ครับ

    <head runat="server">
    <title>Test</title>
    <style>
    .trMouseClick {
    background-color: Transparent;
    color: #A4D1A1 !important;
    text-decoration: none;
    }

    .trMouseOver {
    background-color: #F2FEF2 !important;
    color: #A4D1A1 !important;
    text-decoration: none;
    }

    .trMouseOver a {
    background-color: #F2FEF2 !important;
    color: #A4D1A1 !important;
    }
    </style>
    <script type="text/javascript" src="../Scripts/jquery-1.3.2-vsdoc.js"></script>
    <script type="text/jscript">

    $(document).ready(LoadHandle);

    function LoadHandle() {
    $("tr:gt{0}").hover(function() {
    $(this).addClass("trMouseOver");
    }, function() {
    $(this).removeClass("trMouseOver");
    });
    $("tr:gt{0}").click(function() {
    $(this).addClass("trMouseClick").siblings().removeClass("trMouseClick");
    });
    }

    </script>
    </head>


    โค้ดส่วน Form tag (ใน Body)

    <asp:ScriptManager ID="ScriptManager1" runat="server">:
    </asp:ScriptManager>:
    <div id = "main" style="height:700px;z-index:-10">:
    <div id="text" style="position:absolute">:
    <asp:UpdatePanel ID ="UpdatePanel1" runat="server" UpdateMode="Conditional" RenderMode="Block">:
    <ContentTemplate>:
    <div id="Update" style="position:absolute;top:50px; width:700px">:
    <asp:GridView ID="gvInvoice" runat="server" AllowPaging="True"
    AutoGenerateColumns="False" BorderColor="Gainsboro" BorderStyle="None"
    BorderWidth="1px" CellPadding="0" GridLines="Horizontal"
    AutoGenerateSelectButton="True" DataKeyNames="ItemId">:
    <Columns>:
    <asp:TemplateField SortExpression="ITEMID">:
    <ItemStyle Width="0px">:</ItemStyle>:
    <ItemTemplate>:
    <asp:HiddenField id="hdnRecId" runat="server" Value='<%# Bind("ITEMID") %>:'>:</asp:HiddenField>:
    </ItemTemplate>:
    </asp:TemplateField>:
    <asp:TemplateField HeaderText="Item No">:
    <HeaderStyle Width="40px" HorizontalAlign="center">:</HeaderStyle>:
    <ItemStyle HorizontalAlign="center" />:
    <ItemTemplate>:
    <asp:Label id="lblItemNo" runat="server" Text='<%# Eval("ORDERNO") %>:' >:</asp:Label>:
    </ItemTemplate>:
    </asp:TemplateField>:
    <asp:TemplateField HeaderText="Invoice Date">:
    <ItemStyle Width="100px">:</ItemStyle>:
    <HeaderStyle HorizontalAlign="Center">:</HeaderStyle>:
    <ItemTemplate>:
    <asp:Label style="TEXT-ALIGN: center" id="Label2" runat="server" Width="80px" Text='<%# Bind("InvoiceDate", "{0:dd/MM/yyyy}") %>:'>:</asp:Label>:
    </ItemTemplate>:
    </asp:TemplateField>:
    <asp:TemplateField HeaderText="Product Code">:
    <ItemStyle Width="60px">:</ItemStyle>:
    <HeaderStyle HorizontalAlign="Center">:</HeaderStyle>:
    <ItemTemplate>:
    <asp:Label style="TEXT-ALIGN: center" id="Label3" runat="server" Width="50px" Text='<%# Bind("ProductCode") %>:'>:</asp:Label>:
    </ItemTemplate>:
    </asp:TemplateField>:
    <asp:TemplateField HeaderText="Amount (THB)">:
    <ItemStyle Width="80px">:</ItemStyle>:
    <HeaderStyle HorizontalAlign="Center">:</HeaderStyle>:
    <ItemTemplate>:
    <asp:Label style="TEXT-ALIGN: right" id="Label5" runat="server" Width="70px" Text='<%# Bind("Amount","{0:#,##0.00}") %>:'>:</asp:Label>:
    </ItemTemplate>:
    </asp:TemplateField>:
    <asp:TemplateField ShowHeader="False">:
    <ItemStyle Width="40px">:</ItemStyle>:
    <ItemTemplate>:
    <asp:ImageButton ID="btnEdit" runat="server" CommandName="Select" ImageUrl="~/Images/icon_edit.gif" ToolTip="Edit" />:
    </ItemTemplate>:
    </asp:TemplateField>:
    </Columns>:
    <RowStyle >:</RowStyle>:
    <SelectedRowStyle>:</SelectedRowStyle>:
    <PagerStyle HorizontalAlign="Right" VerticalAlign="Top">:</PagerStyle>:
    <HeaderStyle >:</HeaderStyle>:
    <AlternatingRowStyle >:</AlternatingRowStyle>:
    </asp:GridView>:
    </div>:
    <div id="dvContent" style="position:absolute;top:400px">:
    <asp:TextBox ID="txtId" runat="server" style="width:100px" MaxLength="6">:</asp:TextBox>:
    <asp:TextBox ID="txtDescription" runat="server" style="width:250px">:</asp:TextBox>:
    <asp:TextBox ID="txtAmount" runat="server" style="width:250px">:</asp:TextBox>:
    </div>:
    </ContentTemplate>:
    </asp:UpdatePanel>:
    </div>:
    </div>:


    โค้ดส่วน Code Behind ครับ

    Imports jnithi.Class.Invoice

    Partial Class InvoiceTest
    Inherits System.Web.UI.Page
    #Region "Local fields"

    Private _inv As List(Of Entity.Invoice) = Nothing

    #End Region

    Private Sub GetInvoiceByInvoiceNo()
    If _inv IsNot Nothing Then
    _inv.Clear()
    _inv = Nothing
    End If
    _inv = Manager.Invoice.GetInvoiceByInvoiceNo("Test1")
    Session("Invoice") = _inv
    End Sub

    Private Sub BindGridView()
    gvInvoice.DataSource = _inv
    gvInvoice.DataBind()
    End Sub

    Protected Sub form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles form1.Load

    If Not IsPostBack Then
    GetInvoiceByInvoiceNo()
    Else
    _inv = Session("Invoice")
    End If
    BindGridView()
    End Sub

    Protected Sub gvInvoice_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles gvInvoice.SelectedIndexChanged
    Dim id As Integer = CType(gvInvoice.SelectedValue, Integer)
    Dim item As Entity.Invoice = _inv.Find(Function(i) i.ItemID = id)
    Me.txtId.Text = id
    Me.txtDescription.Text = item.ItemWorkDesc
    Me.txtAmount.Text = item.LCCost.ToString("#,##0.00")
    ScriptManager.RegisterStartupScript(Page, Page.GetType, "GVDataBound", "LoadHandle();", True)
    End Sub


    ผลการทดสอบครับ


    สรุป
    1. ผมแยก LoadHandle Function ออกมาจาก $document.ready() เพื่อที่ว่าให้ function นี้เป็น Event Handler เมื่อทำการ Load ข้อมูลฝั่ง client เสร็จครับ
    2. ใน GridView_SelectedIndexChanged ผมทำการ register script เพื่อให้เรียก LoadHandle functionให้ทำงาน เมื่อ UpdatePanel ทำการ Update ข้อมูลเสร็จแล้ว
    3. ถ้าไม่ใช้ ScriptManager.RegisterStartupScript ซึ่งเป็นการสั่งที่ฝั่ง Server เราสามารถใช้ PageRequestManager (เขียน javascript) แทนได้ครับ

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

    ลองสร้าง Extension Method ใน VB9

    ถ้าพูดถึง Visual Studio 2008 แล้ว พระเอกที่มีคนกล่าวขวัญถึงมากที่สุดไม่น่าจะมี feature ไหนเกิน LINQ ไปได้ครับ หลายๆคนคงได้นำ LINQ ไปใช้ในงานจริงกันบ้างแล้ว ส่วนตัวผมเองตอนที่ VS2008 ยังอยู่ในช่วง Beta และ CTP ก็มีคนพูดถึง LINQ ให้ฟังหลายคน แต่ไม่ค่อยได้สนใจครับ แต่พอมาลองใช้งานดูแล้วต้องบอกว่าเยี่ยมมากครับ

    สำหรับเบื้องหลังของ LINQ นั้น เทคโนโลยีสำคัญอย่างหนึ่งก็คือ Extension Method นั่นเองครับ เพราะด้วย Extension Method ทำให้เราสามารถใช้ LINQ ได้ โดยที่ runtime ยังเป็น v2.0.50727 เหมือนเดิม ที่เป็นแบบนี้เพราะว่า Extension Method นั้นทำให้เราสามารถเพิ่ม behavior ให้กับ Type ของเราได้โดยไม่จำเป็นต้องมี source code เดิมครับ

    สมมติผมต้องการเพิ่มความสามารถให้กับ String Type สำหรับงาน Web Application ถ้าเป็นเมื่อก่อนก็อาจจะสร้าง sealed class ขึ้นมาโดยให้มี static method ที่ต้องการ (เพื่อที่ว่าจะได้ไม่ต้อง new object เวลาใช้งาน) ลองดูตัวอย่างครับ


    Public NotInheritable Class MyExtension
    Public Shared Function ApplyStyle(ByVal s As String) As String
    Return String.Format("<span style='FONT-WEIGHT: bold;font-family:Verdana;color:red;'>{0}</span>", s)
    End Function

    Public Shared Function WriteLine(ByVal s As String) As String
    Return String.Format("{0:#,##0}
    ", s)
    End Function
    End Class


    แต่ถ้าเราสร้าง Extension Method ใน VB.NET นั้น เราจำเป็นต้องสร้างใน Module ครับ (C# จะสร้างใน Static Class ได้เลย แต่ VB.NET ไม่มี Static Class ครับ คือไม่สามารถใส่ "shared" keyword หน้า Class) และก็ใส่ Extension Attribute ด้วยครับ ลองมาดูโค้ดกัน


    Public Module MyExtension2
    <System.Runtime.CompilerServices.Extension()> _
    Public Function EXApplyStyle(ByVal s As String) As String
    Return String.Format("<span style='color:Red;font-weight:bold;font-family:Verdana'>{0}</span>", s)
    End Function

    <System.Runtime.CompilerServices.Extension()>
    Public Function EXWriteLine(ByVal s As String) As String
    Return String.Format("{0:#,##0} <br />", s)
    End Function
    End Module


    คราวนี้มาดูเวลาการใช้งานกันครับ


    Partial Class ModuleDisbursement_DBItemTest
    Inherits System.Web.UI.Page

    Protected Sub form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles form1.Load
    Dim str As String = "Hello world."
    Response.Write(MyExtension.WriteLine(MyExtension.ApplyStyle(str)))
    Response.Write(str.EXApplyStyle().EXWriteLine())
    End Sub

    End Class


    Reponse.Write บรรทัดแรก ใช้ Static Method ส่วนบรรทัดที่สองใช้ Extension Method ครับ สังเกตุได้ว่าเมื่อเราใช้ Extension Method แล้วเราจะเห็นว่า String Type ของเรามี method เพิ่มขึ้นมาอีก 2 ตัวครับ สะดวกดีจริงๆเลย

    ปล. จากการ search ใน google พบว่า

    1. มี developer คนหนึ่งต้องการเพิ่ม method ใน BitMap Type แต่ทำไม่ได้ เพราะเป็น Sealed Class ครับ บางคนแนะนำให้สร้าง BitMap Type ของตัวเอง (ซึ่งเสียเวลาและต้องตามไปแก้โค้ดอีกเยอะเลย) และก็มีคนแนะนำให้ใช้ Extension Method มาแก้ปัญหานี้ครับ

    2. Microsoft MVP ชาวต่างชาติคนหนึ่งเขียนใน blog ว่า Module นี่ไง Static Class ของ VB.NET

    3. Microsoft MVP อีกคนเขียนอีก blog ว่า อยากให้มี Static Class ใน VB10 เพราะว่า Module มันยังไม่ใช่ Static Class จริงๆ (เค้าอธิบายว่ามันไม่เหมือนกันเพราะ Reflection - อืมม ยังไม่ค่อยเข้าใจครับ)



    เพิ่มเติมมีคุยกันเรื่องนี้ที่ greatfreinds ครับ
    มีวิธีที่จะใช้ Extension method โดยไม่ผ่าน Module ไหมครับ

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

    parseInt ทำงานผิดพลาด

    วันนี้ user โทรมาแจ้งว่ามี bug เกิดขึ้น คือเมื่อเลือกวันที่จากปฏิทินแล้วกลายเป็นวันอื่นไป ลองไปไล่โค้ดดูปรากฏว่าผมไปกำหนด onblur event ของ textbox ให้ไปตรวจสอบวันที่และทำการจัด format เป็น dd/MM/yyyy

    ปรากฏว่าโค้ดส่วนที่มี bug คือคำสั่ง parseInt ครับ


    function DMY2Date(dmyStr) {
    var splitDateStr=dmyStr.split('/');
    return new Date(splitDateStr[2],String(parseInt(splitDateStr[1])-1),splitDateStr[0]);
    } //end function


    คือผมสั่ง parseInt(splitDateStr[1]) เพื่อหาค่าเดือนเป็น Integer แต่ปรากฏว่า พอ split data string ออกมา มันได้ค่า "08" ซึ่งใน parseInt function มันมีเงื่อนไขว่าถ้าขึ้นต้นด้วย 0 แปลว่าเป็นเลขฐาน 8 ครับ พอลอง debug ดูปรากฏว่า parseInt("08") ได้ผลลัพธ์เป็น 0 เพราะเลขสูงสุดของเลขฐาน 8 คือ 7 ครับ ถ้า parseInt("010") ถึงจะได้ค่า 8

    ลองดู Reference ที่ www.w3schools.com กันครับ

    Syntax
    parseInt(string, radix)

    If the radix parameter is omitted, JavaScript assumes the following:
    • If the string begins with "0x", the radix is 16 (hexadecimal)

    • If the string begins with "0", the radix is 8 (octal). This feature is deprecated
    • If the string begins with any other value, the radix is 10 (decimal)



    ก็เลยไปแก้โดยกำหนด radix = 10 เข้าไปด้วย เพื่อบอกว่าเราต้องการ parseInt แบบเลขฐาน 10 ไม่ใช่เลขฐาน 8 นะเฟ้ย


    function DMY2Date(dmyStr) {
    var splitDateStr=dmyStr.split('/');
    return new Date(splitDateStr[2],String(parseInt(splitDateStr[1],10)-1),splitDateStr[0]);
    } //end function


    เสร็จแล้วก็เลยต้องไปไล่นั่งตรวจสอบดูว่ามีการใช้ parseInt function ตรงไหนบ้าง เพื่อที่จะไปใส่ radix ให้หมดเพื่อป้องกันปัญหาครับ

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

    javascript กำหนด textbox ให้รับค่า time (hh:mm)

    สมมติผมมี textbox id="txtTime" และต้องการให้ user สามารถคืย์ข้อมูลเฉพาะเวลา โดยให้ format = hh:mm (23:59) ครับ
    ก่อนอื่นก็ต้อง register event handler ให้กับ textbox ก่อน


    $(function() {
    $("#txtTime").keypress(function(e) {txtTime_OnKeyPress(this,e)});
    $("#txtTime").blur(function() { ValidateTime(this) });
    });


    จะเห็นว่าผมสร้าง function สำหรับจัดการ keypress event กับ blur event ให้ textbox ครับ สำหรับ txtTime_OnKeyPress สำหรับกำหนดให้ user สามารถคีย์เฉพาะ d(2).d(2) คือตัวเลขสองตัว ตามด้วยจุดและตัวเลขอีกสองตัว ส่วน ValidateTime สำหรับตรวจสอบว่าค่าที่คีย์ตรงกับ format ที่ต้องการหรือเปล่า


    function txtTime_OnKeyPress(sender, e) {
    var myTime = sender.value;
    if(myTime.length>4) {
    event.keyCode = 0;
    return false;
    }
    var charCode = (e.which) ? e.which : e.keyCode
    switch (myTime.length) {
    case 0:
    if (charCode < 48 || charCode > 50) event.keyCode = 0;
    break;
    case 1:
    if (charCode<48||(myTime == 2&&charCode > 51)) event.keyCode = 0;
    break;
    case 2:
    if (charCode != 46) event.keyCode = 0;
    break;
    case 3:
    if (charCode<48||charCode > 53) event.keyCode = 0;
    break;
    default:
    if (charCode < 48 || charCode > 57) event.keyCode = 0;
    }
    }

    function ValidateTime(sender) {
    if (sender.value.length == 0) return false;
    var regEx = /^(\d{2}).(\d{2})$/;
    var arrMatch = sender.value.match(regEx);
    if (arrMatch == null) {
    alert("Invalid time.");
    sender.value = "";
    return false;
    }
    var hh = arrMatch[1];
    var mm = arrMatch[2];
    if (hh >= 24 || mm >= 60) {
    alert("Invalid time.");
    sender.value = "";
    return false;
    }
    return true;
    }


    ทดสอบกับ IE ได้ผล OK ครับ ใครจะนำไปใช้ก็ทดสอบกันก่อนนะ ว่างๆผมจะมาแก้ function ให้สามารถกำหนด format ของเวลาได้ เผื่อบางทีต้องใช้แบบ 11:59 A.M. แต่ตอนนี้ใช้แค่นี้ก่อนละกันครับ

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

    Visual Studio 2008 Samples

    ผมลองคุยกับหลายๆคน ส่วนใหญ่ไม่ทราบว่าทาง Microsoft ได้ทำการเผยแพร่ "Visual Studio 2008 Samples" ให้ดาวน์โหลดมาลองกันครับ ผมเองได้ลองดาวน์โหลด Visual Studio 2008 RTM Samples มาลองดูตัวอย่าง (โดยเฉพาะ LINQ เปิดอ่านดูบ่อยพอสมควร)

    นอกจาก VS2008 Samples แล้วยังมี Code Samples อื่นๆเลือกให้ลองดาวน์โหลดมาศึกษาเยอะแยะมากครับ

    สนใจเข้าไปดาวน์โหลดได้ที่ Visual Studio 2008 Samples

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

    VBScript ดึงชื่อและอีเมลล์จาก Active Directory

    เมื่อหลายวันก่อนผมต้องทำการดึง ชื่อพนักงาน และ email address ที่เก็บไว้ใน AD ออกมาเพื่อตรวจสอบ และ Import เข้า Database สำหรับไว้ใช้ในการทำ mailling list ใช้ภายในครับ ลอง search ดูใน google ก็พบว่ามีหลายวิธี และที่ปิ๊งที่สุดก็คือการเขียน VBScript เพื่อไปดึงข้อมูลจาก LDAP มาเขียนใส่ text file ครับ


    Dim FileSystem
    'Initialize global variables
    Set FileSystem = WScript.CreateObject("Scripting.FileSystemObject")
    Set OutPutFile = FileSystem.CreateTextFile("email.txt", True)
    Set oContainer=GetObject("LDAP://OU=Department Users,DC=MyDomainName,DC=com")
    'Enumerate Container
    EnumerateUsers oContainer
    'Clean up
    OutPutFile.Close
    Set FileSystem = Nothing
    Set oContainer = Nothing
    WScript.Echo "Finished"
    WScript.Quit(0)

    Sub EnumerateUsers(oCont)
    Dim oUser
    For Each oUser In oCont
    Select Case LCase(oUser.Class)
    Case "user"
    OutPutFile.WriteLine oUser.givenname & " " & oUser.sn & vbtab & oUser.mail
    Case "organizationalunit" , "container"
    EnumerateUsers oUser
    End Select
    Next
    End Sub


    ถ้าอยากรู้ว่า oUser (User Class) มี property อะไรบ้าง ดูตามนี้เลยครับ ไม่แน่ใจว่าครบหรือเปล่านะ


    '// User Property
    'SamAccountName = oUser.samAccountName
    'Cn = oUser.CN
    'FirstName = oUser.GivenName
    'LastName = oUser.sn
    'initials = oUser.initials
    'Descrip = oUser.description
    'Office = oUser.physicalDeliveryOfficeName
    'Telephone = oUser.telephonenumber
    'EmailAddr = oUser.mail
    'WebPage = oUser.wwwHomePage
    'Addr1 = oUser.streetAddress
    'City = oUser.l
    'State = oUser.st
    'ZipCode = oUser.postalCode
    'Title = oUser.Title
    'Department = oUser.Department
    'Company = oUser.Company
    'Manager = oUser.Manager
    'Profile = oUser.profilePath
    'LoginScript = oUser.scriptpath
    'HomeDirectory = oUser.HomeDirectory
    'HomeDrive = oUser.homeDrive
    'AdsPath = oUser.Adspath
    'LastLogin = oUser.LastLogin

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

    jQuery ทำ highlight ขัอมูลใน GridView

    ผมเคยทำโปรแกรมค้นหาข้อมูลจากไฟล์เอกสารที่เก็บไว้ใน File Server โดย user สามารถใส่ key word ได้หลายตัว จากนั้นโปรแกรมจะไปค้นหาว่ามีไฟล์ไหนบ้างที่มีคำหรือประโยคตรงกับเงื่อนไขที่ต้องการ แล้วนำข้อมูลรวมถึง url มาใส่ใน GridView
    ทีนี้ user ก็ต้องการให้โปรแกรมสามารถ highlight คำหรือประโยคใน GridView ที่ตรงกับ key word ด้วย ซึ่งก่อนหน้านี้ผมก็เขียน javascript วนลูปเพื่อทำ highlight ซึ่งโค้ดก็ยาวพอสมควร วันนี้จะลองมา modify โดยใช้ jQuery มาช่วยครับ โดยผมจะเพิ่ม Custom Attribute ให้กับ Control ที่ต้องการจะไปทำ highlight ครับ ซึ่งในแต่ละแถวของ GridView จะให้ไป highlight ที่ label 2 ตัว

    <asp:TemplateField HeaderText="Title" HeaderStyle-Width="400px" HeaderStyle-HorizontalAlign="Left">
    <ItemTemplate>
    <asp:Label ID="lblDocTitle" runat="server" ForeColor="Blue" style="font-size: 10pt" Highlight="true" Font-Bold="False"><%# eval("DocTitle") %></asp:Label>
    </ItemTemplate>
    </asp:TemplateField>
    <asp:TemplateField HeaderText="Abstract" HeaderStyle-Width="300px" HeaderStyle-HorizontalAlign="Left">
    <ItemTemplate>
    <asp:Label ID="lblAbstract" runat="server" Highlight="true" ><%# eval("Abstract") %></asp:Label>
    </ItemTemplate>
    </asp:TemplateField>

    เนื่องจากผมใช้ ScriptManager และ UpdatePanel ด้วย จากบทความเก่าผมใช้วิธีเขียนคำสั่ง jQuery ที่ code behind แล้วสั่ง ScriptManager ให้ Register Script ให้ ตามตำราเรียกวิธีนี้ว่าเป็น Server-centric แต่คราวนี้เราจะมาลองทำ Client-centric ดูบ้าง แทนที่เราจะใช้ UpdateProgress Control ผมก็จะมาใช้ PageRequestManager ในการแสดงรูปภาพแทน รวมถึงทำการ register event handler function ครับ

    <asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>

    <script type="text/javascript" id="MSAjax">
    var prm = Sys.WebForms.PageRequestManager.getInstance();
    prm.add_beginRequest(beginRequest);
    prm.add_pageLoaded(pageLoaded);

    function beginRequest(sender, args) {
    $("#divProgress").show();
    $("#MainGridView").html("");
    }

    function pageLoaded(sender, args) {
    //Add elements'event handler.
    $("txtFreeText").keypress(function(e) { txtFreeText_OnKeyPress(e); });
    $("btnSearch").click(function() { ValidateSearch(); });
    $("#divProgress").hide();

    HighlightSearch();
    }
    </script>

    คราวนี้มาลองดูโค้ดที่สำหรับทำ Highlight กันครับ

    <style type="text/css">
    .highlight{font-weight: bold; font-size: 16px; color: blue; background-color: papayawhip};
    </style>
    <script type="text/javascript" src="Scripts/jquery-1.3.2-vsdoc.js"></script>

      
    <script type ="text/javascript" id="MainScript">

    function trim(str) {
    return str.replace(/^\s*|\s*$/g, "");
    }

    function HighlightSearch(){
    var strSearch = $("#txtFreeText").val();
    if (trim(strSearch)=="") return false;

    strSearch = strSearch.replace(/\"/gi,"");
    strSearch = strSearch.replace(/\s*and\s*/gi, " ");
    strSearch = strSearch.replace(/\s*or\s*/gi," ");
    var arrSearch = strSearch.split(" ");

    $("[Highlight=true]").each(function() {
    var elm = $(this);
    var strAbstarct = elm.html();
    for (var i = 0; i < arrSearch.length; i++) {
    var RE = new RegExp(arrSearch[i], "gi");
    strAbstarct = strAbstarct.replace(RE, "<span class='highlight'>" + arrSearch[i] + "</span>");
    }
    elm.html(strAbstarct)
    });
    }

    </script>

    จากโค้ดด้านบน ผมใช้ Attribute Selector ในการเลือก element ที้ต้องการทำ Highlight มาจากนั้นมาวนลูปด้วย jQuery.each() ครับ แล้วก็ไปแก้ไข้ html ของ element ให้เพิ่ม span เพิ่อทำ highlight คำที่ต้องการ ลองรันดูได้ผลลัพธ์ตามต้องการครับ

    วันอาทิตย์ที่ ๑๒ กรกฎาคม พ.ศ. ๒๕๕๒

    IComparer กับ IComparable ใช้งานอย่างไร

    วันก่อนมีกระทู้ฮอตที่ greatfriends ครับ
    BLOG - Generic Function - MAX(Of T As IComparable(Of T))

    สรุปคือมีการสับสนระหว่าง IComparer กับ IComparable ครับ ซึ่งไม่ใช่เรื่องแปลกเพราะว่าในเวบต่างประเทศเอง ก็มีคำถามเรื่องนี้บ่อยๆ

    อาจารย์สุเทพสรุปดังนี้
    IComparer ใช้สำหรับ
    1. ไม่ต้องการ implement เข้าไปในคลาส เช่น มีรูปแบบการเปรียบเทียบไม่แน่นอน
    2. ไม่สามารถ implement เข้าไปในคลาสได้ เช่น ไม่มี source code

    ผมขอเพิ่มตามความเข้าใจนะครับ IComparable เป็น Interface สำหรับ Class ที่เราต้องการให้สามารถเปรียบเทียบได้ โดยมีลักษณะที่แน่นอน นั่นคือ Object ที่สร้างจาก class นี้ จะไปเปรียบเทียบกับ object อื่นที่สร้างจาก Class เดียวกัน

    Public Class Employee
    Implements IComparable(Of Employee)
    ...
    End Class


    ส่วน IComparer ชื่อก็ค่อนข้างสื่ออยู่แล้ว คือใช้สำหรับสร้าง Comparer Class หมายความว่าเราต้องการ Class สำหรับ Compare Object สรุปคือเป็น third party ที่ใช้เปรียบเทียบ object ที่1 และ object ที่2 ครับ

    Public Class EmployeeComparer
    Implements IComparer(Of Employee)

    Public Enum compareField
    age
    hiredDate
    joinedDate
    End Enum

    Private _compareField As compareField = compareField.age
    Private _sortDirection As System.ComponentModel.ListSortDirection = System.ComponentModel.ListSortDirection.Ascending

    Public Function Compare(ByVal x As [Class].Center.Employee, ByVal y As [Class].Center.Employee) As Integer Implements System.Collections.Generic.IComparer(Of [Class].Center.Employee).Compare

    If _sortDirection = ComponentModel.ListSortDirection.Ascending Then
    .....
    Else
    .....
    End If

    End Function

    End Class



    จากกระทู้มีการคุยกันถึงการใช้ Linq และการใช้ Lamda Expression ในการ Sort หรือเปรียบเทียบ Object ซึ่งจะสะดวกขึ้นมาก ลองเข้าไปอ่านกันดูครับ

    Dim employees As New List(Of BAS.Class.Center.Employee)
    employees.Add(New BAS.Class.Center.Employee With {.EmployeeID = "100", .NameEng = "tabd"})
    employees.Add(New BAS.Class.Center.Employee With {.EmployeeID = "200", .NameEng = "dss"})
    employees.Add(New BAS.Class.Center.Employee With {.EmployeeID = "300", .NameEng = "xxx"})
    employees.Add(New BAS.Class.Center.Employee With {.EmployeeID = "400", .NameEng = "yyy"})

    employees.Sort(Function(x, y) String.Compare(x.NameEng, y.NameEng)) '// Sort ascending
    employees.Sort(Function(x, y) String.Compare(y.NameEng, x.NameEng)) '// Sort descending

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

    การจัดการเอกสาร pdf ด้วย iTextSharp (2)

    หลังจากที่ผมได้นำโปรแกรมไปให้ user ทดสอบ ก็เลยได้ requirement เพิ่มเติมมาอีกครับ คือ user บางกลุ่มจะไม่สามารถ print pdf ออกไปที่ printer ได้ และห้าม save ด้วย

    หลังจากเข้าไปอ่านเอกสารของ iTextSharp ก็พบว่ามันสามารถทำได้ครับ โดยตัว pdf เองนั้นเราสามารถทำการกำหนด permission ได้ และ iTextSharp มี class ชื่อ PdfWriter ซึ่งเราสามารถทำการ Set Encryption เพื่อกำหนด permission ของ pdf ได้ ดังนั้นจากโค้ดเดิมที่ทำไว้ ผมก็ไปเพิ่มคำสั่ง setEncryption ก่อนสั่ง document.open ครับ

    เนื่องจากโค้ดเดิมผมใช้ PdfCopy object สำหรับสร้างเอกสาร pdf ใหม่ แต่ว่า PdfCopy มันไม่มีคำสั่ง setEncryption ครับ พอลองไป view object browser ดูก็พบว่าตัว PdfCopy นั้นมัน inherit มาจาก PdfWriter อีกที ดังนั้นเราสามารถ Casting ไปเป็น PdfWriter ได้ครับ จากนั้นก็สั่ง SetEncryption เพื่อกำหนด permission ว่าจะให้ print ได้หรือไม่ และก็สั่งซ่อน menu bar และ toolbar เพื่อที่ว่า user จะได้สั่ง save ไม่ได้ครับ



    Dim writer As PdfWriter = TryCast(copy, PdfWriter)
    'writer.SetEncryption(PdfWriter.STANDARD_ENCRYPTION_128, Nothing, Nothing, PdfWriter.ALLOW_SCREENREADERS) '// CANNOT PRINT
    writer.SetEncryption(PdfWriter.STANDARD_ENCRYPTION_128, Nothing, Nothing, PdfWriter.ALLOW_PRINTING) '// CAN PRINT
    writer.ViewerPreferences = PdfWriter.HideMenubar + PdfWriter.HideToolbar + PdfWriter.HideWindowUI

    document.Open()
    For i As Integer = 1 To reader.NumberOfPages
    Dim ipage As Integer = i
    If intIgnorePages Is Nothing OrElse intIgnorePages.Find(Function(c) c = ipage) = 0 Then
    copy.AddPage(copy.GetImportedPage(reader, ipage))
    End If
    Next

    document.Close()
    reader.Close()
    copy.Close()
    fs.Close()
    fs.Dispose()


    เรียบร้อยครับ แหมมันช่างเยี่ยมยอดจริงๆ

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

    เขียนโปรแกรมจัดการเอกสาร pdf ด้วย iTextSharp

    เมื่อไม่นานไม่นี้ผมได้รับ requirement ให้โปรแกรมที่พัฒนาขึ้นสามารถค้นหาและแสดงเอกสาร pdf โดยมีฟังก์ชันพิเศษ นั่นคือให้สามารถตัดหน้าบางหน้าของเอกสาร pdf ออก สำหรับ user บางกลุ่ม เช่นตัดหน้าสุดท้ายออก ถ้า user เป็นพนักงานของแผนก xxx ทำให้ผมต้องไปพึ่งบริการของ google ซึ่งพบว่ามี free .net-Pdf library ตัวหนึ่งน่าสนใจมากนั่นคือ iTextSharp ครับ

    หลังจาก download มาลงที่เครื่องเรียบร้อยก็ลองทดสอบกันเลย เริ่มจาก Add Reference ใน ASP.net โปรเจคก่อน และทำการ Import Namespace ให้เรียบร้อย ขั้นตอนการทำงานก็คือ ผมสร้าง Web Form ขึ้นมาโดยให้รับค่า Location ของ pdf file ที่จะทำการเปิดให้ user ผ่าน QueryString จากนั้นก็ไปตรวจสอบสิทธิ์ของ user ว่าจะให้ซ่อนหน้าไหนบ้าง แล้วจึงใช้ iTextSharp.PdfCopy ทำการสร้างเอกสาร pdf ขึ้นมาใหม่ และ copy หน้าที่ user มีสิทธิเห็นไปใส่ในเอกสารใหม่ลองดูโค้ดกันครับ


    Public intIgnorePages As List(Of Integer)

    Protected Sub frmMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles frmMain.Load

    Dim strLocation As String = Server.UrlDecode(Request("Location"))
    Dim fileName As String = "OUTPUT/" & _userCredential.EmployeeID & "/" & fileInfo.Name
    Dim reader As New PdfReader(strLocation)
    Dim document As New iTextSharp.text.Document(reader.GetPageSizeWithRotation(1))
    Dim fs As New FileStream(Server.MapPath(fileName), FileMode.Create)
    Dim page As PdfImportedPage = Nothing
    Dim copy As New PdfCopy(document, fs)

    If File.Exists(fileName) Then File.Delete(fileName)
    intIgnorePages = ValidateUserPermission
    document.Open()
    For i As Integer = 1 To reader.NumberOfPages
    Dim ipage As Integer = i
    If intIgnorePages Is Nothing OrElse intIgnorePages.Find(Function(c) c = ipage) = 0 Then
    copy.AddPage(copy.GetImportedPage(reader, ipage))
    End If
    Next
    document.Close()
    reader.Close()
    copy.Close()
    fs.Close()
    fs.Dispose()
    Response.Redirect(fileName)

    End Sub


    สำหรับโค้ดด้านบนเป็นโค้ดที่ผมใช้ทดสอบโปรแกรมยังไม่ใช่โค้ดที่ใช้งานจริง แต่สำหรับผลลัพธ์เป็นที่น่าพอใจมาก สุดยอดจริงๆครับ

    Reference:
    iTextSharpby blowagie, geraldhenson, psoares33

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

    jQuery อ้างถึง Control ใน gridview

    วันก่อนเข้าไป http://www.blogger.com/www.greatfriends.biz พบคำถามนี้ในบอร์ดพอดีครับ เป็นคำถามที่น่าสนใจทีเดียว ผมตอบไว้แบบนี้ครับ
    ผมสมมติว่า เมื่อผมคลิ๊กที่ column ของ gridview ผมจะให้โปรแกรม alert ค่าทั้งหมดของแถวที่คลิ๊ก และแสดงค่าของ column ที่คลิ๊ก
    เนื่องจากผมใช้ MSAjax และเอา Gridview ไปใส่ใน UpdatePanel ดังนั้น แทนที่ผมจะใช้ $(document).ready(function(){}); ผมจะไป register click event ที่ gvDBItem_DataBound แทนครับ

    และก็ใน click(function(){}); แทนที่ผมจะเขียนโค้ดไปโดยตรง ผมเปลี่ยนให้ไปเรียก function ใช้งานแทน เพื่อที่ว่าตอน debug จะได้ง่ายครับ

    โค้ดส่วน Code Behind

    Protected Sub gvDBItem_DataBound(ByVal sender As Object, ByVal e As System.EventArgs) Handles gvDBItem.DataBound

    Dim sbScript As New Text.StringBuilder

    sbScript.Append("$('#" & gvDBItem.ClientID & " tr').click(function(e){gv_RowClick(this,e);});")

    ScriptManager.RegisterStartupScript(Page, Page.GetType, "GVDataBound", sbScript.ToString, True)

    End Sub



    โค้ดส่วน javascript

    <script type="text/javascript" >

    function gv_RowClick(sender, e) {

    alert(sender.innerText);

    alert(e.srcElement.innerHTML);

    }

    </script>



    ในตัวอย่างผมใช้ gridview id กับ tr เป็น selector แต่ว่าถ้าเราต้องการรู้ ID ของแถวที่กด หรือต้องการเอาค่าใน column ที่ต้องการเท่านั้น ก็สามารถเปลี่ยน selector ได้ครับ

    อีกวิธีที่เพิ่งนึกออก คือเราสามารถใช้ attribute เป็น selector ได้ ดังนั้นเราก็เพิ่ม custom attribute ให้ column ที่เราต้องการเช่น


    <asp:TemplateField HeaderText="Item No">

    <ItemTemplate>

    <asp:Label id="lblItemNo" runat="server" RowIndex="true" Text='<%# Eval("ORDERNO") %>' ></asp:Label>

    </ItemTemplate>

    </asp:TemplateField>



    แล้วใน jQuery เราก็กำหนด selector

    <script type="text/javascript" >

    $(document).ready(function() {

    $("#gvDBItem [RowIndex='true']").click(function(e) { gv_RowIndexClick(this, e); });

    });

    function gv_RowClick(sender, e) {

    alert(sender.innerText);

    alert(e.srcElement.innerHTML);

    }

    </script>



    อันนี้เป็นตัวอย่างแบบไม่ได้ใช้ UpdatePanel ครับ ผมเลยใช้ $(document).ready()

    ลองอ่านกระทู้เต็มๆได้ที่นี่ครับ
    jQuery อ้างถึง Control ใน GridView อย่างไรครับ
    http://greatfriends.biz/?109412

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

    jQuery กับการเรียก ASP.net Web Service

    ต่อเนื่องจากบทความที่แล้ว เราได้ใช้ ScriptManager ในการ add Service Reference เพื่อที่จะให้ VS ทำการสร้าง Proxy สำหรับเรียกใช้ Web Service ซึ่งก็สะดวกดีครับ แต่ทีนี้ตัว jquery เองมีความสามารถของ ajax มาด้วย เราจะมาลองเขียน jQuery สำหรับเรียก Web Service โดยตรง ไม่ต้องผ่าน ScriptManager ดูครับ

    วิธีการก็ไม่ยาก แค่เราเรียกใช้ $.ajax(option) แค่นี้เอง ส่วน option นั้นเรากำหนดค่าในรูปแบบของ json ซึ่งมี member อะไรที่กำหนดได้บ้างนั้น ไปดูได้จาก document ของ jQuery ครับ ดังนั้นเราลอง comment โค้ดเดิมที่เรียก proxy ของ Web Service แล้วมาเขียนคำสั่งของ jQuery ดู

    function txtEmpName_OnKeyUp(e) {
    if (e.keyCode != 13 || e.keyCode != 9) {
    $("#txtEmpId").val("");
    var empName = $("#txtEmpName").val();
    if (empName.length > 2) {
    //BAS.Web.Services.wsFiling.GetStaffsByKeyword(empName, GetStaffSuccess);
    $.ajax({
    type: "POST",
    url: "../wsFiling.asmx/GetStaffsByKeyword",
    data: "{keyWord:'" + empName + "'}",
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function(data) { GetStaffSuccess(data.d, "", "") }
    });
    } else {
    $("#divStaff").hide();
    }
    }
    }


    ก็ไม่ยากอย่างที่คิดครับ ตรง success เราก็ไปเรียก GetStaffSuccess function ตัวเดิมนั่นแหละ ส่วน error ผมไม่ได้ใส่ไว้เหมือนเดิม และในการใช้งานจริงก็ควรสร้าง OnError function ไว้ด้วยครับ

    แต่โดยส่วนตัวขอใช้ผ่าน ScriptManager เหมือนเดิมครับ

    Reference:
    Using jQuery with ASP.NET Web Services and JSON, www.prettycode.org
    Using jQuery to Consume ASP.NET JSON Web Services, www.encosia.com

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

    jQuery กับการทำ Multi Column Autocomplete

    พอดีได้รับ requirement ให้ทำ auto complete function ครับ สมมติว่า user คีย์ชื่อพนักงาน ให้โปรแกรมไปดึงรายชื่อพนักงานทั้งหมดที่ใกล้เคียงมาแสดง พอ user เลือกพนักงานที่ต้องการจากใน list ก็ให้ไปแสดงข้อมูลอื่นๆของพนักงานใน field อื่นๆด้วย เช่นรหัสพนักงาน ชื่อ นามสกุล แผนก วันเริ่มงาน เป็นต้นครับ
    ทีนี้ลอง search ดูพวก autocomplete จาก google พบว่าส่วนใหญ่มันเป็นแบบ single field คือใช้กับ textbox ตัวเดียวครับ เลยต้องเขียน javascript เพื่อไปดึงข้อมูลจาก Web Service มาให้ user เลือก แล้วพอเลือกแล้วก็เขียน javascript ให้ไปตัดข้อมูลลงใน element ต่างๆ
    รู้สึกว่าการเขียน javascript เองนี่มันยาวพอสมควรครับ พอมีเวลาว่างก็เลยมาลองใช้ jQuery ดูว่ามันจะง่ายและดีสมคำร่ำลือหรือเปล่า ผมเลยสร้าง web page สำหรับทดสอบดู เอาเป็นว่ามีแค่ 2 field คือ ID กับ ชื่อนามสกุลก็พอครับ
    ก่อนอื่นมาดูโค้ดหน้าเวบกันก่อน

    <body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    <Services>
    <asp:ServiceReference Path="~/Employee.asmx" />
    </Services>
    </asp:ScriptManager>
    <div>
    <asp:TextBox ID="txtEmpId" runat="server" style="width:100px" MaxLength="6"></asp:TextBox>
    <asp:TextBox ID="txtEmpName" runat="server" style="width:250px"></asp:TextBox>
    </div>
    </form>
    </body>

    จะเห็นว่ามี textbox แค่สองอัน และมีการใช้ ScriptManager เพื่อไปเรียก WebService ครับ
    โค้ดส่วน WebService ขอไม่เขียนนะครับ เพราะไม่ยาก โดยตัว WebService จะคืน Employee Object มาให้ในรูปของ JSON (ตาม default ครับ)
    ทีนี้มาดูโค้ดส่วน javascript กันบ้าง

    <script type="text/javascript" src="../Scripts/jquery-1.3.2-vsdoc.js"></script>
    <script type = "text/javascript">
    $(function() {
    $("#txtEmpName").keyup(function(event) {txtEmpName_OnKeyUp(event)});
    });

    function txtEmpName_OnKeyUp(e) {
    if (e.keyCode != 13 || e.keyCode != 9) {
    $("#txtEmpId").val("");
    var empName = $("#txtEmpName").val();
    if (empName.length > 2) {
    jnithi.Employee.GetStaffsByKeyword(empName, GetStaffSuccess);
    } else {
    $("#divStaff").hide();

    }
    }
    }

    function GetStaffSuccess(result, methodName, context) {
    if ($("#divStaff").length == 0) {
    var txtEmpName = $("#txtEmpName");
    var pos = txtEmpName.position();
    var divStyle = {'position':'absolute','top': pos.top + txtEmpName.outerHeight() + 'px','left': pos.left + 'px','width': txtEmpName.outerWidth() +'px','border':'1px solid #666666' };
    $(document.createElement("div")).attr("id", "divStaff").css(divStyle).appendTo("body");
    }
    var divStaff=$("#divStaff");
    divStaff.empty();

    if (typeof (result) != "undefinded" && result.length > 0) {
    $(document.createElement("dl")).width(divStaff.width()).appendTo("#divStaff");
    var i;
    for (i = 0; i < result.length; i++) {
    var employee = result[i];
    var html = "<dt><a class='search' href='#' onclick='dtStaff_OnClick(this);' >" + employee.NewEmployeeId + ":" + employee.NameEng + "</a></dt>";
    $("dl").append(html);
    }
    if (i > 0) {
    divStaff.show();
    }
    } else {
    divStaff.hide();
    }
    }
    function dtStaff_OnClick(obj) {
    result = obj.innerHTML.split(":");
    $("#txtEmpId").val(result[0]);
    $("#txtEmpName").val(result[1]);
    $("#divStaff").hide();
    }
    </script>


    เริ่มต้นก็ไปกำหนด Event Handler ให้กับ keyup event ของ txtEmpName ครับ
    $(function() {
    $("#txtEmpName").keyup(function(event) {txtEmpName_OnKeyUp(event)});
    });

    โดยปกติตัวอย่างของ jQuery จะเขียน function ไปเลย แต่ปรากฎว่า Visual Studio มัน debug ลำบาก ผมเลยแยกมาเขียน function ต่างหากครับ โดยมี window.event เป็น parameter หรือเราใช้ MSAjax อยู่แล้วอาจจะพิจารณาใช้ $addHandler() ของ MSAjax แทนก็ได้ครับ เมื่อเกิด keyup event มันก็จะไปเรียก txtEmpName_OnKeyUp ให้ทำงาน โดยจะไปเรียก WebService และกำหนด succeed callback function ไปที่ GetStaffSuccess ครับ (จริงๆควรจะกำหนด onFailed callback function ด้วยนะครับ)

    ใน GetStaffSuccess function ผมให้มันทำการสร้าง div ที่บรรจุ dl element ไว้ครับ เพื่อเป็น list ให้ user เลือก สังเกตุว่าใน function นี้ผมทดลองทำการ creat element ไว้ 2 แบบ แบบแรกคือใช้ $(document.createElement(element)).appendTo(element) กับอีกแบบคือ $(element).append(html) แล้วก็ต้องกำหนด onclick event ด้วย เพื่อที่ว่าเมื่อ user เลือกรายชือพนักงานแล้ว จะได้ไปทำ dtStaff_OnClick ซึ่งจะทำการตัดข้อมูลไปใส่ใน textbox ทั้ง 2 ตัวครับ

    ลองเอาโค้ดที่เขียน javascript เอง กับที่เขียนด้วย jQuery เห็นได้ว่าโค้ดสั้นลงประมาณ 30% เพราะใช้ประโยชน์ของ Chainability นั่นเองครับ