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
TextBoxchỉ đọ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ê
LabelhoặcValue, xác định nội dung hiển thị trênTextBox. - 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
TextBoxvà cácHiddenFieldkhi 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.