2014年10月12日 星期日

[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會幫忙做掉。


沒有留言 :

張貼留言