Triển khai điều khiển DropDownList hỗ trợ chọn nhiều tùy chọn

Giới thiệu

Trong một dự án gần đây, yêu cầu đặt ra là phải mở rộng chức năng của điều khiển DropDownList để hỗ trợ chọn nhiều mục thay vì chỉ một như mặc định. Sau khi tìm kiếm nhiều giải pháp có sẵn mà không đạt được trải nghiệm người dùng mong muốn, tôi đã quyết định tự phát triển một điều khiển tùy chỉnh: DropDownCheckBoxList.

Điều khiển này kế thừa từ lớp cơ sở DropDownList, nhưng được mở rộng mạnh mẽ để cung cấp giao diện người dùng dạng hộp kiểm (checkbox) trong vùng thả xuống, cho phép người dùng chọn nhiều giá trị cùng lúc.

Cấu trúc điều khiển

Điều khiển bao gồm các thành phần chính sau:

  • Một TextBox chỉ đọc – hiển thị danh sách các giá trị đã chọn.
  • Hai biểu tượng hình mũi tên (lên và xuống) để điều khiển trạng thái mở/đóng.
  • Một phần tử <div> ẩn – chứa danh sách các checkbox.
  • Hai trường ẩn (HiddenField) – lưu trữ giá trị và nhãn tương ứng của các mục đã chọn.

Thuộc tính tùy chỉnh

Điều khiển cung cấp một số thuộc tính quan trọng giúp linh hoạt hóa hành vi:

  • DisplayMode: Kiểu liệt kê Label hoặc Value, xác định nội dung hiển thị trên TextBox.
  • Splitor: Ký tự phân cách giữa các giá trị khi hiển thị nhiều lựa chọn (mặc định là dấu phẩy).
  • ShowSelectAllOption: Xác định có hiển thị tùy chọn "Chọn tất cả" hay không.
  • SelectAllOptionLabel: Văn bản hiển thị cho tùy chọn chọn tất cả (mặc định: "Chọn tất cả").

Xử lý phía máy chủ

Để tạo giao diện động, điều khiển ghi đè phương thức OnInit để khởi tạo các điều khiển phụ trợ:

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    if (!DesignMode)
    {
        InitializeChildControls();
    }
}

private void InitializeChildControls()
{
    mainTextBox = new TextBox
    {
        ID = ClientID + "_txtMain",
        ReadOnly = true,
        Width = Unit.Pixel(Width.Value - 20),
        Height = Height.IsEmpty ? Unit.Pixel(15) : Height
    };

    valueStorage = new HiddenField { ID = $"{ClientID}_value" };
    textStorage = new HiddenField { ID = $"{ClientID}_text" };

    Controls.Add(mainTextBox);
    Controls.Add(valueStorage);
    Controls.Add(textStorage);
}

Phương thức OnPreRender được sử dụng để đăng ký tập lệnh JavaScript cần thiết:

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);
    if (!DesignMode)
    {
        RegisterClientScripts();
    }
}

private void RegisterClientScripts()
{
    var page = Page;
    var cs = page.ClientScript;

    // Đăng ký file JS nhúng
    var jsUrl = cs.GetWebResourceUrl(GetType(), "CustomControls.Resources.DropDownCheckBoxList.js");
    if (!cs.IsClientScriptIncludeRegistered("DropdownCheckScript"))
    {
        cs.RegisterClientScriptInclude(GetType(), "DropdownCheckScript", jsUrl);
    }

    // Gắn sự kiện click toàn trang để đóng popup
    var formClick = page.Form.Attributes["onclick"] ?? "";
    page.Form.Attributes["onclick"] = $"{formClick} closeOnOutsideClick(event, '{ClientID}');".Trim();

    page.RegisterRequiresPostBack(this);
}

Ghi đè HTML đầu ra

Để kiểm soát hoàn toàn cấu trúc HTML, điều khiển ghi đè thuộc tính TagKey thành bảng:

protected override HtmlTextWriterTag TagKey => DesignMode ? base.TagKey : HtmlTextWriterTag.Table;

Thêm các thuộc tính tùy chỉnh vào thẻ HTML bằng AddAttributesToRender:

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
    if (DesignMode)
    {
        base.AddAttributesToRender(writer);
    }
    else
    {
        writer.AddAttribute(HtmlTextWriterAttribute.Id, ClientID + "_displaytable");
        writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "relative");
        writer.AddStyleAttribute(HtmlTextWriterStyle.ZIndex, "1000");
        writer.AddStyleAttribute(HtmlTextWriterStyle.Left, "-5px");
        writer.AddAttribute(HtmlTextWriterAttribute.Width, Width.ToString());
        writer.AddAttribute(HtmlTextWriterAttribute.Cellpadding, "0");
        writer.AddAttribute(HtmlTextWriterAttribute.Cellspacing, "0");
        writer.AddAttribute(HtmlTextWriterAttribute.Border, "0");
    }
}

Tính toán chiều rộng động

Chiều rộng của vùng thả xuống được điều chỉnh tự động nếu văn bản dài hơn kích thước ban đầu:

private void AdjustDropdownWidth()
{
    int maxWidth = 0;
    foreach (ListItem item in Items)
    {
        int byteLength = Encoding.Default.GetByteCount(item.Text);
        if (byteLength > maxWidth) maxWidth = byteLength;
    }

    int calculatedWidth = maxWidth * 8 + 36; // padding + checkbox width
    if (calculatedWidth > Width.Value)
    {
        Width = Unit.Pixel(calculatedWidth);
    }
}

Hiển thị nội dung

Phương thức RenderContents tạo ra toàn bộ giao diện:

protected override void RenderContents(HtmlTextWriter output)
{
    if (DesignMode)
    {
        base.RenderContents(output);
        return;
    }

    AdjustDropdownWidth();
    RenderMainInputAndToggleIcon(output);
    RenderDropdownPanel(output);
}

Vùng thả xuống chứa danh sách các checkbox và xử lý logic "Chọn tất cả":

private void RenderDropdownPanel(HtmlTextWriter w)
{
    string divId = $"{ClientID}_div";
    w.AddAttribute(HtmlTextWriterAttribute.Id, divId);
    w.AddStyleAttribute(HtmlTextWriterStyle.Display, "none");
    w.AddStyleAttribute(HtmlTextWriterStyle.Position, "absolute");
    w.AddStyleAttribute(HtmlTextWriterStyle.ZIndex, "1001");
    w.AddStyleAttribute(HtmlTextWriterStyle.BackgroundColor, "#f9f9f9");
    w.AddStyleAttribute(HtmlTextWriterStyle.BorderColor, "#ccc");
    w.AddStyleAttribute(HtmlTextWriterStyle.BorderStyle, "solid");
    w.AddStyleAttribute(HtmlTextWriterStyle.BorderWidth, "1px");
    w.AddStyleAttribute(HtmlTextWriterStyle.Width, Width.ToString());
    w.RenderBeginTag(HtmlTextWriterTag.Div);

    RenderOptionTable(w);

    valueStorage.RenderControl(w);
    textStorage.RenderControl(w);

    w.RenderEndTag(); // div
}

Xử lý dữ liệu gửi lên

Phương thức LoadPostData phục hồi trạng thái sau mỗi postback:

protected override bool LoadPostData(string key, NameValueCollection data)
{
    if (DesignMode) return false;

    mainTextBox.Text = data[mainTextBox.UniqueID];
    valueStorage.Value = data[valueStorage.UniqueID];
    textStorage.Value = data[textStorage.UniqueID];

    return true;
}

JavaScript phía client

File DropDownCheckBoxList.js sử dụng jQuery để xử lý tương tác:

  • Mở/đóng vùng thả xuống khi nhấn vào biểu tượng.
  • Đóng vùng nếu người dùng nhấn ra ngoài.
  • Cập nhật giá trị của TextBox và các HiddenField khi thay đổi lựa chọn.

Ví dụ hàm đóng vùng khi nhấn ra ngoài:

function closeOnOutsideClick(event, prefix) {
    const $popup = $('#' + prefix + '_div');
    if ($popup.is(':hidden')) return;

    const targetId = event.target.id || '';
    if (!targetId.includes(prefix)) {
        $popup.hide();
        $('#' + prefix + '_imgDown').show();
        $('#' + prefix + '_imgUp').hide();
    }
}

Kết luận

Điều khiển DropDownCheckBoxList cung cấp giải pháp ổn định, tương thích với các trình duyệt hiện đại như Chrome, Firefox, Safari và IE7+. Nó dễ dàng tích hợp vào các ứng dụng ASP.NET WebForms và có thể tùy biến cao thông qua các thuộc tính công khai.

Lưu ý: Trên IE6, có thể xảy ra xung đột với điều khiển <select> gốc do cơ chế z-index và rendering của trình duyệt này.

Thẻ: ASP.NET WebForms Custom Control DropDownList Multi-Select CheckBox List

Đăng vào ngày 18 tháng 5 lúc 18:20