2012年10月7日 星期日

ASP.NET MVC 資料分頁與 Route - Part.3


上一篇「ASP.NET MVC 資料分頁與 Route - Part.2」說明如何使用產品分類編號以及 Route 設定讓 URL 格式可以有比較好的 SEO,不過必須說的是,修改前與修改後的 URL 格式對於 SEO 其實沒有多大的差別,

修改前:http://localhost:5511/Product/List?category=1&page=2

修改後:http://localhost:5511/Product/Category/1/Page/2

因為我們使用的產品分類的參數值是用「CategoryID」,這並沒有明確的給「Category」有特別的意義,CategoryID 只是用數字組成的編號,對於產品資料的搜尋上並沒有多大的幫助,如果將 URL 中的 Category 使用 CategoryName 來取代的話,在 SEO 就會有比較好的效果,

ex:http://localhost:5511/Product/Beverages/2

在這一篇的內容當中將會逐一介紹。

 


操作示範使用 ASP.NET MVC 4.0 開發,範例資料庫使用「Northwind」,分頁功能使用「MvcPaging 2.0」

初學習 ASP.NET MVC 的 Route (路由)設定,建議先詳讀 MSDN 的「ASP.NET 路由」:

http://msdn.microsoft.com/zh-tw/library/cc668201(v=vs.100).aspx

MvcPaging 2.0

http://blogs.taiga.nl/martijn/2012/04/23/mvcpaging-2-0/

 

Route:

經過前兩篇的說明過後,我們可以很快地增加我們這一次主題所需要的 Route 設定,

// Product - ListByCategoryName
routes.MapRoute(
    "ProductList_CategoryName_Default", // 路由名稱
    "Product", // URL 及參數
    new { controller = "Product", action = "ListByCategoryName", CategoryName = "all", page = UrlParameter.Optional } // 參數預設值
);
 
routes.MapRoute(
    "ProductList_CategoryName", // 路由名稱
    "Product/{CategoryName}", // URL 及參數
    new { controller = "Product", action = "ListByCategoryName", CategoryName = "all", page = UrlParameter.Optional } // 參數預設值
);
 
routes.MapRoute(
    "ProductList_CategoryName_Page", // 路由名稱
    "Product/{CategoryName}/{page}", // URL 及參數
    new { controller = "Product", action = "ListByCategoryName", CategoryName = "all", page = UrlParameter.Optional } // 參數預設值
);

上面的 Route 設定,我們期望所產生的 URL 結構樣式會是以下的內容:

  • http://localhost:5511/Product
  • http://localhost:5511/Product/all/2
  • http://localhost:5511/Product/Beverages
  • http://localhost:5511/Product/Beverages/2

與上一篇使用的 Route 設定除了名稱不同外,只有兩個地方不同, action 名稱與參數名稱 CategoryName,使用 CategoryID 做為產品資料的分類依據是因為產品資料就有存放關連資料的主鍵欄位,所以可以藉由 CategoryID 這個條件來過濾並取出符合的產品資料。

CategoryID 是產品分類資料的主鍵,是唯一值,所以不會有重複性的問題,而現在我們要使用的是 CategoryName,要使用這個資料來當做過濾篩選的條件,就必須要確定是否有重複性的問題,在 Northwind 的 Categories 這個 Table 中,CategoryName 與 CategoryID 一樣都是唯一值,沒有重複,也只有這樣的條件成立下才能使用 CategoryName 來做為過濾資料的依據。

另外也重新檢討了所有的 Route 設定,雖然說我們想要使用「Product」來做為 URL 中的關鍵字,可以明顯的分辨出這是用來顯示產品資料的 URL,但這會遇到一個很明顯的問題,那就是 Route 設定的衝突,我們想讓使用 CategoryID 與使用 CategoryName 的 URL 可以同時並存,但事與願違,一定會有衝突的情況發生。

所以這一次新的 Route 設定不會放在之前 Route 設定的上面,而是會放在下面,另外也對原本的 Route 設定做了一點的修改,如下所是:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    // Product - List
    routes.MapRoute(
        "ProductList_Default", // 路由名稱
        "Product/Category", // URL 及參數
        new { controller = "Product", action = "List", Category = "all", page = UrlParameter.Optional } // 參數預設值
    );
 
    routes.MapRoute(
        "ProductList_Category", // 路由名稱
        "Product/Category/{Category}", // URL 及參數
        new { controller = "Product", action = "List", Category = "all", page = UrlParameter.Optional } // 參數預設值
    );
 
    routes.MapRoute(
        "ProductList_Category_Page", // 路由名稱
        "Product/Category/{Category}/Page/{page}", // URL 及參數
        new { controller = "Product", action = "List", Category = "all", page = UrlParameter.Optional } // 參數預設值
    );
 
 
    // Product - Index
    routes.MapRoute(
        "Product_Page", // 路由名稱
        "Product/Page/{page}", // URL 及參數
        new { controller = "Product", action = "Index", page = UrlParameter.Optional } // 參數預設值
    );
 
 
    // Product - ListByCategoryName
    routes.MapRoute(
        "ProductList_CategoryName_Default", // 路由名稱
        "Product", // URL 及參數
        new { controller = "Product", action = "ListByCategoryName", CategoryName = "all", page = UrlParameter.Optional } // 參數預設值
    );
 
    routes.MapRoute(
        "ProductList_CategoryName", // 路由名稱
        "Product/{CategoryName}", // URL 及參數
        new { controller = "Product", action = "ListByCategoryName", CategoryName = "all", page = UrlParameter.Optional } // 參數預設值
    );
 
    routes.MapRoute(
        "ProductList_CategoryName_Page", // 路由名稱
        "Product/{CategoryName}/{page}", // URL 及參數
        new { controller = "Product", action = "ListByCategoryName", CategoryName = "all", page = UrlParameter.Optional } // 參數預設值
    );
 
}

 

 

Controller / Action:

這邊比較特別的是 CategoryName 的處理,因為需求是要以 CategoryName 來當做篩選取出產品資料的條件,而 CategoryName 的資料中會有「/」的符號,這個「/」會讓我們得到與預期所設定的 Route 會有不同的結果,例如使用這麼一個 URL 樣式「Product/{CategoryName}」CategoryName為「Grains/Cereals」時,URL 就會是「Product/Grains/Cereals」,這個誤會就大了,看看下圖的 Route 匹配結果,

image

CategoryName 為 Grains,而 page 則是取 Cereals ……

 

所以就必須把 CategoryName 內的特殊符號做替換,通常最常替換的字符為「-」,

「Grains/Cereals」→「Grains-Cereals」

「Meat/Poultry」→「Meat-Poultry」

不只是「/」這個字符要注意,還有其他的一些字符最好也需要做替換處理。

public ActionResult ListByCategoryName(string categoryName = "all", int page = 1)
{
    categoryName = categoryName.Replace("-", "/");
 
    int currentPageIndex = page < 0 ? 0 : page - 1;
 
    var category = categoryName.Equals("all") ? null : categoryService.FindOne(categoryName);
 
    ViewBag.CategorySelectList = categoryName.Equals("all", StringComparison.OrdinalIgnoreCase)
        ? this.CategoryNameSelectList("all")
        : this.CategoryNameSelectList(categoryName);
 
    var query = productService.GetAll();
 
    if (!string.IsNullOrWhiteSpace(categoryName) && !categoryName.Equals("all") && category != null)
    {
        query = query.Where(x => x.CategoryID == category.CategoryID);
    }
    query = query.OrderBy(x => x.CategoryID).ThenBy(x => x.ProductID);
 
    ViewData.Model = query.ToPagedList(currentPageIndex, DefaultPageSize);
 
    ViewBag.CategoryID = category == null ? "all" : category.CategoryID.ToString();
    ViewBag.CategoryName = category == null ? "all" : category.CategoryName;
    ViewBag.Page = currentPageIndex;
 
    return View();
}
 
[HttpPost]
public ActionResult ListByCategoryName(string categoryName, int? page)
{
    categoryName = categoryName.Replace("-", "/");
    int currentPageIndex = page.HasValue ? page.Value - 1 : 0;
    
    var category = categoryName.Equals("all") ? null : categoryService.FindOne(categoryName);
 
    return RedirectToRoute("ProductList_CategoryName", new
    {
        Controller = "Product",
        Action = "ListByCategoryName",
        categoryName = (category == null) ? "all" : category.CategoryName.Replace("/", "-"),
        page = 1
    });
} 

下拉選單的產生:

public List<SelectListItem> CategoryNameSelectList(string selectedValue = "all")
{
    List<SelectListItem> items = new List<SelectListItem>();
    items.Add(new SelectListItem()
    {
        Text = "All Category",
        Value = "all",
        Selected = selectedValue.Equals("all", StringComparison.OrdinalIgnoreCase)
    });
 
    var categories = categoryService.GetAll().OrderBy(x => x.CategoryID);
 
    string tempCategoryName = string.Empty;
 
    foreach (var c in categories)
    {
        tempCategoryName = c.CategoryName.Replace("/", "-");
 
        items.Add(new SelectListItem()
        {
            Text = c.CategoryName,
            Value = tempCategoryName,
            Selected = selectedValue.Equals(tempCategoryName, StringComparison.OrdinalIgnoreCase)
        });
    }
    return items;
}

 

 

View:

這裡介紹一下 MvcPaging 的其中一個選項:AlwaysAddFirstPageNumber(這是 MvcPaging 2.0.2 所增加的項目)

https://github.com/martijnboland/MvcPaging

AlwaysAddFirstPageNumber
By default we don't add the page number for page 1 because it results in canonical links. 

拿上一篇的 View 來做示範,我們沒有在 Html.Pager() 加入 AlwaysAddFirstPageNumber() Option 時,預設都不會自動在第一頁的頁碼連結加入 Page 的參數值,如下:

image

如果我們在 Html.Pager() 加入 AlwaysAddFirstPageNumber() Option 後,Pager 上的第一頁頁碼連結的 URL 就會加入第一頁的 Page 值,

image

image

完整的 View 內容如下:

@model IPagedList<MvcPagedBrowserHash.Models.Products>
 
@{
    ViewBag.Title = "ListByCategoryName";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
@using (Html.BeginForm("ListByCategoryName", "Product", FormMethod.Post))
{
    <h2>Products List - Part.3</h2>
    <br />
    <fieldset>
        Category:@Html.DropDownList("categoryName", (IEnumerable<SelectListItem>)ViewBag.CategorySelectList)
        <input type="submit" value="submit" id="ButtonSubmit" />
        <input type="button" value="Reset" id="ButtonReset" />
    </fieldset>
    
    <div class="pager">
        @Html.Pager(Model.PageSize, Model.PageNumber, Model.TotalItemCount).Options(o => o
            .AlwaysAddFirstPageNumber()
            .AddRouteValue("CategoryName", ViewBag.CategoryName))
        Displaying @Model.ItemStart - @Model.ItemEnd of @Model.TotalItemCount item(s)  
    </div>
    <br />
    <table>
        <tr>
            <th>ProductID
            </th>
            <th>ProductName
            </th>
            <th>SupplierID
            </th>
            <th>CategoryID
            </th>
            <th>QuantityPerUnit
            </th>
            <th>UnitPrice
            </th>
            <th>UnitsInStock
            </th>
            <th>UnitsOnOrder
            </th>
            <th>ReorderLevel
            </th>
            <th>Discontinued
            </th>
        </tr>
 
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.ProductID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ProductName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.SupplierID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.CategoryID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.QuantityPerUnit)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.UnitPrice)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.UnitsInStock)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.UnitsOnOrder)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReorderLevel)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Discontinued)
                </td>
            </tr>
        }
 
    </table>
}
@section scripts{
    <script type="text/javascript">
        $(document).ready(function () {
            $('#ButtonReset').click(ButtonResetEventHandler);
        });
 
        var ButtonResetEventHandler = function (){
            location.href = '@Url.RouteUrl("ProductList_CategoryName_Page", new { CategoryName = "all", page = 1 })';
        };
    </script>
}

 

最後的執行結果:

http://localhost:5511/Product/all/1

SNAGHTMLa5bca56

 

http://localhost:5511/Product/all/2

SNAGHTMLa60cb8e

第一頁的連結有 Page 1 的參數值

image

 

http://localhost:5511/Product/Beverages/2

image

SNAGHTMLa624916

 

http://localhost:5511/Product/Grains-Cereals/1

image

SNAGHTMLa63ab52

 


其實這一篇不太好寫,因為 Route 設定必須要做很多的嘗試,Route 設定的順序、參數的對應等等,常常不同的排列組合都會有讓人意想不到的結果出現,Route 設定並不是很困難,但就在於要調配出適當的設定就必須要花點時間來,很多書或是教學都沒有把 Route 設定用很大的篇幅來介紹,其實 Route 設定與其他的功能相比,內容真的少很多,但也不能因為教得少或是內容介紹少就忽略了,反而要多花一點時間來琢磨與學習。

 

系列文章:

ASP.NET MVC 資料分頁與 Route - Part.1
ASP.NET MVC 資料分頁與 Route - Part.2
ASP.NET MVC 資料分頁與 Route - Part.3

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。