ASP.NET Core: Model-View-ViewModel (MVVM)
Trong bài viết này
- Mẫu MVVM
- Kết nối các view model với các view
- Tạo view model theo cách khai báo
- Tạo view model theo chương trình
- Cập nhật view để đáp ứng với các thay đổi trong model hoặc view model cơ bản
- Framework MVVM
- Tương tác giao diện người dùng bằng lệnh và hành vi
- Thực thi các lệnh
- Gọi lệnh từ view
- Thực thi các hành vi
- Gọi các hành vi từ một view
- Tóm tắt
Trải nghiệm của nhà phát triển .NET MAUI thường liên quan đến việc tạo giao diện người dùng trong XAML, sau đó thêm mã phía sau action trên giao diện người dùng. Các vấn đề bảo trì phức tạp có thể phát sinh khi ứng dụng được sửa đổi và phát triển về quy mô cũng như phạm vi. Những vấn đề này bao gồm sự kết hợp chặt chẽ giữa các điều khiển giao diện người dùng và logic nghiệp vụ, làm tăng chi phí thực hiện sửa đổi giao diện người dùng và khó khăn trong việc kiểm tra đơn vị mã đó.
MVVM giúp tách biệt rõ ràng logic trình bày và nghiệp vụ của ứng dụng khỏi giao diện người dùng (UI) của nó. Việc duy trì sự tách biệt rõ ràng giữa logic ứng dụng và giao diện người dùng giúp giải quyết nhiều vấn đề phát triển và giúp ứng dụng dễ dàng kiểm tra, bảo trì và phát triển hơn. Nó cũng có thể cải thiện đáng kể cơ hội sử dụng lại mã và cho phép các nhà phát triển và nhà thiết kế giao diện người dùng cộng tác dễ dàng hơn khi phát triển các phần tương ứng của ứng dụng.
Mẫu MVVM
Có ba thành phần cốt lõi trong mẫu MVVM: Model, View và View Model. Mỗi thành phần phục vụ một mục đích riêng biệt. Sơ đồ dưới đây cho thấy mối quan hệ giữa ba thành phần.
Ngoài việc hiểu rõ trách nhiệm của từng thành phần, điều quan trọng là phải hiểu cách chúng tương tác. Ở mức cao, view "biết về" view model và view model "biết về" model, nhưng model không biết về view model và view model không biết về view. Do đó, view model tách biệt view khỏi model và cho phép model phát triển độc lập với view.
Lợi ích của việc sử dụng mẫu MVVM như sau:
- Nếu việc triển khai model hiện tại gói gọn logic nghiệp vụ hiện có thì việc thay đổi nó có thể khó khăn hoặc rủi ro. Trong trường hợp này, view model hoạt động như một bộ điều hợp cho các lớp model và ngăn bạn thực hiện các thay đổi lớn đối với mã model.
- Nhà phát triển có thể tạo các bài kiểm thử đơn vị cho view model và model mà không cần sử dụng view. Các bài kiểm tra đơn vị cho view model có thể thực hiện chính xác chức năng tương tự như được sử dụng bởi view.
- Giao diện người dùng ứng dụng có thể được thiết kế lại mà không cần chạm vào view model và mã model, miễn là view được triển khai hoàn toàn bằng XAML hoặc C#. Do đó, phiên bản mới của view sẽ hoạt động với view model hiện có.
- Nhà thiết kế và nhà phát triển có thể làm việc độc lập và đồng thời trên các thành phần của họ trong quá trình phát triển. Nhà thiết kế có thể tập trung vào view, trong khi nhà phát triển có thể làm việc trên view model và các thành phần model.
Chìa khóa để sử dụng MVVM một cách hiệu quả nằm ở việc hiểu cách đưa mã ứng dụng vào các lớp chính xác và cách các lớp tương tác. Các phần sau thảo luận về trách nhiệm của từng lớp trong mẫu MVVM.
View
View chịu trách nhiệm xác định cấu trúc, bố cục và hình thức của những gì người dùng nhìn thấy trên màn hình. Lý tưởng nhất là mỗi view được xác định trong XAML, với mã phía sau giới hạn không chứa logic nghiệp vụ. Tuy nhiên, trong một số trường hợp, mã phía sau có thể chứa logic giao diện người dùng thực hiện hành vi trực quan khó diễn đạt trong XAML, chẳng hạn như hoạt ảnh.
Trong ứng dụng .NET MAUI, một view thường là lớp ContentPage
có nguồn gốc hoặc ContentView
có nguồn gốc. Tuy nhiên, các view cũng có thể được biểu thị bằng mẫu dữ liệu, mẫu này chỉ định các thành phần giao diện người dùng được sử dụng để thể hiện trực quan một đối tượng khi nó được hiển thị. Mẫu dữ liệu dưới dạng view không có bất kỳ mã phía sau nào và được thiết kế để liên kết với một loại view model cụ thể.
Mẹo
Tránh bật và tắt các thành phần giao diện người dùng ở phần mã phía sau.
Đảm bảo rằng các view model chịu trách nhiệm xác định các thay đổi trạng thái logic ảnh hưởng đến một số khía cạnh hiển thị của view, chẳng hạn như lệnh có sẵn hay không hoặc dấu hiệu cho thấy một thao tác đang chờ xử lý. Do đó, hãy bật và tắt các thành phần giao diện người dùng bằng cách liên kết để xem các thuộc tính model, thay vì bật và tắt chúng ở mã phía sau.
Có một số tùy chọn để thực thi mã trên view model để phản hồi các tương tác trên view, chẳng hạn như nhấp vào nút hoặc chọn mục. Nếu một điều khiển hỗ trợ các lệnh, property Command của control có thể được liên kết dữ liệu với property ICommand trên view model. Khi lệnh của control được gọi, mã trong view model sẽ được thực thi. Ngoài các lệnh, các hành vi có thể được gắn vào một đối tượng trong view và có thể lắng nghe lệnh được gọi hoặc sự kiện được nêu ra. Để đáp lại, hành vi sau đó có thể gọi ICommand trên view model hoặc một phương thức trên view model.
ViewModel
ViewModel triển khai các property và lệnh mà view có thể liên kết dữ liệu và thông báo cho view về bất kỳ thay đổi trạng thái nào thông qua các sự kiện thông báo thay đổi. Các property và lệnh mà view model cung cấp xác định chức năng được giao diện người dùng cung cấp, nhưng view xác định cách hiển thị chức năng đó.
Mẹo
Giữ giao diện người dùng phản hồi nhanh với các hoạt động không đồng bộ.
Các ứng dụng đa nền tảng phải giữ cho luồng giao diện người dùng không bị chặn để cải thiện nhận thức của người dùng về hiệu suất. Do đó, trong view model, hãy sử dụng các phương thức không đồng bộ cho các hoạt động I/O và nâng cao các sự kiện để thông báo không đồng bộ cho các view về các thay đổi property.
View model cũng chịu trách nhiệm điều phối các tương tác của view với bất kỳ lớp model nào được yêu cầu. Thông thường có mối quan hệ một-nhiều giữa view model và các lớp model. View model có thể chọn hiển thị trực tiếp các lớp model cho view để các control trong view có thể liên kết dữ liệu trực tiếp với chúng. Trong trường hợp này, các lớp model sẽ cần được thiết kế để hỗ trợ các sự kiện thông báo thay đổi và liên kết dữ liệu.
Mỗi view model cung cấp dữ liệu từ một model ở dạng mà view có thể dễ dàng sử dụng. Để thực hiện điều này, view model đôi khi thực hiện chuyển đổi dữ liệu. Việc đặt chuyển đổi dữ liệu này vào view model là một ý tưởng hay vì nó cung cấp các thuộc tính mà view có thể liên kết. Ví dụ: view model có thể kết hợp các giá trị của hai property để giúp view hiển thị dễ dàng hơn.
Mẹo
Tập trung chuyển đổi dữ liệu trong một lớp chuyển đổi.
Bạn cũng có thể sử dụng trình chuyển đổi làm lớp chuyển đổi dữ liệu riêng biệt nằm giữa view model và view. Điều này có thể cần thiết, chẳng hạn như khi dữ liệu yêu cầu định dạng đặc biệt mà view model không cung cấp.
Để view model tham gia liên kết dữ liệu hai chiều với view, các property của nó phải đưa ra sự kiện PropertyChanged
. Các view model đáp ứng yêu cầu này bằng cách triển khai interface INotifyPropertyChanged
và đưa ra sự kiện PropertyChanged
khi property được thay đổi.
Đối với các collection, view thân thiện ObservableCollection<T>
sẽ được cung cấp. Collection này triển khai thông báo thay đổi collection, giúp nhà phát triển không phải triển khai interface INotifyCollectionChanged
trên các collection.
Model
Các lớp model là các lớp không trực quan thực hiện việc đóng gói dữ liệu của ứng dụng. Do đó, model có thể được coi là đại diện cho model miền của ứng dụng, thường bao gồm model dữ liệu cùng với logic nghiệp vụ và xác thực. Ví dụ về các đối tượng model bao gồm các đối tượng truyền dữ liệu (DTO), Đối tượng CLR cũ đơn giản (POCO) cũng như các đối tượng proxy và thực thể được tạo.
Các lớp model thường được sử dụng cùng với các dịch vụ hoặc kho lưu trữ đóng gói quyền truy cập dữ liệu và bộ nhớ đệm.
Kết nối các view model với các view
Các view model có thể được kết nối với các view bằng cách sử dụng khả năng liên kết dữ liệu của .NET MAUI. Có nhiều cách tiếp cận có thể được sử dụng để xây dựng các view và các view model cũng như liên kết chúng trong thời gian chạy. Các cách tiếp cận này chia thành hai loại, được gọi là thành phần view đầu tiên và thành phần view model đầu tiên. Việc lựa chọn giữa bố cục view đầu tiên và bố cục view đầu tiên của model là một vấn đề về mức độ ưu tiên và độ phức tạp. Tuy nhiên, tất cả các cách tiếp cận đều có chung một mục đích, đó là view có view model được gán cho property BindingContext của nó.
Với view đầu tiên, ứng dụng về mặt khái niệm bao gồm các view kết nối với các view model mà chúng phụ thuộc vào. Lợi ích chính của phương pháp này là giúp dễ dàng xây dựng các ứng dụng có thể kiểm thử đơn vị, được ghép nối lỏng lẻo vì các view model không phụ thuộc vào chính các view. Bạn cũng có thể dễ dàng hiểu cấu trúc của ứng dụng bằng cách tuân theo cấu trúc trực quan của nó, thay vì phải theo dõi quá trình thực thi mã để hiểu cách các lớp được tạo và liên kết. Ngoài ra, cấu trúc view đầu tiên phù hợp với hệ thống điều hướng của Microsoft Maui, hệ thống này chịu trách nhiệm xây dựng các trang khi điều hướng xảy ra, điều này làm cho bố cục view model đầu tiên trở nên phức tạp và không khớp với nền tảng.
Với thành phần đầu tiên của view model, ứng dụng về mặt khái niệm bao gồm các view model, với một dịch vụ chịu trách nhiệm định vị view cho view model. Thành phần đầu tiên của view model mang lại cảm giác tự nhiên hơn đối với một số nhà phát triển, vì việc tạo view có thể được trừu tượng hóa, cho phép họ tập trung vào cấu trúc logic phi giao diện người dùng của ứng dụng. Ngoài ra, nó cho phép các view model được tạo bởi các view model khác. Tuy nhiên, cách tiếp cận này thường phức tạp và có thể khó hiểu cách tạo và liên kết các phần khác nhau của ứng dụng.
Mẹo
Giữ view model và view độc lập.
Việc ràng buộc các view với một property trong nguồn dữ liệu phải là phần phụ thuộc chính của view vào view model tương ứng của nó. Cụ thể, không tham chiếu các loại view, chẳng hạn như Button và ListView, từ view model. Bằng cách tuân theo các nguyên tắc được nêu ở đây, các view model có thể được kiểm tra một cách độc lập, do đó làm giảm khả năng xảy ra lỗi phần mềm bằng cách giới hạn phạm vi.
Các phần sau thảo luận về các cách tiếp cận chính để kết nối các view model với các view.
Tạo view model theo cách khai báo
Cách tiếp cận đơn giản nhất là để view khai báo view model tương ứng của nó trong XAML. Khi view được dựng, đối tượng view model tương ứng cũng sẽ được dựng. Cách tiếp cận này được thể hiện trong ví dụ mã sau:
<ContentPage xmlns:local="clr-namespace:eShop">
<ContentPage.BindingContext>
<local:LoginViewModel />
</ContentPage.BindingContext>
<!-- Omitted for brevity... -->
</ContentPage>
Khi ContentPage
được tạo, một phiên bản của view LoginViewModel
sẽ được tự động tạo và đặt làm view BindingContext
.
Việc xây dựng khai báo và gán view model theo view này có ưu điểm là đơn giản nhưng có nhược điểm là nó yêu cầu một hàm tạo mặc định (không có tham số) trong view model.
Tạo view model theo chương trình
Một view có thể có mã trong tệp mã phía sau, dẫn đến view model được gán cho property BindingContext
của nó. Điều này thường được thực hiện trong hàm tạo của view, như trong ví dụ về mã sau:
public LoginView()
{
InitializeComponent();
BindingContext = new LoginViewModel(navigationService);
}
Việc xây dựng và gán view model theo chương trình trong mã phía sau của view có ưu điểm là đơn giản. Tuy nhiên, nhược điểm chính của phương pháp này là view cần cung cấp cho view model mọi phụ thuộc bắt buộc. Việc sử dụng vùng chứa Dependency Injection (DI) có thể giúp duy trì khớp nối lỏng lẻo giữa view và view model.
Cập nhật view để đáp ứng với các thay đổi trong model hoặc view model cơ bản
Tất cả các lớp model và view model mà một view có thể truy cập phải triển khai interface INotifyPropertyChanged. Việc triển khai giao diện này trong view model hoặc lớp model cho phép lớp cung cấp thông báo thay đổi cho bất kỳ control ràng buộc dữ liệu nào trong view khi giá trị property cơ bản thay đổi.
Ứng dụng phải được thiết kế để sử dụng đúng thông báo thay đổi property bằng cách đáp ứng các yêu cầu sau:
- Luôn đưa ra một sự kiện
PropertyChanged
nếu giá trị của property public thay đổi. Đừng cho rằng việc nâng cao sự kiệnPropertyChanged
có thể bị bỏ qua vì đã biết về cách xảy ra ràng buộc XAML. - Luôn đưa ra một sự kiện
PropertyChanged
cho bất kỳ property được tính toán nào có giá trị được các property khác sử dụng trong model hoặc view model. - Luôn đưa ra sự kiện
PropertyChanged
ở cuối phương thức làm thay đổi property hoặc khi đối tượng được biết là ở trạng thái an toàn. Việc đưa ra sự kiện sẽ làm gián đoạn hoạt động bằng cách gọi các trình xử lý sự kiện một cách đồng bộ. Nếu điều này xảy ra ở giữa một thao tác, nó có thể khiến đối tượng phải thực hiện các chức năng gọi lại khi nó ở trạng thái không an toàn, được cập nhật một phần. Ngoài ra, các sự kiện có thể kích hoạt các thay đổi xếp tầngPropertyChanged
. Các thay đổi xếp tầng thường yêu cầu phải hoàn tất cập nhật trước khi thực hiện thay đổi xếp tầng một cách an toàn. - Không bao giờ đưa ra sự kiện
PropertyChanged
nếu property không thay đổi. Điều này có nghĩa là bạn phải so sánh giá trị cũ và mới trước khi đưa ra sự kiệnPropertyChanged
. - Không bao giờ đưa ra sự kiện
PropertyChanged
trong hàm tạo của view model nếu bạn đang khởi tạo một property. Các control ràng buộc dữ liệu trong view sẽ không được đăng ký nhận thông báo thay đổi tại thời điểm này. - Không bao giờ đưa ra nhiều sự kiện
PropertyChanged
có cùng đối số tên property trong một lệnh gọi đồng bộ duy nhất của phương thức public của một lớp. Ví dụ: với một propertyNumberOfItems
có kho dự phòng là trường_numberOfItems
, nếu một phương thức tăng_numberOfItems
năm mươi lần trong khi thực hiện vòng lặp thì phương thức đó chỉ nên đưa ra thông báo thay đổi property trên propertyNumberOfItems
một lần, sau khi tất cả công việc đã hoàn tất. Đối với các phương thức không đồng bộ, hãy tăng sự kiệnPropertyChanged
cho tên property nhất định trong mỗi phân đoạn đồng bộ của chuỗi tiếp tục không đồng bộ.
Một cách đơn giản để cung cấp chức năng này là tạo một phần mở rộng của lớp BindableObject
. Trong ví dụ này, lớp ExtendedBindableObject
cung cấp thông báo thay đổi, được hiển thị trong ví dụ mã sau:
public abstract class ExtendedBindableObject : BindableObject
{
public void RaisePropertyChanged<T>(Expression<Func<T>> property)
{
var name = GetMemberInfo(property).Name;
OnPropertyChanged(name);
}
private MemberInfo GetMemberInfo(Expression expression)
{
// Omitted for brevity ...
}
}
Lớp BindableObject
của .NET MAUI triển khai interface INotifyPropertyChanged
và cung cấp một phương thức OnPropertyChanged
. Lớp ExtendedBindableObject
cung cấp phương thức RaisePropertyChanged
để gọi thông báo thay đổi property và khi làm như vậy sẽ sử dụng chức năng do lớp BindableObject
cung cấp.
Xem các lớp model sau đó có thể xuất phát từ lớp ExtendedBindableObject
. Do đó, mỗi lớp view model sử dụng phương thức RaisePropertyChanged
trong lớp ExtendedBindableObject
để cung cấp thông báo thay đổi property. Ví dụ về mã sau đây cho thấy cách ứng dụng đa nền tảng eShopOnContainers gọi thông báo thay đổi property bằng cách sử dụng biểu thức lambda:
public bool IsLogin
{
get => _isLogin;
set
{
_isLogin = value;
RaisePropertyChanged(() => IsLogin);
}
}
Việc sử dụng biểu thức lambda theo cách này sẽ tốn một ít chi phí hiệu năng vì biểu thức lambda phải được đánh giá cho mỗi lệnh gọi. Mặc dù chi phí hiệu suất nhỏ và thường không ảnh hưởng đến ứng dụng nhưng chi phí có thể tích lũy khi có nhiều thông báo thay đổi. Tuy nhiên, lợi ích của phương pháp này là nó cung cấp sự hỗ trợ tái cấu trúc và an toàn kiểu thời gian biên dịch khi đổi tên các property.
Framework MVVM
Mẫu MVVM được thiết lập tốt trong .NET và cộng đồng đã tạo ra nhiều framework giúp tạo điều kiện thuận lợi cho sự phát triển này. Mỗi framework cung cấp một bộ tính năng khác nhau, nhưng tiêu chuẩn để chúng cung cấp một view model chung với việc triển khai interface là tiêu chuẩn INotifyPropertyChanged
. Các tính năng bổ sung của framework MVVM bao gồm các lệnh tùy chỉnh, trình trợ giúp điều hướng, các thành phần định vị dịch vụ/Dependency Injection và tích hợp nền tảng giao diện người dùng. Mặc dù không cần thiết phải sử dụng các framework này nhưng chúng có thể tăng tốc và tiêu chuẩn hóa quá trình phát triển của bạn. Ứng dụng đa nền tảng eShopOnContainers sử dụng Bộ công cụ MVVM cộng đồng .NET. Khi chọn một framework, bạn nên xem xét nhu cầu của ứng dụng và điểm mạnh của nhóm bạn. Danh sách bên dưới bao gồm một số framework MVVM phổ biến hơn cho .NET MAUI.
Tương tác giao diện người dùng bằng lệnh và hành vi
Trong các ứng dụng đa nền tảng, các hành động thường được gọi để phản hồi lại hành động của người dùng, chẳng hạn như thao tác bấm nút, có thể được triển khai bằng cách tạo trình xử lý sự kiện trong tệp mã sau. Tuy nhiên, trong mẫu MVVM, trách nhiệm thực hiện hành động thuộc về view model và nên tránh đặt mã ở phía sau mã.
Lệnh cung cấp một cách thuận tiện để thể hiện các hành động có thể bị ràng buộc với các control trong giao diện người dùng. Chúng đóng gói mã thực hiện hành động và giúp tách mã đó khỏi biểu diễn trực quan của nó trong view. Bằng cách này, các view model của bạn trở nên linh hoạt hơn với các nền tảng mới vì chúng không phụ thuộc trực tiếp vào các sự kiện do framework giao diện người dùng của nền tảng cung cấp. .NET MAUI bao gồm các control có thể được kết nối khai báo với một lệnh và các control này sẽ gọi lệnh khi người dùng tương tác với control.
Các hành vi cũng cho phép các control được kết nối khai báo với một lệnh. Tuy nhiên, hành vi có thể được sử dụng để gọi một hành động liên quan đến một loạt sự kiện do control đưa ra. Do đó, hành vi giải quyết nhiều tình huống giống như điều khiển kích hoạt bằng lệnh, đồng thời mang lại mức độ linh hoạt và kiểm soát cao hơn. Ngoài ra, các hành vi cũng có thể được sử dụng để liên kết các đối tượng hoặc phương thức lệnh với các control không được thiết kế đặc biệt để tương tác với các lệnh.
Thực thi các lệnh
Các view model thường hiển thị các property public để liên kết từ view triển khai interface ICommand
. Nhiều control và cử chỉ .NET MAUI cung cấp một property Command
, có thể là dữ liệu được liên kết với một đối tượng ICommand
do view model cung cấp. Control nút là một trong những control được sử dụng phổ biến nhất, cung cấp property lệnh thực thi khi nhấp vào nút.
Ghi chú
Mặc dù có thể hiển thị cách triển khai thực tế của interface ICommand
mà view model của bạn sử dụng (ví dụ: Command<T>
hoặc RelayCommand
), nhưng bạn nên hiển thị công khai các lệnh của mình dưới dạng ICommand
. Bằng cách này, nếu sau này bạn cần thay đổi cách triển khai, bạn có thể dễ dàng hoán đổi nó.
Interface ICommand
xác định một phương thức Execute
đóng gói chính hoạt động đó, một phương thức CanExecute
cho biết liệu lệnh có thể được gọi hay không và một sự kiện t CanExecuteChanged
xảy ra khi có thay đổi ảnh hưởng đến việc lệnh có được thực thi hay không. Trong hầu hết các trường hợp, ta sẽ chỉ cung cấp phương thức Execute
cho các lệnh của mình. Để biết tổng quan chi tiết hơn về ICommand
, hãy tham khảo tài liệu Commanding dành cho .NET MAUI.
.NET MAUI được cung cấp là các lớp Command
và Command<T>
triển khai interface ICommand
, trong đó T
là kiểu đối số cho Execute
và CanExecute
. Command
và Command<T>
là những triển khai cơ bản cung cấp bộ chức năng tối thiểu cần thiết cho interface ICommand
.
Ghi chú
Nhiều framework MVVM cung cấp nhiều triển khai interface
ICommand
giàu tính năng hơn.
Hàm khởi tạo Command
hoặc Command<T>
yêu cầu một đối tượng gọi lại Hành động được gọi khi phương thức ICommand.Execute
được gọi. Phương thức CanExecute
là một tham số hàm tạo tùy chọn và là một Func trả về một bool.
Ứng dụng đa nền tảng eShopOnContainers sử dụng RelayCommand và AsyncRelayCommand. Lợi ích chính của các ứng dụng hiện đại là AsyncRelayCommand
cung cấp chức năng tốt hơn cho các hoạt động không đồng bộ.
Đoạn mã sau đây cho thấy cách một phiên bản Command
đại diện cho lệnh đăng ký được xây dựng bằng cách chỉ định một delegate cho phương thức view mode Register:
public ICommand RegisterCommand { get; }
Lệnh được hiển thị trong view thông qua property trả về tham chiếu đến tệp ICommand
. Khi phương thức Execute
được gọi trên đối tượng Command
, nó chỉ chuyển tiếp lời gọi đến phương thức trong view model thông qua delegate đã được chỉ định trong hàm tạo Command
. Một phương thức không đồng bộ có thể được gọi bằng lệnh bằng cách sử dụng từ khóa async và chờ khi chỉ định delegate Execute
của lệnh. Điều này cho biết rằng lời gọi lại là một Task
và nên được chờ đợi. Ví dụ: đoạn mã sau cho thấy cách ICommand
xây dựng một phiên bản, đại diện cho lệnh đăng nhập, bằng cách chỉ định một delegate cho phương thức view model SignInAsync
:
public ICommand SignInCommand { get; }
...
SignInCommand = new AsyncRelayCommand(async () => await SignInAsync());
Các tham số có thể được truyền cho các hành động Execute
và CanExecute
bằng cách sử dụng lớp AsyncRelayCommand<T>
để khởi tạo lệnh. Ví dụ: đoạn mã sau cho biết cách AsyncRelayCommand<T>
sử dụng một thể hiện để chỉ ra rằng phương thức NavigateAsync
sẽ yêu cầu một đối số kiểu chuỗi:
public ICommand NavigateCommand { get; }
...
NavigateCommand = new AsyncRelayCommand<string>(NavigateAsync);
Trong cả hai lớp RelayCommand
và RelayCommand<T>
, việc ủy quyền cho phương thức CanExecute
trong mỗi hàm tạo là tùy chọn. Nếu delegate không được chỉ định, kết quả Command
sẽ trả về true cho CanExecute
. Tuy nhiên, view model có thể chỉ ra sự thay đổi trạng thái của lệnh CanExecute
bằng cách gọi phương thức ChangeCanExecute
trên đối tượng Command
. Điều này khiến sự kiện CanExecuteChanged
được nâng lên. Sau đó, mọi control giao diện người dùng được liên kết với lệnh sẽ cập nhật trạng thái đã bật của chúng để phản ánh tính khả dụng của lệnh liên kết dữ liệu.
Gọi lệnh từ view
Ví dụ mã sau đây cho thấy cách một Grid
trong LoginView
liên kết với RegisterCommand
trong lớp LoginViewModel
bằng cách sử dụng một thể hiện TapGestureRecognizer
:
<Grid Grid.Column="1" HorizontalOptions="Center">
<Label Text="REGISTER" TextColor="Gray"/>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding RegisterCommand}" NumberOfTapsRequired="1" />
</Grid.GestureRecognizers>
</Grid>
Một tham số lệnh cũng có thể được xác định tùy ý bằng property CommandParameter
. Loại đối số dự kiến được chỉ định trong phương thức mục tiêu Execute
và CanExecute
. TapGestureRecognizer
sẽ tự động gọi lệnh đích khi người dùng tương tác với control đính kèm. CommandParameter
nếu được cung cấp, sẽ được truyền làm đối số cho delegate Execute của lệnh.
Thực thi các hành vi
Hành vi cho phép thêm chức năng vào control giao diện người dùng mà không cần phải phân lớp chúng. Thay vào đó, chức năng này được triển khai trong một lớp hành vi và được gắn vào control như thể nó là một phần của chính control đó. Hành vi cho phép bạn triển khai mã mà bạn thường phải viết dưới dạng mã phía sau, vì nó tương tác trực tiếp với API của control, theo cách mà nó có thể được gắn chính xác vào control và được đóng gói để sử dụng lại trên nhiều nền tảng. Trong bối cảnh MVVM, hành vi là một cách tiếp cận hữu ích để kết nối các control với lệnh.
Hành vi được gắn với control thông qua các property được đính kèm được gọi là hành vi được đính kèm. Sau đó, hành vi có thể sử dụng API hiển thị của phần tử được đính kèm để thêm chức năng cho control đó hoặc các control khác trong cây trực quan của view.
Hành vi .NET MAUI là một lớp dẫn xuất từ lớp Behavior
hoặc Behavior<T>
, trong đó T là loại control mà hành vi đó sẽ áp dụng. Các lớp này cung cấp OnAttachedTo
và OnDetachingFrom
các phương thức cần được ghi đè để cung cấp logic sẽ được thực thi khi hành vi được gắn vào và tách khỏi các control.
Trong ứng dụng đa nền tảng eShopOnContainers, lớp BindableBehavior<T>
dẫn xuất từ lớp Behavior<T>
. Mục đích của lớp BindableBehavior<T>
này là cung cấp một lớp cơ sở cho các hành vi .NET MAUI yêu cầu BindingContext
của hành vi đó phải được đặt thành control đính kèm.
Lớp BindableBehavior<T>
cung cấp một phương thức OnAttachedTo
có thể ghi đè để thiết lập BindingContext
của hành vi và một phương thức OnDetachingFrom
có thể ghi đè để dọn sạch BindingContext
.
Ứng dụng đa nền tảng eShopOnContainers bao gồm lớp EventToCommandBehavior được cung cấp bởi bộ công cụ MAUI Community. EventToCommandBehavior
thực hiện một lệnh để đáp lại một sự kiện xảy ra. Lớp này dẫn xuất từ lớp BaseBehavior<View>
để hành vi có thể liên kết và thực thi một ICommand
được chỉ định bởi property Command
hành vi được sử dụng. Ví dụ mã sau đây cho thấy lớp EventToCommandBehavior
:
/// <summary>
/// The <see cref="EventToCommandBehavior"/> is a behavior that allows the user to invoke a <see cref="ICommand"/> through an event. It is designed to associate Commands to events exposed by controls that were not designed to support Commands. It allows you to map any arbitrary event on a control to a Command.
/// </summary>
public class EventToCommandBehavior : BaseBehavior<VisualElement>
{
// Omitted for brevity...
/// <inheritdoc/>
protected override void OnAttachedTo(VisualElement bindable)
{
base.OnAttachedTo(bindable);
RegisterEvent();
}
/// <inheritdoc/>
protected override void OnDetachingFrom(VisualElement bindable)
{
UnregisterEvent();
base.OnDetachingFrom(bindable);
}
static void OnEventNamePropertyChanged(BindableObject bindable, object oldValue, object newValue)
=> ((EventToCommandBehavior)bindable).RegisterEvent();
void RegisterEvent()
{
UnregisterEvent();
var eventName = EventName;
if (View is null || string.IsNullOrWhiteSpace(eventName))
{
return;
}
eventInfo = View.GetType()?.GetRuntimeEvent(eventName) ??
throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't resolve the event.", nameof(EventName));
ArgumentNullException.ThrowIfNull(eventInfo.EventHandlerType);
ArgumentNullException.ThrowIfNull(eventHandlerMethodInfo);
eventHandler = eventHandlerMethodInfo.CreateDelegate(eventInfo.EventHandlerType, this) ??
throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't create event handler.", nameof(EventName));
eventInfo.AddEventHandler(View, eventHandler);
}
void UnregisterEvent()
{
if (eventInfo is not null && eventHandler is not null)
{
eventInfo.RemoveEventHandler(View, eventHandler);
}
eventInfo = null;
eventHandler = null;
}
/// <summary>
/// Virtual method that executes when a Command is invoked
/// </summary>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
protected virtual void OnTriggerHandled(object? sender = null, object? eventArgs = null)
{
var parameter = CommandParameter
?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null);
var command = Command;
if (command?.CanExecute(parameter) ?? false)
{
command.Execute(parameter);
}
}
}
Các phương thức OnAttachedTo
và OnDetachingFrom
được sử dụng để đăng ký và hủy đăng ký một trình xử lý sự kiện cho sự kiện được xác định trong property EventName
. Sau đó, khi sự kiện xảy ra, thì phương thức OnTriggerHandled
sẽ được gọi và thực thi lệnh.
Ưu điểm của việc sử dụng EventToCommandBehavior
để thực thi lệnh khi sự kiện xảy ra là các lệnh có thể được liên kết với các control không được thiết kế để tương tác với lệnh. Ngoài ra, điều này sẽ di chuyển mã xử lý sự kiện để xem các model, nơi nó có thể được kiểm tra đơn vị.
Gọi các hành vi từ một view
EventToCommandBehavior đặc biệt hữu ích khi đính kèm lệnh vào control không hỗ trợ lệnh. Ví dụ: loginView sử dụng EventToCommandBehavior
để thực thi ValidateCommand
khi người dùng thay đổi giá trị mật khẩu của họ, như trong đoạn mã sau:
<Entry
IsPassword="True"
Text="{Binding Password.Value, Mode=TwoWay}">
<!-- Omitted for brevity... -->
<Entry.Behaviors>
<mct:EventToCommandBehavior
EventName="TextChanged"
Command="{Binding ValidateCommand}" />
</Entry.Behaviors>
<!-- Omitted for brevity... -->
</Entry>
Trong thời gian chạy, EventToCommandBehavior
sẽ phản hồi tương tác với Entry
. Khi người dùng nhập vào trường Entry
, thì sự kiện TextChanged
sẽ kích hoạt, sự kiện này sẽ thực thi ValidateCommand
trong LoginViewModel
. Theo mặc định, các đối số sự kiện cho sự kiện này được truyền cho lệnh. Nếu cần, thì property EventArgsConverter
có thể được sử dụng để chuyển đổi giá trị EventArgs
do sự kiện cung cấp thành giá trị mà lệnh mong đợi làm đầu vào.
Để biết thêm thông tin về hành vi, hãy xem Hành vi trên Trung tâm nhà phát triển .NET MAUI.
Tóm tắt
Mẫu Model-View-ViewModel (MVVM) giúp tách biệt rõ ràng logic trình bày và nghiệp vụ của ứng dụng khỏi giao diện người dùng (UI) của nó. Việc duy trì sự tách biệt rõ ràng giữa logic ứng dụng và giao diện người dùng giúp giải quyết nhiều vấn đề phát triển và giúp ứng dụng dễ dàng kiểm tra, bảo trì và phát triển hơn. Nó cũng có thể cải thiện đáng kể cơ hội sử dụng lại mã và cho phép các nhà phát triển và nhà thiết kế giao diện người dùng cộng tác dễ dàng hơn khi phát triển các phần tương ứng của ứng dụng.
Bằng cách sử dụng mẫu MVVM, giao diện người dùng của ứng dụng cũng như logic nghiệp vụ và bản trình bày cơ bản được tách thành ba lớp riêng biệt: view, gói gọn giao diện người dùng và logic giao diện người dùng; view model, gói gọn trạng thái và logic trình bày; và model, gói gọn dữ liệu và logic nghiệp vụ của ứng dụng.