Model-View-ViewModel
data-binding ทำให้ View sync กับ ViewModel โดยอัตโนมัติ
ปัญหาที่ MVVM แก้คือปัญหาเดียวกับ MVC และ MVP: code UI (event handler, การอ่าน/เขียนค่าใน control, การเปิด/ปิดปุ่ม) กับ business/presentation logic ถูกเขียนปนกันอยู่ใน code-behind ของหน้าจอ ทำให้ทดสอบยาก แก้ไข UI แล้วเสี่ยงพัง logic และนักออกแบบ UI กับนักพัฒนาต้องแตะ file เดียวกัน
ทางแก้ของ MVC คือให้ Controller คุม flow ทางแก้ของ MVP คือให้ Presenter ถือ view ผ่าน interface แล้วสั่งอัปเดตแบบ imperative ส่วน MVVM เสนอทางที่สาม — เสนอโดย John Gossman วิศวกรของ Microsoft ใน block ปี 2005 สำหรับ WPF/Silverlight (mvvmlight, Wikipedia) — คือให้ ViewModel ไม่รู้จัก View เลยแม้แต่ interface แล้วปล่อยให้ binding engine ของ platform เป็นคน sync ค่าให้เองทั้งสองทาง ViewModel เพียงแค่ expose property และ command แบบ plain object แล้ว raise event PropertyChanged เมื่อค่าที่ expose เปลี่ยน ส่วนเรื่อง “ใครไปอ่านใครไปเขียน” เป็นหน้าที่ของ data-binding infrastructure ล้วน ๆ
แนวคิดนี้มีรากมาจาก Presentation Model ที่ Martin Fowler เขียนไว้ปี 2004 ในชุดบทความ GUI Architectures — Presentation Model คือ abstraction ของ view ที่ synchronize ตัวเองกับ view จริงอยู่ตลอด ต่างกันตรงที่ MVVM ผูกแนวคิดนี้เข้ากับกลไก data-binding ของ platform โดยตรง (Gossman ตั้งชื่อว่า ViewModel เพราะมันคือ “Model of the View”) MVVM จึงกลายเป็น pattern เริ่มต้นของ platform ที่มี binding engine ในตัว — WPF, WinUI, .NET MAUI, Blazor, และในโลก JS ก็ Angular/Vue/Knockout ก็ยึดแนวคิดเดียวกัน
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class View {
+DataContext ViewModel
}
class ViewModel {
-Model model
+string Property
+ICommand SaveCommand
+PropertyChanged event
}
class Model {
+Data
+BusinessRules
}
class BindingEngine {
+Sync
}
View --> BindingEngine
BindingEngine --> ViewModel
ViewModel o-- Model
ลูกศรจาก View ไป BindingEngine สื่อว่า View ประกาศ binding ไว้ใน markup (XAML/HTML) เท่านั้น ไม่มี code imperative คอยอ่าน-เขียนค่า — binding engine เป็นตัวกลางที่คอยอ่าน PropertyChanged จาก ViewModel แล้วอัปเดต control ให้ และคอยฟัง event ของ control (เช่นพิมพ์ใน textbox) แล้วเขียนกลับเข้า property ของ ViewModel ให้เอง ViewModel เองไม่ถือ reference ไปยัง View เลยแม้แต่นิดเดียว ต่างจาก MVP ที่ Presenter ถือ IView ไว้ตรง ๆ
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”ผู้เล่นหลักสามฝ่าย:
- Model — ข้อมูลและ business logic ล้วน ๆ ไม่รู้จัก View หรือ ViewModel เลย
- ViewModel — ห่อ Model ให้อยู่ในรูปที่ View ใช้ง่าย (แปลงชนิดข้อมูล, รวมค่าหลาย property, expose
ICommandแทน event handler) และ implementINotifyPropertyChanged(สำหรับ collection ใช้ObservableCollection<T>) เพื่อแจ้ง binding engine ว่ามีอะไรเปลี่ยน - View — ประกาศ binding ใน XAML/HTML ผูก control เข้ากับ property/command ของ ViewModel ผ่าน
BindingContext/DataContextcode-behind ควรมีแค่ logic ที่แสดงผลล้วน ๆ (เช่น animation) ไม่ใช่ business logic
ลำดับเหตุการณ์เมื่อผู้ใช้พิมพ์ในช่อง input แล้วกดปุ่ม Save:
sequenceDiagram
participant U as User
participant V as View
participant B as BindingEngine
participant VM as ViewModel
participant M as Model
U->>V: พิมพ์ค่าใหม่ในช่อง input
V->>B: two-way binding แจ้งค่าที่เปลี่ยน
B->>VM: set Property
U->>V: กดปุ่ม Save
V->>B: binding เรียก Command
B->>VM: SaveCommand.Execute
VM->>M: บันทึกข้อมูลผ่าน Model
VM-->>B: raise PropertyChanged
B-->>V: อัปเดต control ที่ bind ไว้อัตโนมัติ
จุดสำคัญคือ ViewModel ไม่เคย “สั่ง” View ให้ทำอะไรแบบตรง ๆ เลย — มันแค่เปลี่ยนสถานะของตัวเองแล้ว raise event บอกว่าเปลี่ยนแล้วเท่านั้น ส่วนปุ่มกดก็ไม่ผูกกับ event handler ปกติ แต่ผูกกับ ICommand ที่ ViewModel expose ไว้ ซึ่งมีทั้ง Execute (ทำงานจริง) และ CanExecute (บอกว่าปุ่มควร enable ไหม) ทำให้แม้แต่ enable/disable ปุ่มก็ทำผ่าน data-binding ไม่ใช่ code imperative ใน code-behind
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// Model — ไม่รู้จัก ViewModel หรือ View เลยpublic class Order{ public string CustomerName { get; set; } = ""; public decimal Total { get; set; }}
// ViewModel — expose property/command ให้ View bind, ไม่ถือ reference ไปยัง Viewpublic class OrderViewModel : INotifyPropertyChanged{ private readonly Order _order; private readonly IOrderRepository _repository; private string _customerName; private bool _isSaving;
public OrderViewModel(Order order, IOrderRepository repository) { _order = order; _repository = repository; _customerName = order.CustomerName; SaveCommand = new RelayCommand( execute: async () => await SaveAsync(), canExecute: () => !_isSaving && !string.IsNullOrWhiteSpace(CustomerName)); }
// property ที่ View bind สองทาง (two-way binding) public string CustomerName { get => _customerName; set { if (_customerName == value) return; // อย่า raise ถ้าค่าไม่เปลี่ยน _customerName = value; OnPropertyChanged(); SaveCommand.RaiseCanExecuteChanged(); // ปุ่ม Save อาจ enable/disable ตามค่านี้ } }
public bool IsSaving { get => _isSaving; private set { _isSaving = value; OnPropertyChanged(); } }
public ICommand SaveCommand { get; } // View bind ปุ่ม Save เข้ากับ command นี้ ไม่ใช่ event handler
private async Task SaveAsync() { IsSaving = true; _order.CustomerName = CustomerName; await _repository.SaveAsync(_order); // เรียก Model/service เท่านั้น ไม่แตะ UI IsSaving = false; }
public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));}<!-- View (XAML) — ประกาศ binding เท่านั้น ไม่มี code สั่งอัปเดต control --><StackPanel DataContext="{Binding OrderViewModel}"> <TextBox Text="{Binding CustomerName, Mode=TwoWay}" /> <Button Content="Save" Command="{Binding SaveCommand}" /> <ProgressBar IsIndeterminate="{Binding IsSaving}" /></StackPanel>สังเกตว่า OrderViewModel ทดสอบได้ทั้งหมดโดยไม่ต้อง render UI จริงเลย — สร้าง instance แล้วเรียก SaveCommand.Execute() ตรง ๆ ในยูนิต test ได้ทันที
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- platform มี data-binding engine ให้ในตัว (WPF, WinUI, .NET MAUI, Blazor, Angular, Vue) — ใช้ MVVM ตรงกับกลไกของ platform ที่สุด
- ต้องการแยก presentation logic ออกมาทดสอบเป็นยูนิต test ได้เต็มที่โดยไม่ต้องพึ่ง UI framework
- ทีมมี designer กับ developer แยกกัน อยากให้ทำงานคู่ขนานบน View (XAML) กับ ViewModel ได้โดยกระทบกันน้อย
- UI ซับซ้อน มี state หลายอย่างที่ต้อง sync กับหลาย control พร้อมกัน (enable/disable, validation message, progress)
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- platform ไม่มี data-binding ในตัว (หรือ binding อ่อนแอ) — จะกลายเป็นต้องเขียนกลไก sync เองจนเสียเปรียบ MVP
- หน้าจอเรียบง่ายมาก มี state น้อย — ภาระของ
INotifyPropertyChanged/command wiring อาจเกินความจำเป็น (Gossman เองก็เตือนว่า MVVM overkill สำหรับ UI ง่าย ๆ) - app ที่ data-binding จำนวนมากกระทบ performance อย่างมีนัยสำคัญ (เช่น list ขนาดใหญ่ที่ผูก binding ต่อ item จำนวนมาก) ต้องชั่งน้ำหนักกับแนวทางที่ควบคุม rendering ตรง ๆ มากกว่า
- ทีมต้อง debug data flow อย่างละเอียด — binding ที่ทำงาน “โดยนัย” ผ่าน engine อาจตามรอย bug ยากกว่า code imperative ของ MVP
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | ข้อดี | ข้อเสีย |
|---|---|---|
| Testability | ViewModel ทดสอบได้เต็มที่โดยไม่ต้องมี UI | ต้องมี discipline ไม่ให้ business logic รั่วเข้า code-behind |
| Coupling | ViewModel ไม่รู้จัก View เลยแม้แต่ interface — หลวมกว่า MVP | บาง state (เช่น focus, animation) ผูกกับ binding ยาก ต้องใช้ behavior/attached property ช่วย |
| Productivity | designer กับ developer ทำงานคู่ขนานได้จริง | เส้นทางข้อมูลที่ “ไม่ปรากฏใน code” (ผ่าน binding markup) debug ยากกว่า call stack ปกติ |
| Boilerplate | ลด code event-handler ซ้ำ ๆ ใน code-behind | property ทุกตัวต้องมี boilerplate ของ INotifyPropertyChanged/ICommand (framework เช่น MVVM Toolkit ช่วยลดได้) |
| Performance | เหมาะกับ UI ที่มี state sync เยอะ | binding จำนวนมากอาจมี overhead ที่ MVP แบบ imperative ไม่มี |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- MVC — ต้นตระกูลที่ MVP และ MVVM แตกแขนงมา
- MVP — ทางเลือกที่ presenter ถือ view ผ่าน interface แบบ imperative แทน binding อัตโนมัติ
- Observer — กลไกเบื้องหลัง
PropertyChanged/data-binding คือรูปแบบหนึ่งของ Observer - Command —
ICommandที่ ViewModel expose ให้ View bind คือการประยุกต์ Command pattern - Dependency Injection — ViewModel มักรับ service/repository ผ่าน constructor เพื่อให้ทดสอบและสลับ implementation ได้
- Separation of Concerns — หลักการที่ MVVM (เช่นเดียวกับ MVC/MVP) ยึดเป็นแกนกลาง