2012年4月21日 星期六

ASP.NET MVC 3 - ViewBag 裡使用方法(Method)

看到標題一定會有人聯想到這篇文章應該是要介紹 VidewBag 的「使用方法」,

但這一篇並不是要說明怎麼用 ViewBag,而是我們去放 Method 在一個 ViewBag 中來使用,

ASP.NET MVC 在 MVC3 以前,Controller 存放資料到 View 會常常使用到 ViewData,而到了 ASP.NET MVC 3 多了一個 ViewBag 的類別可以拿來放資料,也可以用來當做 Controller 傳遞資料給 View 的媒介,ViewData 與 ViewBag 其實本質上都是同一種的,可以從 ASP.NET MVC 的原始碼中看出,

public dynamic ViewBag
{
    get
    {
        if (_dynamicViewDataDictionary == null)
        {
            _dynamicViewDataDictionary = new DynamicViewDataDictionary(() => ViewData);
        }
        return _dynamicViewDataDictionary;
    }
}

ViewBag 是用 dynamic 這個型別的包裝,dynamic 型別的一項特點就是當要使用 ViewBag 中的某個資料時,可以不必做類別轉換,而要使用 ViewData 中的某個資料時,就必須要做類別轉換,不過 ViewBag 雖然說因為 dynamic 的特點,在使用時可以不用類別轉換,但也因為這樣,在速度上是會比 ViewData 慢一些。

就剛剛所說的,ViewData 與 ViewBag 兩者所做的工作目的是一樣的,都是為了要讓 Controller 傳遞資料給 View 使用,而 ViewBag 是 ViewData 使用 dynamic 的再包裝,讓我們可以在 View 中使用資料時不必再多做一次類別轉換的動作,資料拿來就可以直接使用。


ViewData 於 View 的使用

@foreach(var item in ViewData["Customer_OrderDetails"] as List<OrderDetails>)
{          
    OrderID: @item.OrderID , ProductID: @item.ProductID , UnitPrice: @item.UnitPrice , Quantity: @item.Uuantity
}

ViewBag 於 View 的使用

@foreach(var item in ViewBag.Customer_OrderDetails )
{
    CustomerID: @item.CustomerID , OrderID: @item.OrderID , UnitProce: @item.UnitPrice , Quantity : @item.Quantity
}

 


從一開始就先說明一些 ViewData 與 ViewBag 的差異以及使用上的不同,是為了要帶出接下來利用 ViewBag 的特性來做些不一樣的事情,

首先,先來看看一個很簡單的一個例子,例如我們有這樣的一個網頁內容:

image

上面的網頁內容看起來很平常,但如果想要凸顯一些資料的差異性,就必須在網頁上做點變化;

例如說,多一個 Memo 欄位,用來註記每張訂單的金額狀況,訂單金額超過 500 元的用藍色標記,超過 1000 元的用綠色標記,超過 2000 元用紅色標記外也要加粗字體,而其他不超過 500 元的就用黑色表示;

一般平常的作法可能就是直接從 View 上面下手,也因為使用 Razor View Engine,使得我們要在 View 上面加上程式會比較乾淨而且容易許多,改寫出來的 View 內容大概會是以下這樣:

@foreach (var item in Model) {
    <tr>
        <td>
            @item.CustomerID
        </td>
        <td>
            @item.OrderID
        </td>
        <td style="text-align: right;">
            @item.ProductItems
        </td>
        <td style="text-align: right;">
            @item.TotalQuantity
        </td>
        <td style="text-align: right;">
            @String.Format("{0:F}", item.TotalPrice)
        </td>
        <td style="text-align: right;">
            @if (item.TotalPrice >= 500 && item.TotalPrice < 1000)
            {
                <span style="color: blue;">超過500元</span>
            }
            else if (item.TotalPrice >= 1000 && item.TotalPrice < 2000)
            {
                <span style="color: green;">超過1000元</span>
            }
            else if (item.TotalPrice >= 2000)
            {
                <span style="color: red;">超過2000元</span>
            }
            else
            {
                <span style="color: black;">不足500元</span>
            }
        </td>
    </tr>
}

執行出來的畫面就會是如此:

image

 

上面的作法應該沒有太大的問題,但是如果 View 裡面所加上的程式太過於複雜的話,這似乎就會違反 MVC 的原則,真的太過於複雜的內容就可能以將邏輯抽出並且做成一個 helper 方法,這樣一來就可以解決問題,但如果 View 裡面所加的程式並不是其他 View 頁面也會需要用到,要抽出來並且做成一個 helper 方法也太過於「搞工(麻煩)」,其實還可以使用 Razor Page Helper,可以參考 Scott Guthrie blog 的一篇文章來了解,

ASP.NET MVC 3 and the @helper syntax within Razor

不過 Razor Page Helper 會有個問題是,在 MVC 專案的預設狀況下並不會對 View 進行編譯的動作,所以在設計時期無法馬上讓設計人員知道錯誤,甚至於會發生慌忙的設計人員未加以偵錯確認的情況下就讓程式上線,而結果就……

當然 Razor Page Helper 的編譯偵錯問題還是有其他方法可以處理,不過就不在這次的討論範圍中。

 

其實我們可以利用 Func 的委派特性,將一個有指定好 Method 的 Func 給放到 ViewBag 中,在 View 頁面中,只要把相關的資料傳給這個有指定 Func 的 ViewBag 就可以送回處理好的結果,直接看程式,我們就把剛剛在 View 中要對訂單金額做大小判斷的那段程式邏輯搬回到 Controller 去做,

Controller Action 方法

        public ActionResult TestMethod2()
        {
            using (NorthwindEntities db = new NorthwindEntities())
            {
                var orderDatas = from o in db.Orders
                                 orderby o.CustomerID, o.OrderID
                                 select new CustomerOrderData
                                 {
                                     CustomerID = o.CustomerID,
                                     OrderID = o.OrderID,
                                     ProductItems = o.Order_Details.Count(),
                                     TotalQuantity = o.Order_Details.Sum(x => x.Quantity),
                                     TotalPrice = o.Order_Details.Sum(x => x.Quantity * x.UnitPrice)
                                 };
                ViewBag.DetermineTotalPrice = new Func<decimal, string>(DetermineTotalPrice);
                return View(orderDatas.ToList());
            }
        }

Func 所委派執行的方法,傳入訂單金額的資料,然後回傳一個字串內容,

private string DetermineTotalPrice(decimal totalPrice)
{
    if (totalPrice >= 500 && totalPrice < 1000)
    {
        return "<span style=\"color: blue;\">超過500元</span>";
    }
    else if (totalPrice >= 1000 && totalPrice < 2000)
    {
        return "<span style=\"color: green;\">超過1000元</span>";
    }
    else if (totalPrice >= 2000)
    {
        return "<span style=\"color: red;\">超過2000元</span>";
    }
    else
    {
        return "<span style=\"color: black;\">不足500元</span>";
    }
}

而前端的使用,就很簡單,直接把訂單金額給傳到有指定委派 Func 的 ViewBag 就可以,

不過記得要用 @Html.Raw() Unencoded 傳送回來的HTML字串,

@foreach (var item in Model) {
    <tr>
        <td>
            @item.CustomerID
        </td>
        <td>
            @item.OrderID
        </td>
        <td style="text-align: right;">
            @item.ProductItems
        </td>
        <td style="text-align: right;">
            @item.TotalQuantity
        </td>
        <td style="text-align: right;">
            @String.Format("{0:F}", item.TotalPrice)
        </td>
        <td style="text-align: right;">
            @Html.Raw(ViewBag.DetermineTotalPrice(item.TotalPrice))
        </td>
    </tr>
}

頁面的執行結果如下(不較跟上一個直接在 View 中寫程式的結果一樣…)

image

這樣一來,在 View 中所要參雜處理的程式邏輯就會單純許多,而程式邏輯的部份搬回到 Controller 中,也比較容易偵錯與測試。

 

就這樣嗎?

當然不,接下來再多處理一點不一樣的操作,不然只是單純的判斷訂單金額並且做不同顏色的處理就顯示太過於雞肋,我們可以將這個訂單金額轉為國字大寫,其實這一類的文字轉換處理應該是可以另外開一個 ViewModel,然後在 ViewModel 多一個字串屬性,在 Action 方法裡取得 Model 資料後就把轉換後的國字大寫字串直接放到 ViewModel 的屬性中,或是 ViewModel 的屬性中就直接取用訂單金額的屬性值,並且叫用方法來產生國字大寫金額的字串。

 

不過咧,我們就是來試試看在 ViewBag 中使用 Func 委派方法的方式來做做看,首先我們看看 Controller Action 方法的內容,

public ActionResult CustomerOrderData()
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        var orderDatas = from o in db.Orders
                         orderby o.CustomerID, o.OrderID
                         select new CustomerOrderData
                         {
                             CustomerID = o.CustomerID,
                             OrderID = o.OrderID,
                             ProductItems = o.Order_Details.Count(),
                             TotalQuantity = o.Order_Details.Sum(x => x.Quantity),
                             TotalPrice = o.Order_Details.Sum(x => x.Quantity * x.UnitPrice)
                         };
        ViewBag.DetermineTotalPrice = new Func<decimal, string>(DetermineTotalPrice);
        ViewBag.ConvertToChineseWords = new Func<decimal, string>(ConvertToChineseWords);
        return View(orderDatas.ToList());
    }
}

Acton 方法中 多了一個「ConveretToChineseWords」的 ViewBag 內容,而這個 ViewBag 放了一個 Func 並指定一個委派方法,執行 ConvertToChineseWords 這一個 Method,

 

ConvertToChineseWords 這一個 Method 中,除了還是會判斷訂單金額的大小來做不同顏色的顯示外,最重要的就是有顏色標示的那一行,就是要把訂單金額做轉換國字大寫的字串處理,

private string ConvertToChineseWords(decimal totalPrice)
{
    int price = Convert.ToInt32(totalPrice);
    string textColor = string.Empty;
    if (price >= 500 && price < 1000)
    {
        textColor = "color: blue;";
    }
    else if (price >= 1000 && price < 2000)
    {
        textColor = "color: green;";
    }
    else if (price >= 2000)
    {
        textColor = "color: red; font-weight: bolder;";
    }
    else
    {
        textColor = "color: black;";
    }
    
    return string.Format
    (
        "約 <span style=\"{0} text-decoration: underline;\">{1}</span> 元",
        textColor,
        ProjectHelper.ConvertChineseNumber(price, ChineseNumberType.Captial)
    );
}

這邊特別說明一下,因為我這個數字轉換國字大寫的 Method 只有針對 Integer 做處理,所以在原本 Decimal 數值轉為 Integer 時就自動對小數點做了進位或捨去的處理,所以最後轉出來的國字大寫字串是一個約略值,其實還是可以做進一步的精確處理,不過就以後有機會再說。

 

前端的 View 內容:

@foreach (var item in Model) {
    <tr>
        <td>
            @item.CustomerID
        </td>
        <td>
            @item.OrderID
        </td>
        <td style="text-align: right;">
            @item.ProductItems
        </td>
        <td style="text-align: right;">
            @item.TotalQuantity
        </td>
        <td style="text-align: right;">
            @String.Format("{0:F}", item.TotalPrice)
        </td>
        <td style="text-align: right;">
            @Html.Raw(ViewBag.DetermineTotalPrice(item.TotalPrice))
        </td>
        <td style="text-align: right;">
            @Html.Raw(ViewBag.ConvertToChineseWords(item.TotalPrice))
        </td>
    </tr>
}

程式執行的結果:

image

 

這邊提供一個不一樣的 ViewBag 使用方法,讓各位在處理 View 的資料呈顯時能有多一種的應用選擇,至於在 View 中的程式邏輯要用什麼要的方式,就沒有一個定見,端看應用上哪一種比較合適以及合理就用哪一種方式吧!

 

參考資料:

 

Jeffrey Palermo (.com)

How to access controller methods from a view in ASP.NET MVC

Headspring - Jeffrey Palermo
How to add a method to ViewBag in ASP.NET MVC


 

以上

4 則留言:

  1. 要是我
    如果遇到重用的部份,我會用html.renderpartial的方式來處理
    不過這也算是種特別的方法,肝溫分享

    回覆刪除

提醒

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