Sử dụng data binding – Series Giải pháp winforms (5)

0

Data binding là một công cụ rất mạnh của winform nhưng thường không được các bạn chú ý tới khi làm project. Thay vào đó, các bạn thường dùng cách thủ công để gán dữ liệu và đọc dữ liệu từ điều khiển. Kết quả là số lượng code behind rất lớn, mất nhiều công sức, dễ gặp lỗi, khó thay đổi thiết kế, khó đồng bộ dữ liệu giữa các điều khiển.

Trong phần 2 của loạt bài này chúng ta đã áp dụng data binding trong thiết kế giao diện winform. Tuy vậy, chúng ta chưa giải thích chi tiết kỹ thuật này.

Trong bài viết này, chúng ta sẽ trình bày chi tiết về kỹ thuật và cách áp dụng data binding trong winform. Chúng ta sẽ áp dụng data binding thực hiện những vấn đề sau: (1) đồng bộ dữ liệu giữa object và điều khiển; (2) bật/tắt (disable/enable) nút bấm theo điều kiện giá trị của object.

Hai giải pháp này có giá trị rất lớn giúp bạn phát huy công cụ data binding của windows forms để giảm số lượng code, giúp chương trình hoạt động ổn định và an toàn hơn.

Loạt bài “Các giải pháp dành cho lập trình winform”:
Phần 1 – Lỗi thường gặp trong lập trình winforms
Phần 2 – Thiết kế giao diện với Data Sources và BindingSource
Phần 3 – Phân chia code thành module sử dụng Interface
Phần 4 – Sử dụng thư viện DevExpress cho winforms
Phần 5 – Sử dụng Data Binding
Phần 6 – Sử dụng Entity Framework

Kỹ thuật Data binding trong C# và Winform

Trước khi đi vào thực hiện, chúng ta tìm hiểu qua khái niệm data binding trong winform và C#.

Data binding là gì?

Theo Wikipedia, data binding là kỹ thuật liên kết một nguồn dữ liệu với nơi sử dụng dữ liệu đó, và đồng bộ hóa dữ liệu giữa chúng.

Trong winform có thể hiểu đơn giản như thế này: nguồn dữ liệu là một object nào đó; nơi sử dụng dữ liệu là các điều khiển. Data binding cho phép liên kết một điều khiển với một object (hoặc danh sách các object). Điều khiển hiển thị thông tin chứa trong object.

Khi người dùng thay đổi giá trị trên điều khiển, giá trị mới sẽ được cập nhật ngay về object. Ở đây, sự đồng bộ dữ liệu diễn ra theo chiều điều khiển => object.

Ở chiều ngược lại, khi thay đổi giá trị của object chưa chắc đã dẫn đến sự cập nhật của điều khiển. Để thực hiện đồng bộ theo chiều ngược lại (tức là object => điều khiển), chúng ta phải sử dụng một giải pháp khác sẽ xem xét ở phần sau.

Nhiều điều khiển có thể cùng bind với một object. Nếu một trong số đó thay đổi giá trị của object, sự thay đổi này cần cập nhật đến tất cả các điều khiển khác. Đây là sự đồng bộ dữ liệu giữa các điều khiển.

Data binding trong winform

Một số lượng lớn các điều khiển cơ bản của winform hỗ trợ data binding. Cơ chế này bind (gắn kết) một thuộc tính (property) của điều khiển với một property của một object chứa dữ liệu và đồng bộ chúng. Ví dụ, Form có thuộc tính Text hỗ trợ binding với bất kỳ thuộc tính nào có kiểu string của object.

Một số điều khiển của winform không hỗ trợ data binding. Ví dụ, các điều khiển trên thanh toolbar hoặc status bar.

Để kiểm tra xem một điều khiển của winform có hỗ trợ data binding hay không, trước hết click chọn nó trên form. Ấn F4 để mở cửa sổ Properties và chọn cách sắp xếp là Categorized. Tìm đến mục Data. Nếu điều khiển hỗ trợ data binding, mục Data sẽ xuất hiện một mục con DataBindings như dưới đây.

Hỗ trợ data binding trong windows forms
Hỗ trợ data binding trong windows forms

Thư viện điều khiển của các hãng thứ ba cung cấp thường hỗ trợ data binding tốt hơn nhiều so với điều khiển gốc của windows forms. Bạn có thể tìm hiểu cách sử dụng thư viện Devexpress.

Data Binding trực tiếp và gián tiếp, lớp BindingSource của Winform

Binding trực tiếp là bind giữa control tới object của class chúng ta tự tạo. Binding gián tiếp là bind giữa control với một object của một class đặc biệt do .NET tạo ra có tên gọi là BindingSource.

Đây không phải là một phân loại chính thức. Sở dĩ có sự phân biệt này là do lớp BindingSource được .NET cung cấp.

Lớp BindingSource được .NET tạo ra với vai trò một nguồn dữ liệu trung gian, nằm giữa object chứa dữ liệu và điều khiển. BindingSource giúp đơn giản hóa quá trình binding cũng như đồng bộ giữa các điều khiển.

Khi thực hiện project ở phần 2 – thiết kế giao diện, chúng ta đã vận dụng cả hai cách này. Các điều khiển trên form (DataGridView, TextBox) bind gián tiếp qua BindingSource. Riêng tiêu đề của Form bind trực tiếp với một property của view model.

Binding gián tiếp qua BindingSource hỗ trợ sẵn việc đồng bộ ngược (từ object về điều khiển) cũng như đồng bộ các điều khiển. Khi dùng binding trực tiếp chúng ta phải tự thực hiện quá trình đồng bộ này.

Giao diện INotifyPropertyChanged

Khi thực hiện binding trực tiếp với object, quá trình đồng bộ chỉ diễn ra theo chiều điều khiển => object. Để sự thay đổi của object được cập nhật trên điều khiển, object cần thực thi giao diện INotifyPropertyChanged.

Chúng ta đã thực hiện giải pháp này khi bind thuộc tính Text của form Contacts với thuộc tính Title của ContactsViewModel.

Hiểu đơn giản, INotifyPropertyChanged yêu cầu class thực thi nó phải xây dựng sự kiện

public event PropertyChangedEventHandler PropertyChanged;

Các điều khiển của winform hỗ trợ data binding sẽ tự đăng ký theo dõi sự kiện này nếu class thực thi giao diện tương ứng. Khi một property của object thay đổi giá trị, sự thay đổi này sẽ được thông báo ra cho những nơi đăng ký theo dõi. Do đó, điều khiển sẽ cập nhật lại sự thay đổi theo giá trị của property của object. Đây là quá trình đồng bộ ngược object => điều khiển.

Áp dụng kỹ thuật data binding trong winform

Cập nhật dữ liệu trên điều khiển bằng code

Trước hết chúng ta sẽ thử nghiệm chức năng tính giá trị từ code rồi update giá trị của điều khiển.

Trong DxContacts form xây dựng ở bài trước đặt thêm một nút bấm Set Alias như dưới đây

Set Alias button trên ribbon

Nhiệm vụ của nút bấm này là tự động tạo biệt danh (alias) cho contact từ họ tên nếu người dùng không nhập thông tin cho trường Alias.

Mở file IContactsViewModel và bổ sung mô tả sau:

void SetAlias();

Thực thi phương thức này trong ContactsViewModel như sau:

public void SetAlias()
{
    if(ContactBindingSource.Current is Contact contact)
    {
        if(string.IsNullOrEmpty(contact.Alias))
        {
            contact.Alias = $"{contact.FirstName} {contact.LastName}";
        }
    }
}

Bổ sung lệnh sau vào code behind của DxContacts form:

SetAliasButton.ItemClick += delegate { _vm.SetAlias(); };

Dịch và chạy thử chương trình. Bạn để ý rằng, khi bấm nút Set Alias, giá trị của trường này trong GridView không thay đổi ngay. Chỉ khi chuyển focus sang ô khác, giá trị của ô Alias mới được cập nhật. Việc cập nhật này được thực hiện bởi BindingSource.

Kết quả này không hoàn toàn phù hợp khi ta thay đổi giá trị của object bằng code. Chúng ta mong muốn rằng, khi bấm nút Set Alias, giá trị của ô Alias tương ứng sẽ được thay đổi ngay.

Sở dĩ phải đặt ra yêu cầu này là vì toàn bộ việc xử lý dữ liệu được tiến hành ở view model. Một khi dữ liệu đã xử lý, kết quả phải được cập nhật ngay trên điều khiển. Đây là một yêu cầu rất quan trọng khi sử dụng data binding.

Tạo class hỗ trợ INotifyPropertyChanged

Thêm một project mới thuộc loại Class Library (.NET Framework) vào solution và đặt tên là Framework. Trong project này thêm class Bindable (file Bindable.cs) và code như sau:

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Framework
{
    [Serializable]
    public class Bindable : INotifyPropertyChanged
    {
        // INotifyPropertyChanged Implementation

        [field: NonSerialized] // this event should not be serialized by a formatter (error may occur)
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Child class may call this method explicitly in the property setter
        /// </summary>
        /// <param name="property"></param>
        protected virtual void Notify([CallerMemberName] string property = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
        }

        // Shortcuts to quickly set property value

        /// <summary>
        /// Set value to a property and raise the PropertyChanged event
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="member"></param>
        /// <param name="value"></param>
        /// <param name="property"></param>
        protected virtual void Set<T>(ref T member, T value, [CallerMemberName] string property = null)
        {
            if (Equals(member, value)) return;
            member = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
        }

        /// <summary>
        /// Set value to a property, raise the PropertyChanged event of itself and a list of other properties
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="member"></param>
        /// <param name="value"></param>
        /// <param name="properties">The related properties to be notified</param>
        /// <param name="property"></param>
        protected virtual void Set<T>(ref T member, T value, string[] properties, [CallerMemberName] string property = null)
        {
            if (Equals(member, value)) return;
            member = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));

            if (properties.Length <= 0) return;
            foreach (var p in properties)
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(p));
        }

        /// <summary>
        /// Set value to a property as well as a predicate to validate value.
        /// This requires the control to enable the ValidatesOnExceptions in the binding
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="member"></param>
        /// <param name="value"></param>
        /// <param name="predicate">A predicate to check the input</param>
        /// <param name="message">A message to display when invalid input occurs</param>
        /// <param name="property"></param>
        protected virtual void Set<T>(ref T member, T value, Predicate<T> predicate, string message, [CallerMemberName] string property = null)
        {
            if (Equals(member, value)) return;
            if (!predicate(value)) throw new Exception(message);
            member = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
        }
    }
}

Cho project Models tham chiếu sang Framework. Mở file Contact.cs và điều chỉnh code như sau:

using System;
using System.Collections.Generic;
namespace Models
{
    using Framework;

    [Serializable]
    public class Contact : Bindable
    {
        private string _alias;

        public int Id { get; set; }
        public string ContactName { get; set; }
        public string Alias { get => _alias; set => Set(ref _alias, value); }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime DateOfBirth { get; set; } = DateTime.Now;
        public List<Email> Emails { get; set; } = new List<Email>();
        public List<Phone> Phones { get; set; } = new List<Phone>();
    }
}

Dịch và chạy thử chương trình. Giờ bạn bấm nút Set Alias, kết quả sẽ được cập nhật ngay lập tức mà không cần chuyển focus ô.

Với bất kỳ property nào của model cần cập nhật thông tin ngay như trên (tức là tính giá trị trong code), bạn áp dụng phương thức Set kế thừa từ Bindable.

Những thay đổi chúng ta thực hiện ở trên tuy không lớn nhưng có ý nghĩa rất quan trọng trong data binding. Từ đây trở đi, việc gắn kết điều khiển với object trở nên rất đơn giản. Việc cập nhật dữ liệu thực hiện theo cả hai chiều. Object có thể thay đổi giá trị thông qua code, đồng thời cập nhật ngay giá trị lên điều khiển.

Tự động disable/enable nút theo giá trị

Chúng ta thực hiện thay đổi cuối cùng của bài này: disable/enable nút theo giá trị.

Như ở trên chúng ta cho phép tự động tính Alias nếu như trường này người dùng không nhập gì. Nếu người dùng đã nhập giá trị thì thôi không tự tính nữa. Vậy sẽ đơn giản hơn nhiều cho người dùng nếu như nút Set Alias sẽ bị disable nếu trường Alias đã có giá trị.

Đây là một tình huống rất thường gặp: nếu không đạt điều kiện thực hiện, nút lệnh tương ứng nên được disable. Kỹ thuật này giúp ứng dụng an toàn hơn và “chuyên nghiệp” hơn.

Trong project Framework tạo một class mới Command (file Command.cs) và code như sau:

using System;
using System.Windows.Forms;

namespace Framework
{
    public class Command<T> : Bindable
    {
        private readonly Action<T> _action;
        private readonly Func<bool> _predicate;
        public bool Enabled => _predicate == null ? true : _predicate.Invoke();
        public Binding EnabledBinding { get; private set; }

        public Command(Action<T> action, Func<bool> predicate)
        {
            _action = action;
            _predicate = predicate;

            EnabledBinding = new Binding("Enabled", this, nameof(Enabled));
        }

        public Command(Action<T> action) : this(action, null) { }

        public void Execute(T t) => _action.Invoke(t);

        public void NotifyChange(string property = nameof(Enabled))
        {
            Notify(property);
        }
    }

    public class Command : Bindable
    {
        private readonly Action _action;
        private readonly Func<bool> _predicate;
        public bool Enabled => _predicate == null ? true : _predicate.Invoke();
        public Binding EnabledBinding { get; private set; }

        public Command(Action action, Func<bool> predicate)
        {
            _action = action;
            _predicate = predicate;

            EnabledBinding = new Binding("Enabled", this, nameof(Enabled));
        }

        public Command(Action action) : this(action, null) { }

        public void Execute() => _action.Invoke();

        public void NotifyChange(string property = nameof(Enabled))
        {
            Notify(property);
        }
    }
}

Cho project Interfaces, ViewModels và App cùng tham chiếu tới Framework.

Bổ sung mô tả sau vào IContactsViewModel:

Command SetAliasCommand { get; set; }

Bổ sung code sau vào ContactsViewModel

private bool SetAliasCanExecute()
{
    if (ContactBindingSource.Current is Contact contact)
    {
        if (string.IsNullOrEmpty(contact.Alias))
        {
            return true;
        }
    }
    return false;
}

public Command SetAliasCommand { get; set; }

Thay đổi phương thức Initialize của ContactsViewModel như sau:

public void Initialize()
{
    ContactBindingSource.CurrentChanged += delegate { Notify("Title"); };
    ContactBindingSource.CurrentChanged += delegate { SetAliasCommand.NotifyChange(); };
    ContactBindingSource.CurrentItemChanged += delegate { SetAliasCommand.NotifyChange(); };

    SetAliasCommand = new Command(SetAlias, SetAliasCanExecute);
}

Thay đoạn code cũ:

SetAliasButton.ItemClick += delegate { _vm.SetAlias(); };

bằng:

SetAliasButton.ItemClick += delegate { _vm.SetAliasCommand.Execute(); };
SetAliasButton.DataBindings.Add(_vm.SetAliasCommand.EnabledBinding);

Dịch và chạy thử chương trình. Xóa Alias (nếu có) ở một dòng bất kỳ rồi di chuyển sang dòng khác. Theo dõi xem nút Set Alias có bật/tắt theo đúng ý không.

Disable Enable nút Set Alias khi chọn dòng dữ liệu

Kết phần

Trong bài viết này chúng ta đã thực hiện một số thay đổi quan trọng để khai thác khả năng data binding của winform. Sự thay đổi này giúp chúng ta bind một điều khiển với bất kỳ giá trị nào và đảm bảo đồng bộ hóa dữ liệu giữa chúng. Chúng ta cũng đã áp dụng kỹ thuật binding để bật/tắt nút bấm theo điều kiện của dữ liệu.

Để đảm bảo nắm được cách sử dụng đã học ở bài này, chúng tôi khuyên bạn nên tìm hiểu kỹ hơn về kỹ thuật data binding trong windows forms, vốn rất nhiều trên Internet. Do đây không phải là một tập bài giảng về windows forms, chúng tôi không trình bày chi tiết các vấn đề đó.

Chúc bạn thành công!

Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần Bình luận ở cuối trang. Nếu cần trao đổi riêng, hãy gửi email hoặc nhắn tin qua form liên hệ. Nếu bài viết hữu ích với bạn, hãy giúp chúng tôi chia sẻ tới mọi người. Cảm ơn bạn!

Loạt bài “Các giải pháp dành cho lập trình winform”:
Phần 1 – Lỗi thường gặp trong lập trình winforms
Phần 2 – Thiết kế giao diện với Data Sources và BindingSource
Phần 3 – Phân chia code thành module sử dụng Interface
Phần 4 – Sử dụng thư viện DevExpress cho winforms
Phần 5 – Sử dụng Data Binding
Phần 6 – Sử dụng Entity Framework

Bình luận

avatar
  Đăng ký theo dõi  
Thông báo về