MVVM - ลำดับชั้นและการนำทาง
เมื่อสร้างแอปพลิเคชัน MVVM โดยทั่วไปคุณจะแยกหน้าจอข้อมูลที่ซับซ้อนออกเป็นชุดของมุมมองหลักและมุมมองย่อยโดยที่มุมมองลูกจะอยู่ในมุมมองหลักในแผงควบคุมหรือตัวควบคุมคอนเทนเนอร์และสร้างลำดับชั้นของการใช้งานเอง
หลังจากแยกมุมมองที่ซับซ้อนแล้วไม่ได้หมายความว่าเนื้อหาย่อยแต่ละส่วนที่คุณแยกออกเป็นไฟล์ XAML ของตัวเองจำเป็นต้องเป็นมุมมอง MVVM
ส่วนของเนื้อหาเป็นเพียงโครงสร้างในการแสดงผลบางสิ่งบนหน้าจอและไม่สนับสนุนการป้อนข้อมูลหรือการจัดการใด ๆ โดยผู้ใช้สำหรับเนื้อหานั้น
อาจไม่จำเป็นต้องมี ViewModel แยกต่างหาก แต่อาจเป็น XAML แบบก้อนที่แสดงผลตามคุณสมบัติที่ผู้ปกครอง ViewModel เปิดเผย
สุดท้ายหากคุณมีลำดับชั้นของ Views และ ViewModels พาเรนต์ ViewModel สามารถกลายเป็นฮับสำหรับการสื่อสารเพื่อให้ ViewModel ลูกแต่ละตัวยังคงแยกออกจาก ViewModels ลูกอื่น ๆ และจากพาเรนต์ได้มากที่สุด
ลองดูตัวอย่างที่เราจะกำหนดลำดับชั้นอย่างง่ายระหว่างมุมมองต่างๆ สร้างโปรเจ็กต์ WPF Application ใหม่MVVMHierarchiesDemo
Step 1 - เพิ่มสามโฟลเดอร์ (Model, ViewModel และ Views) ลงในโปรเจ็กต์ของคุณ
Step 2 - เพิ่มคลาสลูกค้าและใบสั่งในโฟลเดอร์ Model, CustomerListView และ OrderView ในโฟลเดอร์ Views และ CustomerListViewModel และ OrderViewModel ในโฟลเดอร์ ViewModel ดังที่แสดงในภาพต่อไปนี้
Step 3- เพิ่มบล็อกข้อความทั้งใน CustomerListView และ OrderView นี่คือไฟล์ CustomerListView.xaml
<UserControl x:Class="MVVMHierarchiesDemo.Views.CustomerListView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Customer List View"/>
</Grid>
</UserControl>
ต่อไปนี้คือไฟล์ OrderView.xaml
<UserControl x:Class = "MVVMHierarchiesDemo.Views.OrderView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Order View"/>
</Grid>
</UserControl>
ตอนนี้เราต้องการบางสิ่งเพื่อโฮสต์มุมมองเหล่านี้และเป็นสถานที่ที่ดีสำหรับสิ่งนั้นใน MainWindow ของเราเนื่องจากเป็นแอปพลิเคชันที่เรียบง่าย เราต้องการตัวควบคุมคอนเทนเนอร์ที่เราสามารถวางมุมมองของเราและเปลี่ยนมุมมองในการนำทางได้ เพื่อจุดประสงค์นี้เราจำเป็นต้องเพิ่ม ContentControl ในไฟล์ MainWindow.xaml ของเราและเราจะใช้คุณสมบัติเนื้อหาและเชื่อมโยงเข้ากับการอ้างอิง ViewModel
ตอนนี้กำหนดเทมเพลตข้อมูลสำหรับแต่ละมุมมองในพจนานุกรมทรัพยากร ต่อไปนี้คือไฟล์ MainWindow.xaml สังเกตว่าเทมเพลตข้อมูลแต่ละรายการแมปชนิดข้อมูล (ประเภท ViewModel) กับมุมมองที่สอดคล้องกันอย่างไร
<Window x:Class = "MVVMHierarchiesDemo.MainWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content = "{Binding CurrentView}"/>
</Grid>
</Window>
เมื่อใดก็ตามที่โมเดลมุมมองปัจจุบันถูกตั้งค่าเป็นอินสแตนซ์ของ CustomerListViewModel โมเดลจะแสดง CustomerListView พร้อมกับ ViewModel ที่เชื่อมต่อ มันคือ ViewModel คำสั่งมันจะแสดงผล OrderView และอื่น ๆ
ตอนนี้เราต้องการ ViewModel ที่มีคุณสมบัติ CurrentViewModel และตรรกะและคำสั่งบางอย่างเพื่อให้สามารถสลับการอ้างอิงปัจจุบันของ ViewModel ภายในคุณสมบัติได้
มาสร้าง ViewModel สำหรับ MainWindow ชื่อ MainWindowViewModel เราสามารถสร้างอินสแตนซ์ของ ViewModel ของเราจาก XAML และใช้สิ่งนั้นเพื่อตั้งค่าคุณสมบัติ DataContext ของหน้าต่าง สำหรับสิ่งนี้เราจำเป็นต้องสร้างคลาสพื้นฐานเพื่อห่อหุ้มการใช้งาน INotifyPropertyChanged สำหรับ ViewModels ของเรา
แนวคิดหลักที่อยู่เบื้องหลังคลาสนี้คือการห่อหุ้มการใช้งาน INotifyPropertyChanged และจัดเตรียมวิธีการช่วยเหลือให้กับคลาสที่ได้รับเพื่อให้สามารถเรียกใช้การแจ้งเตือนที่เหมาะสมได้อย่างง่ายดาย ต่อไปนี้คือการนำคลาส BindableBase ไปใช้
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class BindableBase : INotifyPropertyChanged {
protected virtual void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
ตอนนี้ถึงเวลาเริ่มทำการเปลี่ยนมุมมองโดยใช้คุณสมบัติ CurrentViewModel ของเรา เราแค่ต้องการวิธีขับเคลื่อนการตั้งค่าของคุณสมบัตินี้ และเราจะสร้างมันขึ้นมาเพื่อให้ผู้ใช้สามารถสั่งไปที่รายชื่อลูกค้าหรือไปที่มุมมองคำสั่งซื้อ ขั้นแรกให้เพิ่มคลาสใหม่ในโครงการของคุณซึ่งจะใช้อินเทอร์เฟซ ICommand ต่อไปนี้คือการนำอินเทอร์เฟซ ICommand มาใช้
using System;
using System.Windows.Input;
namespace MVVMHierarchiesDemo {
public class MyICommand<T> : ICommand {
Action<T> _TargetExecuteMethod;
Func<T, bool> _TargetCanExecuteMethod;
public MyICommand(Action<T> executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
#region ICommand Members
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
T tparm = (T)parameter;
return _TargetCanExecuteMethod(tparm);
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime is
longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod((T)parameter);
}
}
#endregion
}
}
ตอนนี้เราจำเป็นต้องตั้งค่าการนำทางระดับบนสุดไปยัง ViewModels และตรรกะสำหรับการสลับนั้นควรอยู่ใน MainWindowViewModel สำหรับสิ่งนี้เราจะใช้วิธีการที่เรียกว่าการนำทางซึ่งรับปลายทางสตริงและส่งคืนคุณสมบัติ CurrentViewModel
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
สำหรับการนำทางของ Views ที่แตกต่างกันเราต้องเพิ่มปุ่มสองปุ่มในไฟล์ MainWindow.xaml ของเรา ต่อไปนี้คือการใช้งานไฟล์ XAML ที่สมบูรณ์
<Window x:Class = "MVVMHierarchiesDemo.MainWindow"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid x:Name = "NavBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
</Grid.ColumnDefinitions>
<Button Content = "Customers"
Command = "{Binding NavCommand}"
CommandParameter = "customers"
Grid.Column = "0" />
<Button Content = "Order"
Command = "{Binding NavCommand}"
CommandParameter = "orders"
Grid.Column = "2" />
</Grid>
<Grid x:Name = "MainContent" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}" />
</Grid>
</Grid>
</Window>
ต่อไปนี้คือการใช้งาน MainWindowViewModel ที่สมบูรณ์
using MVVMHierarchiesDemo.ViewModel;
using MVVMHierarchiesDemo.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class MainWindowViewModel : BindableBase {
public MainWindowViewModel() {
NavCommand = new MyICommand<string>(OnNav);
}
private CustomerListViewModel custListViewModel = new CustomerListViewModel();
private OrderViewModel orderViewModelModel = new OrderViewModel();
private BindableBase _CurrentViewModel;
public BindableBase CurrentViewModel {
get {return _CurrentViewModel;}
set {SetProperty(ref _CurrentViewModel, value);}
}
public MyICommand<string> NavCommand { get; private set; }
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
}
}
รับ ViewModels ทั้งหมดของคุณจากคลาส BindableBase เมื่อโค้ดด้านบนถูกคอมไพล์และดำเนินการคุณจะเห็นผลลัพธ์ต่อไปนี้
อย่างที่คุณเห็นเราได้เพิ่มปุ่มเพียงสองปุ่มและ CurrentViewModel บนหน้าต่างหลักของเรา หากคุณคลิกปุ่มใด ๆ ระบบจะนำทางไปยังมุมมองนั้น ให้คลิกที่ปุ่มลูกค้าและคุณจะเห็นว่า CustomerListView ปรากฏขึ้น
เราขอแนะนำให้คุณดำเนินการตามตัวอย่างข้างต้นในลักษณะทีละขั้นตอนเพื่อความเข้าใจที่ดีขึ้น