WP7 Tutorial : #2 Silverlight, XAML, DataContext!?!?

กระทู้สนทนา
image

คุณยังรู้สึกว่า ตอนนี้เขียนโปรแกรมทำอะไรที่ต้องแสดงข้อมูลที เป็นเรื่องยุ่งยากน่าเบื่อหรือเปล่า? แล้วทำไม คุณ Pandula ถึงสามารถเขียนโปรแกรม Twitter ได้ง่ายมากๆ บน WP7? ลองมาดูกันว่า WP7/Silverlight มีอะไรที่ช่วยให้ชีวิตเราง่ายขึ้นไม่น่าเบื่อขึ้นบ้างกันดีกว่า

*** ขออภัยท่านผู้อ่านครับ ผมกำลังพยายามหาทางใส่ Source Code ที่อ่านง่ายกว่านี้อยู่ครับ ***
*** Tool สำหรับบทความนี้ โหลดได้จาก ตาม Link ในโพสนี้

ผมว่า ทุกท่านคงจะต้องเคยเขียนโค๊ดกันแบบนี้

this.txtName.Text = MyDataObject.Name;

หรือไม่ก็.

MyDataObject.Name = this.txtName.Text;

หรือไม่ก็

foreach( var item in DataLayer.GetCustomer() )
{
     this.ListView1.Items.Add( );

}

หรือแม้แต่การใช้ Object Data Source ก็ตาม

แต่ในการพัฒนาโปรแกรมด้วย Silverlight รวมไปถึง Windows phone 7 นั้น เราจะไม่ได้ใช้วิธีการแบบเดิมๆ ในการติดต่อกับข้อมูลแบบนั้นอีกแล้ว เพราะ WP7/Silverlight นั้น มีความสามารถด้านการใช้งานข้อมูลที่แจ่มกว่านั้นแบบ Built-in ที่เรียกว่า Binding ครับ

Binding?

แปลกันตรงๆ ก็คือการผูกครับ ถ้าลองกลับไปดู Code ใน WP7 Tutorial #1 จะเห็นว่า คุณ pandula แทบไม่ได้เขียนโค๊ดเลย แต่สามารถแสดงผล Tweet ออกมาเป็นรายการสวยงามได้ พร้อมรูปประกอบ (ดังรูป) เขาทำได้อย่างไรกัน?

06-2

ลองดู Hilight ของ Code XAML ซึ่งหน้าตาคล้ายกับ จากบทความของคุณ pandula ครับ

image

สังเกตที่คำว่า Text=Binding UserName และ Text=Binding Message มันเป็นการบอกว่า เราจะผูก Property Text ของ TextBlock (คือ Label แบบหนึ่ง) เข้ากับ UserName และ Message ครับ แล้ว Username กับ Message มาจากไหนละ? ก็คลาสที่คุณ pandula สร้างไว้นั่นเองครับ

image

แล้วเราเอา TwitterItem หลายๆ อัน ใส่เข้า ListBox ตอนไหน??? ก็คือตอนที่ WebClient ทำการ Download ข้อมูลจาก Twitter เสร็จแล้ว คุณ Pandula ก็ทำการสร้างรายการของ TwitterItem ด้วย LINQ (ที่เห็นเป็น LINQ-to-XML)

image

NOTE: ถ้าสงสัยว่า อะไรคือ LINQ มันคือภาษาในการเลือกข้อมูลออกมาจาก Object ครับ ลักษณะคล้าย SQL ผมได้เขียนแนะนำไว้ มีด้วยกัน สองตอน  ซึ่งคุณ chaowman ก็ได้เขียน Tutorial แปลจากภาษา SQL เป็น LINQ ไว้แล้วด้วย สำหรับผู้ที่คล่อง SQL ตามอ่านได้จากบทความของคุณ chaowman ตอนที่ 1,ตอนที่ 2, ตอนที่ 3 ครับ

เดี๋ยวนะ ทำไมทำโปรแกรม แล้วมี Xml ด้วยละ

นั่นก็เพราะว่า ต่อไปนี้ โปรแกรม จะไม่ได้มีแค่ Code อย่างเดียวแล้วครับ เพราะหลักฐานทางวิทยาศาสตร์ ซึ่งก็ได้แก่โปรแกรมทั้งหลายที่เราเขียนกันอยู่ ได้พิสูจน์แล้วว่า โปรแกรมเมอร์ ไม่อาจสร้างโปรแกรมที่ดูดีได้ ในขณะที่ยังสามารถส่งได้ทันเวลา และมี Feature ครบได้ ด้วย Code เพียงอย่างเดียว มันจะต้องให้มือดีอย่าง Designer เขาเข้ามาช่วย

image

จะเห็นว่า เมื่อเราสร้าง Project ขึ้นมาใหม่ด้วย Windows phone Tool หน้า Main ของเรา มันจะไม่ได้มีแค่ .cs แล้ว แต่จะมีไฟล์ .xaml คู่กันด้วย โดยใน .xaml ก็จะมีแค่ Markup (xml) ที่กำหนดว่า หน้าตาโปรแกรมจะออกมาเป็นอย่างไร และ .cs ก็จะมีแต่ Code ที่จะกำหนดว่า โปรแกรมจะทำงานอย่างไร เห็นภาพแล้วใช่ไหมครับ? Designer ก็แก้ไฟล์ .xaml ไป เราก็แก้ไฟล์ .cs ของเราไป ไม่ต้องยุ่งเกี่ยวกัน แต่ว่าทำงานร่วมกันได้ คนนึงออกแบบหน้าจอ คนนึงกำหนดว่า หน้าจอนั้น จะทำงานได้อย่างไร

ทดลองเล่นกับ xaml ดูครับ ลองลาก Listbox ออกมาวางไว้ จะเห็นว่า ใน Xaml ก็จะมี Tag Listbox ปรากฏขึ้น

image

และถ้าเราทำการแก้ไข Property ใดๆ ก็ตาม เช่น การเพิ่ม Items เข้าใน Listbox เราก็จะเห็นว่า มี Tag เกิดขึ้นตามมาด้วย

image

เตรียมพร้อม Project เราให้รับ DataBinding

การที่จะใช้ DataBinding ได้อย่างสะดวกเต็มที่ และทำให้เกิดการแยกระหว่าง UI และ Code ได้จริงๆ เราจะต้องมีการเตรียมการเล็กน้อยครับ ซึ่งเทคนิคหนึ่งที่ผมชอบใช้ ก็คือสิ่งที่เรียกว่า Data Island เป็น การสร้างคลาสที่เป็นศูนย์รวมของข้อมูลที่เราจะใช้ในโปรแกรมนั่นเองครับ

เช่น ถ้าโปรแกรม Twitter ของเรา ที่ใช้ Username และ List ของ Twitter เราก็อาจสร้างคลาสที่เป็น Data Island ได้ ดังนี้

ProgramData.cs (Data Island)

  1. using System;
  2. using System.Net;
  3. using System.Collections.Generic;
  4. using System.ComponentModel;
  5. namespace WindowsPhoneApplication2
  6. {
  7. public class ProgramData : INotifyPropertyChanged
  8.     {
  9. #region (Relaxed) Singleton Pattern
  10. private static ProgramData _Default;
  11. ///
  12. /// Get the default instance of ProgramData
  13. ///
  14. public static ProgramData Default
  15.         {
  16. get
  17.             {
  18. if ( _Default == null )
  19.                 {
  20.                     _Default = new ProgramData();
  21.                 }
  22. return _Default;
  23.             }
  24.         }
  25. #endregion
  26. private string _Username;
  27. private List _Tweets;
  28. ///
  29. ///Get or set the value of Username
  30. ///
  31. public string Username
  32.         {
  33. get
  34.             {
  35. return _Username;
  36.             }
  37. set
  38.             {
  39.                 _Username = value;
  40. this.OnPropertyChanged("Username");
  41.             }
  42.         }
  43. ///
  44. ///Get or set the value of Tweets
  45. ///
  46. public List Tweets
  47.         {
  48. get
  49.             {
  50. return _Tweets;
  51.             }
  52. set
  53.             {
  54.                 _Tweets = value;
  55. this.OnPropertyChanged("Tweets");
  56.             }
  57.         }
  58. #region INotifyPropertyChanged Members
  59. public event PropertyChangedEventHandlerPropertyChanged;
  60. protected void OnPropertyChanged(string propertyName)
  61.         {
  62. if (PropertyChanged != null)
  63.             {
  64.                 PropertyChanged(this,
  65. newPropertyChangedEventArgs(propertyName));
  66.             }
  67.         }
  68. #endregion
  69.     }
  70. }

การสร้างคลาสที่เป็น INotifyPropertyChanged จะเป็นการสร้างคลาส ที่รองรับการ Binding โดยสมบูรณ์ เพราะว่า คลาสนี้ สามารถส่ง Event ออกมาบอกชาวโลกได้ว่า มีอะไรเปลี่ยนแปลง (Event PropertyChanged) โดยคอนโทรลต่างๆ ที่เป็น UI เมื่อ Bind เข้ากับ Property ใดๆ แล้ว มันก็จะรอฟัง Event นี้ นั่นเอง

ดังนั้น แทนที่เราจะดาวน์โหลด Twitter แล้ว Return ตรงๆ เราก็จะทำแบบนี้แทน

ProgramService.cs

  1. using System;
  2. using System.Windows;
  3. using System.Net;
  4. using System.Linq;
  5. using System.Xml.Linq;
  6. namespace WindowsPhoneApplication2
  7. {
  8. public static class ProgramService
  9.     {
  10. public static void DownloadTweet()
  11.         {
  12. WebClient client = new WebClient();
  13.             client.DownloadStringCompleted += delegate(object sender, DownloadStringCompletedEventArgs e)
  14.             {
  15. if ( e.Error != null )
  16.                 {
  17. MessageBox.Show( e.Error.Message );
  18. return;
  19.                 }
  20. ProgramData.Default.Tweets = (from tweet in XElement.Parse(e.Result).Descendants("status")
  21. select new TwitterItem
  22.                                               {
  23.                                                   AvatarUrl = tweet.Element("user").Element("profile_image_url").Value,
  24.                                                   Message = tweet.Element("text").Value,
  25.                                                   UserName = tweet.Element("user").Element("name").Value
  26.                                               }).ToList(); // Read data now, not later
  27.             };
  28.             client.DownloadStringAsync(new Uri("http://api.twitter.com/1/statuses/user_timeline.xml?screen_name=" + ProgramData.Default.Username));
  29.         }
  30.     }
  31. }
ข้อสังเกต
  • ผมได้ทำการเซ็ท Tweets ลงไปใน ProgramData.Tweets โดยตรง ถึงการเซ็ทนี้ ก็จะทำให้เกิด Event PropertyChanged ปริยาย
  • การที่ผมต้อง .ToList และที่ต้องให้ ProgramData.Tweets เป็น List แทนที่จะเป็น IEnumberable เนื่องจาก LINQ เป็น Lazy Evaluation ครับ ผมอยากให้มีข้อมูลพร้อมเลยทันที สำหรับการแสดงผล ไม่ใช่ว่าต้องมาอ่านอีกที
  • Code ตั้งแต่บรรทัดที่ 14 ถึง 29 รันคนละ Thread กับ Thread UI ครับ ถ้าเราเขียนแบบปกติ ใช้คอนโทรลตรงๆ เราจะต้องระวัง แต่แบบนี้ ไม่ต้องเลย ตัว Framework มัน Handle เองอยู่แล้ว Smile

เปิด Blend ขึ้นมาซะ!

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

image

ซึ่งการจะเริ่มผูกทุกอย่างได้ เราจะต้องมี DataContext ซึ่งก็คือ แหล่งข้อมูล ที่เราจะผูกด้วย และบังเอิญเราก็มีแหล่งข้อมูลที่พร้อมใช้แล้วพอดี ก็ ProgramData นั่นเอง การใช้ DataContext ทำได้โดยการเลือกที่คอนโทรลตัวบนสุด แล้วกด New ที่ Property DataContext ทางขวามือ

image

แล้วเลือก ProgramData จากรายการที่มี (ถ้าไม่มี ลอง ไปที่เมนู Project แล้วสั่ง Build 1 ครั้ง)
image

เจ้า DataContext นี้ มันจะ Propagate ลงไปทุกคอนโทรล ที่อยู่ภายใต้ตัวที่เรา Set DataContext ดังนั้น การ Set ที่ตัวใหญ่สุด ก็คือการ Set DataContext สำหรับทั้งหน้านั่นเอง

จากนั้น ก็ถึงเวลาที่เราจะเริ่ม Bind กันละครับ สำหรับโปรแกรมของเรา ก็ควรจะมี Textbox หนึ่งตัว สำหรับใส่ชื่อ และ Listbox อีกตัว เพื่อแสดงรายการของ Tweet ที่โหลดมาได้ เริ่มจากการ Bind เจ้า Textbox ก่อนเลย กดเครื่องหมายสี่เหลี่ยม ด้านหลัง Property Text ครับ จะมีเมนูขึ้นมา ให้เราเลือก Data Binding

image

แล้วเลือกว่า จะ Bind จาก Data Context และใช้ Property Username

image

ก็จะเห็นข้อความใน Textbox เปลี่ยนเป็นค่าที่เราตั้งไว้ให้กับ Property ของ Username ทันที ซึ่งผมแอบไปเพิ่ม Code ให้มันเป็น default ส่วนของ Listbox นั้นทำแบบเดียวกัน แต่เป็นการ Bind Property ที่ชื่อ ItemsSource เข้ากับ Tweets ครับ ถ้าทดลองตั้งข้อมูลใน List ไว้ ข้อมูลจะปรากฏขึ้นใน Listbox ด้วย

image

Data Template

แต่ว่า การแสดงผล Tweet มันยังไม่สวยเหมือนของคุณ Pandula เลยใช่ไหมละ? ข้อดีอีกอย่างของ WP7/Silverlight ก็คือ เราสามารถกำหนดการแสดงผลของข้อมูลใน Listbox ได้ด้วย เราเรียกมันว่า Data Template ครับ การสร้าง Data Template นั้น มีหลายวิธี แต่หนึ่งในวิธีที่ผมพบว่าสะดวกก็คือ ใช้มันตรงๆ ในหน้านั้นเลย เริ่มจากการคลิ๊กขวา แล้วเข้าเมนู

image

แล้วก็ตั้งชื่อให้มัน

image

จากนั้น Blend ก็จะเข้าสู่โหมดการแก้ไข Template ซึ่งเราสามารถนำคอนโทรล วางลงไปใน Template ได้ตามปกติ แล้ว Blend ก็จะ Preview ให้ดูด้วยว่า เมื่อวางลงไปแล้ว หน้าตามันจะออกมาเป็นอย่างไร แน่นอนว่า คอนโทรลที่เราวางลงไปใน Template ก็ยังสามารถ Binding ได้ครับ

image

โดยตอนที่ Bind จะเห็นว่า จะเป็นการ Bind กับ TweetItem ซึ่งก็คือ Item แต่ละชิ้นภายที่ถูกส่งเข้ามายัง Listbox นั่นเอง

image

หลังจากแก้ไขจนหนำใจแล้ว ก็กด Breadcrumb ด้านบน เพื่อกลับสู่หน้าจอปกติ

image

Finishing Touch

คราวนี้ เราก็พร้อมที่จะให้โปรแกรมทำงานแล้ว เราอาจจะสร้างปุ่มขึ้นมา แล้วผูก Event Click เพื่อให้โปรแกรมโหลด Tweet ใหม่ ก็ได้

image

หลังจากเราตั้งชื่อ Event แล้วกด Enter Blend ก็จะเปิดหน้า Source มาให้แก้ไขได้ทันที เราจะพิมพ์จากใน Blend เลยก็ได้ครับ ไม่ว่ากัน แต่ถ้าลองรันดู จะเห็นว่า กดปุ่มไป ก็ไม่มีอะไรเกิดขึ้น แม้ว่าจะเขียนถูกแล้วก็ตาม ทำไมละ???

ข้อควรระวัง

อย่าลืมว่า ProgramService จะทำการ Assign ค่า ใส่ ProgramData.Default.Tweet ซึ่งเป็นคนละ Instance กันกับตัวที่ Blend สร้างให้ เราจะต้องทำการตั้งค่า DataContext ของหน้าจอนี้ เป็น ProgramData.Default ก่อนครับ ซึ่ง Code ที่สมบูรณ์ จะมีดังนี้

MainPage.cs

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using System.Windows.Documents;
  8. using System.Windows.Input;
  9. using System.Windows.Media;
  10. using System.Windows.Media.Animation;
  11. using System.Windows.Shapes;
  12. using Microsoft.Phone.Controls;
  13. namespace WindowsPhoneApplication2
  14. {
  15. public partial class MainPage : PhoneApplicationPage
  16. {
  17. public MainPage()
  18. {
  19. InitializeComponent();
  20. SupportedOrientations = SupportedPageOrientation.Portrait | SupportedPageOrientation.Landscape;
  21. this.DataContext = ProgramData.Default;
  22. }
  23. private void LoadButton_Click(object sender, System.Windows.RoutedEventArgs e)
  24. {
  25. // TODO: Add event handler implementation here.
  26. ProgramService.DownloadTweet();
  27. }
  28. }
  29. }

ยังไม่จบ

ตอนหน้า เราจะมาทำให้โปรแกรมนี้สมบูรณ์ขึ้น ด้วย Visual State Manager ครับ

แก้ไขข้อความเมื่อ
แสดงความคิดเห็น
โปรดศึกษาและยอมรับนโยบายข้อมูลส่วนบุคคลก่อนเริ่มใช้งาน อ่านเพิ่มเติมได้ที่นี่