บทความ CultingEdge ประจำเดือนสิงหาคม ปี 2003 ได้พูดถึงวิธีการขยายขอบเขตการทำงานของ ASP.NET Datagrid server control ให้สามารถใช้ตัวเก็บข้อมูลที่มีตารางจำนวนมากได้ อาทิเช่นการใช้ออปเจ็กต์ DataSet เป็นแหล่งข้อมูล ถ้าหาก DataSet มีตารางที่มีข้อมูลเกี่ยวข้องกัน คอนโทรลจะทำการสร้างปุ่มคอลัมน์ขึ้นมา เมื่อใดก็ตามที่ตารางที่แสดงอยู่เป็น parent ของตารางที่เกี่ยวข้อง แล้วผู้ใช้คลิกที่ปุ่มคอลัมน์ DataGrid ลูกก็จะแสดงผลขึ้นมา โดยทำการแจกแจงแถวของเรกคอร์ดที่เลือกเอาไว้ตามความสัมพันธ์ พฤติกรรมทั้งหมดแสดงเอาไว้ในภาพที่ 1 โดยที่มีความคล้ายคลึงกับการทำงานของ Windows Forms DataGrid controls ในสถานการณ์ที่คล้ายคลึงกัน

ภาพที่ 1 DataGrids แม่และลูก
แอพพลิเคชันที่แสดงเอาไว้ในภาพที่ 1 เป็นยูซเซอร์คอนโทรลที่ประกอบด้วยคอนโทรล DataGrid จำนวนสองอันที่ทำงานร่วมกัน ยูซเซอร์คอนโทรลดังกล่าว (ดูซอร์ซโค้ดจากบทความเดือนสิงหาคม ปี 2003) มีโลจิกทั้งหมดที่จำเป็นสำหรับการทำให้กริดทั้งสองชุดมีข้อมูลที่สอดคล้องกัน โดย DataGrid ที่เป็น parent เชื่อมโยงอยู่กับ DataSet และแสดงข้อมูลในตาราง parent เมื่อรูปแบบดังกล่าวเกิดขึ้น ยูซเซอร์คอนโทรลจะตรวจสอบให้แน่ใจว่าความสัมพันธ์นี้จะอยู่ภายใน DataSet โดยที่ตารางที่แสดงอยู่จะทำตัวเป็น parent ส่วน DataGrid ที่เป็นลูกจะเชื่อมโยงกับข้อมูลที่แสดงอยู่ ซึ่งประกอบด้วยเรกคอร์ดทั้งหมดในตารางลูกที่เกี่ยวข้องกับเรกคอร์ดในตาราง parent ที่เลือกเอาไว้เท่านั้น ผลก็คือถ้าหากคุณมี DataSet ซึ่งมีการกำหนดความสัมพันธ์ของตารางทั้งสองอันเอาไว้ ยูซเซอร์คอนโทรลจะช่วยให้คุณไม่ต้องเขียนโค้ดเพิ่มเติมสำหรับกลไกการแสดงผลพิเศษอีก
ถ้าอย่างนั้นแนวทางนี้ผิดปกติตรงไหน? เราต้องขอบอกว่าไม่มีความผิดปกติแต่อย่างใด ถ้าหากคุณต้องการใช้ฟังก์ชันแบบพื้นฐานเท่านั้น อย่างไรก็ตามผู้อ่านบางคนตั้งข้อสังเกตว่าอาจจะไม่ได้มีคอนโทรล DataGrid แค่สองอันที่ทำงานอย่างอิสระก็เป็นได้ ยูซเซอร์คอนโทรลสร้างกำแพงล้อมรอบคอนโทรลที่กำหนด ดังนั้นวิธีการเพียงอย่างเดียวที่คุณจะเรียกใช้คอนโทรลดังกล่าวได้ก็คือการทำมิเรอร์ property และ method หรือใช้คอนโทรลภายในทั้งหมด ถ้าหากดูจากมุมมองเรื่องการเขียนโปรแกรมแล้ว การมีคอนโทรล DataGrid เพียงชุดเดียวเพื่อแสดงผลข้อมูลที่มีโครงสร้างเป็นชั้นๆจะช่วยให้โปรแกรมเมอร์ทำงานได้ง่ายขึ้นอย่างมาก ปมเด่นอย่างหนึ่งก็คือคุณไม่จำเป็นต้องกังวลเกี่ยวกับตัวแปรของตาราง parent คุณสามารถใช้อินเทอร์เฟซมาตรฐานของคอนโทรล DataGrid ได้เลย ส่วนกริดลูกที่แสดงข้อมูลที่เกี่ยวข้องสามารถสร้างขึ้นมาพร้อมๆกันและแสดงผลในเลย์เอาท์ของกริดหลักได้

ภาพที่ 2: DataGrid ลูกแบบฝังตัว
ในทางตรงกันข้าม คุณต้องจำเอาไว้ว่าคอนโทรล DataGrid ไม่ได้ถูกออกแบบมาให้จัดเก็บข้อมูลที่มีโครงสร้างแตกสาขาแต่อย่างใด เลย์เอาท์ที่อยู่ภายในคอนโทรลตัวนี้เหมาะกับข้อมูลที่เป็นตารางเท่านั้น ในขณะที่คอนโทรล DataList น่าจะเป็นตัวเลือกที่ดีกว่า แต่คอนโทรลตัวนี้ไม่มีบริการจัดเพจโดยตรง และจำเป็นต้องเขียนโค้ดบางอย่างเพื่อให้ DataList ทำงานได้เหมือน DataGrid ถ้าหากลองใช้ Google ค้นหาข้อความว่า "nested DataGrid" เราจะพบกับบทความที่พูดถึงวิธีการฝัง DataGrids เอาไว้ในคอนโทรล DataList ดังนั้นผมจึงได้ไอเดียมาเขียนบทความนี้ขึ้นมา ผมจึงสร้างคอนโทรลพิเศษโดยดัดแปลงมาจากคลาส DataGrid โดยที่คอนโทรลตัวนี้จะมีการใช้ประเภทคอลัมน์ลักษณะเฉพาะ (Expand CommandColumn) และมีโลจิกทั้งหมดที่จำเป็นสำหรับแสดงผลเรกคอร์ดที่เกี่ยวข้องกับ item ที่ถูกคลิก การแสดงผลส่วนต่อขยายเป็นของ DataGrid ลูกที่ฝังอยู่ใน parent ภาพที่ 2 แสดงผลลัพธ์ที่เกิดขึ้นว่าจะมีหน้าตาอย่างไร
การสร้างกริดเชิงซ้อน
คอนโทรล DataGrid ที่มีโครงสร้างแตกสาขาเหมาะกับแหล่งข้อมูลที่เป็นออปเจ็กต์ DataSet ที่เก็บความสัมพันธ์ระหว่างตารางเอาไว้เท่านั้น ตัวอย่างเช่นถ้าหากเรามี DataSet ที่เก็บตาราง Customers และ Orders เอาไว้โดยมีการกำหนด DataRelation ของตารางทั้งสองเอาไว้ที่คอลัมน์ CustomerID ถ้าหาก DataGrid ยังคงมีคอลัมน์ปุ่มอยู่ แล้วคุณคลิกที่ปุ่มดังกล่าว นั่นเท่ากับคุณได้สร้างการแสดงผลระดับลูกของชื่อลูกค้าที่เลือกเอาไว้ จากนั้นเชื่อมโยงออปเจ็กต์ DataView ผลลัพธ์ไปยังกริดลูกได้
การที่คอนโทรลใหม่ (ใช้ชื่อว่า NestedGrid ในโค้ดตัวอย่าง) ดัดแปลงมาจากคลาส DataGrid คุณจึงสามารถใช้คอนโทรลดังกล่าวได้เมื่อใดก็ตามที่เหมาะกับการใช้ออปเจ็กต์ DataGrid อย่างไรก็ตามเรื่องนี้ยังมีปัญหาอยู่บ้างเล็กน้อย ซึ่งโดยทั่วไปแล้วเมื่อคุณดัดแปลงคอนโทรลมาจากคลาสพื้นฐาน ในบางสถานการณ์คอนโทรลที่ดัดแปลงมาอาจไม่สามารถไปแทนที่คอนโทรลเดิมได้ เนื่องจากส่วนต่อเดิมและส่วนขยาย ซึ่งในบทความนี้ผมจะไม่ใช้เวลามากนักในการทำให้คอมโพเน้นต์ NestedGrid คอมแพตทิเบิลย้อนหลังไปหาคลาส DataGrid พื้นฐาน ดังนั้นเพื่อช่วยให้การทำงานง่ายขึ้น ผมเดาว่าคุณน่าจะเชื่อมดยงคอมโพเน้นต์ NestedGrid กับออปเจ็กต์ DataSet อยู่แล้ว
ผมตั้งข้อสรุปเกี่ยวกับคอนโทรล NestedGrid เอาไว้สองสามข้อ ซึ่งจะเห็นได้อย่างชัดเจนขึ้นในช่วงต่อไป ถ้าหากพูดให้เจาะจงลงไป คุณกำลังรับผิดชอบกับการเพิ่มคอลัมน์ปุ่มที่ใช้เป็นตัวกำหนดสภาพการขยาย/ยุบแถวแต่ละแถวอยู่ ซึ่งในทางทฤษฎีแล้วคอลัมน์นี้สามารถวางเอาไว้ที่ใดในกริดก็ได้ ผมสรุปว่าคอลัมน์ขยายก็คือคอลัมน์แรกของกริด (อย่างที่บอกเอาไว้ในบทความเดือนสิงหาคมปี 2003 คุณสามารถแก้ไขพฤติกรรมให้สร้างคอลัมน์นี้ก็ต่อเมื่อ DataGrid ที่เชื่อมโยงกับ DataSet มีตารางเกี่ยวข้องกันเท่านั้น)
ถ้าหากคุณเคยใช้คอนโทรล DataGrid มาบ้าง คุณคงทราบแล้วว่าแม้ว่าคอนโทรลตัวนี้จะมีประสิทธิภาพและปรับแต่งได้ดีมากก็ตาม แต่มันไม่ค่อยพอใจกับการเปลี่ยนแปลงเลย์อาท์ของตนเองซักเท่าใดนัก เลย์เอาท์ของกริดเป็นตัวแทนตารางของข้อมูล ซึ่งเป็นการสร้างแถวที่มีขนาดเท่ากันขึ้นมาเรื่อยๆ ถ้าอย่างนั้นคุณจะใส่กริดลูกลงไปในข้อจำกัดนี้ได้อย่างไร?
ประเด็นสำคัญที่คุณต้องจำเอาไว้ก็คือการสร้างภาพกริดขึ้นมาจะอยู่ในรูปของตาราง HTML แบบมาตรฐาน ซึ่งเมื่อเซลถูกสร้างเป็นเลย์เอาท์ตารางปกติขึ้นมาแล้ว คุณสามารถใส่อะไรก็ได้ลงไปในแต่ละเซล รวมทั้งตารางลูกที่เป็นตัวแทนของกริดลูก (โดยใช้ rowspan tag) ก่อนอื่นคุณต้องแก้ไขจำนวนของเซลที่ประกอบขึ้นมาเป็นแถวที่เลือกไว้เสียก่อน (นั่นก็คือแถวที่ปุ่มคำสั่งขยายที่ผู้ใช้จะคลิกตั้งอยู่) โดยการลบเซลทั้งหมดยกเว้นเซลที่เก็บปุ่มคำสั่งเอาไว้ ขั้นตอนนี้ทำได้ง่ายมาก ถ้าหากคุณสรุปว่าคอลัมน์คำสั่งขยายอยู่ทางซ้ายมือสุด หลังจากที่เซลทั้งหมดถูกลบแล้ว จากนั้นคุณสามารถสรางเซลขึ้นมาใหม่ที่ขยายจำนวนคอลัมน์ได้ตามความจำเป็น ซึ่งเท่ากับจำนวนของ item ใน Columns collections ของคอนโทรล DataGrid นั่นเอง
เมื่อถึงจุดนี้ คุณมีเซลพิเศษที่ใช้สร้างการขยายแถวแล้ว เราสามารถเขียนโปรแกรมให้เซลพิเศษนี้มีเซิร์ฟเวอร์คอนโทรลใดๆก็ได้ ตัวอย่างเช่นคุณอาจจะใส่ตารางที่แถวแรกเลียนแบบโครงสร้างของเซลที่ถูกลบทิ้งไปก็ได้ (โดยปกติแล้วจะเป็นข้อมูลของแถว parent) และแถวด้านล่างที่ใส่ DataGrid ลูกเอาไว้ คอนโทรลในภาพที่ 2 สร้างขึ้นมาอิงกับแนวทางนี้
คลาส NestedGrid
อย่างที่ผมเคยบอกเอาไว้ก่อนหน้านี้ คลาส NestedGrid เป็นการดัดแปลงมาจากคลาส System Web DataGrid และมีการเพิ่ม properties พิเศษบางอย่างลงไป (ดูภาพที่ 3) นอกจากนั้นคอนโทรลเลอร์ยังเรียก UpdatedView event พิเศษขึ้นมาเมื่อใดก็ตามที่มีการเชื่อมโยงข้อมูลอีกด้วย ถ้าหากต้องการหลีกเลี่ยงการควบคุมประเภทของออปเจ็กต์ที่กำหนดให้แต่ DataSource property อย่างเข้มงวดแล้ว (และตรวจสอบให้แน่ใจว่าเป็น DataSet) คุณสามารถสร้าง property ใหม่ขึ้นมาทับ DataSource property ได้ตามตัวอย่างโค้ดต่อไปนี้
public override object DataSource
{
get {return base.DataSource;}
set {
if (!(value is DataSet)) {
// throw an exception
}
base.DataSource = value;
}
}
คอนโทรล NestedGrid จะเริ่มทำงานทันทีเมื่อผู้ใช้คลิกที่ปุ่มแถวเพื่อขยายเรกคอร์ด (ลูกค้า) ออกไปสำหรับดูรายละเอียด (รายการสั่งซื้อที่เกี่ยวข้อง) ด้วยเหตุนี้กริดเชิงซ้อนจึงจำเป็นต้องมีคอลัมน์ปุ่มที่มีคุณสมบัติที่เจาะจงบางอย่างเอาไว้ด้วย คุณสมบัติอย่างหนึ่งก็คือกริดต้องมีระบบจัดการ ItemCommand event ที่เอาไว้รองรับคำสั่งขยาย/ยุบคอลัมน์ ระบบจัดการจะกำหนด ExpandedItemIndex property ไปเป็นอินเดกซ์เรกคอร์ดที่เป็นศูนย์ ซึ่งเมื่อถูกคลิกก็จะทำการอัพเดตการแสดงผลของกริดได้ ถ้าอย่างนั้นเราควรแก้ไขเลย์เอาท์ของแถวที่ถูกคลิกตอนไหนดี?
ItemDataBound event จะเริ่มทำงานหลังสุดตามลำดับเหตุการณ์ตอนที่มีการสร้างกริดเลย์เอาท์ขึ้นมา หลังจาก ItemDataBound เริ่มทำงานแล้ว ขั้นตอนการเชื่อมโยงข้อมูลก็เกือบจะสมบูรณ์แบบและเซลทั้งหมดก็พร้อมที่จะแสดงผลแล้ว เลย์อาท์และข้อมูลที่คุณเห็นตั้งแต่ตรงนี้เป็นต้นไปจะไม่มีการเปลี่ยนแปลงอีก ด้วยเหตุนี้ผมจึงตัดสินใจทำการเปลี่ยนแปลงที่จำเป็นก่อนที่จะจัดการกับ ItemDataBound event
ก่อนที่จะลงลึกลงไปในรายละเอียดของการนำเอาคอนโทรลตัวนี้ไปใช้งาน ผมขอแจกแจงอะไรบางอย่างให้ฟังก่อน เรื่องแรก ExpandedItemIndex property เป็นศูนย์ แต่เป็นตัวแทนของตำแหน่งที่ตายตัวของแถวที่ถูกคลิก property ตัวนี้ต่างจาก grid properties อื่นๆที่คล้ายคลึงกัน (อาทิเช่น SelectedItemIndex และ EditItemIndex) ในแง่ที่ว่ามันไม่ได้เป็นตัวแทนค่าของเพจ เรื่องที่สอง NestedGrid จะทำการจัดเพจเป็นการภายใน ซึ่งในฐานะของคอนโทรลที่ใช้เลือกเพจต่างๆในตาราง คุณไม่จำเป็นต้องทำอะไรยกเว้นจัดการกับ UpdateView event แล้วส่งข้อมูลที่เชื่อมโยงกันไปเท่านั้น
void UpdateView(object sender, EventArgs e) {
BindData();
}
void BindData() {
dataGrid.DataSource = (DataSet) Cache["MyData"];
dataGrid.DataBind();
}
void PageIndexChanged(object sender, DataGridPageChangedEventArgs e)
{
CurrentPageIndex = e.NewPageIndex;
SelectedIndex = -1;
EditItemIndex = -1;
ExpandedItemIndex = -1;
if (UpdateView != null)
UpdateView(this, EventArgs.Empty);
}
องค์ประกอบหลักในโครงสร้างคอนโทรล NestedGrid ก็คือคอลัมน์ปุ่ม เพื่อช่วยให้การทำงานง่ายขึ้น คอนโทรลเวอร์ชันนี้ใช้ได้กับ item ขยายเพียงอันเดียวเท่านั้น แต่เราสมารถขยายขอบเขตของคุณสมบัตินี้โดยง่ายโดยการเปลี่ยน ExpandedItemIndex property จากเลขจำนวนเต็มไปเป็น array หรือ collection ก็ได้
คลาส ExpandCommandColumn
เราสามารถสร้างภาพของคอลัมน์ที่ขยายออกมาโดยใช้สตริง (อาทิเช่น +/- หรือขยาย/ยุบ) หรือเป็นภาพบิตแมพก็ได้ และคุณอาจต้องการใช้ภาพแบบอื่นๆสำหรับแอพพลิเคชันที่ต่างออกไปก็ได้ วิธีการที่คล่องตัวที่สุดในการใช้คุณสมบัตินี้ก็คือการใช้ properties อย่าง ExpandText และ CollapseText เป็นต้น ดังนั้นคุณควรที่จะกำหนด properties เหล่านี้ในคลาส NestedGrid หรือไม่? ถ้าหากเป็นสถานการณ์ที่คล้ายคลึงกัน (การแก้ไขในตัว) ทีม ASP.NET จะสร้างคอลัมน์ DataGrid พิเศษขึ้นมา แล้วใส่ properties อย่าง EditText ลงไป ถ้าหากอิงกับตัวอย่างนี้ ผมได้สร้างคลาส ExpendCommandColumn ขึ้นมา บวกกับ text properties อีกสองสามชนิดเพื่อเป็นตัวแทนเอาท์พุด HTML สำหรับขยายและยุบการแสดงผลโค้ดสั้นๆต่อไปนี้แสดงวิธีการผสานคอลัมน์พิเศษร่วมกับกริด
<cc1:NestedDataGrid id="dataGrid" runat="server" ...>
<Columns>
<cc1:ExpandCommandColumn
CollapseText="<img src=images/collapse.gif>"
ExpandText="<img src=images/expand.gif>" >
<ItemStyle Width="15px" />
</cc1:ExpandCommandColumn>
•••
</Columns>
</cc1:NestedDataGrid>
การสร้างคอลัมน์ DataGrid พิเศษขึ้นมาไม่ใช่เรื่องยาก คุณไม่ต้องทำอะไรมากมายเกินกว่าการสร้างคลาสใหม่ที่ดัดแปลงมาจาก DataGridColumn ขึ้นมา ซึ่งในคลาสใหม่นี้คุณต้องเขียนโค้ด properties พิเศษใดๆที่คุณจำเป็นต้องใช้ รวมทั้งโค้ดใหม่ทับ InitializeCell method อีกด้วย method นี้จะถูกเรียกเมื่อใดก็ตามที่มีการสร้างเซลขึ้นมาสำหรับคอลัมน์ ทุกอย่างที่ปรากฏขึ้นมาตั้งแต่แรกภายในคอลัมน์เซลจะถูกกำหนดโดย method นี้ โค้ดต่อไปนี้แสดงวิธีการใช้ ExpandText property
public class ExpandCommandColumn : DataGridColumn
{
public string ExpandText {
get {
object data = ViewState["ExpandText"];
if (data != null)
return (string) data;
return "+";
}
set {
ViewState["ExpandText"] = value;
}
}
•••
}
CollapseText ต่างไปเฉพาะชื่อของ viewstate slot และค่าเริ่มต้นเท่านั้น ("-")
สิ่งที่น่าสังเกตอีกอย่างหนึ่งก็คือค่าเริ่มต้นของ server control properties ควรที่จะกำหนดเอาไว้ใน get accessor แทนที่จะอยู่ใน constructor หรืออยู่ในช่วงเหตุการณ์เริ่มต้นทำงานอาทิเช่น Init หรือ Load เป็นต้น รูปแบบดังกล่าวเป็นแนวทางที่ไมโครซอฟท์ใช้ในทุกจุดของ ASP.NET การใส่โค้ดดังกล่าวเอาไว้ใน get accessor ของ property คุณจะได้ประโยชน์จาก code encapsulation รวมทั้งมีการแยกระหว่างตรรกะที่อยู่เบื้องหลังค่าของ property และคอนโทรลส่วนที่เหลืออย่างเด็ดขาดอีกด้วย โดยเฉพาะอย่างยิ่งเมื่อค่าเริ่มต้นของ property ขึ้นอยู่กับกฎเกณฑ์ที่ซับซ้อน คุณจะมีจุดที่ใช้ควบคุมเพียงจุดเดียว ซึ่งจะทำให้การดูแลโค้ดทั้งหมดทำได้ง่ายขึ้น เมื่อพูดถึงแนวทางปฏิบัติที่ดีที่สุดแล้ว คุณต้องจำเอาไว้ว่าค่าที่ส่งมาให้ property จำเป็นต้องมีการตรวจสอบว่าเป็นค่า null หรือไม่ รวมทั้งต้องมีการปรับแต่งค่าตามความจำเป็นด้วย ตัวอย่างเช่น property ของ type string ไม่ควรที่จะส่งกลับมาเป็น null แต่ควรส่งกลับมาเป็นสตริงว่างๆมากกว่า
คอลัมน์ DataGrid อิงอยู่กับ InitializeCell method เป็นหลัก เราสามารถกำหนดให้ method นี้เป็น public และ virtual (สามารถเขียนทับลงไปในคลาสที่แปลงมาได้) และโค้ดที่อยู่ในคอนโทรล DataGrid จะเป็นตัวเรียก method นี้ เมื่อใดก็ตามที่ต้องการแสดงผลคอลัมน์ดังกล่าว แม้ว่าจะกำหนดเป็น public แล้วก็ตาม แต่ส่วนใหญ่นักพัฒนาคอนโทรลจะเป็นผู้ใช้ method ดังกล่าว รูปแบบของ method มีอยู่ตามตัวอย่างด้านล่างนี้
public override void InitializeCell(
TableCell cell,
int columnIndex,
ListItemType itemType)
เมื่อโค้ด DataGrid เรียก method ดังกล่าวแล้วส่งออปเจ็กต์ที่เป็นตัวแทนของเซลที่ถูกสร้างขึ้นมา อินเดกซ์ของคอลัมน์ที่อยู่ใน Columns collection ของกริด และประเภทของเซลที่จะแสดงผล (หัวเรื่อง ท้ายเรื่อง item และอื่นๆ) ออกไป คุณคงเห็นแล้วว่าไม่มีข้อมูลเกี่ยวกับอินเดกซ์ของเซลในเพจของกริดแต่อย่างใด ข้อมูลดังกล่าวมีความสำคัญหรือเปล่า? ถ้าหากพิจารณาจากประเภทของกริดคอลัมน์ที่กำหนดเอาไว้ก่อน คำตอบก็คือไม่มีความสำคัญ (ที่จริงแล้วข้อมูลนี้ไม่ได้มีการส่งออกไปด้วย) กริดคอลัมน์ที่กำหนดเอาไว้ก่อน (เชื่อมโยงข้อมูลปุ่ม ไฮเปอร์ลิงก์ เทมเพล็ตคอลัมน์) ใช้อัลกอริธึมหนึ่งหรือสองชุดเพื่อสร้างเซลขึ้นมา ถ้าหากมีการกำหนด Text properties แล้ว เซลทั้งหมดจะเก็บค่าคงที่เอาไว้ แต่ถ้าหากมีการกำหนด data-bound property (อาทิเช่น DataField) ข้อมูลของแต่ละเซลจะมีการเปลี่ยนแปลงไปตามขั้นตอนการเชื่อมโยงข้อมูล
ถ้าอย่างนั้น ExpandCommandColumn type จัดอยู่ในประเภทใด? คำตอบก็คือไม่ได้อยู่ในประเภทใดเลย ซึ่งปกติแล้วคำสั่งตัวนี้จะแสดงผลข้อความของเซลโดยใช้ ExpandText หรือ CollapseText ขึ้นอยู่กับสภาพของ item ที่ต้องการแสดงผล ถ้าหากเซลอินเดกซ์ตรงกับ Expand ItemIndex property (หรืออยู่ในชุดของ item เลย) ระบบจะใช้ค่าของ CollapesText มิฉะนั้นแล้วก็จะมีการใช้ค่าเริ่มต้นของ ExpandText property แต่ InitializedCell method ของคอลัมน์นี้ทราบข้อมูลของเซลอินเดกซ์ได้อย่างไร?
ความคิดแรกที่เกิดขึ้นในใจของผมก็คือระบบจะส่ง TableCell object เข้าไปใน method ตัวนี้ เรียก NamingContainer property แล้วส่งผลลัพธ์ไปยัง DataGridItem ถ้าหากออปเจ็กต์ที่ส่งมาไม่ได้มีค่าเป็น null นั้นหมายความว่าตัวเก็บเซลและ ItemIndex property ต้องมีข้อมูลที่ต้องการ แต่โชคไม่ดีที่เรื่องต่างๆไม่ได้ง่ายดายอย่างนั้น การที่ naming container ของเซลออปเจ็กต์มีค่าเป็น null เนื่องจากเมื่อตอนเรียก InitializeCell แต่ออปเจ็กต์ TableCell ยังไม่ได้ถูกส่งลงไปใน grid item container ผลที่ตามมาก็คือมันจึงไม่ได้เป็นของ parent container ใดๆ ดังนั้น NamingContainer property จึงมีค่าเป็น null
วิธีการแก้ปัญหาเรื่องนี้ ผมจึงใส่รายการ method ภายในที่เขียนทับได้ของคอนโทรล DataGrid ลงไป โดยที่ InitializeItem method ของ DataGrid เป็นสิ่งที่ผมต้องการ เนื่องจาก method ตัวนี้รับผิดชอบกับการจัดเตรียมกริดคอลัมน์ในขณะที่สร้างกริดเลย์เอาท์ขึ้นมา แม้ว่ามีการพูดถึง InitializeItem method เอาไว้ในคู่มือของ ASP.NET ในเว็บไซต์ MSDN บ้างแล้วก็ตาม แต่ก็ไม่ได้มีการอธิบายอย่างละเอียด พฤติกรรมของ InitializeItem ใน ASP.NET 1.x ค่อนข้างเรียบง่ายอย่างมาก InitializeItem ใช้พารามิเตอร์สองตัวก็คือออปเจ็กต์ DataGridItem ซึ่งเป็นตัวแทนแถวของกริดที่กำลังถูกแสดงผล และอะเรย์ออปเจ็กต์ DataGridColumn (คอลัมน์ของแถวดังกล่าว)
protected virtual void InitializeItem(
DataGridItem item,
DataGridColumn[] columns
);
InitializeItem จะทำการวนลูปรอบคอลัมน์เหล่านี้ แล้วสร้างออปเจ็กต์ TableCell ใหม่ขึ้นมาสำหรับแต่ละคอลัมน์ ออปเจ็กต์นี้จะถูกส่งไปยัง InitializeCell method ของแต่ละคอลัมน์แล้วถูกเพิ่มลงไปใน Cells collection ของ DataGridItem object (เมื่อถึงจุดนี้คุณคงพอเดาออกว่า naming container ของ TableCell จะมีค่าที่ไม่ใช่ null แล้ว) โค้ดในภาพที่ 4 แสดง InitializeItem เวอร์ชันที่ถูกเขียนทับ ซึ่งส่ง flag พิเศษต่อไปยังคอลัมน์คลาส ExpandCommandColumn
ภาพที่ 5 มีโค้ดที่ใช้ในการจัดเตรียมเซลของคลาส Expand CommnadColumn ระบบจะแสดงผลแต่ละเซลเป็นปุ่มลิงก์พร้อมกับข้อความที่กำหนดโดย Boolean argument พิเศษ (ดูภาพที่ 6) สิ่งที่เหมือนกับองค์ประกอบอื่นๆจำนวนมากของคอนโทรล DataGrid ก็คือเราสามารถใช้ข้อความ HTML ได้อย่างเต็มที่ ดังนั้นคุณจึงสามารถใช้ภาพเป็นตัวแทนของคุณสมบัติการขยาย/ยุบเซลได้
ภาพที่ 6 เซลในรูปของปุ่มลิงก์
จะเกิดอะไรขึ้นเมื่อมีการคลิกที่ปุ่มลิงก์ของคอลัมน์? ถ้าหากคุณต้องการพฤติกรรมเฉพาะคอลัมน์แล้วละก็ ให้คุณเพิ่มระบบจัดการเหตุการณ์ Click ลงไป โค้ดดังกล่าวจะประมวลผลก่อนเป็นอันดับหนึ่งเมื่อผู้ใช้คลิกปุ่ม ขั้นต่อมาเหตุการณ์ที่กำหนดเอาไว้ในคลาส DataGridItem จะโผล่ขึ้นมา ตามคำสั่ง ItemCommand event ในระดับ DataGrid
การแสดงผลกริดลูก
แม้ว่าสามารถปรับแต่งได้มากเป็นพิเศษก็ตาม แต่คอนโทรล DataGrid ก็ไม่มีสิ่งอำนายความสะดวกเพื่อใช้แก้ไขเลย์เอาท์ HTML ของแถวแต่อย่างใด โลจิกการปรับแต่ง DataGrid ถูกพัฒนาขึ้นมาโดยอิงกับแนวคิดที่ว่ากริดประกอบด้วยคอลัมน์ต่างๆ และแถวเป็นผลลัพธ์ของคอลัมน์ที่อยู่ติดๆกันเท่านั้น แต่ในตอนนี้คุณต้องการแก้ไขโครงสร้างของแถวที่เลือกเอาไว้เพื่อนำไปสู่คอนโทรล Datagrid ลูก ในขั้นตอนนี้มีแค่สองจุดเท่านั้นที่คุณสามารถแก้ไขเลย์เอาท์ของกริดได้ นั่นก็คือ ItemCreate event หรือที่ดีกว่านั้นก็คือ ItemDataBound event โดยที่ ItemDataBound จะเริ่มทำงานหลังจากวงจรชีวิตของ grid item เล็กน้อย และมันเป็นเหตุการณ์สุดท้ายที่คุณเห็นก่อนที่จะมีการเพิ่มแถวใหม่ลงไปในตาราง HTML สุดท้าย
เมื่อผู้ใช้คลิกที่ปุ่มลิงก์ปุ่มใดปุ่มหนึ่งที่อยู่ในคอลัมน์คำสั่ง ItemCommand event จะโผล่ขึ้นมาในกริด ถ้าหากชื่อคำสั่งเท่ากับ Expand (ชื่อคำสั่งของปุ่มคอลัมน์) ก่อนอื่นโค้ดจะทำการตรวจสอบดูว่าอินเดกซ์ของ item ที่คลิกตรงกับ ExpandItemIndex property หรือไม่ ถ้าหากเป็นอย่างนั้น นั่นหมายความว่าผู้ใช้ได้คลิกไปที่ item ที่ขยายอยู่แล้ว ดังนั้น item จะถูกยุบ ภาพที่ 7 แสดงโค้ดที่ใช้กลไกนี้ อย่างที่เคยบอกเอาไว้ก่อนหน้านี้ก็คือ ExpandItemIndex property เป็น absolute index ที่มีค่าตั้งแต่ 0 ไปจนถึงจำนวนของ item ที่อยู่ในแหล่งข้อมูล ด้วยเหตุนี้คุณจึงจำเป็นต้องทำการเปรียบเทียบโมดูลของอินเดกซ์กับอินเดกซ์ของ item ในขณะที่สร้างเพจของกริดขึ้นมาเสียก่อน
ขั้นตอนสุดท้ายของโค้ดที่อยู่ในภาพที่ 7 เป็นการเรียก UpdateView event ขึ้นมา ถ้าหากเป็นคอนโทรล NestedGrid แล้ว เหตุการณ์นี้เป็นตัวแทนของจุดเริ่มต้นขั้นตอนการสร้างยูซเซอร์อินเทอร์เฟซขึ้นมา อุปกรณ์ที่เป็นไคล์เอ็นต์จะเป็นตัวจัดการกับเหตุการณ์ดังกล่าว เชื่อมโยงกับข้อมูลที่จำเป็นและท้ายสุดเรียก DataBind method ของกริด เมื่อถึงจุดนี้วงจรชีวิตของคอนโทรลจะเป็นเหตุการณ์ต่อเนื่อง เหตุการณ์แรกก็คือ DataBinding ต่อมาคือ ItemCreated และ ItemDataBound ที่เรียกมาจัดการกับแต่ละ item ของกริด รวมทั้งหัวเรื่อง แถวข้อมูล และท้ายเรื่อง เมื่อถึงตอนนี้ จะมีการเรียก InitializeItem เพื่อสร้างเซลของแต่ละคอลัมน์ที่เชื่อมโยงกันขึ้นมา
ในภาพที่ 2 คุณจะพบว่ากริดเตรียมช่องว่างเอาไว้สำหรับกริดฝังตัวอื่นๆแล้ว ซึ่งจะแสดงแถวลูกของเรกคอร์ดที่ขยายออกไป สมมติว่าคอลัมน์ขยายอยู่ซ้ายมือสุด คุณสามารถลบเซลทั้งหมดที่ตามมาได้ แล้วแทนที่โดยใช้เซลใหม่ที่กำหนดเอาไว้ใน RowSpan attribute คุณสามารถกำหนดข้อมูลของเซลใหม่ได้ แต่เซลใหม่ควรมีข้อมูลอย่างน้อยดังต่อไปนี้ เซลที่ถูกลบออกไป (ใช่แล้ว ข้อมูลของเรกคอร์ดที่คุณกำลังจะขยาย) และกริดลูก เซลจำนวนหนึ่งที่ถูกลบทิ้งไปแล้วถูกเพิ่มเข้าไปใหม่ในภายหลังอาจฟังดูแล้วประหลาดๆ แต่การประนีประนอมในลักษณะนี้เป็นสิ่งที่จำเป็นสำหรับการจับเงื่อนไขตรงกันข้ามกันมาอยู่ด้วยกัน นั่นก็คือการใส่ตารางลูกลงไปพร้อมๆกับเก็บเลย์เอาท์ของตารางที่เหลือเอาไว้
วิธีการทำงานของผมก็คือผมทำแคชข้อความและความกว้างของแต่ละเซลที่ถูกลบออกไปก่อน หรืออีกแนวทางหนึ่งคุณอาจพิจารณาย้ายออปเจ็กต์ TableCell จาก Cells collection หนึ่งไปสู่อีกอันหนึ่งก็ได้ (ดูภาพที่ 8) เซลใหม่จะเก็บตารางสองแถวเอาไว้โดยที่แถวแรกเก็บเซลเดิมเอาไว้และแถวที่สองขยายความกว้างเพื่อแสดงผล DataGrid ลูก
เมื่อตอนที่ผมทดสอบโค้ดนี้เป็นครั้งแรก มันก่อให้เกิดปัญหาตามมา โดยก่อนที่จะเขียนโค้ดใน ASP.NET ผมตรวจสอบความถูกต้องของแนวคิดโดยใช้ HTML ธรรมดา ผมมีความมั่นใจว่าเลย์เอาท์ที่อธิบายเอาไว้ด้านบนเหมาะสมแล้ว ผมจึงเขียนโค้ดเอาไว้ภายใน ItemDataBound event ของ ASP.NET DataGrid สิ่งที่ทำให้ผมแปลกใจอย่างมากก็คือคอลัมน์ไม่ได้ขยายออกไปอย่างเหมาะสม ผมต้องใช้เวลาพักใหญ่ๆจึงจะรู้ว่าความผิดปกติอยู่ตรงไหน ความลับอยู่ตรงที่ผมกำหนดความกว้างเป็นพิกเซลให้แต่ละคอลัมน์แบบตายตัว รวมทั้งคอลัมน์แรกที่ต่อจากคอลัมน์ขยายด้วย
<asp:boundcolumn runat="server"
headertext="ID"
datafield="ID"
itemstyle-width="150px" />
ถ้าหากพิจารณาให้ดีเราจะพบว่าเซลใหม่ (เซลที่ถูกกำหนดให้เก็บกริดลูกเอาไว้) ยังคงเป็นคอลัมน์แรกที่ต่อจากคอลัมน์ขยาย ดังนั้นมันจึงได้รับความกว้างเป็นพิกเซลของคอลัมน์ ID เดิมติดมาด้วย ถ้าหากต้องการให้เซลนี้ขยายไปยังพื้นที่ว่างที่เหลือได้ เราจะต้องไม่กำหนด Width property โดยปล่อยว่างเอาไว้ ไม่ว่าคุณจะทำอะไรในโค้ดก็ตาม (ดูเพิ่มเติมจาก ItemDataBound event handler) เฟรมเวิร์กภายในของ DataGrid จะสร้างรูปแบบจากสไตล์ตามความกว้างเป็นพิกเซลเดิมเสมอ ถ้าหากคุณดูซอร์ซ HTML ของเซลนี้ คุณจะพบกับการออกแบบที่คล้ายคลึงกันดังนี้
style="width:150px;...;width='';"
มีการกำหนดคุณสมบัติความกว้างสองครั้ง (การกำหนดครั้งที่สองอยู่ใน ItemDataBound) แต่ค่าแรกเท่านั้นที่มีความสำคัญต่อบราวเซอร์ ดังนั้นผมจึงพบวิธีการแก้ปัญหาที่ดีกว่าการกำหนดความกว้างแบบตายตัวแล้ว ถ้าหากคอลัมน์ต้องการกำหนดความกว้าง คุณสามารถกำหนดคุณสมบัติเฉพาะขึ้นมาได้ (ตามตัวอย่างนี้ใช้ชื่อว่า HostColumnWidth) ซึ่งเป็นตัวแทนหน่วยความกว้างของคอลัมน์หลัก (คอลัมน์แรกหลังจากที่ขยายคอลัมน์) โค้ดตัวอย่างสั้นๆต่อไปนี้แสดงวิธีการกำหนดความกว้างของคอลัมน์ ซึ่งให้ผลลัพธ์เท่ากับการกำหนดสไตล์ของ item
if (e.Item.ItemIndex != (ExpandedItem % this.PageSize)) {
// Equivalent to setting itemstyle-width declaratively
e.Item.Cells[1].Width = HostColumnWidth;
return;
}
การสร้างกริดลูก
เมื่อถึงจุดนี้งานของเราก็ใกล้จะเสร็จแล้ว โดยมีงานค้างอยู่อีกอย่างหนึ่งก็คือการสร้าง DataGrid ลูกขึ้นมา การที่คุณจะทำงานนี้ได้ขึ้นอยู่กับเลย์เอาท์ภายในของข้อมูลที่มีโครงสร้างแยกสาขาที่คุณดูแลอยู่ อย่างไรก็ตามสมมติว่าคุณเก็บข้อมูลหลายระดับเอาไว้ใน ADO.NET DataSet โดยมีการจับคู่ระหว่างตารางต่างๆ และใช้ออปเจ็กต์ DataView ลูก นั่นถือเป็นวิธีการที่เหมาะสมแล้ว (วิธีการนี้คล้ายคลึงกับสิ่งที่ผมเคยพูดเอาไว้ในบทความประจำเดือนสิงหาคมปี 2003)
ผู้ใช้เลือกตาราง parent ที่แสดงกริดและความสัมพันธ์ที่เป็นตัวกำหนดการแสดงผลของตารางลูก โดยที่ตารางลูกถูกสร้างขึ้นมาโดยการเรียก CreateChildView method ที่อยู่ในออปเจ็กต์ DataRowView ซึ่งเป็นตัวแทนของเรกคอร์ด parent เพื่อสร้าง child view ขึ้นมา
DataTable dt = ds.Tables[this.DataMember];
DataView theView = new DataView(dt);
DataRowView drv = theView[ExpandedItemIndex];
DataView detailsView = drv.CreateChildView(this.RelationName);
กลุ่มของเรกคอร์ดที่เกี่ยวข้องกับแถวที่ขยายออกไปจะถูกรวมกลุ่มเอาไว้ในออปเจ็กต์ DataView อันใหม่ ตัวอย่างเช่นถ้าหากดูจากตัวอย่างความสัมพันธ์ลูกค้ากับคำสั่งซื้อ กลุ่มของเรกคอร์ดลูกก็คือคำสั่งซื้อที่มาจากลูกค้าแต่ละราย กริดลูกจะถูกสร้างขึ้นมาและถูกปรับแต่งทันทีตามตัวอย่างด้านล่างนี้
detailsGrid = new DataGrid();
detailsGrid.ID = "detailsGrid";
detailsGrid.Font.Name = this.Font.Name;
detailsGrid.Font.Size = this.Font.Size;
detailsGrid.Width = Unit.Percentage(100);
detailsGrid.AllowPaging = true;
detailsGrid.PageSize = 5;
detailsGrid.PageIndexChanged += new DataGridPageChangedEventHandler(
detailsGrid_PageIndexChanged);
BindDetails(detailsGrid);
ถ้าหากพูดให้เจาะจงลงไป กริดลูกจะควบคุมการจัดเพจเป็นการภายใน นอกจากนั้นมันยังมีระบบจัดการภายในสำหรับ PageIndexChanged event ซึ่งจะย้ายกริดไปยังเพจต่อไปโดยอัตโนมัติเมื่อผู้ใช้คลิกไปที่หน้าเพจ ในขณะที่โปรแกรมเมอร์ไม่ต้องเขียนโค้ดเพื่อทำให้คุณสมบัตินี้ใช้การได้แต่อย่างใด
เหตุการณ์ใดๆที่เกิดขึ้นในกริดแบบฝังตัวจะมองไม่เห็นภายนอกกริดที่อยู่นอกสุด แต่เราอาจเขียนยูซเซอร์โค้ดขึ้นมาจัดการกับการคลิกที่แถบหมายเลขหน้าของกริดลูกได้ เรามีวิธีแก้ปัญหาข้อจำกัดเรื่องโครงสร้างนี้หรือเปล่า? ความเป็นไปได้อย่างหนึ่งก็คือคุณเรียกเหตุการณ์ใหม่จากภายในระบบจัดการภายในเลย
ถ้าหากคุณไม่ชอบการจัดหน้าของกริดลูก คุณสามารถใช้แถบเลื่อนและหุ้มกริดไปอยู่ในช่องเลื่อนได้ใดก็ได้ โดย overflow CSS attribute ของ HTML 4.0 สามารถแปลงกลุ่มองค์ประกอบ HTML ไปเป็นพื้นที่ที่เลื่อนไปมาได้ ถ้าหากข้อมูลเกินขอบเขตมิติที่กำหนด ถ้าหากต้องการให้กริดลูกเลื่อนไปมาได้ คุณจำเป็นต้องหุ้มมันเอาไว้ใน Panel (สอดคล้องกับ <div> tag> แล้วกำหนดให้ช่องดังกล่าวมี overflow attribute (ดูภาพที่ 9) ในตอนนี้หัวเรื่องของกริดจะเลื่อนไปพร้อมๆกับคอนโทรลที่เหลือ พฤติกรรมนี้ต้องอาศัยการออกแบบที่ซับซ้อนอย่างมากซึ่งผมจะไม่พูดถึงในบทความนี้
สรุป
ออปเจ็กต์ ADO.NET DataSet ทำงานเหมือนกับดาต้าเบสที่อยู่ในเมมโมรี ซึ่งเป็นศูนย์รวมของตารางและความสัมพันธ์และยอมให้คุณสร้างระบบแสดงผลข้อมูลที่มีโครงสร้างแยกสาขาได้ คุณสามารถแสดงผลข้อมูลดังกล่าวผ่านทางกริด โดยใช้การผสมผสานระหว่างคอนโทรลที่ทำงานซ้ำๆและคอนโทรลที่เชื่อมโยงกับข้อมูล (อาทิเช่น DataList, Repeater หรือ Label) เพื่อจำลองความสัมพันธ์ขึ้นมา ข้อเสียของแนวทางนี้ก็คือคุณจำเป็นต้องเขียนโค้ดที่ชัดเจนสำหรับการจัดเพจ แม้ DataGrid ยังมีคุณสมบัติที่น่าสนใจอีกมากก็ตาม แต่มันไม่มีสิ่งอำนวยความสะดวกสำหรับการแสดงผลข้อมูลหลายๆตารางที่แยกสาขาแต่อย่างใด ในคอลัมน์ Cutting Edge ประจำเดือนสิงหาคมปี 2003 ผมได้พูดถึงวิธีการแก้ปัญหาโดยใช้ยูซเซอร์คอนโทรลตัวหนึ่ง ส่วนในบทความนี้ผมได้พูดถึงการขยายขอบเขตการทำงานของคอนโทรล DataGrid ผ่านการนำมาดัดแปลงใหม่