[ASP.NET & jQuery]一對多物件,使用Listview與RowSpan呈現
前言
合併儲存格這需求,相信很多作Web的人都有碰到過,也有許多文章用不同的方式實作。在ASP.NET Webform來說,基本上不外乎就是三種方式:
- GridView在PreRender時針對每一列去檢查並做合併row的動作。
- 巢狀的GridView/ListView,來呈現一對多的物件集合關係。
- 使用jQuery在HTML Render完後,去做td的RowSpan。
通常合併儲存格要呈現的資料關係,就是一對多的關係,例如:主單與子單、客戶與訂單、角色與人員等等…這篇文章針對的,就是以Entity為觀念當出發點,當Entity是一對多的集合時,該怎麼樣使用ListView,並用jQuery來達到rowspan的效果。(jQuery的部份,則是使用黑暗執行緒的以jQuery實現Table相同欄位的上下合併)
需求
以Role-Person為例,一個角色可以有多個人的情況,最後要呈現出所有角色其對應的相關人員資料。Entity如下:
Person
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
}
Role
public class Role
{
public string Id { get; set; }
public List<Person> People { get; set; }
}
查詢結果的資料集合
public List<Role> GetSource()
{
var dataSource = new List<Role>
{
new Role
{
Id="1",
People= new List<Person>
{
new Person{ Id="p1", Name="Name1"},
new Person{ Id="p11", Name="Name11"}
}
},
new Role
{
Id="2",
People= new List<Person>
{
new Person{ Id="p2", Name="Name2"},
new Person{ Id="p21", Name="Name21"}
}
}
};
return dataSource;
}
實作
不是把資料餵給ListView,再套用黑大的jQuery就可以了嗎?很不幸的,不是這麼單純。
將上面的資料餵給ListView,得到的會是2筆Role的資料,也就是ListView只會呈現2筆record,那就無法作RowSpan。所以這邊的需求就是,需要將資料從List<Role>轉成List<Person>,且每一筆Person,還要帶著RoleId的資訊。就這麼簡單,這個部分搞定,其他的就都不難。
資料來源的改變
怎麼樣把資料從一對多,轉成多筆資料帶著『一』的相關資訊?用loop就遜掉了。江湖一點訣,只要懂LINQ的SelectMany,要達到這個目的就輕而易舉了。
Sample.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
var source = this.GetSource();
var result = source.SelectMany(x => x.People, (x, y) => new { RoleId = x.Id, Person = y });
this.ListView1.DataSource = result;
this.ListView1.DataBind();
}
}
public List<Role> GetSource()
{
var dataSource = new List<Role>
{
new Role
{
Id="1",
People= new List<Person>
{
new Person{ Id="p1", Name="Name1"},
new Person{ Id="p11", Name="Name11"}
}
},
new Role
{
Id="2",
People= new List<Person>
{
new Person{ Id="p2", Name="Name2"},
new Person{ Id="p21", Name="Name21"}
}
}
};
return dataSource;
}
這一行:『var result = source.SelectMany(x => x.People, (x, y) => new { RoleId = x.Id, Person = y });』的意思指的是:
這邊用到的SelectMany是使用:
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector);
-
第一個參數是Func<TSource, IEnumerable<TCollection>>,TSource指的便是List<Role>,而return的TCollection,則是每一個Role裡面的People屬性,也就是List<Person>。這意味著最後的結果筆數要以List<Person>為主,而最後所有的List<Person>都會被攤平成IEnumerable<Person>。
看一下參數的命名是collectionSelector,也就是選出要當collection的集合。
-
第二個參數是Func<TSource, TCollection, TResult>,input有兩個參數,x就是TSource型別,也就是List<Role>。y就是TCollection型別,也就是第一個參數的結果,也就是Person的集合。return的則是TResult,也就是可以自己定義return的型別為何,會影響最後var result的型別。在這邊範例使用的匿名型別,也就是new{ RoleId=x.Id, Person=y }。代表一個匿名型別,有RoleId的屬性,並assign 原本List<Role>裡面,每一筆的RoleId。有一個Person的屬性,assign SelectMany第一個參數的結果中每一筆的Person。
看一下參數的命名是resultSelector,也就是選出最後的結果。而第一個參數與第二個參數的TCollection型別是一樣的,在第一個參數是collectionSelector這個委派的結果型別,第二個參數則是resultSelector這個委派的輸入參數型別,可以想像在背後的運作,應該是將第一個委派的集合,當做第二個委派的input參數。
-
Result的結果:
jQuery的使用
.aspx的部份
<form id="form1" runat="server">
<div>
<asp:ListView ID="ListView1" runat="server">
<LayoutTemplate>
<table id="tbLvHeader" runat="server" class="grid" cellpadding="0" cellspacing="0"
border="1" style="empty-cells: show;">
<tbody>
<tr>
<td>
Role ID
</td>
<td>
Person ID
</td>
<td>
Person Name
</td>
</tr>
<tr id="itemPlaceholder" runat="server">
</tr>
</tbody>
</table>
</LayoutTemplate>
<ItemTemplate>
<tr id="row" runat="server">
<td class="RoleId">
<asp:HyperLink ID="HyperLink1" runat="server" NavigateUrl='<%# string.Format("{0}?id={1}","~/Sample.aspx", Eval("RoleId"))%>'><%#Eval("RoleId")%></asp:HyperLink>
</td>
<td>
<%# Eval("Person.ID")%>
<asp:TextBox ID="txtPersonId" runat="server"></asp:TextBox>
</td>
<td>
<%# Eval("Person.Name")%>
</td>
</tr>
</ItemTemplate>
</asp:ListView>
<asp:Button ID="Button1" runat="server" Text="讀取listview textbox" OnClick="Button1_Click" />
<asp:Label ID="lblResult" runat="server" Text=""></asp:Label>
</div>
</form>
js的部份
<%--參考自黑暗執行緒:http://blog.darkthread.net/post-2011-06-24-jquery-auto-rowspan.aspx--%>
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.1.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
var $lastCell = null;
var mergeCellSelector = ".RoleId";
$("table.grid td.RoleId").each(function () {
//跟上列的td.c-No比較,是否相同
if ($lastCell && $lastCell.text() == $(this).text()) {
//取得上一列,將要合併欄位的rowspan + 1
$lastCell.closest("tr").children(mergeCellSelector)
.each(function () {
this.rowSpan = (this.rowSpan || 1) + 1;
});
//將本列被合併的欄位移除
$(this).closest("tr").children(mergeCellSelector).remove();
}
else //若未發生合併,以目前的欄位作為上一欄位
$lastCell = $(this);
});
});
</script>
在aspx,在ListView上增加一些控制項,用來測試當ListView是可編輯或是非純文字的情況,功能一樣可以正常運作。最後也增加了一個Button,來確定即使合併儲存格後,仍可以得到ListView上server control的資料。
[註]這邊合併儲存格的條件是用$lastCell.text(),若要比較整個html,請改用html(),但html()在這個case裡面會碰到DOM的id不同,導致判斷這兩個cell不一致的情況。anyway,了解了原理,要怎麼變化就取決於各位的需求囉。
結果畫面
輸入資料,按按鈕後
結論
- 巢狀的物件集合,需要攤平的時候,可以透過SelectMany來做,不要害怕委派方法跟泛型,了解之後會更能體會設計的藝術與美。
- 黑大的文章,向來目標明確,舉例淺顯易懂,程式精簡,且看了不只考試可以得一百分,還能會心一笑,實在是居家旅行,必備良藥。
Sample Project:ListViewRowSpan.zip
blog 與課程更新內容,請前往新站位置:http://tdd.best/