meekob

พัฒนา Silverlight Project ด้วย MVVM

เรตติ้ง
เขียนโดย Suwitcha Chandhorn เมื่อวันที่ 19 April 2009 ตอน 11:32

ในการพัฒนา Rich Internet Application ด้วย Silverlight ถึงแม้ว่ากระบวนการทำงานจะถูกออกแบบมาเพื่อให้นักพัฒนา และนักออกแบบสามารถทำงานร่วมกันได้ โดยแบ่งการทำงานออกเป็นส่วนตามความถนัดของแต่ละฝ่าย แต่นักพัฒนาส่วนใหญ่ ยังคงทำการเขียนโค้ดลงในส่วน Code-Behind ของ UserControl เป็นหลัก เนื่องจากมีความสะดวก รวดเร็ว จึงเหมาะกับการพัฒนาโครงการขนาดเล็ก อย่างไรก็ดี การเขียนโค้ดแบบนี้มักจะขาดประสิทธิภาพในการพัฒนาโครงการขนาดใหญ่ ซึ่งมักจะมีนักพัฒนาหลายคนร่วมกันพัฒนา จะทำให้แบ่งส่วนการทำงานไม่ชัดเจน และการทดสอบโค้ดแต่ละส่วน (Unit-Testing) ก็ทำได้ยากด้วยเช่นกัน เพื่อให้การทำงานเป็นไปอย่างคล่องตัว และการพัฒนามีความยืดหยุ่น MVVM จึงเป็นวิธีการหนึ่งซึ่งนักพัฒนาในต่างประเทศนิยมใช้ในการแก้ปัญหา

MVVM หรือ Model View ViewModel เป็น Design Pattern ชนิดหนึ่งซึ่งได้รับอิทธิพลจาก Model View Controller (MVC) Pattern ซึ่งจะแบ่งการเขียนโค้ดออกเป็น 3 ส่วนหลักๆ กล่าวคือ

1. Model เป็นโค้ดส่วนที่แทนโครงสร้างของข้อมูลที่อยู่เบื้องหลังแอพพลิเคชั่น

2. View เป็นส่วนที่เกี่ยวข้องกับส่วนติดต่อผู้ใช้ ซึ่งมักจะเป็นส่วนที่นักออกแบบต้องทำงานด้วยมากที่สุด จึงมักจะเป็นส่วนของ XAML ล้วนๆ และจะมีส่วนที่เป็นโค้ดน้อยที่สุด โดยมากมักจะเป็นโค้ดที่เกี่ยวกับการทำ Data-Binding เพื่อระบุตำแหน่งที่จะแสดงผลของข้อมูลเท่านั้น

3. ViewModel เป็นส่วนที่ใช้เชื่อมระหว่าง Model และ View เป็นส่วนที่ดึงข้อมูลตาม Model จากแหล่งข้อมูลต่างๆ (เช่น เว็บเซอร์วิส หรือฐานข้อมูล) มาเตรียมเพื่อแสดงผลใน View

ที่จริงแล้วยังไม่มีการกำหนดลำดับก่อนหลังให้กับการพัฒนาแบบนี้อย่างเป็นทางการ นักพัฒนาอาจจะเริ่มจากการพัฒนา Model ก่อน แล้วจึงพัฒนา ViewModel หรือ พัฒนาตาม View ที่นักออกแบบเตรียมไว้ให้ก็ได้ ในตัวอย่างที่จะยกต่อไปนี้ จะเริ่มจาก Model ก่อน แล้วจึงพัฒนา ViewModel และนำไปรวมกับ View เป็นลำดับสุดท้าย เพื่อให้เห็นลำดับชั้นการทำงานได้อย่างชัดเจนยิ่งขึ้น

มาดูโจทย์ของเรากันเลยดีกว่า

เพื่อให้ง่ายกับการทำความเข้าใจ เราจะทำแอพพลิเคชั่นง่ายๆ เช่น แบบทดสอบคณิตศาสตร์ขึ้นมา แบบทดสอบที่ว่า จะมีการแสดงโจทย์ เช่น 1+1=? และมีช่องคำตอบซึ่งเป็น TextBox เตรียมไว้ให้ เมื่อผู้ใช้กรอกคำตอบก็จะไปตรวจสอบว่าคำตอบถูกต้องหรือไม่ แล้วแสดงผลบอกผู้ใช้ทาง CheckBox ท้ายที่สุดผู้ใช้สามารถกดปุ่มเพื่อส่งผลไปยังเซอร์ฟเวอร์เพื่อบันทึกคำตอบได้ แอพพลิเคชั่นนี้จะเน้นที่การแบ่งส่วนการทำงานเป็นลำดับชั้น ดังนั้นจะไม่นำเสนอโค้ดส่วนที่ไม่เกี่ยวข้องต่างๆ เช่น การตรวจสอบรูปแบบของคำตอบ เป็นต้น เพื่อไม่ให้เกิดความสับสน ก่อนอื่น ให้เราเปิด Visual Studio ขึ้นมาเพื่อสร้าง Silverlight Project กันก่อน (ผู้เขียนไม่ขอกล่าวถึงขั้นตอนการติดตั้ง Silverlight Templates บน Visual Studio เนื่องจากเป็นเรื่องพื้นฐาน ขอให้ผู้อ่านดูขั้นตอนการติดตั้งได้จาก http://silverlight.net/GetStarted/)

p1

เริ่มจาก Model กันก่อน

ดังที่กล่าวไปแล้วข้างต้น Model เป็นส่วนที่ใช้แทนโครงสร้างข้อมูล ดังนั้นในที่นี้จึงเป็นคลาสที่มีคุณสมบัติ (Properties) ต่างๆของข้อมูลคำถามอยู่ Model จะอิมพลีเมนท์ INotifyPropertyChanged เพื่อให้คลาสที่นำออปเจ็คของมันไปใช้ทราบเมื่อเกิดการเปลี่ยนแปลงของข้อมูลภายใน

เมื่อสร้าง Silverlight Project เรียบร้อยแล้ว ให้เราสร้างโฟลเดอร์ชื่อ Model ขึ้นมา จากนั้นเพิ่มคลาสใหม่ชื่อ Question เพื่อใช้บรรจุข้อมูลคำถาม คลาส Question นี้จะอิมพลีเมนท์อินเทอร์เฟส INotifyPropertyChanged ซึ่งจะบังคับให้คลาสจะต้องมีอีเวนท์ที่ชื่อ PropertyChanged เกิดขึ้น จากนั้นเราจะเพิ่มเมธอดชื่อ RaisePropertyChanged เข้าไป เพื่อใช้ช่วยในการสร้างอีเวนท์เมื่อเกิดการเปลี่ยนแปลงของข้อมูล

    1 Imports System.ComponentModel

    2

    3 Public Class Question

    4 Implements INotifyPropertyChanged

    5

    6 Public Event PropertyChanged(ByVal sender As Object, _

    7 ByVal e As System.ComponentModel.PropertyChangedEventArgs) _

    8 Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

    9

   10 Private Sub RaisePropertyChanged(ByVal propertyName As String)

   11 RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))

   12 End Sub

   13 End Class

จากนั้นเราจะเพิ่มคุณสมบัติต่างๆของคำถามเข้าไปในคลาส เช่น Text แทนตัวคำถาม, ProvidedAnswer แทนคำตอบที่ผู้ใช้จะป้อนเข้ามา, ActualAnswer แทนคำตอบที่ถูกต้อง และ IsCorrectAnswer แทนความถูกต้องของคำตอบ

    3 Public Class Question

    4 Implements INotifyPropertyChanged

    5

    6 Private _text As String

    7 Public Property Text() As String

    8 Get

    9 Return _text

   10 End Get

   11 Set(ByVal value As String)

   12             _text = value

   13 End Set

   14 End Property

   15

   16 Private _actualAnswer As String

   17 Public Property ActualAnswer() As String

   18 Get

   19 Return _actualAnswer

   20 End Get

   21 Set(ByVal value As String)

   22             _actualAnswer = value

   23 End Set

   24 End Property

   25

   26 Private _providedAnswer As String

   27 Public Property ProvidedAnswer() As String

   28 Get

   29 Return _providedAnswer

   30 End Get

   31 Set(ByVal value As String)

   32             _providedAnswer = value

   33             RaisePropertyChanged("ProvidedAnswer")

   34             RaisePropertyChanged("IsCorrectAnswer")

   35 End Set

   36 End Property

   37

   38 Public Property IsCorrectAnswer() As Boolean

   39 Get

   40 Return (ActualAnswer = ProvidedAnswer)

   41 End Get

   42 Set(ByVal value As Boolean)

   43             RaisePropertyChanged("IsCorrectAnswer")

   44 End Set

   45 End Property

จะสังเกตว่าข้อมูลที่จะมีการเปลี่ยนแปลงจากอินพุตของผู้ใช้ (หรือจากโค้ด) เช่น ProvidedAnswer และ IsCorrectAnswer เมื่อถูก Set จะเรียกไปยังเมธอด RaisePropertyChanged ด้วย เพื่อแจ้งให้สิ่งที่เรียกใช้ออปต์เจ็คตัวนี้ทราบเมื่อมีการเปลี่ยนแปลงของข้อมูลนั่นเอง เมื่อสร้างคลาสนี้เสร็จ เราก็จะไปเรียกใช้งานมันจากส่วน ViewModel กันต่อในลำดับถัดไป

หน้าที่ของ ViewModel

หน้าที่ของ ViewModel ก็คือกาวประสานระหว่างอินเทอร์เฟส (View) กับข้อมูล (Model) ดังนั้นมันจะทำหน้าที่ต่างๆ เช่น การเรียกข้อมูลจากแหล่งข้อมูลต่างๆมาเก็บไว้ใน Model เพื่อให้ตรงกับ View ที่ต้องการนำไปแสดงผล หรือจะบันทึกข้อมูลที่ได้จาก View กลับลงไปยังแหล่งข้อมูลที่กำหนดก็ได้เช่นกัน ให้เราสร้างโฟลเดอร์ชื่อ ViewModel ขึ้นมา จากนั้นเพิ่มคลาส QuestionViewModel เข้าไปภายใน เนื่องจากเราจะเรียกใช้ ObservableCollection ในคลาส จึงต้องอิมพอร์ต System.Collections.ObjectModel ไว้ที่ต้นคลาสด้วย จากนั้นเราจะเพิ่มคุณสมบัติ Questions ซึ่งเป็นลิสต์ของ ObservableCollection(Of Question) เข้าไปเพื่อเก็บรายการคำถาม ObservableCollection เป็นลิสต์ของออปต์เจ็คที่จะแจ้งเตือนทุกครั้งเมื่อมีการเพิ่ม, ลบรายการ หรือมีการรีเฟรชลิสต์ ดังนั้นเมื่อมีการเปลี่ยนแปลงค่าในคำถามแต่ละข้อ แอพพลิเคชั่นก็จะได้รับการอัพเดตโดยอัตโนมัติ

    1 Imports System.Collections.ObjectModel

    2

    3 Public Class QuestionViewModel

    4

    5 Private _questions As ObservableCollection(Of Question)

    6 Public Property Questions() As ObservableCollection(Of Question)

    7 Get

    8 Return _questions

    9 End Get

   10 Set(ByVal value As ObservableCollection(Of Question))

   11             _questions = value

   12 End Set

   13 End Property

   14

   15 End Class

จากนั้นจะเราจะเพิ่มเมธอด FetchQuestions() เพื่อเรียกข้อมูลมาแสดง ในตัวอย่างนี้เราจะสร้างข้อมูลตัวอย่างขึ้นมาเพื่อความสะดวก แต่ในความเป็นจริงเราอาจเรียกข้อมูลจากแหล่งข้อมูลอื่นมาแสดงได้ตามที่ต้องการ จากนั้นนำลิสต์ของข้อมูลที่ได้ไปเก็บไว้ในคุณสมบัติ Questions เพื่อรอให้ View นำไปใช้

   15 Public Sub FetchQuestions()

   16

   17 Dim q As New ObservableCollection(Of Question)

   18

   19 ' You may get actual data from the database

   20         q.Add(New Question With {.Text = "1 + 1 = ?", .ActualAnswer = "2"})

   21         q.Add(New Question With {.Text = "1 + 2 = ?", .ActualAnswer = "3"})

   22

   23         Questions = q

   24

   25 End Sub

เมื่อเสร็จในส่วนนี้เราก็จะมี ViewModel พร้อมให้ใช้งานแล้ว จะสังเกตได้ว่า โค้ดทั้งสองส่วนนี้ จะถูกออกแบบให้สามารถทำ Unit-Testing ได้ โดยไม่ผูกพันกับอินเทอร์เฟส ทำให้การพัฒนามีความถูกต้องแม่นยำมากขึ้น ในส่วนต่อไปเราจะไปดูการทำงานของ View และการนำข้อมูลจาก ViewModel ไปใช้กัน

ส่วนติดต่อผู้ใช้ใน View

ส่วนของ View เป็นส่วนติดต่อผู้ใช้ที่โดยมากนักออกแบบจะเป็นผู้รับผิดชอบ หากถูกออกแบบมาดี ก็มักจะมีโค้ดน้อยและเน้นไปในเรื่องของดีไซน์ ซึ่งในที่นี้ก็คือการใช้ Declarative Markup เช่น XAML เสียเป็นส่วนใหญ่ จะมีโค้ดมาเกี่ยวข้องอยู่บ้างก็แต่ในส่วนของการทำ Data-Binding ของข้อมูลไปยังตำแหน่งต่างๆที่จะแสดงผล เราจะสร้างโฟลเดอร์ชื่อ View ขึ้นแล้วเพิ่ม Silverlight UserControl ชื่อ QuestionView.xaml เข้าไปในโฟลเดอร์นั้น เพื่อใช้แสดงรายการคำถาม

    1 <UserControl x:Class="SLMVVM1.QuestionView"

    2     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    3     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    4     Width="400" Height="300">

    5     <Grid x:Name="LayoutRoot" Background="White">

    6     <StackPanel Orientation="Vertical">

    7         <ItemsControl ItemsSource="{Binding Path=Questions}">

    8             <ItemsControl.ItemTemplate>

    9                 <DataTemplate>

   10                 <StackPanel Orientation="Horizontal">

   11                     <TextBlock x:Name="QuestionText"

   12                                Text="{Binding Path=Text}"></TextBlock>

   13                     <TextBox x:Name="QuestionAnswer"

   14                              Text="{Binding Path=ProvidedAnswer, Mode=TwoWay}">

   15 </TextBox>

   16                     <CheckBox x:Name="GradeCheckBox"

   17                               IsChecked="{Binding Path=IsCorrectAnswer}"></CheckBox>

   18 </StackPanel>

   19 </DataTemplate>

   20 </ItemsControl.ItemTemplate>

   21 </ItemsControl>

   22 </StackPanel>

   23 </Grid>

   24 </UserControl>

โดยปรกติ หากเราต้องการจะผูกข้อมูลเข้ากับส่วนติดต่อผู้ใช้ เราจะกำหนดแหล่งข้อมูลให้กับ DataContext ของ View ก่อน จากนั้นจึงจะกำหนดตำแหน่งของข้อมูลด้วยแท็ก “{Binding}” โดยกำหนด Path ให้ตรงกับข้อมูลที่จะแสดง ในที่นี้เรากำหนดรายการคำถามให้กับ StackPanel ผ่านทาง ItemSource ส่วนค่าของแต่ละฟิลด์ที่จะถูกนำไปผูกกับคอนโทรลต่างๆตามชื่อของข้อมูล ก็จะได้รายการคำถามมาแสดง ในส่วนของ DataContext นั้นเราจะไปกำหนดเมื่อเราเรียกใช้ UserControl นี้ในส่วนถัดไป อย่างไรก็ดีจะสังเกตได้ว่า ในส่วนของ View ที่เราสร้างขึ้นมาใหม่นี้ ยังไม่มีการเขียนโค้ดเลย และส่วนของ DataBinding ก็ทำไปเพื่อกำหนดตำแหน่งของข้อมูลที่จะนำมาแสดงเท่านั้น

    1 <UserControl x:Class="SLMVVM1.Page"

    2     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    3     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    4     xmlns:views="clr-namespace:SLMVVM1"

    5     Width="400" Height="300">

    6     <Grid x:Name="LayoutRoot" Background="White">

    7         <views:QuestionView x:Name="QuestionDataView" />

    8 </Grid>

    9 </UserControl>

p2

จากนั้นเราจะนำ View ที่สร้างไปใส่ไว้ใน UserControl ที่เป็นหน้าหลัก ซึ่งในที่นี้ก็คือ Page.xaml ซึ่งถูกสร้างมาเมื่อเราสร้าง Silverlight Project ใหม่ โดยกำหนด xmlns ชื่อ views ให้ จากนั้นเพิ่ม UserControl เข้าไปตามปรกติ (นักออกแบบอาจใช้ Microsoft Expression Blend 2 ในการเพิ่ม UserControl นี้เข้ามาก็ได้) จากนั้นเพิ่มโค้ดเพื่อโหลดออปต์เจ็คมาเก็บไว้ใน DataContext ให้กับ View ในส่วนฟังก์ชั่น Page_Loaded ใน Code-Behind

    8 Private Sub Page_Loaded(ByVal sender As Object, _

    9 ByVal e As System.Windows.RoutedEventArgs) _

   10 Handles Me.Loaded

   11

   12 Dim qdata = New QuestionViewModel()

   13         qdata.FetchQuestions()

   14         QuestionDataView.DataContext = qdata

   15

   16 End Sub

จากนั้นกด F5 เพื่อทำการ Build ทดสอบ Visual Studio จะเปิดเบราเซอร์ขึ้นมาและแสดงรายการคำถาม เมื่อเรากรอกตัวเลขคำตอบใน TextBox ที่เตรียมไว้ให้ ค่าของออปต์เจ็คคำถามซึ่งอยู่ในลิสต์ใน DataContext ก็จะเปลี่ยนแปลง ทำให้เกิดอีเวนท์ PropertyChanged ของคำถาม ขึ้น ObservableCollection ของคำถามที่อยู่ใน DataContext ก็จะแจ้งให้ View รับรู้และเปลี่ยนแปลงค่าของข้อมูลที่ผูกไว้อีกทอดหนึ่งโดยอัตโนมัติ เช่น หากเรากรอกคำตอบ 2 ให้กับคำถามข้อแรก ซึ่งเป็นคำตอบที่ถูกต้อง คุณสมบัติ ProvidedAnswer ของคำถามจะมีการเปลี่ยนแปลง และมีการเรียกเมธอด RaisePropertyChanged เพื่อสร้างอีเวนท์ไปแจ้งให้กับ ObsevableCollection ได้รู้ เมื่อ ObservableCollection รับรู้, DataContext ของ View ก็จะรู้การเปลี่ยนแปลงและทำการเรียกค่าใหม่มาแสดง ซึ่งเมื่อเรียกไปยังค่า IsCorrectAnswer ของคำถามก็จะพบค่าใหม่ จึงทำการปรับปรุงคุณสมบัติ IsChecked ใน CheckBox ซึ่งเราผูกข้อมูลไว้ โดยเปลี่ยนค่าจากเดิม False ไปเป็น True ทำให้ CheckBox แสดงเครื่องหมายถูกขึ้นมา เป็นต้น

การบันทึกข้อมูลที่เปลี่ยนแปลง

จากรายการคำถามหากเราต้องการบันทึกข้อมูลคำตอบของผู้ใช้ก็สามารถทำได้ผ่าน View และ ViewModel โดยง่าย โดยการเพิ่มเมธอดในการบันทึกเข้าไปใน ViewModel และเพิ่มเมธอดเพื่อสั่งให้บันทึกในส่วน Code-Behind ของ View ในที่นี้จะเราจะเพิ่มเมธอด SubmitChanges() เข้าไปใน ViewModel ดังนี้

   27 Public Sub SubmitChanges()

   28

   29 ' Persist this ViewModel to the database here.

   30 Dim correctQuestions As Integer = 0

   31 For Each q In Questions

   32 If q.IsCorrectAnswer Then correctQuestions += 1

   33 Next

   34         MessageBox.Show(correctQuestions)

   35

   36 End Sub

โค้ดในฟังก์ชั่นจะแสดง MessageBox บอกจำนวนคำถามที่ผู้ใช้ตอบถูก โดยดึงข้อมูลจากลิสต์ Questions ในออปต์เจ็คเดียวกัน จากนั้นให้เพิ่มปุ่ม Submit เข้าไปใน QuestionView.xaml ดังนี้

<Button x:Name="SubmitButton" Content="Submit" Click="SubmitButton_Click"/>

แล้วเพิ่มเมธอด SubmitButton_Click ในส่วน Code-Behind ของ QuestionView.xaml ดังนี้

    9 Private Sub SubmitButton_Click(ByVal sender As System.Object, _

   10 ByVal e As System.Windows.RoutedEventArgs)

   11

   12 Dim data As QuestionViewModel = CType(Me.DataContext, QuestionViewModel)

   13         data.SubmitChanges()

   14

   15 End Sub

ใน View จะปรากฎปุ่มเพื่อให้เราส่งข้อมูลขึ้นมา เมื่อผู้ใช้กดก็จะทำการดึงออปต์เจ็คที่อยู่ใน DataContext ออกมาแล้วเรียกเมธอด SubmitChanges() ในออปต์เจ็คนั้น เมื่อเมธอดนี้ถูกเรียกก็จะทำการแสดง MessageBox ตามที่เราต้องการออกมา นักพัฒนาอาจจะปรับปรุงโค้ดส่วนนี้ให้ทำการส่งข้อมูลกลับไปบันทึกยังฐานข้อมูลบนเซอร์ฟเวอร์ได้เช่นกัน

บทสรุป

จากตัวอย่างที่ผ่านข้างต้นคงพอจะอธิบายโครงสร้างง่ายๆของการพัฒนา Silverlight ด้วย Model View ViewModel Pattern ให้ผู้อ่านได้เข้าใจพอสมควรแล้ว ในการพัฒนางานจริง นักพัฒนาอาจเลือกวิธีการที่ซับซ้อนยิ่งขึ้น เช่น การเลือกใช้อินเทอร์เฟสช่วยในการกำหนดโครงสร้างในส่วน ViewModel เพื่อให้สามารถสับเปลี่ยนแหล่งที่มาของข้อมูลได้ง่าย หรือปรับปรุงการทำงานในส่วน ViewModel ให้มีความซับซ้อนยิ่งขึ้นตามความต้องการ อย่างไรก็ดี นักพัฒนาก็จะสามารถแยกส่วนการทำงานระหว่างอินเทอร์เฟสและโค้ดออกมาเพื่อพัฒนาและทำการทดสอบได้สะดวกยิ่งขึ้น รวมถึงสามารถจัดการแบ่งส่วนงานได้อย่างมีประสิทธิภาพยิ่งขึ้นอีกด้วย

 

c4rking said:

เยี่ยมครับ อธิบายละเอียดและเข้าใจง่ายครับ

October 20, 2009 5:10 PM

Leave a Comment

(required)  
(optional)
(required)  
Add