Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[iThome 第七屆鐵人賽 19] 框架產生下拉式資料的內容

這一篇,回到Controller常常需要做的一件事情,那就是當如果欄位屬於下拉式選單的時候,需要準備好下拉式清單的資料。

如果用的是預設的方式去產生下拉式選單其實有很多問題,這一篇想透過框架的方式,讓產生下拉式清單的資料能夠自動化。

預設Scaffolding的問題

如果是Mvc Scaffolding內建的話,會在Controller的時候產生下拉式清單資料並且塞到ViewBag裡面。

這個最大的問題是:假設某一個地方有需要下拉式選單的資料,但是忘記產生了(最長發生這個問題是在Edit的時候,驗證失敗需要重新顯示View的時候),畫面就會炸掉。

而且這個只有在runtime的時候才會發生,完全沒有辦法在compile的時候檢測出來。

假設有東西能夠保證需要下拉式清單的資料的時候,一定會產生出來,就不需要擔心到底有沒有忘記呼叫要把資料塞到ViewBag裡面。

這就是我們框架要解決的問題。

解決思路

首先,需要下拉式清單的資料的時候,就需要產生出來。能夠確認每一次需要的時候就會產生,就要透過Filter來做。

如果透過Filter來處理產生的邏輯,那麼還需要準備資料給Filter,讓它產生對應的資料並且塞到ViewBag裡面。因此,ViewModel是最適合的,因為在Filter裡面可以取得ViewModel的內容,因此可以做一個特殊的欄位,專門存放這些準備資料。

最後,在顯示的部份,就可以用一般的HtmlHelper產生即可。

實作內容

接下來就看看如何實作。

SelectListViewModel的定義

首先,定義一個SelectListViewModel的Class,這個Class將代表需要產生的下拉式選單:

/// <summary>
/// 代表一個要被產生的SelectList
/// </summary>
public class SelectListViewModel
{
    /// <summary>
    /// 取得或設定此SelectList要和那個ViewModel Property對應
    /// </summary>
    /// <value>
    /// 此SelectList要和那個ViewModel Property對應
    /// </value>
    public string SelectListId { get; set; }

    /// <summary>
    /// 取得或設定資料來源
    /// </summary>
    /// <value>
    /// 資料來源
    /// </value>
    public string Source { get; set; }

    /// <summary>
    /// 取得或設定SelectList值的欄位來源
    /// </summary>
    /// <value>
    /// SelectList值的欄位來源
    /// </value>
    public string DataValueField { get; set; }

    /// <summary>
    /// 取得或設定SelectList顯示的欄位來源
    /// </summary>
    /// <value>
    /// SelectList顯示的欄位來源
    /// </value>
    public string DataTextField { get; set; }

    /// <summary>
    /// 取得或設定SelectList被選取的值
    /// </summary>
    /// <value>
    /// SelectList被選取的值
    /// </value>
    public object SelectedValue { get; set; }

    /// <summary>
    /// SelectList要從那裡被產生出來
    /// </summary>
    private string codeWhere;

    /// <summary>
    /// 取得或設定SelectList要從那裡被產生出來
    /// </summary>
    /// <value>
    /// SelectList要從那裡被產生出來
    /// </value>
    public string CodeWhere
    {
        get
        {
            return codeWhere;
        }

        set { codeWhere = value; }
    }
}

定義ViewModelBase


再來,所有的ViewModel都要從某一個Base做繼承,而這個ViewModelBase會有一個Property,會回傳Array of SelectListViewModel。這個property將代表這個ViewModel會用到的下拉式選單清單。

 /// <summary>
/// Core View Model 的 Base class。所有ViewModel將會繼承這一個。
/// </summary>
public abstract class CoreViewModelBase
{
    /// <summary>
    /// 如果需要產生SelectList到ViewData裡面,
	/// 那麼child class會複寫這個Property,輸入需要產生的SelectList資訊。
    /// </summary>
    /// <value>
    /// 
    /// </value>
    public virtual SelectListFill.SelectListViewModel[] NeedFillSelectList 
    { 
        get 
        { 
            return null; 
        } 
    }
}

因此,某一個的實作可能會是:

public class Create : CoreViewModelBase, IHaveCustomMapping
{
...
	public string PostType {get; set;}

	/// <summary>
	/// 如果需要產生SelectList到ViewData裡面,
	/// 那麼child class會複寫這個Property,
	///	輸入需要產生的SelectList資訊。
	/// </summary>
	public override SelectListViewModel[] NeedFillSelectList
	{
		get
		{
			List<SelectListViewModel> temp = new List<SelectListViewModel>();
	
			temp.Add(new SelectListViewModel()
                {
                    CodeWhere = "where",
                    DataTextField = "Text",
                    DataValueField = "Value",
                    SelectedValue = PostType,
                    SelectListId = "PostType",
                    Source = "Code"
                });
	
			return temp.ToArray();
		}
	}
	
	...
}

上面的例子是有一個PostType的下拉式選單


Filter定義


再來就是實際產生下拉式清單的地方:

/// <summary>
/// 把ViewModel裡面有設定要產生的SelectList產生出來並且寫到ViewData。
/// 需要由此Class的Child來複寫產生SelectList的邏輯
/// </summary>
public abstract class FillSelectListActionFilterBase : ActionFilterAttribute
{
    /// <summary>
    /// 產生SelectList的邏輯
    /// </summary>
    /// <param name="viewModel">提供要如何產出SelectList的資訊</param>
    /// <returns>
    /// 依照ViewModel的資訊產出對應的SelectList
    /// </returns>
    public override System.Web.Mvc.SelectList 
		GetSelectList(SelectListViewModel viewModel)
    {
        SelectList result;

        // 依照SelectListViewModel的值,去產生SelectList

        return result;
    }

    /// <summary>
    /// 把產出的SelectList注入到ViewData裡面
    /// </summary>
    /// <param name="filterContext">The filter context.</param>
    public override void OnActionExecuted
		(ActionExecutedContext filterContext)
    {
        var viewResult = filterContext.Result as ViewResult;

        if (viewResult != null && viewResult.Model is CoreViewModelBase)
        {
            var selectListViewModelArray = 
			((CoreViewModelBase)viewResult.Model).NeedFillSelectList;

            // 假設有設定ViewModel才要做產出的動作
            if (selectListViewModelArray != null
			 && selectListViewModelArray.Count() > 0)
            {
                foreach (var item in selectListViewModelArray)
                {
                    // 假設目前ViewData裡面沒有這個SelectList才產生。
					// 因此,在別的地方產出的SelectList的權重比這一個
                    // filter還高。
                    if (viewResult.ViewData[item.SelectListId] 
						as System.Web.Mvc.SelectList == null)
                    {
                        viewResult.ViewData[item.SelectListId] 
							= this.GetSelectList(item);
                    }
                }
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

基本上就是依照SelectListViewModel去產生SelectList。然後把產生的SelectList塞到ViewData裡面,使用的Key會是 SelectListViewModel裡面的SelectListId


View的使用


最後,就是View的呼叫:

// Create.cshtml

...

@Html.DropDownListFor(model => model.PostType, null, 
		htmlAttributes: new { @class = "form-control" })

....

這邊注意到我們給nullHtml.DropDownListFor,因為如果沒有給SelectList,Mvc會去找ViewData裡面看有沒有一樣的key有這個值,有的話就會使用那個作為清單。而我們的Filter則會產生出來那個清單,因此一定會有東西。


結語


透過使用這種方法,再也不需要擔心需要下拉式清單的時候忘記產生,因為Filter會幫忙做掉。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus