ทำความเข้าใจกับตัวอักษรในคอมพิวเตอร์

ดองบล็อคมานาน ได้หัวข้อเขียนซะที เป็นเรื่องเกี่ยวกับ ASCII, Unicode, UTF-8 และอื่นๆ เพื่อแก้ไขความสับสนของตัวเองด้วย อ่านมาหลายรอบ แต่จำไม่ได้ซะทีเหมือนกัน

ความแตกต่างระหว่าง Binary กับ Text

Binary ในที่นี้จะหมายถึง byte (8 bit-binary แต่ต่อไปนี้จะขอใช้ byte แทนที่ binary ทั้งหมด ยกเว้นเมื่อต้องการเน้นความแตกต่าง) ที่เรียงต่อกันอ่านเป็นภาษาคนไม่รู้เรื่อง ส่วน Text คือ character (ตัวอักษร) ที่เรียงต่อกัน อ่านเป็นภาษาคนรู้เรื่อง (ภาษาคนที่เขียนไม่รู้เรื่อง เช่น w,ji^hginjv’ ก็ถือว่าเป็น text เหมือนกันนะ) ตัวอย่าง เราจะเห็น Text ได้จากไฟล์ เช่น .txt และ Binary ได้จากไฟล์ .jpg (ต้องเปิดเป็น binary) เป็นต้น

Text จริงๆ แล้วก็คือ Byte

อย่างที่ทุกคนทราบ ข้อมูลที่คอมพิวเตอร์ที่เราใช้ในปัจจุบันรู้จักแต่เลข 0 กับ 1 คำถามคือ ตัวอักษรที่กำลังอ่านอยู่มาจากไหน?
คำตอบคือ มาจาก Byte เพียงแต่เรารู้วิธี Interpret (ตีความ) หรือ Decode (ถอดรหัส) (ต่อไปนี้จะขอใช้คำว่าถอดรหัส) มันออกมา ซึ่งถ้าเราไม่รู้วิธีถอดรหัส เราก็จะเห็นมันเป็นตัวอักษรมั่วอ่านไม่ออก
คำถามต่อไปคือ Byte ที่ว่าไปอยู่ในคอมได้อย่างไร คำตอบคือ เราป้อนข้อมูลภาษาคนเข้าไปให้คอม Encode (เข้ารหัส) (ต่อไปนี้จะขอใช้คำว่าเข้ารหัส) เพื่อให้อยู่ในคอมซึ่งรู้จักแต่ 0 กับ 1 ได้

ASCII Encoding

ASCII เป็นหนึ่งในมาตรฐานการเข้ารหัสตัวอักษรในยุคแรกๆ (คนเขียนเขียนเอง) ที่เข้ารหัสตัวอักษรภาษาอังกฤษทั้งหมดและตัวอักษรควบคุม (control character เช่น \n) ไว้ภายใน 7 bit แต่คอมสมัยนั้นใช้ Byte ที่มีขนาด 8 bit ดังนั้นบิตที่ 8 ที่เหลือจึงถูกนำไปใช้เพื่อแสดงตัวอักษรตามแต่ผู้ผลิตจะเพิ่ม หรือจะเอาบิตที่ 8 ไปทำอะไรก็ตามแต่ต้องการ
ปัญหาคือ โค้ดที่ถูกใส่เพิ่มเข้าไปนั้นแตกต่างกันตามแต่ละผู้ผลิต ถ้าหากเราส่งข้อความที่มีตัวอักษรซึ่งถูกเข้ารหัสตั้งแต่ 128-255 ไปหาคอมที่ใช้คนละมาตรฐานและมันไม่ compatible กัน (ไม่เข้ากัน) ก็จะเห็นตัวอักษรคนละอย่างกัน

Code Page Encoding

เป็น Set ของ character encoding อีกเช่นเคย แต่ทุกคนใช้โค้ดที่ต่ำกว่า 128 เหมือนกันหมด (ตรงกับ ASCII) ทำให้เข้ากันได้ เพียงแต่โค้ดตั้งแต่ 128 ขึ้นไปก็เป็นไปตามแต่ละผู้ผลิตและผู้ผลิตก็มีหลาย Code page หาดูได้ว่า Code page แต่ละอันมีโค้ดอะไรบ้างได้ที่ http://www.i18nguy.com/unicode/code… Windows ก็มี Code page ภาษาไทยชื่อ Windows CP 874
Code page ที่ว่านี่ไม่ได้ใช้แค่ 1 byte เพื่อเก็บโค้ดแล้ว แต่มีถึง 2 bytes ด้วยกัน แต่ปัญหาเดิมๆ ก็ยังคงอยู่ คือ มีมาตรฐานเยอะมาก จึงเกิดมาตรฐานใหม่ขึ้นมาอีกคือ

Unicode

จากปัญหาที่ ASCII Encoding รองรับแค่ภาษาอังกฤษเพียงภาษาเดียวและมี Code page ตามแต่ละผู้ผลิตจะออกมาเต็มไปหมด จึงมี Unicode Consortium (สมาคม Unicode) เกิดขึ้น โดยพยายามที่จะสร้างมาตรฐานเกี่ยวกับการจัดการตัวอักษรขึ้นมา
อย่างแรกที่ถูกกำหนดขึ้นมาคือเซ็ตของตัวอักษรที่ Unicode จะดูแล และให้ตัวเลขเพื่อแสดง/แทนตัวอักษรเหล่านี้เรียกว่า Code point ซึ่งปกติจะเขียนด้วย “U+” ตามด้วยตัวเลขฐาน 16 ซึ่งเป็น Code point นั้น เช่น U+0061 (code point ของ a) Code point ไม่ได้เกี่ยวอะไรกับการเก็บตัวอักษรนี้ในคอมตรงๆ แม้แต่น้อย สำหรับ Unicode มีได้สูงสุดถึง 1,114,112 ตัว แต่ก็ไม่ใช่ว่าทุก Code point จะถูกกำหนดไปแทนตัวษร
อย่างต่อมาที่ถูกกำหนดขึ้นคือ Encoding ซึ่งก็คือกำหนดวิธีการเก็บตัวอักษรที่ถูกระบุใน Unicode เอาไว้ในคอมพิวเตอร์

UTF-16

UTF ย่อมาจาก Unicode transformation format ถ้าแปลตรงตัวก็อาจจะเป็น “รูปแบบการเปลี่ยน Unicode” ซึ่งตรงกับสิ่งที่ทำก็คือเป็นมาตรฐานการเปลี่ยน Code point มาเป็นเป็นตัวเลข 16 bits หรือ 2 bytes เรียกว่า 1 unit code ซึ่งเก็บ Unicode ได้ตั้งแต่ช่วง U+0000 ถึง U+D7FF และ U+E000 ถึง U+FFFF โดยเรียก Code point ช่วงนี้ว่า Basic Multilingual Plane ภาษาญี่ปุ่น, จีนและไทยก็อยู่ในช่วงนี้ด้วย ตัวเลขของทั้ง Code point และตัวอักษรที่ถูกเข้ารหัสแบบ UTF-16 จะเท่ากันเป๊ะ เช่น ก มี Code point เป็น U+0E01 ตรงกับ UTF-16 นั่นคือ 0E01

UTF-16 ไม่ได้ใช้เพียงแค่ 16 bit หรือ 2 bytes เท่านั้น แต่ยังใช้ Surrogate pairs นั่นคือใช้ 2 unit codes (ทั้งหมดเป็น 32 bit หรือ 4 bytes) เพื่อแสดงตัวอักษร Unicode เพียงตัวเดียว ใช้เข้ารหัสตัวอักษรอยู่นอกเหนือจาก Basic Multilingual Plane ตัวอย่างเช่น Emoji เป็นต้น

BOM (อาจจะข้ามไปก็ได้)

UTF-16 ยังมีเรื่อง Byte order mark หรือ BOM เข้ามาเกี่ยวข้องด้วย โดยจะเป็นตัวระบุว่าเราเข้ารหัส UTF-16 แบบไหนระหว่าง Big-endian หรือ Little-endian (ต่อไปนี้ขอย่อเป็น BE และ LE) อธิบาย Endianness แบบง่ายๆ คือหมายถึงลำดับที่ใช้ในการแปลความหมายของ Byte ตัวอย่างง่ายๆ ในชีวิตประจำวันก็เช่น หนึ่งร้อยยี่สิบสาม จะถูกเขียนแทนด้วย 123 ซึ่งเป็นแบบ BE โดยเขียนตัวเลขหลักที่มีค่ามากที่สุดไว้ทางซ้ายมือและหลักที่มีค่าน้อยที่สุดไว้ทางขวามือ ในทางตรงกันข้าม ถ้าต้องการเขียนเลข  หนึ่งร้อยยี่สิบสาม เป็นแบบ LE จะเขียนว่า 321 ซึ่งถ้าไม่รู้ว่าตัวเลขนี้ถูกเขียนแบบไหน ก็จะตีความผิดไปทันที UTF-16 มีปัญหาตรงนี้เพราะใช้ 2 bytes เพื่อเก็บข้อมูล ตัวอย่างเช่น ตัวอักษร a (U+0061) ถ้าถูกเข้ารหัสเป็น UTF-16 BE จะได้ 00 61 แต่ถ้าเป็น UTF-16 LE ก็จะได้ 61 00 ซึ่งไม่มีอะไรผิดเพราะขึ้นอยู่กับสถาปัตยกรรม

BOM ที่ว่านี้คือค่า U+FEFF ซึ่งถูกใส่ไว้ข้างหน้าโค้ดของจริงตัวแรก ถ้าทั้งฝั่งเข้ารหัสและฝั่งถอดรหัสใช้ Endianness เหมือนกัน ก็จะอ่านได้เป็นตัวอักษรตัวเดียวกัน (U+FEFF) แต่กลับกันถ้าฝั่งเข้ารหัสและฝั่งถอดรหัสไม่ได้ใช้ Endianness เดียวกัน พอเวลาอ่านเจอ BOM จะอ่านได้เป็นอีกค่าหนึ่งคือ U+FFFE ซึ่งตัวอักษรตัวนี้ก็ถูกกันเอาไว้เพื่อเป็น BOM แล้ว แต่ถ้าไม่มี BOM มาตรฐาน  RFC 2781 ระบุว่าให้เดาว่าเป็น BE เสมอ แต่ในทางปฏิบัติ Windows ใช้ LE เป็นค่าเริ่มต้น (น่าจะเป็นเพราะ Intel ใช้ LE) และแอปพลิเคชันอื่นๆ ก็มีเดาไว้ว่าเป็น LE ด้วยเหมือนกัน

ตามมาตรฐานยังอนุญาติให้ระบุ Endianness เอาไว้ได้ จึงมี UTF-16BE และ UTF-16LE ด้วย และถ้าถูกระบุชัดเจนแล้ว ไม่จำเป็นต้องใส่ BOM ลงไปอีก แต่ถ้ายังมีอยู่ก็ให้จัดการเป็นตัวอักษร ZERO WIDTH NO-BREAK SPACE

นอกใช้ BOM เพื่อระบุ Endianness แล้ว Web browser ยังใช้ BOM เพื่อดู Encoding ด้วย

UTF-8

มาถึง UTF-8 ที่ทุกคนน่าจะได้ยินบ่อยที่สุดในตระกูล UTF เนื่องจากถูกนำไปกำหนดเป็นมาตรฐานการเข้ารหัสสำหรับมาตรฐานอื่นๆ; UTF-8 สามารถเข้ารหัส Unicode ได้ทุกตัวโดยไม่ต้องใช้ Surrogate pairs แล้ว เนื้อที่ที่ใช้ในการเก็บใช้หน่วย 8 bit = 1 code unit และใช้สูงสุด 4 code units (32 bit)

UTF-8 ถูกออกแบบมาให้ใช้ร่วมกับ ASCII ได้เพราะ UTF-8 ใช้จำนวนบิตน้อยที่สุดคือ 8 bit ตรงกันข้ามกับ UTF-16 ที่ใช้จำนวนบิตน้อยที่สุดคือ 16 bit จึงทำให้ใช้ร่วมกับ ASCII ไม่ได้ ตัวอย่างตัวอักษร a ที่เข้ารหัสเป็น ASCII จะได้(ทั้งหมดเป็นฐาน 16) 61, Unicode code Code point ของ a เท่ากับ U+0061 เมื่อเข้ารหัสเป็น UTF-16 จะได้ 0061 แต่ถ้าเข้ารหัสเป็น UTF-8 จะได้ 61 จะเห็นได้ว่า ถ้าเราเข้ารหัสด้วย ASCII และมาลองอ่านโดยถอดรหัสแบบ UTF-8 เราจะยังอ่านได้ถูกต้อง แต่ถ้ามาถอดรหัสเป็นแบบ UTF-16 เราจะอ่านไม่รู้เรื่อง

UTF-8 ยังถูกออกแบบมาให้แก้ปัญหาเรื่อง Endianness ไม่ตรงกันระหว่างผู้เข้ารหัสและถอดรหัส ทำให้ไม่จำเป็นต้องใช้ BOM อีกต่อไป แต่ก็ยังมีบางครั้งที่ใส่ BOM เข้าไปเพื่อให้สามารถระบุได้ว่าเป็น Data stream ของ UTF-8 แต่อาจจะมีปัญหากับเครื่องมือหรืออะไรก็ตามแต่ ที่อ่าน UTF-8 แต่ไม่ได้ต้องการจัดการกับ BOM ตัวอย่างแอปพลิเคชันที่ใส่ BOM เข้าไปใน UTF-8 คือ Notepad

UTF-32

UTF-32 ใช้ 32 bits = 1 code unit และใช้สูงสุดเพียงแค่ 1 code unit เท่านั้น ตัวเลขที่ได้จากการเข้ารหัส UTF-32 จะเท่ากับ Unicode Code point ทั้งหมด จบ…

(แถม) Binary to Text encoding

ข้างบนทั้งหมดเป็นการเก็บเอาตัวหนังสือไว้ในคอมด้วยการเข้ารหัสตัวอักษรไปเป็น binary แต่กลับกันก็มีการเข้ารหัส byte ไปเป็นตัวอักษรเพื่อใช้งานกับโปรโตคอลที่เป็น Text-based เช่น HTTP, JSON เป็นต้น และก็อยากที่กล่าวไว้ด้านบน ถ้าตัวอักษรที่ได้จากการเข้ารหัส Binary เหล่านี้จะเป็นต้องอยู่บนคอมพิวเตอร์ ก็ต้องถูกเข้ารหัสเป็น Binary อีกทีหนึ่งเสมอ (ยกเว้นจะพิมพ์ใส่กระดาษมาอ่าน แล้วลบทิ้ง – -*)

base64 encoding

เป็นมาตรฐานการเข้ารหัสจาก byte เป็น text ที่นิยมใช้สูงมาก ตรงตามชื่อคือมีตัวอักษรที่ถูกเลือกมาใช้ทั้งหมด 64 ตัว แต่ก็มีมาตรฐานย่อยๆ อีกซึ่งตัวอักษรทั้ง 64 ตัวอาจจะไม่เหมือนกันทุกตัว

คำถามที่อาจจะสงสัย

Q: ถ้า Unicode ไม่ใช่การเข้ารหัส แล้วทำไม Notepad บน Windows ถึงมี Unicode เป็นตัวเลือกเวลาเข้ารหัส
A: เพราะ UTF-16 เข้ารหัสตัวอักษรที่มีค่าเท่ากับ code point (เฉพาะ BMP) (คิดเอาเอง) Windows จึงเรียก Unicode

อ้างอิง

วิธีระบุค่าที่ถูกเลือกใน select tag โดยใช้ Select Tag Helper ใน ASP.NET MVC 6

บางครั้งเวลาออกแบบ navigation ของเว็บ เราอาจจะมีโอกาสใช้ select เพื่อให้ผู้ใช้ระบุเงื่อนไขที่ต้องการ และแสดงเงื่อนไขปัจจุบันที่ใช้งานอยู่ด้วย ตัวอย่างเช่น Jitta ใช้ selectเพื่อให้ผู้ใช้เลือกเงื่อนไขตลาดที่จะแสดง และแสดงว่าปัจจุบันแสดงข้อมูลตลาดไหนอยู่(ใช้ selectize.js ทำ)

สำหรับ Select Tag Helper ใน MVC 6 นั้นมีอยู่ 2 อย่างที่ใช้คือ asp-for สำหรับกำหนดตัวแปรที่จะเก็บค่าที่ถูกเลือกไว้ใน select (เหมือนกับ input tag อื่นๆ)และ asp-item เพื่อระบุว่าจะแสดงอะไรให้เลือกได้บ้างใน select ในที่นี้ผมจะไม่พูดถึง asp-for เพราะคิดว่าใช้กันเป็นแล้ว มาดูที่ asp-item กัน

asp-item รับค่าประเภท SelectList ดู constructor ของคลาส SelectList ได้ที่ SelectList Constructor จากที่เห็นเราสามารถกำหนดค่าที่จะถูกเลือกได้ง่ายๆ ดังนี้

ที่ controller

ที่ view

ถ้าดูจากเอกสารของ constructor ก็จะเห็นว่าด้วยวิธีนี้สามารถควบคุมค่าเริ่มต้นที่จะถูกแสดงได้ง่ายๆ

กรณีใช้ Enum แสดงผล

Html Helper มี method ชื่อ GetEnumSelectList(Type enumType) ซึ่งจะแปลง enum มาแสดง

ที่ view

แต่ปัญหากับ Enum คือมันจะเลือกค่าเริ่มต้นจาก Enum แรกสุดเสมอ ในตัวอย่างนี้คือ One

วิธีแก้คือเขียน Html Helper ขึ้นมาเอง เพิ่ม parameter ให้รับค่าเริ่มต้นที่จะแสดงเข้ามา

ใช้ reflect อ่าน attribute เพื่อแก้ชื่อที่แสดงตามที่ผู้ใช้อาจจะใส่
เวลาใช้ที่ View

เป็นอันจบ

ทำความรู้จัก .NET สำหรับคนธรรมดา

สวัสดีครับ ขอเขียนอธิบายบทความเกี่ยวกับ .NET สำหรับคนธรรมดา เนื่องจากผมเองซึ่งศึกษามาทางนี้ยังรู้สึกสับสนกับความใหญ่และซับซ้อนของมัน คิดว่าอย่างน้อยก็คงมีซักคนที่อยากรู้ครับ

ศัพท์ที่ต้องรู้

Framework

คือสิ่งที่เป็นพื้นฐานสำหรับสร้างสิ่งใดสิ่งหนึ่ง ในที่นี้คือโปรแกรมหรือแอพพลิเคชั่นหรือส่วนใหญ่ๆ ภายในโปรแกรม ตัวอย่างใน .NET ถ้าเราจะสร้างบ้าน ต้องมีอุปกรณ์, แบบบ้าน,และวิศวกร/คนงาน สิ่งเหล่านี้เป็น Framework นั้นเอง

ส่วนถ้าพูดถึงรวมๆ ในวงการพัฒนาซอฟแวร์ Framework จะเหมือนชุดคำสั่งทางคอมพิวเตอร์ระดับพื้นฐานที่นำมาสร้างสิ่งใดซักสิ่งหนึ่ง เช่น Caliburn Micro เป็น Framework สำหรับสร้างโปรแกรมด้วยเทคโนโลยี WPF ด้วย ตามรูปแบบ MVVM ซึ่งภายใน Framework นี้ก็จะมีคำสั่งทั้งหมดที่จำเป็นที่จะทำให้โปรแกรมเราเป็นไปตามรูปแบบ MVVM

Library

คือเซตของคำสั่งที่เจาะจงใช้ทำอะไรอย่างหนึ่ง แต่ไม่ใหญ่ถึงขนาด Framework อาจจะเป็นสับเซตของ Framework  หรืออาจจะหมายถึงที่ๆ รวมเอา API เอาไว้ ตัวอย่างเช่น Task Parallel Library เป็นส่วนหนึ่งของ .NET Framework ประกอบด้วยคลาสต่างๆ ที่ใช้เขียนโปรแกรมแบบขนาน

API – Application programming interface

เป็นคำสั่งหรือเครื่องมือที่โปรแกรมเมอร์ใช้คุยกับคอมพิวเตอร์หรือ services ต่างๆ ในการสร้างโปรแกรมขึ้นมา แตกต่างจากภาษาโปรแกรมที่ใช้กำหนดถึงวิธีการเขียน, ไวยกรณ์ เป็นต้น

เมื่อรวมทั้ง 3 อย่างเข้าด้วยกัน เราสามารถพูดได้ว่า เราพัฒนา library สำหรับติดต่อกับบริการของผม library ตัวนี้ใช้ .NET Framework เป็นฐานในการเขียน(คือ ใช้ API ที่มีใน .NET Framework) ภายใน library นี้บรรจุ API เพื่อให้นักพัฒนานำไปใช้ทำอย่างอื่นต่อ

ต่อไปเข้าเรื่อง .NET เลยนะครับ

.NET Framework

เป็น Framework พื้นฐานสำหรับนักพัฒนาโปรแกรมบน Windows ทำได้งานแต่บน Windows เท่านั้น ปัจจุบันพัฒนาถึงเวอร์ชั่น 4.6 ซี่งมีติดมากับ Windows 10 ส่วน Windows เวอร์ชั่นต่ำกว่านี้ต้องโหลดมาลงเอง(จริงๆ ตอนนี้มีถึง 4.6.1 Windows 10 ก็ต้องโหลดมาลงด้วย) ประกอบด้วย

Framework Class Library เป็นที่บรรจุ API เอาไว้ในนักพัฒนาใช้งาน มี library แยกย่อยอีกทีนึงอีก เป็น Base Class Library(BCL) ซึ่งเป็นตัวหลักให้ library อื่นๆ ที่มาพร้อมกันอีกประเภทคือ Framework Class Library(FCL) ใช้งาน; FCL ที่มี เช่น LINQ Windows Forms, WPF, ASP.NET เป็นต้น

Compiler เอาไว้เปลี่ยนจากภาษาที่เราเขียน(ที่ .NET Framework รองรับ) เป็นภาษา Common Intermediate Language โดยจะถูกแพ็คในไฟล์ .exe หรือ .dll เรียกอีกอย่างว่า Assemblies

Common Language Runtime(CLR) หลังจากไฟล์ source code ถูก compile เป็น Assemblies แล้วเมื่อมันถูกเรียกใช้งาน Assemblies นี้จะถูก compile อีกทีหนึ่งเป็นภาษาเครื่องด้วย CLR ตรงนี้ยังมีรายละเอียดอื่นอยู่ แต่ละเอียดเกินขอไม่พูดถึงต่อนะครับ

.NET Core

เป็น Framework ที่ถูกทำขึ้นใหม่ โดยใช้แบบจาก .NET Framework แต่ถูกพัฒนาขึ้นโดยคำนึงถึงเรื่องแยกส่วน .NET Core ทำงานได้ทั้ง Windows, Linux และ OSX และ open source ด้วย

.NET Core มี 3 ส่วนหลักเหมือน .NET Framework แต่ส่วน Framework Class Library มีรายละเอียดดังนี้

dot net core stack

Unified BCL เป็น API ที่ใช้ร่วมกันไม่ว่าจะพัฒนา Windows Store App หรือ ASP.NET 5 

ASP.NET 5 เป็น framework ตัวใหม่ทั้งก้อน ที่สามารถเลือกว่าจะใช้ทำงานบน .NET Framework หรือ .NET Core ผมอาจจะยังไม่ได้บอกไปตอนต้นว่า ASP.NET ใช้ทำเว็บไซต์ฝั่งเซิฟเวอร์

Windows Store App เป็น framework ที่ทำงานบน .NET Core ใช้พัฒนาอะไรก็ตามที่เอาขึ้น Windows Store(Universal Windows Platform –  UWP)ได้

ข้อสังเกตเกี่ยวกับ .NET Core คือชื่อเวอร์ชั่นปัจจุบันคือ .NET Core 5 ซึ่งถูกนับต่อจาก .NET Framework ซึ่งในความเป็นจริงมันคือของใหม่ รวมถึง ASP.NET 5 ที่ออกแบบมาให้ใช้งานกับ .NET Core ก็เป็นของใหม่ที่ภายในอาจไม่เกี่ยวข้องอะไรกับ ASP.NET 4.6 ใน .NET Framework เลย

ด้วยเหตุนี้อนาคตจึงจะมีการเปลี่ยนชื่อดังนี้(ปัจจุบันยังไม่เปลี่ยน)

  • .NET Core 5 เปลี่ยน .NET Core 1.0.
  • ASP.NET 5 เปลี่ยนเป็น ASP.NET Core 1.0.

อีกข้อสังเกตของ .NET Core คือผู้ใช้ไม่จำเป็นต้องสนใจว่ามีมันอยู่ในเครื่อง เพราะมันจะถูกรวมมากับโปรแกรมที่เราโหลดมาจาก Windows App Store ซึ่งแตกต่างกับ .NET Framework ซึ่งจะรวมเอาแค่ส่วน 3rd party framework/library มาด้วยเท่านั้น

ข้อมูลจาก .NET Framework Wikipedia และ Introducing .NET Core

แชร์วิธีการทำโปรแกรมดาวน์โหลดไฟล์แบบง่ายๆ

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

โปรแกรมที่ผมทำมีชื่อว่า TirkxDownloader เป็นโปรแกรมจัดการการดาวน์โหลด(ดาวน์โหลดไฟล์,ทำคิวการดาวน์โหลด)

โปรแกรมนี้เขียนด้วย C# เกือบทั้งหมด มี Javascript บ้างตอนเขียน Extension สำหรับ Chrome ตัว GUI ใช้ WPF ซึ่งผมคิดว่าดีกว่า platform ใหม่ที่ Microsoft เปิดตัวใหม่อย่าง UWP(Universal windows plateform) ซะอีก(จริงๆ มันคนละเป้าหมายล่ะนะ)

รายละเอียดการเขียน

MVVM pattern

เป็น pattern การเขียนโปรแกรมของ WPF หรือแม้กระทั่ง platform อื่นที่สามารถนำไปประยุกต์ได้ รายละเอียดมีประมาณนี้

Model เป็นตัวข้อมูลที่จะถูกแสดงใน View ซึ่งส่วนใหญ่ผม implement INotifyPropertyChanged ไว้เพื่อทำ data binding ผ่าน ViewModel ตรงๆ เมื่อข้อมูลใน Model เปลี่ยนก็จะมีการแจ้งเตือนและทำให้ View อัพเดทข้อมูลตามไปด้วย นอกจากนี้ยังมี event OnDownloadComplete อีกเพื่อใช้สำหรับการจัดคิวต่อ

ViewModel เป็นส่วนที่ช่วยแสดงผล Model ใน View ซึ่ง 1 ViewModel จะมี 1 View ที่ตรงกันอยู่(บางอันมี 2) ที่ส่วนนี้ก็จะมีพวก event handler อยู่ด้วยซึ่งใช้ระบบ event ในแบบของ framework ที่ผมใช้อยู่

View เป็นส่วน GUI ที่เราเห็นจริงๆ ตรงนี้ผมเขียนแต่ xaml ล้วนๆ

MVVM Infrastructure framework ที่ ผมใช้คือ Caliburn Micro ซึ่งใช้งานง่ายมาก แต่การที่เน้น convention มากกว่า configuration อาจจะทำให้คนที่ไม่รู้เรื่อง WPF เลยแล้วมาเริ่มต้นงงซักเลยน้อย(ผมก็ด้วย) ดังนั้นถ้าจะใช้ WPF อย่างน้อยผมขอให้อ่านบทความ XAML Overview (WPF) หรือหาหนังสือสอน WPF มาอ่านก่อน

การโหลดไฟล์

ส่ง http request file ไปที่ server และ อ่าน response stream แบบ asynchronous จากนั้นก็เขียนลงเครื่องโดยตรง

[c-sharp] var request = (HttpWebRequest)HttpRequest.Create(url);
var response = await request.GetResponseAsync();
var inStream = response.GetResponseStream();
byte[] buffer = new byte[102400];
int readbyte;

using (FileStream outStream = new FileStream(_currentItem.FullName, FileMode.Open, FileAccess.Write, FileShare.None, 65536))
{
do {
readbyte = await inStream.ReadAsync(buffer, 0, buffer.Length);
await outStream.WriteAsync(buffer, 0, readbyte);
while {
}

[/c-sharp]

ซึ่งวิธีนี้เราจะได้ Stream มาซึ่งทำให้เราสามารถวัดความเร็วการดาวน์โหลด, ดาวน์โหลดต่อจากที่หยุดไว้และที่สำคัญที่สุดคือจำกัดความเร็วได้
วิธีที่ง่ายกว่านี้คือใช้ WebClient ซึ่งมี method DownloadFile ให้อยู่แล้ว วิธีนี้เราจะได้เปอเซนต์ที่เราดาวน์โหลดไปแล้ว, จำนวนข้อมูลที่เรารับมา(รับมาแล้วกี่ byte)และจำนวนข้อมูลทั้งหมดที่จะถูกดาวน์โหลดผ่านทาง event DownloadProgressChanged ตัวอย่าง

[c-sharp] public Task DownloadFile(string uri, string locationAndFileName)
{
DownloadProgressChanged += DownloadProgressCallback;
return await DownloadFileAsync(uri, locationAndFileName);
}

private static void DownloadProgressCallback(object sender, DownloadProgressChangedEventArgs e)
{
// Displays the operation identifier, and the transfer progress.
Console.WriteLine("{0} downloaded {1} of {2} bytes. {3} % complete…",
(string)e.UserState,
e.BytesReceived,
e.TotalBytesToReceive,
e.ProgressPercentage);
}
[/c-sharp]

event นี้จะใช้ได้ก็ต่อเมื่อเราใช้ asynchronous api รายละเอียดตามนี้ครับ WebClient.DownloadProgressChanged

อีกเรื่องของการดาวน์โหลดคือการดาวน์โหลดแบบพร้อมกัน ถ้าไม่เคยรู้จัก asynchronous มาก่อนอาจคิดว่าต้องใช้ Thread แต่ในความเป็นจริงแล้วเราใช้ asynchronous api ได้ โดยไม่ต้องใช้ Thread ให้ยุ่งยากเลยดังตัวอย่างด้านบน การใช้ Thread จะเริ่มยุ่งยากขึ้นเมื่อเราต้องการหยุดการทำงานของ Thread นั้น เนื่องจากจะมีบัคตามมาขึ้นอยู่กับว่าเราใช้ api อะไรไปบ้าง ดังนั้นผมไม่แนะนำให้ใช้ Thread ตรงๆ ยกเว้นแต่ต้องใช้จริงๆ เช่น ต้องการตั้ง ApartmentState(มันคืออะไร – -)

ทำ Queue

ใช้ Queue(เป็น collection แบบหนึ่ง) เก็บ model ที่ต้องการดาวน์โหลดเอาไว้ และมี method ตัวหนึ่งทำหน้าที่เป็นตัวสั่งการดาวน์โหลดเองในตอนต้นและเป็น event handler ให้กับ model ที่การดาวน์โหลดเสร็จแล้วในภายหลัง(ไม่ว่าจะเสร็จสมบูรณ์, มี error และอื่นๆ) model ภายใน Queue จะถูกถึงออกมาดาวน์โหลดเรื่อยๆ จนกว่าจะหมดและจะไม่เพิ่มการดาวน์โหลดหากถึงจำนวนสูงสุดที่อนุญาติให้ดาวน์โหลดแล้ว
รายละเอียดโค้ดตรงนี้ยาวมากๆ ขอให้ไปดูรายละเอียดที่ Downloader เลยครับ

เก็บ Credential(username กับ password ของ website)

ผมเก็บไว้ใน Credential Manager ของ Windows  ซึ่งผมใช้ CredentialManagement  library ในการเข้าใช้งาน Credential Manager ตัวอย่างโค้ดก็ตามนี้ครับ AuthorizationManager และอีกส่วนที่ใช้ในการหาว่า url นี้จะใช้ credential อันไหนก็ตามนี้เลยครับ FileHostingUtil (อยู่ใน method FillCredential)

ในตอนนี้นั้นผมยังไม่ได้ทำให้โปรแกรมปลอดภัย วิธีทำให้ปลอยภัยขึ้นอีกก็ไม่ยาก เวลาเราจะให้ผู้ใช้ใส่รหัส ให้เราใช้ PasswordBox แทน TextBox ซึ่ง PasswordBox นั้นจะมี property ชื่อ SecurePassword อยู่ซึ่งเป็น SecureString จากนั้นเวลาจะสร้าง Credential หรือเก็บลงเครื่อง ให้ใช้ method หรือ constructor ที่รับ password เป็น SecureString เท่านั้น ห้ามเปลี่ยน SecureString เป็น string โดยเด็ดขาด ไม่อย่างงั้นก็จบเกม

สาเหตุที่เป็นชื่อนี้เป็นเพราะว่าตอนแรกนั้นต้องการจะทำโปรแกรมที่ใช้เปิดหาอนิเมะบนเว็บ Tirkx ได้โดยตรงโดยไม่ต้องเปิด web browser และไม่ใช้ API จากเว็บ Tirkx เองด้วย เพราะช่วงนั้นอยากลองใช้ html agility pack ในการอ่านหน้าเว็บไซต์และนำข้อมูลมาแสดง ซึ่งท้ายที่สุดก็ต้องยอมแพ้เนื่องจากส่วนที่แสดงไฟล์ให้ดาวน์โหลดนั้น เป็น javascript ซึ่งจำเป็นต้องใช้ web browser มา interpret มันออกมา ซึ่งช้าและใช้ทรัพยากรเครื่องมากเกินไป ท้ายที่สุดจึงทำออกมาเป็นโปรแกรม download manager ไว้ใช้ดาวน์โหลดไฟล์ทั้งหมดซะเลย

การใช้ Parameters ในการสร้าง query string สำหรับใช้กับ MySql Connector Net C# เบื้องต้น

สวัสดีครับ วันนี้จะมาแนะนำการใช้ parameter เพื่อสร้าง query string สำหรับใช้ในโปรแกรมที่เขียนด้วย C#  และใช้ MySql Connector Net เบื้องต้นนะครับ

ความสำคัญของการใช้ parameter คือเพื่อป้องกันการโจมตีแบบ  SQL Injection ซึ่งเป็นการใส่ command เข้ามาพร้อมกัน query ที่ถูกสร้างขึ้น สังเกตุการส้าง query string ด้านล่างนี้นะครับ

[code language=”csharp”] string query = "SELECT * FROM recipe WHERE name = " + name;
[/code]

จะเห็นได้ว่าเราได้สร้าง query string ขึ้นมาจากการรวมข้อความกับตัวแปรที่ค่าในตัวแปรนั้นอาจจะเป็นอะไรก็ตามแต่ที่ผู้ใช้งานป้อนเข้ามาซึ่งอาจจะเป็น query ที่ใช้โจมตีเราด้วยก็เป็นไปได้ ดังนั้นถ้าเราใช้ parameter

[code language=”csharp”] string query = "SELECT * FROM recipe WHERE name = @name";
[/code]

ในส่วนของ parameter จะถูกมองว่าเป็นเพียงแค่ฟิลด์ข้อมูลเท่านั้น(หมายความว่าส่วนของ parameter จะไม่ถูก execute) ดังนั้นการใช้ parameter จึงป้องกัน SQL injection ได้

สำหรับการใช้ parameter กับ MySql Connector Net นั้นก็ไม่ได้ยากอะไร ก่อนอื่นขอแนะนำส่วนที่เกี่ยวข้องก่อน

  • MySqlCommand.Parameters เป็น collection ที่เก็บ instance ของ Parameter class
  • Parameter class เก็บข้อมูล parameter เพื่อให้ MySqlCommand ใช้
  • MySqlCommand.Parameters.AddWithValue(string parameterName, object value) สร้าง Parameter class พร้อมกับระบุชื่อและค่าของ parameter และเก็บเข้าสู่ Parameters collection

ทั้งหมดที่จำเป็นก็มีเพียงเท่านี้ จริงๆ แล้วเราสามารถกำหนดได้ถึง ชนิดข้อมูลและขนาดของ parameter ผ่านการสร้าง Parameter class จากนั้นก็ Add parameter class เข้าสู่ Parameters collection อีกที แต่จากการทดสอบพบว่าไม่ผลอะไรกับการที่ไม่ได้กำหนดชนิดและขนาดของข้อมูล(ตัวอักษรเก็บได้กว้างสูงสุดเท่าที่เรากำหนดไว้อยู่แล้ว ตัวเลขส่วนตัวทดสอบแล้วเจอ exception ตั้งแต่ข้อมูลเกินตัวแปร int) ดังนั้นใช้ AddWithValue จะสะดวกกว่า

อย่างแรกคือการสร้าง query string ที่ระบุข้อมูลเป็นชื่อ parameter ก่อน โดยชื่อ parameter นั้นให้เติม prefix “@” ไว้

[code language=”csharp”] string query = "SELECT * FROM recipe WHERE name = @name";
[/code]

ต่อไปก็ทำการเพิ่ม parameter ให้กับ Parameters collection โดยต้องระบุชื่อ parameter ให้ตรงกับค่าที่ต้องการ

ข้อสังเกตเล็กน้อยคือเราจะไม่สามารถเพิ่ม Parameter ที่มีชื่อเหมือนกับ Parameter ที่มีอยู่ก่อนใน Parameters collection ได้ หรือก็คือไม่สามารถตั้งชื่อ parameter ซ้ำกันได้นั่นเอง ต่อไปจะเป็นตัวอย่างต่างๆ ในการใช้ parameter นะครับ

ใช้แบบปกติ

ผมใช้ query เดียวภายใน method ไม่ได้เปลี่ยน query เพื่อรันภายใน method ต่อโดยใช้ MySqlCommand เดิมแต่อย่างใด

[code language=”csharp”] public bool Update(int ID, int quantity)
{
try
{
string query = "UPDATE recipe SET quantity = @quantity WHERE recipe_id = @id";

if (OpenConnection())
{
MySqlCommand cmd = new MySqlCommand(query, connection);
cmd.Parameters.AddWithValue("@quantity", quantity);
cmd.Parameters.AddWithValue("@id", ID);
cmd.ExecuteNonQuery();
CloseConnection();

return true;
}
}
catch (MySqlException ex)
{
MessageBox.Show(ex.Message + Environment.NewLine + ex.StackTrace, "Error number : " + ex.Number,
MessageBoxButtons.OK, MessageBoxIcon.Information);

return false;
}

return false;
}
[/code]

ใช้ใน Loop

เนื่องจากเราไม่สามารถเพิ่ม parameter ที่มีชื่อซ้ำกันเข้าไปภายใน MySqlCommand.Parameters.Clear() ในการลบ parameter เดิมภายใน collection ออกและทำการเพิ่มเข้าไปใหม่ หรือเราสามารถเปลี่ยนค่า parameter ผ่าน index ตรงๆ เลยก็ได้เช่น MySqlCommand.Parameters[i].Value = value; หรือ MySqlCommand.Parameters[“@parameterName”].Value = value; แต่ผมขอเลือกใช้วิธีการลบ parameter เดิมออก (ด้านล่างนี้เป็นตัวอย่างจริงๆ)

[code language=”csharp”] public void testmethod(string name, List<string>[] data)
{
if (OpenConnection())
{
string query = "INSERT INTO some_table (name, id, value)" +
"VALUES(@name, @id, @value)";
MySqlCommand cmd = new MySqlCommand(query, connection);

for (int i = 0; i < data[0].Count; i++)
{
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@id", data[0][i]);
cmd.Parameters.AddWithValue("@value", data[1][i]);
cmd.CommandText = query;
cmd.ExecuteNonQuery();
cmd.Parameters.Clear();
}
}
}
[/code]

จากตัวอย่างล่าสุดนี้สามารถปรับไปใช้แบบกำปรับค่า parameter ผ่าน index ก็ได้ ลักษณะจะเป็นประมาณนี้

[code language=”csharp”] public void testmethod(string name, List<string>[] data)
{
if (OpenConnection())
{
string query = "INSERT INTO some_table (name, id, value)" +
"VALUES(@name, @id, @value)";
MySqlCommand cmd = new MySqlCommand(query, connection);
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@id", data[0][0]);
cmd.Parameters.AddWithValue("@value", data[1][0]);

for (int i = 0; i < data[0].Count; i++)
{
cmd.Parameters["@id"].Value = data[0][i];
cmd.Parameters["@value"].Value = data[1][i];
cmd.CommandText = query;
cmd.ExecuteNonQuery();
}
}
}
[/code]

2 ตัวอย่างล่าสุดสามารถเขียนในแบบอื่นๆ ได้อีกหลายแบบ สามารถดูรายละเอียดได้จาก MySqlCommand Members ทั้งหมดนี้ก็เป็นพื้นฐานการใช้ parameter กับ MySqlConnector net ครับ ถ้ามีคำถามหรือผมอธิบายอะไรผิดไป คอมเม้นบอกกันได้เลยนะครับ