MVVM - คู่มือฉบับย่อ
วิธีที่ได้รับคำสั่งอย่างดีและอาจใช้ซ้ำได้มากที่สุดในการจัดระเบียบโค้ดของคุณคือการใช้รูปแบบ 'MVVM' Model, View, ViewModel (MVVM pattern) เป็นข้อมูลเกี่ยวกับแนวทางในการจัดระเบียบและโครงสร้างโค้ดของคุณเพื่อเขียนแอปพลิเคชันที่บำรุงรักษาทดสอบได้และขยายได้
Model - เพียงแค่เก็บข้อมูลและไม่มีส่วนเกี่ยวข้องกับตรรกะทางธุรกิจใด ๆ
ViewModel - ทำหน้าที่เชื่อมโยง / เชื่อมต่อระหว่าง Model และ View และทำให้สิ่งต่างๆดูสวยงาม
View - เพียงแค่เก็บข้อมูลที่จัดรูปแบบและมอบหมายทุกอย่างให้กับโมเดล
การนำเสนอแบบแยกส่วน
เพื่อหลีกเลี่ยงปัญหาที่เกิดจากการใส่ตรรกะของแอปพลิเคชันไว้ในโค้ดหลังหรือ XAML ควรใช้เทคนิคที่เรียกว่าการนำเสนอแบบแยกส่วน เรากำลังพยายามหลีกเลี่ยงสิ่งนี้โดยที่เราจะมี XAML และ code-behind พร้อมด้วยค่าต่ำสุดที่จำเป็นสำหรับการทำงานกับออบเจ็กต์ส่วนติดต่อผู้ใช้โดยตรง คลาสอินเทอร์เฟซผู้ใช้ยังมีรหัสสำหรับพฤติกรรมการโต้ตอบที่ซับซ้อนตรรกะของแอปพลิเคชันและอื่น ๆ อีกมากมายดังแสดงในรูปต่อไปนี้ทางด้านซ้าย
ด้วยการนำเสนอที่แยกจากกันคลาสอินเทอร์เฟซผู้ใช้จะง่ายกว่ามาก แน่นอนว่ามันมี XAML แต่โค้ดที่อยู่เบื้องหลังนั้นใช้งานได้จริง
ตรรกะของแอปพลิเคชันอยู่ในคลาสที่แยกต่างหากซึ่งมักเรียกว่าโมเดล
อย่างไรก็ตามนี่ไม่ใช่เรื่องราวทั้งหมด หากคุณหยุดตรงนี้คุณมีแนวโน้มที่จะทำซ้ำข้อผิดพลาดทั่วไปที่จะนำคุณไปสู่เส้นทางของความวิกลจริตที่ผูกข้อมูล
นักพัฒนาจำนวนมากพยายามใช้การผูกข้อมูลเพื่อเชื่อมต่อองค์ประกอบใน XAML โดยตรงกับคุณสมบัติในโมเดล
ตอนนี้บางครั้งมันก็โอเค แต่บ่อยครั้งที่มันไม่ใช่ ปัญหาคือโมเดลเกี่ยวข้องกับสิ่งที่แอปพลิเคชันทำและไม่เกี่ยวข้องกับวิธีที่ผู้ใช้โต้ตอบกับแอปพลิเคชัน
วิธีการนำเสนอข้อมูลของคุณมักจะแตกต่างจากวิธีการจัดโครงสร้างภายใน
ยิ่งไปกว่านั้นอินเทอร์เฟซผู้ใช้ส่วนใหญ่มีสถานะบางอย่างที่ไม่ได้อยู่ในรูปแบบแอปพลิเคชัน
ตัวอย่างเช่นหากอินเทอร์เฟซผู้ใช้ของคุณใช้การลากและวางสิ่งที่ต้องติดตามสิ่งต่างๆเช่นตอนนี้รายการที่ลากไปอยู่ที่ไหนลักษณะที่ปรากฏควรเปลี่ยนไปเมื่อเคลื่อนผ่านเป้าหมายที่ลดลงที่เป็นไปได้ เปลี่ยนเมื่อรายการถูกลากไป
สถานะประเภทนี้อาจมีความซับซ้อนอย่างน่าประหลาดใจและจำเป็นต้องได้รับการทดสอบอย่างละเอียด
ในทางปฏิบัติโดยปกติคุณต้องการให้คลาสอื่นนั่งอยู่ระหว่างอินเทอร์เฟซผู้ใช้และโมเดล สิ่งนี้มีสองบทบาทที่สำคัญ
ขั้นแรกจะปรับรูปแบบแอปพลิเคชันของคุณสำหรับมุมมองอินเทอร์เฟซผู้ใช้เฉพาะ
ประการที่สองเป็นที่ที่ตรรกะการโต้ตอบที่ไม่สำคัญใด ๆ อาศัยอยู่และด้วยเหตุนี้ฉันหมายถึงรหัสที่จำเป็นเพื่อให้อินเทอร์เฟซผู้ใช้ของคุณทำงานในแบบที่คุณต้องการ
รูปแบบ MVVM เป็นโครงสร้างที่ทันสมัยของรูปแบบ MVC ในที่สุดดังนั้นเป้าหมายหลักจึงยังคงเหมือนเดิมเพื่อให้มีการแยกที่ชัดเจนระหว่างลอจิกโดเมนและเลเยอร์การนำเสนอ นี่คือข้อดีและข้อเสียของรูปแบบ MVVM
ประโยชน์ที่สำคัญคือช่วยให้สามารถแยกระหว่างมุมมองและโมเดลได้อย่างแท้จริงนอกเหนือจากการแยกและประสิทธิภาพที่คุณได้รับจากการมีสิ่งนั้น สิ่งที่หมายความตามจริงก็คือเมื่อโมเดลของคุณจำเป็นต้องเปลี่ยนก็สามารถเปลี่ยนได้อย่างง่ายดายโดยไม่จำเป็นต้องมีมุมมองและในทางกลับกัน
มีสามสิ่งสำคัญที่เกิดจากการใช้ MVVM ซึ่งมีดังต่อไปนี้
การบำรุงรักษา
การแยกโค้ดประเภทต่างๆออกจากกันอย่างชัดเจนควรทำให้ง่ายต่อการเข้าสู่ส่วนที่ละเอียดกว่าและเน้นเฉพาะอย่างน้อยหนึ่งส่วนและทำการเปลี่ยนแปลงโดยไม่ต้องกังวล
นั่นหมายความว่าคุณยังคงคล่องตัวและก้าวไปสู่รุ่นใหม่ได้อย่างรวดเร็ว
ทดสอบได้
ด้วย MVVM โค้ดแต่ละชิ้นมีความละเอียดมากขึ้นและหากนำไปใช้อย่างถูกต้องการพึ่งพาภายนอกและภายในของคุณจะอยู่ในส่วนของโค้ดที่แยกจากส่วนต่างๆด้วยตรรกะหลักที่คุณต้องการทดสอบ
ทำให้ง่ายขึ้นมากในการเขียนการทดสอบหน่วยเทียบกับตรรกะหลัก
ตรวจสอบให้แน่ใจว่าทำงานได้ถูกต้องเมื่อเขียนและทำงานต่อไปแม้ว่าสิ่งต่างๆจะเปลี่ยนไปในการบำรุงรักษา
ความสามารถในการขยาย
บางครั้งมันทับซ้อนกับความสามารถในการบำรุงรักษาเนื่องจากขอบเขตการแยกที่สะอาดและโค้ดที่ละเอียดกว่า
คุณมีโอกาสที่ดีกว่าในการทำให้ชิ้นส่วนเหล่านั้นสามารถนำกลับมาใช้ใหม่ได้มากขึ้น
นอกจากนี้ยังมีความสามารถในการแทนที่หรือเพิ่มโค้ดชิ้นใหม่ที่ทำสิ่งที่คล้ายกันในตำแหน่งที่เหมาะสมในสถาปัตยกรรม
จุดประสงค์ที่ชัดเจนของรูปแบบ MVVM คือนามธรรมของมุมมองซึ่งช่วยลดจำนวนตรรกะทางธุรกิจในโค้ดเบื้องหลัง อย่างไรก็ตามต่อไปนี้เป็นข้อดีที่มั่นคงอื่น ๆ -
- ViewModel นั้นง่ายต่อการทดสอบหน่วยมากกว่าโค้ดเบื้องหลังหรือโค้ดที่ขับเคลื่อนด้วยเหตุการณ์
- คุณสามารถทดสอบได้โดยไม่ต้องใช้ระบบอัตโนมัติและการโต้ตอบ UI ที่น่าอึดอัดใจ
- เลเยอร์การนำเสนอและตรรกะอยู่คู่กันอย่างหลวม ๆ
ข้อเสีย
- บางคนคิดว่าสำหรับ UI แบบธรรมดา MVVM อาจเกินความจำเป็น
- ในกรณีที่ใหญ่กว่านั้นการออกแบบ ViewModel อาจทำได้ยาก
- การดีบักจะค่อนข้างยากเมื่อเรามีการเชื่อมโยงข้อมูลที่ซับซ้อน
รูปแบบ MVVM ประกอบด้วยสามส่วน - Model, View และ ViewModel นักพัฒนาส่วนใหญ่ในช่วงเริ่มต้นจะสับสนเล็กน้อยว่า Model, View และ ViewModel ควรมีหรือไม่ควรมีอะไรบ้างและความรับผิดชอบของแต่ละส่วนคืออะไร
ในบทนี้เราจะเรียนรู้ความรับผิดชอบของแต่ละส่วนของรูปแบบ MVVM เพื่อให้คุณเข้าใจอย่างชัดเจนว่าโค้ดประเภทใดไปที่ใด MVVM เป็นสถาปัตยกรรมแบบเลเยอร์สำหรับฝั่งไคลเอ็นต์ดังแสดงในรูปต่อไปนี้
เลเยอร์การนำเสนอประกอบด้วยมุมมอง
โลจิคัลเลเยอร์เป็นโมเดลมุมมอง
เลเยอร์การนำเสนอคือการรวมกันของวัตถุแบบจำลอง
บริการไคลเอ็นต์ที่ผลิตและคงอยู่ไม่ว่าจะเป็นการเข้าถึงโดยตรงในแอปพลิเคชันสองชั้นหรือผ่านทางบริการโทรเข้าและไปยังแอปพลิเคชันของคุณ
บริการไคลเอ็นต์ไม่ได้เป็นส่วนหนึ่งของรูปแบบ MVVM อย่างเป็นทางการ แต่มักใช้กับ MVVM เพื่อแยกส่วนเพิ่มเติมและหลีกเลี่ยงรหัสซ้ำ
ความรับผิดชอบของโมเดล
โดยทั่วไปโมเดลเป็นสิ่งที่เข้าใจง่ายที่สุด เป็นโมเดลข้อมูลฝั่งไคลเอ็นต์ที่รองรับมุมมองในแอปพลิเคชัน
ประกอบด้วยวัตถุที่มีคุณสมบัติและตัวแปรบางตัวที่มีข้อมูลในหน่วยความจำ
คุณสมบัติเหล่านั้นบางอย่างอาจอ้างอิงอ็อบเจ็กต์โมเดลอื่นและสร้างกราฟอ็อบเจ็กต์ซึ่งโดยรวมคืออ็อบเจ็กต์โมเดล
โมเดลอ็อบเจ็กต์ควรแจ้งการเปลี่ยนแปลงคุณสมบัติซึ่งใน WPF หมายถึงการผูกข้อมูล
ความรับผิดชอบสุดท้ายคือการตรวจสอบความถูกต้องซึ่งเป็นทางเลือก แต่คุณสามารถฝังข้อมูลการตรวจสอบความถูกต้องบนโมเดลอ็อบเจ็กต์ได้โดยใช้คุณลักษณะการตรวจสอบการผูกข้อมูล WPF ผ่านอินเทอร์เฟซเช่น INotifyDataErrorInfo / IDataErrorInfo
ดูความรับผิดชอบ
วัตถุประสงค์หลักและความรับผิดชอบของมุมมองคือการกำหนดโครงสร้างของสิ่งที่ผู้ใช้เห็นบนหน้าจอ โครงสร้างสามารถประกอบด้วยชิ้นส่วนแบบคงที่และแบบไดนามิก
ส่วนแบบคงที่คือลำดับชั้นของ XAML ที่กำหนดการควบคุมและโครงร่างของตัวควบคุมที่มุมมองประกอบด้วย
ส่วนไดนามิกก็เหมือนกับภาพเคลื่อนไหวหรือการเปลี่ยนแปลงสถานะที่กำหนดให้เป็นส่วนหนึ่งของมุมมอง
เป้าหมายหลักของ MVVM คือไม่ควรมีโค้ดอยู่ข้างหลังในมุมมอง
เป็นไปไม่ได้ที่จะไม่มีรหัสอยู่ในมุมมอง ในมุมมองคุณต้องมีตัวสร้างและการเรียกเพื่อเริ่มต้นองค์ประกอบ
แนวคิดก็คือโค้ดลอจิกการจัดการเหตุการณ์การดำเนินการและการจัดการข้อมูลไม่ควรอยู่ในโค้ดที่อยู่เบื้องหลังใน View
นอกจากนี้ยังมีโค้ดประเภทอื่น ๆ ที่ต้องอยู่ในโค้ดหลังโค้ดใด ๆ ที่จำเป็นต้องมีการอ้างอิงถึงองค์ประกอบ UI คือโค้ดดูโดยเนื้อแท้
ความรับผิดชอบของ ViewModel
ViewModel เป็นจุดหลักของแอปพลิเคชัน MVVM ความรับผิดชอบหลักของ ViewModel คือการให้ข้อมูลกับมุมมองเพื่อให้มุมมองนั้นสามารถใส่ข้อมูลนั้นบนหน้าจอได้
นอกจากนี้ยังช่วยให้ผู้ใช้สามารถโต้ตอบกับข้อมูลและเปลี่ยนแปลงข้อมูลได้
ความรับผิดชอบหลักอื่น ๆ ของ ViewModel คือการห่อหุ้มตรรกะการโต้ตอบสำหรับมุมมอง แต่ไม่ได้หมายความว่าตรรกะทั้งหมดของแอปพลิเคชันควรเข้าสู่ ViewModel
ควรสามารถจัดการลำดับการโทรที่เหมาะสมเพื่อให้สิ่งที่ถูกต้องเกิดขึ้นตามผู้ใช้หรือการเปลี่ยนแปลงใด ๆ ในมุมมอง
ViewModel ควรจัดการตรรกะการนำทางเช่นการตัดสินใจเมื่อถึงเวลาที่จะไปยังมุมมองอื่น
ในบทนี้เราจะเรียนรู้วิธีการใช้รูปแบบ MVVM สำหรับหน้าจอป้อนข้อมูลอย่างง่ายและแอปพลิเคชัน WPF ที่คุณอาจคุ้นเคยอยู่แล้ว
มาดูตัวอย่างง่ายๆที่เราจะใช้แนวทาง MVVM
Step 1 - สร้างโครงการแอปพลิเคชัน WPF ใหม่ MVVMDemo
Step 2 - เพิ่มสามโฟลเดอร์ (Model, ViewModel และ Views) ลงในโปรเจ็กต์ของคุณ
Step 3 - เพิ่มคลาส StudentModel ในโฟลเดอร์ Model และวางโค้ดด้านล่างในคลาสนั้น
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get {
return firstName;
}
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get {return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Step 4 - เพิ่มคลาส StudentViewModel อื่นในโฟลเดอร์ ViewModel และวางรหัสต่อไปนี้
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
Step 5 - เพิ่ม User Control (WPF) ใหม่โดยคลิกขวาที่โฟลเดอร์ Views และเลือก Add> New Item ...
Step 6- คลิกปุ่มเพิ่ม ตอนนี้คุณจะเห็นไฟล์ XAML เพิ่มรหัสต่อไปนี้ลงในไฟล์ StudentView.xaml ซึ่งมีองค์ประกอบ UI ที่แตกต่างกัน
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Step 7 - เพิ่ม StudentView ลงในไฟล์ MainPage.xaml ของคุณโดยใช้รหัสต่อไปนี้
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl" Loaded = "StudentViewControl_Loaded"/>
</Grid>
</Window>
Step 8 - นี่คือการนำไปใช้งานสำหรับเหตุการณ์ Loaded ในไฟล์ MainPage.xaml.cs ซึ่งจะอัปเดต View จาก ViewModel
using System.Windows;
namespace MVVMDemo {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
private void StudentViewControl_Loaded(object sender, RoutedEventArgs e) {
MVVMDemo.ViewModel.StudentViewModel studentViewModelObject =
new MVVMDemo.ViewModel.StudentViewModel();
studentViewModelObject.LoadStudents();
StudentViewControl.DataContext = studentViewModelObject;
}
}
}
Step 9 - เมื่อโค้ดด้านบนถูกคอมไพล์และเรียกใช้งานคุณจะได้รับผลลัพธ์ต่อไปนี้บนหน้าต่างหลักของคุณ
เราขอแนะนำให้คุณดำเนินการตามตัวอย่างข้างต้นในลักษณะทีละขั้นตอนเพื่อความเข้าใจที่ดีขึ้น
ในบทนี้เราจะกล่าวถึงวิธีต่างๆที่คุณสามารถทำให้มุมมองของคุณเชื่อมต่อกับ ViewModel ก่อนอื่นมาดูที่ View first construction ซึ่งเราสามารถประกาศใน XAML ดังที่เราได้เห็นตัวอย่างในบทสุดท้ายที่เราได้เชื่อมต่อมุมมองจากหน้าต่างหลัก ตอนนี้เราจะเห็นวิธีอื่น ๆ ในการเชื่อมต่อมุมมอง
เราจะใช้ตัวอย่างเดียวกันในบทนี้ด้วย ต่อไปนี้เป็นการใช้งานคลาส Model เดียวกัน
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get { return firstName; }
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get { return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
นี่คือการใช้งานคลาส ViewModel เวลานี้วิธี LoadStudents ถูกเรียกในตัวสร้างเริ่มต้น
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel{
public class StudentViewModel {
public StudentViewModel() {
LoadStudents();
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
ไม่ว่ามุมมองจะเป็นหน้าต่างการควบคุมผู้ใช้หรือหน้าโดยทั่วไปตัวแยกวิเคราะห์จะทำงานจากบนลงล่างและจากซ้ายไปขวา เรียกตัวสร้างเริ่มต้นสำหรับแต่ละองค์ประกอบเมื่อพบ มีสองวิธีในการสร้างมุมมอง คุณสามารถใช้อะไรก็ได้
- ดูโครงสร้างแรกใน XAML
- ดูการก่อสร้างครั้งแรกใน Code-behind
ดูโครงสร้างแรกใน XAML
วิธีหนึ่งคือเพิ่ม ViewModel ของคุณเป็นองค์ประกอบที่ซ้อนกันใน setter สำหรับคุณสมบัติ DataContext ดังที่แสดงในโค้ดต่อไปนี้
<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>
นี่คือไฟล์ View XAML ที่สมบูรณ์
<UserControl x:Class="MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
ดูการก่อสร้างครั้งแรกใน Code-behind
อีกวิธีหนึ่งคือคุณสามารถรับโครงสร้าง View first ได้โดยการสร้างโมเดลมุมมองด้วยตัวคุณเองในโค้ดด้านหลัง View ของคุณโดยตั้งค่าคุณสมบัติ DataContext ที่นั่นด้วยอินสแตนซ์
โดยทั่วไปคุณสมบัติ DataContext จะถูกตั้งค่าในเมธอดคอนสตรัคเตอร์ของมุมมอง แต่คุณยังสามารถเลื่อนการสร้างได้จนกว่าเหตุการณ์โหลดของมุมมองจะเริ่ม
using System.Windows.Controls;
namespace MVVMDemo.Views {
/// <summary>
/// Interaction logic for StudentView.xaml
/// </summary>
public partial class StudentView : UserControl {
public StudentView() {
InitializeComponent();
this.DataContext = new MVVMDemo.ViewModel.StudentViewModel();
}
}
}
เหตุผลหนึ่งในการสร้างโมเดลมุมมองใน Code-behind แทนที่จะเป็น XAML คือตัวสร้างโมเดล View รับพารามิเตอร์ แต่การแยกวิเคราะห์ XAML สามารถสร้างองค์ประกอบได้หากกำหนดไว้ในตัวสร้างเริ่มต้นเท่านั้น
ในกรณีนี้ไฟล์ XAML ของ View จะมีลักษณะดังที่แสดงในโค้ดต่อไปนี้
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300"
d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal"<
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
คุณสามารถประกาศ View นี้ใน MainWindow ดังที่แสดงในไฟล์ MainWindow.XAML
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl"/>
</Grid>
</Window>
เมื่อโค้ดด้านบนถูกคอมไพล์และดำเนินการคุณจะเห็นผลลัพธ์ต่อไปนี้ในหน้าต่างหลักของคุณ
เราขอแนะนำให้คุณดำเนินการตามตัวอย่างข้างต้นในลักษณะทีละขั้นตอนเพื่อความเข้าใจที่ดีขึ้น
ในบทนี้เราจะพูดถึงวิธีการเชื่อมต่อ ViewModel มันเป็นความต่อเนื่องของบทสุดท้ายที่เราได้พูดถึง View first construction ตอนนี้รูปแบบต่อไปของการก่อสร้างครั้งแรกคือไฟล์meta-pattern ซึ่งเรียกว่า ViewModelLocator. เป็นรูปแบบหลอกและวางซ้อนทับรูปแบบ MVVM
ใน MVVM แต่ละ View จะต้องเชื่อมต่อกับ ViewModel
ViewModelLocator เป็นวิธีง่ายๆในการรวมรหัสไว้ที่ส่วนกลางและแยกส่วนมุมมองให้มากขึ้น
หมายความว่าไม่จำเป็นต้องรู้อย่างชัดเจนเกี่ยวกับประเภทของ ViewModel และวิธีการสร้าง
มีหลายวิธีในการใช้ ViewModelLocator แต่ที่นี่เราใช้วิธีที่คล้ายกันมากที่สุดซึ่งเป็นส่วนหนึ่งของกรอบงาน PRISM
ViewModelLocator เป็นมาตรฐานที่สอดคล้องกันประกาศและเชื่อมโยงกันอย่างหลวม ๆ ในการดูโครงสร้างแรกซึ่งทำให้กระบวนการรับ ViewModel เชื่อมต่อกับ View โดยอัตโนมัติ รูปต่อไปนี้แสดงถึงกระบวนการระดับสูงของ ViewModelLocator
Step 1 - ดูว่ากำลังสร้างมุมมองประเภทใด
Step 2 - ระบุ ViewModel สำหรับประเภท View นั้น ๆ
Step 3 - สร้าง ViewModel นั้น
Step 4 - ตั้งค่า Views DataContext เป็น ViewModel
เพื่อทำความเข้าใจแนวคิดพื้นฐานเรามาดูตัวอย่างง่ายๆของ ViewModelLocator โดยดำเนินการต่อจากตัวอย่างเดียวกันจากบทสุดท้าย หากคุณดูไฟล์ StudentView.xaml คุณจะเห็นว่าเราได้เชื่อมต่อ ViewModel แบบคงที่
ตามที่แสดงในโปรแกรมต่อไปนี้แสดงความคิดเห็นโค้ด XAML เหล่านี้จะลบโค้ดออกจาก Code-behind
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
ตอนนี้ให้สร้างโฟลเดอร์ใหม่ VML และเพิ่มคลาสสาธารณะใหม่ ViewModelLocator ซึ่งจะมีคุณสมบัติที่แนบมาเดียว (คุณสมบัติการพึ่งพา) AutoHookedUpViewModel ดังแสดงในรหัสต่อไปนี้
public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
return (bool)obj.GetValue(AutoHookedUpViewModelProperty);
}
public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) {
obj.SetValue(AutoHookedUpViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for AutoHookedUpViewModel.
//This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false,
AutoHookedUpViewModelChanged));
และตอนนี้คุณสามารถดูนิยามคุณสมบัติพื้นฐานของไฟล์แนบได้ ในการเพิ่มลักษณะการทำงานให้กับคุณสมบัติเราจำเป็นต้องเพิ่มตัวจัดการเหตุการณ์ที่เปลี่ยนแปลงสำหรับคุณสมบัตินี้ซึ่งมีกระบวนการอัตโนมัติในการเชื่อมต่อ ViewModel สำหรับ View รหัสในการทำมีดังนี้ -
private static void AutoHookedUpViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
if (DesignerProperties.GetIsInDesignMode(d)) return;
var viewType = d.GetType();
string str = viewType.FullName;
str = str.Replace(".Views.", ".ViewModel.");
var viewTypeName = str;
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
ต่อไปนี้คือการนำคลาส ViewModelLocator ไปใช้อย่างสมบูรณ์
using System;
using System.ComponentModel;
using System.Windows;
namespace MVVMDemo.VML {
public static class ViewModelLocator {
public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
return (bool)obj.GetValue(AutoHookedUpViewModelProperty);
}
public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) {
obj.SetValue(AutoHookedUpViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for AutoHookedUpViewModel.
//This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
typeof(bool), typeof(ViewModelLocator), new
PropertyMetadata(false, AutoHookedUpViewModelChanged));
private static void AutoHookedUpViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
if (DesignerProperties.GetIsInDesignMode(d)) return;
var viewType = d.GetType();
string str = viewType.FullName;
str = str.Replace(".Views.", ".ViewModel.");
var viewTypeName = str;
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
}
}
สิ่งแรกที่ต้องทำคือเพิ่มเนมสเปซเพื่อให้เราสามารถเข้าถึงประเภท ViewModelLocator นั้นในรูทของโปรเจ็กต์ของเรา จากนั้นในองค์ประกอบเส้นทางซึ่งเป็นประเภทมุมมองให้เพิ่มคุณสมบัติ AutoHookedUpViewModel และตั้งค่าเป็นจริง
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
นี่คือการใช้งานไฟล์ StudentView.xaml ที่สมบูรณ์
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
เมื่อโค้ดด้านบนถูกคอมไพล์และเรียกใช้งานคุณจะเห็นว่า ViewModelLocator กำลังเชื่อมต่อ ViewModel สำหรับ View นั้น ๆ
สิ่งสำคัญที่ต้องสังเกตเกี่ยวกับเรื่องนี้คือมุมมองไม่ได้เชื่อมโยงกันอีกต่อไปว่า ViewModel เป็นประเภทใดหรือสร้างอย่างไร นั่นคือทั้งหมดที่ถูกย้ายออกไปยังตำแหน่งกลางภายใน ViewModelLocator
ในบทนี้เราจะเรียนรู้ว่าการผูกข้อมูลสนับสนุนรูปแบบ MVVM อย่างไร การผูกข้อมูลเป็นคุณสมบัติหลักที่ทำให้ MVVM แตกต่างจากรูปแบบการแยก UI อื่น ๆ เช่น MVC และ MVP
สำหรับการเชื่อมโยงข้อมูลคุณต้องมีมุมมองหรือชุดขององค์ประกอบ UI ที่สร้างขึ้นจากนั้นคุณต้องมีวัตถุอื่นที่การเชื่อมโยงจะชี้ไป
องค์ประกอบ UI ในมุมมองถูกผูกไว้กับคุณสมบัติที่ ViewModel เปิดเผย
ลำดับที่สร้าง View และ ViewModel ขึ้นอยู่กับสถานการณ์เนื่องจากเราได้กล่าวถึง View ก่อน
View และ ViewModel จะถูกสร้างขึ้นและ DataContext ของ View จะถูกตั้งค่าเป็น ViewModel
การเชื่อมโยงอาจเป็นการเชื่อมโยงข้อมูล OneWay หรือ TwoWay เพื่อส่งข้อมูลไปมาระหว่าง View และ ViewModel
มาดูการเชื่อมโยงข้อมูลในตัวอย่างเดียวกัน ด้านล่างนี้คือรหัส XAML ของ StudentView
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
หากคุณดูรหัส XAML ด้านบนคุณจะเห็นว่า ItemsControl ถูกผูกไว้กับคอลเลกชันนักเรียนที่เปิดเผยโดย ViewModel
นอกจากนี้คุณยังสามารถดูได้ว่าคุณสมบัติของ Student model มีการเชื่อมโยงของแต่ละบุคคลเช่นกันและสิ่งเหล่านี้จะถูกผูกไว้กับ Textboxes และ TextBlock
ItemSource ของ ItemsControl สามารถผูกกับคุณสมบัติ Students ได้เนื่องจาก DataContext โดยรวมสำหรับ View ถูกตั้งค่าเป็น ViewModel
การผูกคุณสมบัติแต่ละรายการที่นี่ยังเป็นการรวม DataContext แต่จะไม่เชื่อมโยงกับ ViewModel เองเนื่องจากวิธีการทำงานของ ItemSource
เมื่อแหล่งไอเท็มเชื่อมโยงกับคอลเล็กชันมันจะแสดงผลคอนเทนเนอร์สำหรับแต่ละไอเท็มในการแสดงผลและตั้งค่า DataContext ของคอนเทนเนอร์นั้นเป็นไอเท็ม ดังนั้น DataContext โดยรวมสำหรับแต่ละกล่องข้อความและบล็อกข้อความภายในแถวจะเป็นนักเรียนแต่ละคนในคอลเล็กชัน และคุณยังสามารถดูได้ว่าการผูกข้อมูลเหล่านี้สำหรับ TextBox เป็นการผูกข้อมูลแบบ TwoWay และสำหรับ TextBlock เป็นการผูกข้อมูล OneWay เนื่องจากคุณไม่สามารถแก้ไข TextBlock ได้
เมื่อคุณเรียกใช้แอปพลิเคชันนี้อีกครั้งคุณจะเห็นผลลัพธ์ต่อไปนี้
ตอนนี้ให้เราเปลี่ยนข้อความในกล่องข้อความที่สองของแถวแรกจาก Allain เป็น Upston และกดแท็บเพื่อสูญเสียโฟกัส คุณจะเห็นว่าข้อความ TextBlock ได้รับการอัปเดตด้วย
เนื่องจากการผูกของกล่องข้อความถูกตั้งค่าเป็น TwoWay และจะอัปเดตโมเดลด้วยและจากแบบจำลองอีกครั้งมีการอัปเดต TextBlock
เทมเพลตอธิบายรูปลักษณ์โดยรวมและลักษณะที่มองเห็นของตัวควบคุม สำหรับตัวควบคุมแต่ละตัวจะมีเทมเพลตเริ่มต้นที่เชื่อมโยงซึ่งทำให้เกิดลักษณะที่ปรากฏของตัวควบคุมนั้น ในแอปพลิเคชัน WPF คุณสามารถสร้างเทมเพลตของคุณเองได้อย่างง่ายดายเมื่อคุณต้องการปรับแต่งลักษณะการทำงานของภาพและลักษณะที่มองเห็นของตัวควบคุม การเชื่อมต่อระหว่างลอจิกและเทมเพลตสามารถทำได้โดยการผูกข้อมูล
ใน MVVM มีรูปแบบหลักอีกรูปแบบหนึ่งซึ่งเรียกว่า ViewModel first construction
แนวทางการสร้างแบบแรกของ ViewModel ใช้ประโยชน์จากความสามารถของเทมเพลตข้อมูลโดยนัยใน WPF
เทมเพลตข้อมูลโดยนัยสามารถเลือกเทมเพลตที่เหมาะสมโดยอัตโนมัติจากพจนานุกรมทรัพยากรปัจจุบันสำหรับองค์ประกอบที่ใช้การผูกข้อมูล พวกเขาทำสิ่งนี้ตามชนิดของออบเจ็กต์ข้อมูลซึ่งแสดงผลโดยการผูกข้อมูล ขั้นแรกคุณต้องมีองค์ประกอบบางอย่างที่เชื่อมโยงกับวัตถุข้อมูล
มาดูตัวอย่างง่ายๆของเราอีกครั้งซึ่งคุณจะเข้าใจวิธีดูโมเดลก่อนใช้ประโยชน์จากเทมเพลตข้อมูลโดยเฉพาะเทมเพลตข้อมูลโดยนัย นี่คือการนำคลาส StudentViewModel ของเราไปใช้งาน
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public StudentViewModel() {
LoadStudents();
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
คุณจะเห็นว่า ViewModel ด้านบนไม่มีการเปลี่ยนแปลง เราจะดำเนินการต่อด้วยตัวอย่างเดียวกันจากบทที่แล้ว คลาส ViewModel นี้จะแสดงคุณสมบัติคอลเลกชันนักเรียนและเติมข้อมูลลงในโครงสร้าง ไปที่ไฟล์ StudentView.xaml ลบการใช้งานที่มีอยู่และกำหนดเทมเพลตข้อมูลในส่วนทรัพยากร
<UserControl.Resources>
<DataTemplate x:Key = "studentsTemplate">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
ตอนนี้เพิ่มกล่องรายการและข้อมูลผูกกล่องรายการนั้นกับคุณสมบัติของนักเรียนตามที่แสดงในรหัสต่อไปนี้
<ListBox ItemsSource = "{Binding Students}" ItemTemplate = "{StaticResource studentsTemplate}"/>
ในส่วนทรัพยากร DataTemplate มีคีย์ของ studentsTemplate จากนั้นในการใช้เทมเพลตนั้นจริงเราจำเป็นต้องใช้คุณสมบัติ ItemTemplate ของ ListBox ตอนนี้คุณจะเห็นได้ว่าเราสั่งให้ listbox ใช้เทมเพลตนั้นเพื่อแสดงผลนักเรียนเหล่านั้น ต่อไปนี้คือการนำไฟล์ StudentView.xaml ไปใช้งานอย่างสมบูรณ์
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate x:Key = "studentsTemplate">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox
ItemsSource = "{Binding Students}"
ItemTemplate = "{StaticResource studentsTemplate}"/>
</Grid>
</UserControl>
เมื่อโค้ดด้านบนถูกคอมไพล์และดำเนินการคุณจะเห็นหน้าต่างต่อไปนี้ซึ่งมีกล่องรายการหนึ่งกล่อง ListBoxItem แต่ละรายการมีข้อมูลอ็อบเจ็กต์คลาสนักเรียนซึ่งแสดงบน TextBlock และกล่องข้อความ
ในการสร้างเทมเพลตโดยนัยเราจำเป็นต้องลบคุณสมบัติ ItemTemplate ออกจากกล่องรายการและเพิ่มคุณสมบัติ DataType ในข้อกำหนดเทมเพลตของเราดังที่แสดงในโค้ดต่อไปนี้
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource = "{Binding Students}"/>
</Grid>
ใน DataTemplate ส่วนขยายมาร์กอัป x: Type มีความสำคัญมากซึ่งเปรียบเสมือนตัวดำเนินการประเภทหนึ่งใน XAML ดังนั้นโดยพื้นฐานแล้วเราต้องชี้ไปที่ประเภทข้อมูล Student ซึ่งอยู่ใน MVVMDemo โมเดลเนมสเปซ ต่อไปนี้เป็นไฟล์ XAML ที่อัปเดตแล้ว
<UserControl x:Class="MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource = "{Binding Students}"/>
</Grid>
</UserControl>
เมื่อคุณเรียกใช้แอปพลิเคชันนี้อีกครั้งคุณจะยังคงได้รับการเรนเดอร์ของนักเรียนที่มีเทมเพลตข้อมูลเนื่องจากจะมีการจับคู่ประเภทของวัตถุที่แสดงผลโดยอัตโนมัติโดยการค้นหา DataTemplate ที่เหมาะสม
เราขอแนะนำให้คุณดำเนินการตามตัวอย่างข้างต้นด้วยวิธีการทีละขั้นตอนเพื่อความเข้าใจที่ดีขึ้น
ในบทนี้เราจะเรียนรู้วิธีเพิ่มการโต้ตอบให้กับแอปพลิเคชัน MVVM และวิธีการเรียกใช้ลอจิกอย่างหมดจด คุณจะเห็นว่าทั้งหมดนี้ทำได้โดยการรักษาข้อต่อหลวมและโครงสร้างที่ดีซึ่งเป็นหัวใจของรูปแบบ MVVM เพื่อทำความเข้าใจทั้งหมดนี้ก่อนอื่นให้เราเรียนรู้เกี่ยวกับคำสั่ง
ดู / ViewModel การสื่อสารผ่านคำสั่ง
รูปแบบคำสั่งได้รับการจัดทำเป็นเอกสารอย่างดีและมักใช้รูปแบบการออกแบบเป็นเวลาสองสามทศวรรษ ในรูปแบบนี้มีผู้แสดงหลักสองคนคือผู้เรียกร้องและผู้รับ
Invoker
Invoker เป็นโค้ดส่วนหนึ่งที่สามารถเรียกใช้ตรรกะที่จำเป็นบางอย่างได้
โดยทั่วไปแล้วจะเป็นองค์ประกอบ UI ที่ผู้ใช้โต้ตอบด้วยในบริบทของกรอบงาน UI
มันอาจเป็นโค้ดตรรกะอีกชิ้นที่อื่นในแอปพลิเคชัน
ผู้รับ
ตัวรับคือตรรกะที่มีไว้สำหรับการดำเนินการเมื่อผู้เรียกใช้เริ่มทำงาน
ในบริบทของ MVVM ตัวรับมักเป็นวิธีการใน ViewModel ของคุณที่ต้องถูกเรียกใช้
ระหว่างสองสิ่งนี้คุณมีเลเยอร์สิ่งกีดขวางซึ่งหมายความว่าผู้เรียกและผู้รับไม่จำเป็นต้องรู้เรื่องกันและกันอย่างชัดเจน โดยทั่วไปจะแสดงเป็นนามธรรมของอินเทอร์เฟซที่เปิดเผยต่อผู้เรียกใช้และการใช้งานอินเทอร์เฟซนั้นอย่างเป็นรูปธรรมสามารถเรียกผู้รับได้
มาดูตัวอย่างง่ายๆที่คุณจะได้เรียนรู้คำสั่งและวิธีใช้คำสั่งเหล่านี้เพื่อสื่อสารระหว่าง View และ ViewModel ในบทนี้เราจะดำเนินการต่อด้วยตัวอย่างเดียวกันจากบทสุดท้าย
ในไฟล์ StudentView.xaml เรามี ListBox ที่เชื่อมต่อข้อมูลนักเรียนจาก ViewModel ตอนนี้เรามาเพิ่มปุ่มสำหรับลบนักเรียนออกจาก ListBox
สิ่งสำคัญคือการทำงานกับคำสั่งบนปุ่มนั้นง่ายมากเพราะมีคุณสมบัติคำสั่งเพื่อเชื่อมต่อกับ ICommand
ดังนั้นเราจึงสามารถแสดงคุณสมบัติบน ViewModel ของเราที่มี ICommand และผูกเข้ากับคุณสมบัตินั้นจากคุณสมบัติคำสั่งของปุ่มดังที่แสดงในโค้ดต่อไปนี้
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
มาเพิ่มคลาสใหม่ในโครงการของคุณซึ่งจะใช้อินเทอร์เฟซ ICommand ต่อไปนี้คือการนำอินเทอร์เฟซ ICommand มาใช้
using System;
using System.Windows.Input;
namespace MVVMDemo {
public class MyICommand : ICommand {
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public MyICommand(Action executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod){
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
return _TargetCanExecuteMethod();
}
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();
}
}
}
}
อย่างที่คุณเห็นนี่เป็นการใช้งาน ICommand แบบง่าย ๆ โดยที่เรามีผู้รับมอบสิทธิ์สองคนคนหนึ่งสำหรับ executeMethod และอีกหนึ่งคนสำหรับ canExecuteMethod ซึ่งสามารถส่งต่อในการก่อสร้างได้
ในการใช้งานข้างต้นมีตัวสร้างที่โอเวอร์โหลดสองตัวตัวหนึ่งสำหรับ executeMethod เท่านั้นและอีกตัวหนึ่งสำหรับทั้ง executeMethod และฉัน canExecuteMethod
มาเพิ่มคุณสมบัติของประเภท MyICommand ในคลาส StudentView Model ตอนนี้เราต้องสร้างอินสแตนซ์ใน StudentViewModel เราจะใช้ตัวสร้างที่โอเวอร์โหลดของ MyICommand ที่รับสองพารามิเตอร์
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
ตอนนี้เพิ่มการใช้งานวิธี OnDelete และ CanDelete
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
เรายังต้องเพิ่ม SelectedStudent ใหม่เพื่อให้ผู้ใช้สามารถลบ Selected Item จาก ListBox ได้
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
ต่อไปนี้คือการนำคลาส ViewModel ที่สมบูรณ์ไปใช้งาน
using MVVMDemo.Model;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
}
}
ใน StudentView.xaml เราจำเป็นต้องเพิ่มคุณสมบัติ SelectedItem ในกล่องรายการซึ่งจะผูกกับคุณสมบัติ SelectStudent
<ListBox ItemsSource = "{Binding Students}" SelectedItem = "{Binding SelectedStudent}"/>
ต่อไปนี้เป็นไฟล์ xaml ที่สมบูรณ์
<UserControl x:Class = "MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation = "Horizontal">
<ListBox ItemsSource = "{Binding Students}"
SelectedItem = "{Binding SelectedStudent}"/>
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
</StackPanel>
</Grid>
</UserControl>
เมื่อโค้ดด้านบนถูกคอมไพล์และรันคุณจะเห็นหน้าต่างต่อไปนี้
คุณจะเห็นว่าปุ่มลบถูกปิดใช้งาน จะเปิดใช้งานเมื่อคุณเลือกรายการใด ๆ
เมื่อคุณเลือกรายการใด ๆ และกดลบ คุณจะเห็นว่ารายการที่เลือกถูกลบและปุ่มลบอีกครั้งจะถูกปิดใช้งาน
เราขอแนะนำให้คุณดำเนินการตามตัวอย่างข้างต้นในลักษณะทีละขั้นตอนเพื่อความเข้าใจที่ดีขึ้น
เมื่อสร้างแอปพลิเคชัน 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 ปรากฏขึ้น
เราขอแนะนำให้คุณดำเนินการตามตัวอย่างข้างต้นในลักษณะทีละขั้นตอนเพื่อความเข้าใจที่ดีขึ้น
ในบทนี้เราจะเรียนรู้เกี่ยวกับการตรวจสอบความถูกต้อง นอกจากนี้เราจะดูวิธีที่สะอาดในการตรวจสอบความถูกต้องด้วยการผูก WPF ที่รองรับอยู่แล้ว แต่ผูกเข้ากับส่วนประกอบ MVVM
การตรวจสอบความถูกต้องใน MVVM
เมื่อแอปพลิเคชันของคุณเริ่มยอมรับการป้อนข้อมูลจากผู้ใช้ปลายทางคุณต้องพิจารณาตรวจสอบความถูกต้องของอินพุตนั้น
ตรวจสอบให้แน่ใจว่าสอดคล้องกับความต้องการโดยรวมของคุณ
WPF มีโครงสร้างและคุณสมบัติที่ยอดเยี่ยมบางอย่างในระบบการเชื่อมโยงสำหรับการตรวจสอบความถูกต้องของอินพุตและคุณยังสามารถใช้ประโยชน์จากคุณสมบัติเหล่านั้นทั้งหมดเมื่อทำ MVVM
โปรดทราบว่าตรรกะที่สนับสนุนการตรวจสอบความถูกต้องของคุณและกำหนดกฎที่มีอยู่สำหรับคุณสมบัติใดที่ควรเป็นส่วนหนึ่งของ Model หรือ ViewModel ไม่ใช่ View เอง
คุณยังคงสามารถใช้ทุกวิธีในการแสดงการตรวจสอบความถูกต้องที่รองรับโดยการผูกข้อมูล WPF ได้แก่ -
- มีการตั้งค่าข้อยกเว้นในการโยนทรัพย์สิน
- การติดตั้งอินเทอร์เฟซ IDataErrorInfo
- การใช้ INotifyDataErrorInfo
- ใช้กฎการตรวจสอบ WPF
โดยทั่วไปแนะนำให้ใช้ INotifyDataErrorInfo และได้รับการแนะนำให้รู้จักกับ WPF .net 4.5 และสนับสนุนการสืบค้นวัตถุสำหรับข้อผิดพลาดที่เกี่ยวข้องกับคุณสมบัติและยังแก้ไขข้อบกพร่องสองสามข้อด้วยตัวเลือกอื่น ๆ ทั้งหมด โดยเฉพาะจะช่วยให้สามารถตรวจสอบความถูกต้องแบบอะซิงโครนัสได้ อนุญาตให้คุณสมบัติมีข้อผิดพลาดมากกว่าหนึ่งข้อที่เกี่ยวข้อง
การเพิ่มการตรวจสอบความถูกต้อง
มาดูตัวอย่างที่เราจะเพิ่มการรองรับการตรวจสอบความถูกต้องให้กับมุมมองการป้อนข้อมูลของเราและในแอปพลิเคชันขนาดใหญ่คุณอาจต้องใช้สถานที่นี้ในแอปพลิเคชันของคุณ บางครั้งใน Views บางครั้งใน ViewModels และบางครั้งบนวัตถุตัวช่วยเหล่านี้จะมีการห่อหุ้มรอบวัตถุแบบจำลอง
เป็นแนวทางปฏิบัติที่ดีในการวางการสนับสนุนการตรวจสอบความถูกต้องไว้ในคลาสพื้นฐานทั่วไปซึ่งคุณสามารถรับช่วงจากสถานการณ์ต่างๆได้
คลาสฐานจะรองรับ INotifyDataErrorInfo เพื่อให้การตรวจสอบถูกทริกเกอร์เมื่อคุณสมบัติเปลี่ยนไป
สร้างเพิ่มคลาสใหม่ชื่อ ValidatableBindableBase เนื่องจากเรามีคลาสพื้นฐานสำหรับการจัดการการเปลี่ยนแปลงคุณสมบัติแล้วเรามารับคลาสฐานจากมันและใช้อินเทอร์เฟซ INotifyDataErrorInfo ด้วย
ต่อไปนี้คือการนำคลาส ValidatableBindableBase ไปใช้
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace MVVMHierarchiesDemo {
public class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo {
private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
public event EventHandler<DataErrorsChangedEventArgs>
ErrorsChanged = delegate { };
public System.Collections.IEnumerable GetErrors(string propertyName) {
if (_errors.ContainsKey(propertyName))
return _errors[propertyName];
else
return null;
}
public bool HasErrors {
get { return _errors.Count > 0; }
}
protected override void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
base.SetProperty<T>(ref member, val, propertyName);
ValidateProperty(propertyName, val);
}
private void ValidateProperty<T>(string propertyName, T value) {
var results = new List<ValidationResult>();
//ValidationContext context = new ValidationContext(this);
//context.MemberName = propertyName;
//Validator.TryValidateProperty(value, context, results);
if (results.Any()) {
//_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList();
} else {
_errors.Remove(propertyName);
}
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
}
ตอนนี้เพิ่ม AddEditCustomerView และ AddEditCustomerViewModel ในโฟลเดอร์ที่เกี่ยวข้อง ต่อไปนี้เป็นรหัสของ AddEditCustomerView.xaml
<UserControl x:Class = "MVVMHierarchiesDemo.Views.AddEditCustomerView"
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>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Grid x:Name = "grid1"
HorizontalAlignment = "Left"
DataContext = "{Binding Customer}"
Margin = "10,10,0,0"
VerticalAlignment = "Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "Auto" />
<ColumnDefinition Width = "Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Label Content = "First Name:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "0"
VerticalAlignment = "Center" />
<TextBox x:Name = "firstNameTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "0"
Text = "{Binding FirstName, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Last Name:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "1"
VerticalAlignment = "Center" />
<TextBox x:Name = "lastNameTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "1"
Text = "{Binding LastName, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Email:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "2"
VerticalAlignment = "Center" />
<TextBox x:Name = "emailTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "2"
Text = "{Binding Email, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Phone:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "3"
VerticalAlignment = "Center" />
<TextBox x:Name = "phoneTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "3"
Text = "{Binding Phone, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
</Grid>
<Grid Grid.Row = "1">
<Button Content = "Save"
Command = "{Binding SaveCommand}"
HorizontalAlignment = "Left"
Margin = "25,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
<Button Content = "Add"
Command = "{Binding SaveCommand}"
HorizontalAlignment = "Left"
Margin = "25,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
<Button Content = "Cancel"
Command = "{Binding CancelCommand}"
HorizontalAlignment = "Left"
Margin = "150,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
</Grid>
</Grid>
</UserControl>
ต่อไปนี้คือการใช้งาน AddEditCustomerViewModel
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.ViewModel {
class AddEditCustomerViewModel : BindableBase {
public AddEditCustomerViewModel() {
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
private bool _EditMode;
public bool EditMode {
get { return _EditMode; }
set { SetProperty(ref _EditMode, value);}
}
private SimpleEditableCustomer _Customer;
public SimpleEditableCustomer Customer {
get { return _Customer; }
set { SetProperty(ref _Customer, value);}
}
private Customer _editingCustomer = null;
public void SetCustomer(Customer cust) {
_editingCustomer = cust;
if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged;
Customer = new SimpleEditableCustomer();
Customer.ErrorsChanged += RaiseCanExecuteChanged;
CopyCustomer(cust, Customer);
}
private void RaiseCanExecuteChanged(object sender, EventArgs e) {
SaveCommand.RaiseCanExecuteChanged();
}
public MyIcommand CancelCommand { get; private set; }
public MyIcommand SaveCommand { get; private set; }
public event Action Done = delegate { };
private void OnCancel() {
Done();
}
private async void OnSave() {
Done();
}
private bool CanSave() {
return !Customer.HasErrors;
}
}
}
ต่อไปนี้คือการนำคลาส SimpleEditableCustomer ไปใช้
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Model {
public class SimpleEditableCustomer : ValidatableBindableBase {
private Guid _id;
public Guid Id {
get { return _id; }
set { SetProperty(ref _id, value); }
}
private string _firstName;
[Required]
public string FirstName {
get { return _firstName; }
set { SetProperty(ref _firstName, value); }
}
private string _lastName;
[Required]
public string LastName {
get { return _lastName; }
set { SetProperty(ref _lastName, value); }
}
private string _email;
[EmailAddress]
public string Email {
get { return _email; }
set { SetProperty(ref _email, value); }
}
private string _phone;
[Phone]
public string Phone {
get { return _phone; }
set { SetProperty(ref _phone, value); }
}
}
}
เมื่อโค้ดด้านบนถูกคอมไพล์และรันคุณจะเห็นหน้าต่างต่อไปนี้
เมื่อคุณกดปุ่มเพิ่มลูกค้าคุณจะเห็นมุมมองต่อไปนี้ เมื่อผู้ใช้เว้นช่องว่างไว้ช่องนั้นจะถูกไฮไลต์และปุ่มบันทึกจะปิดใช้งาน
ในบทนี้เราจะพูดคุยสั้น ๆ เกี่ยวกับการฉีดแบบพึ่งพา เราได้กล่าวถึงการผูกข้อมูลที่แยกมุมมองและ ViewModels ออกจากกันแล้วซึ่งช่วยให้พวกเขาสามารถสื่อสารได้โดยไม่ทราบแน่ชัดว่าเกิดอะไรขึ้นที่ส่วนอื่น ๆ ของการสื่อสาร
ตอนนี้เราต้องการสิ่งที่คล้ายกันเพื่อแยก ViewModel ของเราออกจากบริการไคลเอ็นต์
ในช่วงแรก ๆ ของการเขียนโปรแกรมเชิงวัตถุนักพัฒนาประสบปัญหาในการสร้างและเรียกใช้อินสแตนซ์ของคลาสในแอปพลิเคชัน มีการเสนอวิธีแก้ปัญหาต่างๆสำหรับปัญหานี้
ในช่วงไม่กี่ปีที่ผ่านมาการฉีดแบบพึ่งพาและการควบคุมแบบผกผัน (IoC) ได้รับความนิยมในหมู่นักพัฒนาและมีความสำคัญเหนือกว่าโซลูชันรุ่นเก่าบางอย่างเช่นรูปแบบ Singleton
การฉีดขึ้นอยู่กับภาชนะ / IoC
IoC และการฉีดพึ่งพาเป็นรูปแบบการออกแบบสองแบบที่มีความสัมพันธ์กันอย่างใกล้ชิดและโดยพื้นฐานแล้วคอนเทนเนอร์นั้นเป็นส่วนหนึ่งของรหัสโครงสร้างพื้นฐานที่ทำทั้งสองรูปแบบให้คุณ
รูปแบบ IoC เป็นเรื่องเกี่ยวกับการมอบหมายความรับผิดชอบในการก่อสร้างและรูปแบบการฉีดพึ่งพาเป็นเรื่องเกี่ยวกับการให้การอ้างอิงกับวัตถุที่สร้างขึ้นแล้ว
พวกเขาทั้งสองสามารถถือเป็นแนวทางสองเฟสในการสร้าง เมื่อคุณใช้คอนเทนเนอร์คอนเทนเนอร์จะมีความรับผิดชอบหลายประการดังนี้ -
- สร้างวัตถุเมื่อถูกถาม
- คอนเทนเนอร์จะกำหนดว่าวัตถุนั้นขึ้นอยู่กับอะไร
- การสร้างการอ้างอิงเหล่านั้น
- ฉีดเข้าไปในวัตถุที่กำลังสร้าง
- ทำซ้ำกระบวนการ
ลองมาดูกันว่าเราจะใช้การฉีดพึ่งพาเพื่อแยกการแยกส่วนระหว่าง ViewModels และบริการไคลเอ็นต์ได้อย่างไร เราจะวางสายการจัดการการบันทึกแบบฟอร์ม AddEditCustomerViewModel โดยใช้การฉีดขึ้นต่อกันที่เกี่ยวข้องกับสิ่งนั้น
ก่อนอื่นเราต้องสร้างอินเทอร์เฟซใหม่ในโครงการของเราในโฟลเดอร์ Services หากคุณไม่มีโฟลเดอร์บริการในโครงการของคุณให้สร้างก่อนและเพิ่มอินเทอร์เฟซต่อไปนี้ในโฟลเดอร์ Services
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Services {
public interface ICustomersRepository {
Task<List<Customer>> GetCustomersAsync();
Task<Customer> GetCustomerAsync(Guid id);
Task<Customer> AddCustomerAsync(Customer customer);
Task<Customer> UpdateCustomerAsync(Customer customer);
Task DeleteCustomerAsync(Guid customerId);
}
}
ต่อไปนี้คือการนำ ICustomersRepository ไปใช้
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq; using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Services {
public class CustomersRepository : ICustomersRepository {
ZzaDbContext _context = new ZzaDbContext();
public Task<List<Customer>> GetCustomersAsync() {
return _context.Customers.ToListAsync();
}
public Task<Customer> GetCustomerAsync(Guid id) {
return _context.Customers.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<Customer> AddCustomerAsync(Customer customer){
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return customer;
}
public async Task<Customer> UpdateCustomerAsync(Customer customer) {
if (!_context.Customers.Local.Any(c => c.Id == customer.Id)) {
_context.Customers.Attach(customer);
}
_context.Entry(customer).State = EntityState.Modified;
await _context.SaveChangesAsync();
return customer;
}
public async Task DeleteCustomerAsync(Guid customerId) {
var customer = _context.Customers.FirstOrDefault(c => c.Id == customerId);
if (customer != null) {
_context.Customers.Remove(customer);
}
await _context.SaveChangesAsync();
}
}
}
วิธีง่ายๆในการจัดการบันทึกคือการเพิ่มอินสแตนซ์ใหม่ของ ICustomersRepository ใน AddEditCustomerViewModel และโอเวอร์โหลดตัวสร้าง AddEditCustomerViewModel และ CustomerListViewModel
private ICustomersRepository _repo;
public AddEditCustomerViewModel(ICustomersRepository repo) {
_repo = repo;
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
อัพเดตเมธอด OnSave ตามที่แสดงในโค้ดต่อไปนี้
private async void OnSave() {
UpdateCustomer(Customer, _editingCustomer);
if (EditMode)
await _repo.UpdateCustomerAsync(_editingCustomer);
else
await _repo.AddCustomerAsync(_editingCustomer);
Done();
}
private void UpdateCustomer(SimpleEditableCustomer source, Customer target) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
ต่อไปนี้คือ AddEditCustomerViewModel ที่สมบูรณ์
using MVVMHierarchiesDemo.Model;
using MVVMHierarchiesDemo.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.ViewModel {
class AddEditCustomerViewModel : BindableBase {
private ICustomersRepository _repo;
public AddEditCustomerViewModel(ICustomersRepository repo) {
_repo = repo;
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
private bool _EditMode;
public bool EditMode {
get { return _EditMode; }
set { SetProperty(ref _EditMode, value); }
}
private SimpleEditableCustomer _Customer;
public SimpleEditableCustomer Customer {
get { return _Customer; }
set { SetProperty(ref _Customer, value); }
}
private Customer _editingCustomer = null;
public void SetCustomer(Customer cust) {
_editingCustomer = cust;
if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged;
Customer = new SimpleEditableCustomer();
Customer.ErrorsChanged += RaiseCanExecuteChanged;
CopyCustomer(cust, Customer);
}
private void RaiseCanExecuteChanged(object sender, EventArgs e) {
SaveCommand.RaiseCanExecuteChanged();
}
public MyIcommand CancelCommand { get; private set; }
public MyIcommand SaveCommand { get; private set; }
public event Action Done = delegate { };
private void OnCancel() {
Done();
}
private async void OnSave() {
UpdateCustomer(Customer, _editingCustomer);
if (EditMode)
await _repo.UpdateCustomerAsync(_editingCustomer);
else
await _repo.AddCustomerAsync(_editingCustomer);
Done();
}
private void UpdateCustomer(SimpleEditableCustomer source, Customer target) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
private bool CanSave() {
return !Customer.HasErrors;
}
private void CopyCustomer(Customer source, SimpleEditableCustomer target) {
target.Id = source.Id;
if (EditMode) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
}
}
}
เมื่อรวบรวมและดำเนินการโค้ดด้านบนคุณจะเห็นผลลัพธ์เดียวกัน แต่ตอนนี้ ViewModels แยกออกจากกันอย่างหลวม ๆ
เมื่อคุณกดปุ่มเพิ่มลูกค้าคุณจะเห็นมุมมองต่อไปนี้ เมื่อผู้ใช้เว้นช่องว่างไว้ช่องนั้นจะถูกไฮไลต์และปุ่มบันทึกจะปิดใช้งาน
เหตุการณ์คือโครงสร้างการเขียนโปรแกรมที่ตอบสนองต่อการเปลี่ยนแปลงสถานะโดยแจ้งให้ทราบถึงจุดสิ้นสุดใด ๆ ที่ลงทะเบียนสำหรับการแจ้งเตือน โดยพื้นฐานแล้วเหตุการณ์จะใช้เพื่อแจ้งข้อมูลที่ผู้ใช้ป้อนผ่านเมาส์และคีย์บอร์ด แต่ประโยชน์ของมันไม่ได้ จำกัด อยู่แค่นั้น เมื่อใดก็ตามที่ตรวจพบการเปลี่ยนแปลงสถานะบางทีเมื่อวัตถุถูกโหลดหรือเริ่มต้นเหตุการณ์อาจถูกยิงเพื่อแจ้งเตือนบุคคลภายนอกที่สนใจ
ในแอ็พพลิเคชัน WPF ที่ใช้รูปแบบการออกแบบ MVVM (Model-View-ViewModel) โมเดลมุมมองเป็นส่วนประกอบที่รับผิดชอบในการจัดการตรรกะและสถานะการนำเสนอของแอปพลิเคชัน
ไฟล์ code-behind ของมุมมองไม่ควรมีโค้ดสำหรับจัดการเหตุการณ์ที่ยกขึ้นจากองค์ประกอบส่วนติดต่อผู้ใช้ (UI) เช่นปุ่มหรือ ComboBox และไม่ควรมีตรรกะเฉพาะโดเมนใด ๆ
ตามหลักการแล้วโค้ดด้านหลังของ View มีเพียงตัวสร้างที่เรียกใช้เมธอด InitializeComponent และอาจมีโค้ดเพิ่มเติมเพื่อควบคุมหรือโต้ตอบกับเลเยอร์มุมมองที่ยากหรือไม่มีประสิทธิภาพในการแสดงใน XAML เช่นภาพเคลื่อนไหวที่ซับซ้อน
มาดูตัวอย่างเหตุการณ์การคลิกปุ่มง่ายๆในแอปพลิเคชันของเรา ต่อไปนี้เป็นรหัส XAML ของไฟล์ MainWindow.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>
คุณจะเห็นว่าคุณสมบัติของปุ่ม Click ไม่ได้ใช้ในไฟล์ XAML ด้านบน แต่ใช้คุณสมบัติ Command และ CommandParameter เพื่อโหลดมุมมองที่แตกต่างกันเมื่อกดปุ่ม ตอนนี้คุณต้องกำหนดการใช้งานคำสั่งในไฟล์ MainWindowViewModel.cs แต่ไม่ใช่ในไฟล์ View ต่อไปนี้คือการใช้งาน 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 ปรากฏขึ้น
เราขอแนะนำให้คุณดำเนินการตามตัวอย่างข้างต้นด้วยวิธีการทีละขั้นตอนเพื่อความเข้าใจที่ดีขึ้น
แนวคิดเบื้องหลังการทดสอบหน่วยคือการใช้โค้ด (หน่วย) แบบแยกส่วนและเขียนวิธีการทดสอบที่ใช้โค้ดในลักษณะที่คาดหวังจากนั้นทดสอบเพื่อดูว่าได้ผลลัพธ์ตามที่คาดหวังหรือไม่
การทดสอบหน่วยจะถูกรวบรวมเช่นเดียวกับส่วนอื่น ๆ ของโครงการ
นอกจากนี้ยังดำเนินการโดยซอฟต์แวร์ทดสอบซึ่งสามารถเร่งความเร็วในการทดสอบแต่ละครั้งได้อย่างมีประสิทธิภาพยกนิ้วโป้งขึ้นหรือยกนิ้วลงเพื่อระบุว่าการทดสอบผ่านหรือล้มเหลวตามลำดับ
ลองมาดูตัวอย่างที่สร้างขึ้นก่อนหน้านี้ ต่อไปนี้คือการนำ Student Model ไปใช้
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get { return firstName; }
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get { return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
ต่อไปนี้คือการใช้งาน StudentView
<UserControl x:Class="MVVMDemo.Views.StudentView"
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:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation = "Horizontal">
<ListBox ItemsSource = "{Binding Students}"
SelectedItem = "{Binding SelectedStudent}"/>
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
</StackPanel>
</Grid>
</UserControl>
ต่อไปนี้คือการใช้งาน StudentViewModel
using MVVMDemo.Model;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
public int GetStudentCount() {
return Students.Count;
}
}
}
ต่อไปนี้คือไฟล์ MainWindow.xaml
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl"/>
</Grid>
</Window>
ต่อไปนี้คือการใช้งาน MyICommand ซึ่งใช้อินเทอร์เฟซ ICommand
using System;
using System.Windows.Input;
namespace MVVMDemo {
public class MyICommand : ICommand {
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public MyICommand(Action executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
return _TargetCanExecuteMethod();
}
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();
}
}
}
}
เมื่อโค้ดด้านบนถูกคอมไพล์และดำเนินการคุณจะเห็นผลลัพธ์ต่อไปนี้ในหน้าต่างหลักของคุณ
ในการเขียนการทดสอบหน่วยสำหรับตัวอย่างข้างต้นให้เพิ่มโครงการทดสอบใหม่ลงในโซลูชัน
เพิ่มการอ้างอิงไปยังโครงการโดยคลิกขวาที่การอ้างอิง
เลือกโครงการที่มีอยู่แล้วคลิกตกลง
ตอนนี้ให้เราเพิ่มแบบทดสอบง่ายๆซึ่งจะตรวจสอบจำนวนนักเรียนตามที่แสดงในรหัสต่อไปนี้
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MVVMDemo.ViewModel;
namespace MVVMTest {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void TestMethod1() {
StudentViewModel sViewModel = new StudentViewModel();
int count = sViewModel.GetStudentCount();
Assert.IsTrue(count == 3);
}
}
}
ในการดำเนินการทดสอบนี้ให้เลือกทดสอบ→เรียกใช้→ตัวเลือกเมนูการทดสอบทั้งหมด
คุณสามารถเห็นใน Test Explorer ว่าการทดสอบผ่านแล้วเนื่องจากใน StudentViewModel มีการเพิ่มนักเรียนสามคน เปลี่ยนเงื่อนไขการนับจาก 3 เป็น 4 ดังแสดงในรหัสต่อไปนี้
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MVVMDemo.ViewModel;
namespace MVVMTest {
[TestClass]
public class UnitTest1 {
[TestMethod] public void TestMethod1() {
StudentViewModel sViewModel = new StudentViewModel();
int count = sViewModel.GetStudentCount();
Assert.IsTrue(count == 4);
}
}
}
เมื่อแผนการทดสอบถูกดำเนินการอีกครั้งคุณจะเห็นว่าการทดสอบล้มเหลวเนื่องจากจำนวนนักเรียนไม่เท่ากับ 4
เราขอแนะนำให้คุณดำเนินการตามตัวอย่างข้างต้นด้วยวิธีการทีละขั้นตอนเพื่อความเข้าใจที่ดีขึ้น
ในบทนี้เราจะพูดถึงชุดเครื่องมือ MVVM หรือเฟรมเวิร์กที่พร้อมใช้งาน คุณยังสามารถใช้เฟรมเวิร์กเหล่านี้เพื่อที่คุณจะได้ไม่ต้องเขียนโค้ดซ้ำ ๆ มากมายเพื่อใช้รูปแบบ MVVM ด้วยตัวคุณเอง นี่คือเฟรมเวิร์กยอดนิยมบางส่วน -
- Prism
- ไฟ MVVM
- คาลิเบิร์นไมโคร
ปริซึม
Prism ให้คำแนะนำในรูปแบบของตัวอย่างและเอกสารประกอบที่ช่วยให้คุณออกแบบและสร้างแอปพลิเคชันเดสก์ท็อป Windows Presentation Foundation (WPF) ที่หลากหลายยืดหยุ่นและดูแลรักษาได้ง่าย Rich Internet Applications (RIA) ที่สร้างขึ้นด้วยปลั๊กอินเบราว์เซอร์ Microsoft Silverlight และแอปพลิเคชัน Windows
ปริซึมใช้รูปแบบการออกแบบที่รวบรวมหลักการออกแบบสถาปัตยกรรมที่สำคัญเช่นการแยกข้อกังวลและการเชื่อมต่อแบบหลวม ๆ
Prism ช่วยให้คุณออกแบบและสร้างแอปพลิเคชันโดยใช้ส่วนประกอบแบบหลวม ๆ ที่สามารถพัฒนาได้อย่างอิสระ แต่สามารถรวมเข้ากับแอปพลิเคชันโดยรวมได้อย่างง่ายดายและราบรื่น
แอปพลิเคชันประเภทนี้เรียกว่าแอปพลิเคชันคอมโพสิต
ปริซึมมีคุณสมบัติหลายอย่างที่พร้อมใช้งาน ต่อไปนี้เป็นคุณสมบัติที่สำคัญบางประการของปริซึม
รูปแบบ MVVM
ปริซึมรองรับรูปแบบ MVVM มีคลาส Bindablebase คล้ายกับคลาสที่ใช้งานในบทก่อนหน้านี้
มันมี ViewModelLocator ที่ยืดหยุ่นซึ่งมีข้อตกลง แต่ช่วยให้คุณสามารถลบล้างข้อตกลงเหล่านั้นและเชื่อมต่อ Views และ ViewModels ของคุณอย่างเปิดเผยควบคู่กันไปอย่างหลวม ๆ
ความเป็นโมดูลาร์
มันเป็นความสามารถในการแยกโค้ดของคุณออกเป็นไลบรารีคลาสที่เชื่อมต่อกันอย่างหลวม ๆ เป็นส่วน ๆ และนำมารวมกันที่รันไทม์เป็นส่วนที่เหนียวแน่นสำหรับผู้ใช้ปลายทางในขณะที่โค้ดยังคงถูกแยกออกอย่างสมบูรณ์
องค์ประกอบ UI / ภูมิภาค
เป็นความสามารถในการเชื่อมต่อมุมมองเข้ากับคอนเทนเนอร์โดยไม่ต้องดูที่กำลังทำการเสียบจำเป็นต้องมีการอ้างอิงอย่างชัดเจนไปยังคอนเทนเนอร์ UI เอง
การนำทาง
ปริซึมมีคุณสมบัติการนำทางที่เลเยอร์ด้านบนของพื้นที่เช่นการนำทางไปข้างหน้าและข้างหลังและสแต็กการนำทางที่ช่วยให้โมเดลมุมมองของคุณเข้าร่วมในกระบวนการนำทางได้โดยตรง
คำสั่ง
ปริซึมมีคำสั่งดังนั้นจึงมีคำสั่งมอบหมายที่คล้ายกับ MyICommand ที่เราเคยใช้ในบทก่อนหน้านี้มากยกเว้นว่ามันมีความทนทานเป็นพิเศษเพื่อปกป้องคุณจากการรั่วไหลของหน่วยความจำ
เหตุการณ์ Pub / Sub
Prism ยังรองรับเหตุการณ์ Pub / Sub เหตุการณ์เหล่านี้เป็นเหตุการณ์ที่เกิดขึ้นร่วมกันอย่างหลวม ๆ ซึ่งผู้เผยแพร่และผู้ติดตามอาจมีช่วงชีวิตที่แตกต่างกันและไม่จำเป็นต้องมีการอ้างอิงถึงกันอย่างชัดเจนเพื่อสื่อสารผ่านเหตุการณ์ต่างๆ
ไฟ MVVM
MVVM Light ผลิตโดย Laurent Bugnion และช่วยให้คุณแยกมุมมองของคุณออกจากโมเดลของคุณซึ่งจะสร้างแอพพลิเคชั่นที่สะอาดและง่ายต่อการบำรุงรักษาและขยาย
นอกจากนี้ยังสร้างแอปพลิเคชันที่สามารถทดสอบได้และช่วยให้คุณมีเลเยอร์อินเทอร์เฟซผู้ใช้ที่บางกว่ามาก (ซึ่งยากต่อการทดสอบโดยอัตโนมัติ)
ชุดเครื่องมือนี้ให้ความสำคัญเป็นพิเศษในการเปิดและแก้ไขอินเทอร์เฟซผู้ใช้ใน Blend รวมถึงการสร้างข้อมูลเวลาออกแบบเพื่อให้ผู้ใช้ Blend "เห็นบางสิ่ง" เมื่อทำงานกับการควบคุมข้อมูล
คาลิเบิร์นไมโคร
นี่เป็นอีกหนึ่งเฟรมเวิร์กโอเพนซอร์สขนาดเล็กที่ช่วยให้คุณใช้งานรูปแบบ MVVM และยังรองรับหลายสิ่งที่ไม่อยู่ในกรอบ
Caliburn Micro เป็นเฟรมเวิร์กขนาดเล็ก แต่ทรงพลังซึ่งออกแบบมาสำหรับการสร้างแอปพลิเคชันบนแพลตฟอร์ม XAML ทั้งหมด
ด้วยการสนับสนุน MVVM และรูปแบบ UI อื่น ๆ ที่ได้รับการพิสูจน์แล้ว Caliburn Micro จะช่วยให้คุณสร้างโซลูชันได้อย่างรวดเร็วโดยไม่จำเป็นต้องเสียสละคุณภาพโค้ดหรือความสามารถในการทดสอบ