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

เปรียบเทียบ 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 ถ้ามีเวลาจะกลับมาเขียนเพิ่มครับ