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

  • 8656
  • 0

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

前言
合併儲存格這需求,相信很多作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

 


blog 與課程更新內容,請前往新站位置:http://tdd.best/