[轉貼] [ASP.NET & jQuery]一對多物件,使用Listview與RowSpan呈現

2012040909:21
出處:http://www.dotblogs.com.tw/hatelove/archive/2012/01/06/listview-rowspan-convert-one-to-many-entity-to-many-with-one-by-selectmany-function.aspx

前言
合併儲存格這需求,相信很多作Web的人都有碰到過,也有許多文章用不同的方式實作。在ASP.NET Webform來說,基本上不外乎就是三種方式:

  1. GridView在PreRender時針對每一列去檢查並做合併row的動作。
  2. 巢狀的GridView/ListView,來呈現一對多的物件集合關係。
  3. 使用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);
  1. 第一個參數是Func<TSource, IEnumerable<TCollection>>,TSource指的便是List<Role>,而return的TCollection,則是每一個Role裡面的People屬性,也就是List<Person>。這意味著最後的結果筆數要以List<Person>為主,而最後所有的List<Person>都會被攤平成IEnumerable<Person>。

    看一下參數的命名是collectionSelector,也就是選出要當collection的集合。
     
  2. 第二個參數是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參數。
     
  3. Result的結果:
    image


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,了解了原理,要怎麼變化就取決於各位的需求囉。

結果畫面
image

輸入資料,按按鈕後
image 

結論

  1. 巢狀的物件集合,需要攤平的時候,可以透過SelectMany來做,不要害怕委派方法跟泛型,了解之後會更能體會設計的藝術與美。
  2. 黑大的文章,向來目標明確,舉例淺顯易懂,程式精簡,且看了不只考試可以得一百分,還能會心一笑,實在是居家旅行,必備良藥。


Sample ProjectListViewRowSpan.zip