Model-View-ViewModel (MVVM) 是 XAML 應用程式的標準架構模式。本文透過完整的產品管理範例(CRUD),示範三層職責劃分:Model 處理資料存取與實體定義,ViewModel 暴露可繫結屬性與 ICommand 行為,View 純 XAML 聲明式繫結,零程式碼後置。
[DataContract(Name="product")]
public class Product : INotifyPropertyChanged {
private int _productId;
[DataMember(Name="productId", IsRequired=true, Order=1)]
public int ProductId {
get { return _productId; }
set { _productId = value; RaisePropertyChanged("ProductId"); }
}
private string _name;
[DataMember(Name="name", IsRequired=true, Order=2)]
public string Name {
get { return _name; }
set { _name = value; RaisePropertyChanged("Name"); }
}
private string _category;
[DataMember(Name="category", IsRequired=true, Order=3)]
public string Category {
get { return _category; }
set { _category = value; RaisePropertyChanged("Category"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string name) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
模擬記憶體資料庫,提供查詢、新增、更新、刪除方法:
public class ProductDatabase {
private List _products;
public List GetProducts(string name, string category) {
return _products.Where(p => p.Name.Contains(name) && p.Category.Contains(category)).ToList();
}
public Product Add(string name, string category) { /* 新增邏輯 */ }
public void Update(Product product) { /* 更新邏輯 */ }
public void Delete(Product product) { /* 刪除邏輯 */ }
}
public class ProductViewModel {
public ObservableCollection Products { get; set; } // 繫結集合
public Product Product { get; set; } // 查詢/新增用的暫存物件
private ProductDatabase _context = new ProductDatabase();
public ProductViewModel() {
Products = new ObservableCollection();
Product = new Product();
}
// 查詢命令
public ICommand GetProductsCommand { get { return new GetProductsCommand(this); } }
public void GetProducts(Product query) {
var list = _context.GetProducts(query.Name, query.Category);
Products.Clear();
foreach (var p in list) Products.Add(p);
}
// 新增命令
public ICommand AddProductCommand { get { return new AddProductCommand(this); } }
public void AddProduct(Product product) {
var newProduct = _context.Add(product.Name, product.Category);
Products.Insert(0, newProduct);
}
// 更新命令
public ICommand UpdateProductCommand { get { return new UpdateProductCommand(this); } }
public void UpdateProduct(Product product) {
product.Name += "U"; product.Category += "U"; // 示範修改
_context.Update(product);
}
// 刪除命令
public ICommand DeleteProductCommand { get { return new DeleteProductCommand(this); } }
public void DeleteProduct(Product product) {
_context.Delete(product);
Products.Remove(product);
}
}
每個命令封裝單一行為,CanExecute 控制按鈕啟用狀態:
// 新增命令 - 無參數
public class AddProductCommand : ICommand {
private ProductViewModel _vm;
public AddProductCommand(ProductViewModel vm) { _vm = vm; }
public bool CanExecute(object parameter) => true;
public event EventHandler CanExecuteChanged;
public void Execute(object parameter) => _vm.AddProduct(_vm.Product);
}
// 刪除命令 - 需選中項目
public class DeleteProductCommand : ICommand {
private ProductViewModel _vm;
public DeleteProductCommand(ProductViewModel vm) { _vm = vm; }
public bool CanExecute(object parameter) => parameter is Product; // 未選中時按鈕禁用
public event EventHandler CanExecuteChanged;
public void Execute(object parameter) => _vm.DeleteProduct((Product)parameter);
}
// 更新命令 - 需選中項目
public class UpdateProductCommand : ICommand { /* 同 DeleteProductCommand */ }
// 查詢命令 - 無參數
public class GetProductsCommand : ICommand { /* 同 AddProductCommand */ }
<Page ...>
<Grid Background="Transparent">
<StackPanel Margin="120 0 0 0">
<!-- 設定 DataContext 為 ViewModel -->
<StackPanel.DataContext>
<vm:ProductViewModel />
</StackPanel.DataContext>
<!-- 列表繫結 -->
<ListView x:Name="listView" ItemsSource="{Binding Products}" Width="300" Height="300">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" FontSize="14.667"/>
<TextBlock Text="{Binding Category}" FontSize="14.667" Margin="10 0 0 0"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- 查詢/新增輸入區 -->
<StackPanel Orientation="Horizontal" DataContext="{Binding Product}" Margin="0 10 0 0">
<TextBlock Text="Name:" VerticalAlignment="Center"/>
<TextBox Text="{Binding Name, Mode=TwoWay}" Width="200"/>
<TextBlock Text="Category:" VerticalAlignment="Center" Margin="20 0 0 0"/>
<TextBox Text="{Binding Category, Mode=TwoWay}" Width="200"/>
</StackPanel>
<!-- 命令按鈕 -->
<StackPanel Orientation="Horizontal" Margin="0 10 0 0">
<Button Content="查詢" Command="{Binding GetProductsCommand}" Margin="10 0 0 0"/>
<Button Content="新增" Command="{Binding AddProductCommand}" Margin="10 0 0 0"/>
<Button Content="更新" Command="{Binding UpdateProductCommand}"
CommandParameter="{Binding SelectedItem, ElementName=listView}" Margin="10 0 0 0"/>
<Button Content="刪除" Command="{Binding DeleteProductCommand}"
CommandParameter="{Binding SelectedItem, ElementName=listView}" Margin="10 0 0 0"/>
</StackPanel>
</StackPanel>
</Grid>
</Page>
ItemsSource="{Binding Products}":ObservableCollection 自動通知 UI 更新DataContext="{Binding Product}":建立子範圍,簡化輸入欄位繫結路徑Mode=TwoWay:輸入控制項雙向繫結CommandParameter="{Binding SelectedItem, ElementName=listView}":將選中項目傳遞給命令CanExecute 回傳 false 時按鈕自動禁用,無需手動控制 IsEnabled專案若需全域導航服務、訊息傳遞、ViewModel 定位器等進階基礎設施,建議安裝 MVVM Light Toolkit,其 Visual Studio 擴充包含專案範本與程式碼片段,大幅加速開發。
MVVM 核心在於:View 不知 Model 存在,ViewModel 不知 View 存在,雙向繫結與命令機制實現鬆耦合。本範例展示最精簡的手工實作,理解原理後可引入框架減少樣板程式碼。