ข้าม​ไป​ยัง​เนื้อหา

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) และ implement INotifyPropertyChanged (สำหรับ collection ใช้ ObservableCollection<T>) เพื่อ​แจ้ง binding engine ว่า​มี​อะไร​เปลี่ยน
  • View — ประกาศ binding ใน XAML/HTML ผูก control เข้า​กับ property/command ของ ViewModel ผ่าน BindingContext/DataContext code-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

// Model — ไม่รู้จัก ViewModel หรือ View เลย
public class Order
{
public string CustomerName { get; set; } = "";
public decimal Total { get; set; }
}
// ViewModel — expose property/command ให้ View bind, ไม่ถือ reference ไปยัง View
public 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
ด้านข้อดีข้อ​เสีย
TestabilityViewModel ทดสอบ​ได้​เต็ม​ที่​โดย​ไม่​ต้อง​มี UIต้อง​มี discipline ไม่​ให้ business logic รั่ว​เข้า code-behind
CouplingViewModel ไม่รู้จัก View เลย​แม้แต่ interface — หลวม​กว่า MVPบาง state (เช่น focus, animation) ผูก​กับ binding ยาก ต้อง​ใช้ behavior/attached property ช่วย
Productivitydesigner กับ developer ทำงาน​คู่​ขนาน​ได้​จริงเส้นทาง​ข้อมูล​ที่ “ไม่​ปรากฏ​ใน code” (ผ่าน binding markup) debug ยาก​กว่า call stack ปกติ
Boilerplateลด code event-handler ซ้ำ ๆ ใน code-behindproperty ทุก​ตัว​ต้อง​มี 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
  • CommandICommand ที่ ViewModel expose ให้ View bind คือ​การ​ประยุกต์ Command pattern
  • Dependency Injection — ViewModel มัก​รับ service/repository ผ่าน constructor เพื่อ​ให้​ทดสอบ​และ​สลับ implementation ได้
  • Separation of Concerns — หลักการ​ที่ MVVM (เช่น​เดียว​กับ MVC/MVP) ยึด​เป็น​แกน​กลาง