Tối ưu hóa phát triển WPF với mẫu MVVM

Mẫu MVVM (Model-View-ViewModel) là một kiến trúc thiết kế mạnh mẽ, đặc biệt phù hợp với các ứng dụng WPF. Nó giúp tách biệt rõ ràng giữa giao diện người dùng (View) và logic nghiệp vụ (Model), thông qua một thành phần trung gian gọi là ViewModel. Việc áp dụng MVVM đòi hỏi sự quen thuộc với cơ chế Binding và Commands của WPF để đạt hiệu quả cao nhất.

Sự Ra Đời Của MVVM

Trong quá trình phát triển phần mềm, chúng ta thường trừu tượng hóa các đối tượng thực tế thành các Model. Tuy nhiên, Model tự nó không thể tương tác trực tiếp với người dùng. Do đó, chúng ta cần một giao diện (View) để người dùng có thể tương tác. Liên kết (Binding) đóng vai trò cầu nối, cho phép dữ liệu giữa View và Model được đồng bộ hóa. Ví dụ, một ô nhập liệu trên View có thể được liên kết với thuộc tính "Tên người dùng" trong Model, cho phép người dùng xem và sửa đổi dữ liệu này.

Trong thực tế, việc liên kết trực tiếp giữa Model và các Control trên View thường gặp khó khăn do sự không khớp về kiểu dữ liệu hoặc yêu cầu xử lý dữ liệu bổ sung. Lúc này, một lớp "trợ giúp" (helper) trở nên cần thiết. Ban đầu, logic này có thể được đặt trong code-behind của View. Tuy nhiên, để đạt được khả năng thay thế View (ví dụ: chuyển đổi từ ứng dụng desktop sang web hoặc mobile) và tuân theo các nguyên tắc thiết kế, các mẫu như MVC và MVP đã ra đời.

MVVM, một biến thể của các mẫu trên, không chỉ giải quyết vấn đề tách biệt mã nguồn mà còn đáp ứng sự thay đổi trong phương thức phát triển phần mềm theo nhóm. Đặc biệt trong WPF, MVVM giúp phân chia công việc giữa lập trình viên và nhà thiết kế UI/UX một cách hiệu quả. Để thực hiện tốt MVVM, cần hai điều kiện:

  • Có một nhà thiết kế UI/UX có khả năng sử dụng các công cụ như Blend để tạo ra XAML, tập trung vào UI/UE và có tư duy lập trình nhất định để tạo ra các thành phần có thể hoạt động tốt.
  • Có một lập trình viên có khả năng viết mã chất lượng cao mà không cần phụ thuộc quá nhiều vào lớp View.

MVVM hỗ trợ điều kiện thứ hai này bằng cách cho phép trích xuất logic của View sang ViewModel. Trong các mẫu như MVC và MVP, lớp View thường chứa nhiều logic, và lập trình viên là người thực thi chính. Với WPF, các cơ chế như Binding và Commands cho phép xử lý dữ liệu và tương tác người dùng trực tiếp trong XAML. ViewModel trở thành nơi chứa logic này, được tùy chỉnh cho View cụ thể, cung cấp thông tin cần thiết cho Binding (như Converters), DataContext, và định nghĩa Commands. Nó cũng đóng vai trò là một biến thể của Controller, chịu trách nhiệm điều phối luồng xử lý nghiệp vụ.

ViewModel và Kiểm thử Đơn vị

Khi áp dụng MVVM, lập trình viên nên tập trung vào ViewModel và coi View như một "người tiêu dùng" của ViewModel, tương tự như kiểm thử đơn vị (Unit Test). Josh Smith đã chỉ ra rằng "View và Unit Test chỉ là hai loại người tiêu dùng khác nhau của ViewModel". ViewModel là một biểu diễn trừu tượng của View, có "linh hồn" của View nhưng không có các Control trực quan. Do đó, việc phát triển ViewModel có thể coi là hoàn thành phần logic của View.

Để đảm bảo ViewModel hoạt động đúng đắn trước khi kết hợp với View thực tế, kiểm thử đơn vị là cực kỳ quan trọng. ViewModel nên được kiểm thử một cách độc lập, không phụ thuộc vào bất kỳ Control UI nào. Điều này gợi ý về việc áp dụng TDD (Test-Driven Development) cho ViewModel.

Kỹ thuật AttachBehavior

Thông thường, các tính năng của WPF như Commands, Binding và Attached Properties là đủ để View và ViewModel phối hợp nhịp nhàng. Ví dụ, khi một Button được nhấp, chúng ta có thể liên kết một Command với nó. Tuy nhiên, nếu cần thực hiện một hành động khi Button được tải (Load), Command không phải là giải pháp trực tiếp vì Button không có sự kiện Load kích hoạt Command.

Việc viết trình xử lý sự kiện Load trực tiếp trong file code-behind của XAML sẽ đi ngược lại nguyên tắc thiết kế MVVM. Một giải pháp khả dĩ nhưng không tối ưu là kế thừa Button và thêm logic xử lý sự kiện Load. Thay vào đó, kỹ thuật AttachBehavior mang lại giải pháp thanh lịch hơn. AttachBehavior không phải là một tính năng của WPF mà là một thực hành tốt (best practice) hoặc một mẫu thiết kế (pattern).

Nó cho phép mở rộng chức năng của các Control hiện có mà không cần kế thừa, tương tự như cách Attached Properties hoạt động. Dưới đây là một ví dụ về cách triển khai AttachBehavior để xử lý sự kiện `Selected` của `TreeViewItem`:

    public static class TreeViewItemBehavior
    {
        #region IsBroughtIntoViewWhenSelected

        public static bool GetIsBroughtIntoViewWhenSelected(TreeViewItem treeViewItem)
        {
            return (bool)treeViewItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
        }

        public static void SetIsBroughtIntoViewWhenSelected(TreeViewItem treeViewItem, bool value)
        {
            treeViewItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
        }

        public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
            DependencyProperty.RegisterAttached(
            "IsBroughtIntoViewWhenSelected",
            typeof(bool),
            typeof(TreeViewItemBehavior),
            new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));

        static void OnIsBroughtIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            TreeViewItem item = depObj as TreeViewItem;
            if (item == null)
                return;

            if (e.NewValue is bool == false)
                return;

            if ((bool)e.NewValue)
                item.Selected += OnTreeViewItemSelected;
            else
                item.Selected -= OnTreeViewItemSelected;
        }

        static void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
        {
            if (!Object.ReferenceEquals(sender, e.OriginalSource))
                return;

            TreeViewItem item = e.OriginalSource as TreeViewItem;
            if (item != null)
                item.BringIntoView();
        }

        #endregion // IsBroughtIntoViewWhenSelected
    }
    

Sau đó, bạn có thể áp dụng behavior này trong XAML như sau:

    <TreeView ItemsSource="{Binding FirstGeneration}" FontSize="18">
          <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
              <Setter 
                Property="local:TreeViewItemBehavior.IsBroughtIntoViewWhenSelected"    
                Value="True" 
                />
              <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
              <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
              <Setter Property="FontWeight" Value="Normal" />
              <Style.Triggers>
                <Trigger Property="IsSelected" Value="True">
                  <Setter Property="FontWeight" Value="Bold" />
                </Trigger>
              </Style.Triggers>
            </Style>
          </TreeView.ItemContainerStyle>

          <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Children}">
              <TextBlock Text="{Binding Name}" />
            </HierarchicalDataTemplate>
          </TreeView.ItemTemplate>
        </TreeView>
    

Việc sử dụng AttachBehavior giúp đơn giản hóa code-behind, tránh logic trong View và xử lý các sự kiện không thuộc về Command một cách hiệu quả.

Thẻ: WPF mvvm ViewModel Binding Commands

Đăng vào ngày 30 tháng 7 lúc 06:17