使用列舉與旗標設計多選

by vivid 14. 六月 2017 10:23

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:
N170618401
出刊日期: 2017/6/14

Flags Attribute可以應用在Enum型別多選的情況,通常是在列舉代表一堆旗標(Flag)所成的集合時使用,可以表達一個以上的值。這種類型的列舉型別會搭配位元運算子來操作(bitwise operator)。我們只要在Enum型別套用System.FlagsAttribute attribute,列舉值則以2的倍數來定義,就可以搭配AND、OR、NOT、XOR位元運算子來操作。

 

使用Flags定義列舉

例如以下程式碼,定義一個列舉,描述影片撥放時段,可能是白天(Day)、晚上(Night)、平日(WeekDay)或假日(Holiday),列舉上方套用[Flags] Attribute:

[Flags]
public enum ShowOptions
{
    Day = 1,
    Night = 2,
    WeekDay = 4,
    Holiday = 8
}

只要列舉值是2的倍數就可以運作,上述的程式碼等同於使用以下位元運算子計算出的值:

[Flags]
public enum ShowOptions
{
    Day = 1 << 0,
    Night = 1 << 1,
    WeekDay = 1 << 2,
    Holiday = 1 << 3
}

也可以等同於以下程式碼:

[Flags]
public enum ShowOptions
{
    Day = 1 << 0,
    Night = 1 << Day,
    WeekDay = 1 << Night,
    Holiday = 1 << WeekDay
}

若是C# 7可以使用「0b」開始來表達位元資料:

[Flags]
public enum ShowOptions {
  Day =     0b00000001,
  Night =   0b00000010,
  WeekDay = 0b00000100,
  Holiday = 0b00001000
}

 

使用列舉

定義完列舉之後,我們就可以在程式中這樣使用,參考以下主控台範例程式碼,宣告一個myOptions變數,使用「|」(OR)運算子設定兩個值Day與Night:

class TestClass
{
    static void Main(string[] args)
    {
        ShowOptions myOptions = ShowOptions.Day | ShowOptions.Night;
        Console.WriteLine(myOptions); //Day, Night
        Console.WriteLine((int)myOptions); //3
    }
}

在底層會進行如下的位元運算,得到myOptions的值為「3」,請參考下圖所示:

clip_image002

圖 1

我們可以利用And(&)運算子,進行位元運算來判斷myOptions是否包含特定的列舉值:

if ((myOptions & ShowOptions.Day) == ShowOptions.Day)
{
    Console.WriteLine("myOptions包含Day");
}
if ((myOptions & ShowOptions.Night) == ShowOptions.Night)
{
    Console.WriteLine("myOptions包含Night");
}
if ((myOptions & ShowOptions.WeekDay) == ShowOptions.WeekDay)
{
    Console.WriteLine("myOptions包含WeekDay");
}
if ((myOptions & ShowOptions.Holiday) == ShowOptions.Holiday)
{
    Console.WriteLine("myOptions包含Holiday");
}

這段程式碼運算的結果會輸出:

 

myOptions包含Day
myOptions包含Night

以這段程式碼為例,測試目前的myOptopns是否包含Day:

if ((myOptions & ShowOptions.Day) == ShowOptions.Day)
{
    Console.WriteLine("myOptions包含Day");
}

myOptions目前為0011,Day為0001,做And(&)運算完得到0001 (同Day),因此只要And運算完的結果等同於列舉值,就表示有包含此列舉值,請參考下圖所示:

clip_image004

圖 2

換句話說,參考以下範例程式碼,測試目前的myOptopns是否包含Day,我們可以如此改寫之,And運算完的結果若不為0則表示包含其值:

if ((3 & 1) != 0)
{
    Console.WriteLine("myOptions包含Day");
}

 

And運算完的結果,請參考下圖所示:

clip_image006

圖 3

依此類推,以下這段程式碼,測試目前的myOptopns是否包含Night::

if ((3 & 2) != 0)
{
    Console.WriteLine("myOptions包含Night");
}

 

And運算完的結果,請參考下圖所示:

clip_image008

圖 4

以下這段程式碼,測試目前的myOptopns是否包含Weekday:

if ((3 & 4) != 0)
{
    Console.WriteLine("myOptions包含Weekday");
}

 

And運算完的結果,請參考下圖所示:

clip_image010

圖 5

 

以下這段程式碼,測試目前的myOptopns是否包含Holiday::

if ((3 & 8) != 0)
{
  Console.WriteLine("myOptions包含Holiday");
}

And運算完的結果,請參考下圖所示:

clip_image012

圖 6

 

Enum型別也包含一個HasFlag方法,可以用來判斷是否包含特定的值,我們簡化上述程式碼,程式改寫如下:

if (myOptions.HasFlag(ShowOptions.Day))
{
    Console.WriteLine("myOptions包含Day");
}
if (myOptions.HasFlag(ShowOptions.Night))
{
    Console.WriteLine("myOptions包含Night");
}
if (myOptions.HasFlag(ShowOptions.WeekDay))
{
    Console.WriteLine("myOptions包含WeekDay");
}
if (myOptions.HasFlag(ShowOptions.Holiday))
{
    Console.WriteLine("myOptions包含Holiday");
}

若要判斷是否必需同時包含Day與Night,則可以這樣寫:

 

myOptions = ShowOptions.Day | ShowOptions.Night;
if (myOptions.HasFlag(ShowOptions.Day | ShowOptions.Night))
{
    Console.WriteLine("myOptions包含Day與Night");
}

判斷是否包含A或B兩者其一,則可以這樣寫:

myOptions = ShowOptions.Day | ShowOptions.Night;
if (myOptions.HasFlag(ShowOptions.Day) || myOptions.HasFlag(ShowOptions.Night))
{
     Console.WriteLine("myOptions包含Day或Night");
}

 

若只允許接受特定列舉值,我們可以這樣寫;

myOptions = ShowOptions.Day | ShowOptions.Night;
ShowOptions allowOptions = ShowOptions.Holiday ;
Console.WriteLine((myOptions & allowOptions) != 0); //false
allowOptions = ShowOptions.Day;
Console.WriteLine((myOptions & allowOptions) != 0); //true

 

取得列舉中所有列舉值

若要取得列舉中所有列舉值,可以利用一段foreach迴圈來處理,參考以下程式碼:

foreach (var e in Enum.GetValues(typeof(ShowOptions)))
{
    Console.WriteLine($"{e} = {(int)e}");
}

執行後將印出以下資訊:

Day = 1
Night = 2
WeekDay = 4
Holiday = 16

 

加總列舉值

若使用者選取了Day、Night、WeekDay,可計算其加總:

var selectdValues = new[] { ShowOptions.Day, ShowOptions.Night, ShowOptions.WeekDay };
//等同於 var selectdValues = new[] { 1, 2, 4 };
var total = selectdValues.Aggregate(0, (current, v) => current | (int)v);
Console.WriteLine(total); //7

也可以使用LINQ,參考以下範例程式碼:

var total2 = selectdValues.Sum(x => (int)x);
Console.WriteLine(total2); //7

 

在MVC專案使用列舉與旗標

接下來讓我們說明在MVC專案使用列舉與旗標來設計多選的功能。假設目前有一個Movie模型描述電影的編號(MovieId)與電影名稱(Title),參考以下程式碼,其中的ShowOptions屬性的型別就是一個列舉旗標,描述影片撥放時段,可能是白天(Day)、晚上(Night)、平日(WeekDay)或假日(Holiday),可以有多選:

[Flags]
public enum ShowOptions
{
    Day = 1,
    Night = 2,
    WeekDay = 4,
    Holiday = 8
}

public class Movie {
  public int MovieId { get; set; }
  public string Title { get; set; }
  public ShowOptions ShowOptions { get; set; }
}

 

在專案中定義MovieContext類別如下,以便透過Entity Framework將資料寫到資料庫:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
using EnumFlagMovieDemo.Models;

namespace EnumFlagMovieDemo.DAL {
    public class MovieContext : DbContext {
        public DbSet<Movie> Movies { get; set; }
    }
}


然後使用Visual Studio工具Scaffold功能自動產生利用Entity Framework讀寫資料的控制器與檢視程式碼,請參考下圖所示,新增一個控制器,從範本中選取「MVC 5 Controller with views, using Entity Framework」:

clip_image014

圖 7

下一步設定模型為「Movie」;Data context class為「MovieContext」,控制器的名稱為「MoviesConttoller」,請參考下圖所示,然後按下「Add」按鈕產生程式碼:

clip_image016

圖 8

工具產生的Create檢視中的程式碼,預設採用文字方塊來顯示ShowOptions,這樣不易使用者進行資料的輸入。讓我們修改產生出來的Create檢視,加入以下<ul>標籤,利用四個Checkbox讓使用者輸入ShowOptions:

@model EnumFlagMovieDemo.Models.Movie

@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Create</title>
</head>
<body>
  @using (Html.BeginForm()) {
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
      <h4>Movie</h4>
      <hr />
      @Html.ValidationSummary(true, "", new { @class = "text-danger" })
      <div class="form-group">
        @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
          @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        @Html.LabelFor(model => model.ShowOptions, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          <ul>
            <li>
              <input type="checkbox" name="showOptions" value="Day" id="showOptions1" />
              <label for="showOptions1">Day</label>
            </li>
            <li>
              <input type="checkbox" name="showOptions" value="Night" id="showOptions2" />
              <label for="showOptions2">Night</label>
            </li>
            <li>
              <input type="checkbox" name="showOptions" value="Weekday" id="showOptions3" />
              <label for="showOptions3">Weekday</label>
            </li>
            <li>
              <input type="checkbox" name="showOptions" value="Holiday" id="showOptions4" />
              <label for="showOptions4">Holiday</label>
            </li>
          </ul>
          @Html.ValidationMessageFor(model => model.ShowOptions, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          <input type="submit" value="Create" class="btn btn-default" />
        </div>
      </div>
    </div>
  }

  <div>
    @Html.ActionLink("Back to List", "Index")
  </div>
</body>
</html>

因為MVC預設的模型繫結器(Model Binder)無法處理Flags類型的列舉值,因此我修改在MoviesController控制器標識HttpPost的Create方法,宣告一個ShowOptions[]型別的陣列參數來接資料,Create方法中計算出Enum Flag加總的值,填回Opera物件ShowOptions屬性,以更新到資料庫:

public ActionResult Create() {
  return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "MovieId,Title,ShowOptions")] Movie movie, ShowOptions[] showOptions) {
  if (ModelState.IsValid) {
    movie.ShowOptions = (ShowOptions)showOptions.Sum(i => (int)i);
    db.Movies.Add(movie);
    db.SaveChanges();
    return RedirectToAction("Index");
  }

  return View(movie);
}

 

執行Create檢視,在網頁中,便可以透過CheckBox來進行多選,請參考下圖所示:

clip_image018

圖 9

在檢視使用迴圈產生CheckBox

在檢視中寫死產生四個Checkbox程式碼的做法較不彈性,讓我改以Edit檢視來說明,以和Create檢視做個對照,修改Edit檢視利用程式根據列舉值的個數,在檢視使用迴圈來產生CheckBox,參考以下程式碼:

@model EnumFlagMovieDemo.Models.Movie
@using EnumFlagMovieDemo.Models
@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Edit</title>
</head>
<body>
  @using (Html.BeginForm()) {
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
      <h4>Movie</h4>
      <hr />
      @Html.ValidationSummary(true, "", new { @class = "text-danger" })
      @Html.HiddenFor(model => model.MovieId)

      <div class="form-group">
        @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
          @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        @Html.LabelFor(model => model.ShowOptions, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          <div>
            @foreach (ShowOptions ops in Enum.GetValues(typeof(ShowOptions))) {
              <input type="checkbox"
                     @(Model.ShowOptions.HasFlag(ops) ? "checked" : string.Empty)
                     name="showOptions" value="@ops" />
              @Html.Label("showOptions", ops.ToString())
            }
          </div>
          @Html.ValidationMessageFor(model => model.ShowOptions, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          <input type="submit" value="Save" class="btn btn-default" />
        </div>
      </div>
    </div>
  }

  <div>
    @Html.ActionLink("Back to List", "Index")
  </div>
</body>
</html>

 

同樣地,需要修改MoviesController控制器的程式碼,讓標識HttpPost的Edit方法,宣告一個ShowOptions[]型別的陣列參數來接資料,並在Edit方法中計算出Flag加總的值,填回Opera物件ShowOptions屬性,以更新到資料庫:

public ActionResult Edit(int? id) {
  if (id == null) {
    return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  }
  Movie movie = db.Movies.Find(id);
  if (movie == null) {
    return HttpNotFound();
  }
  return View(movie);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "MovieId,Title,ShowOptions")] Movie movie, ShowOptions[] showOptions) {
  if (ModelState.IsValid) {
   movie.ShowOptions = (ShowOptions)showOptions.Sum(i => (int)i);
    db.Entry(movie).State = EntityState.Modified;
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return View(movie);
}

 

執行後編輯畫面參考如下:

clip_image020

圖 10

若使用者執行階段選取Day與Night項目,foreach這段迴圈程式產生的標籤將會如下:

<div>
<input type="checkbox" checked name="showOptions" value="Day">
<label for="showOptions">Day</label>
<input type="checkbox" checked name="showOptions" value="Night">
<label for="showOptions">Night</label>
<input type="checkbox" name="showOptions" value="WeekDay">
<label for="showOptions">WeekDay</label>
<input type="checkbox" name="showOptions" value="Holiday">
<label for="showOptions">Holiday</label>
</div>

 

自訂模型繫結器

由於預設的模型繫結器不支援列舉旗標,因此較好的做法可以自訂模型繫結器(Model Binder)來處理繫結的問題,這樣就不必在控制器方法的參數額外宣告一個ShowOptions[]型別的參數來接列舉值。以編輯為例,修改標識HttpPost的Edit方法,回到工具產生的程式碼:

public ActionResult Edit(int? id) {
   if (id == null) {
     return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
   }
   Movie movie = db.Movies.Find(id);
   if (movie == null) {
     return HttpNotFound();
   }
   return View(movie);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "MovieId,Title,ShowOptions")] Movie movie) {
   if (ModelState.IsValid) {
     db.Entry(movie).State = EntityState.Modified;
     db.SaveChanges();
     return RedirectToAction("Index");
   }
   return View(movie);
}

 

在專案中加入一個MyFlagEnumModelBinder類別,繼承DefaultModelBinder,自訂模型繫結器,然後改寫BindModel()方法,在方法中從ValueProvider取得RawValue,它存放使用者選取的列舉項目,如Day、Night,然後利用Activator.CreateInstance動態建立列舉型別,再透過Enum.Parse將字串轉成列舉值回傳:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;

namespace EnumFlagMovieDemo {
  public class MyFlagEnumModelBinder : DefaultModelBinder {
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
      var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
      Type type = bindingContext.ModelType;
      if (value != null) {
        var rawValue = value.RawValue as string[];
        if (rawValue != null) {
          var result = (Enum)Activator.CreateInstance(type);
          try {
            result = (Enum)Enum.Parse(type, string.Join(",", rawValue));
            return result;
          } catch {
            return base.BindModel(controllerContext, bindingContext);
          }
        }
      }
      return base.BindModel(controllerContext, bindingContext);
    }
  }
}

 

最後在Global.asax註冊自訂的MyFlagEnumModelBinder型別:

using EnumFlagMovieDemo.DAL;
using EnumFlagMovieDemo.Models;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace EnumFlagMovieDemo {
  public class MvcApplication : System.Web.HttpApplication {
    protected void Application_Start() {
      AreaRegistration.RegisterAllAreas();
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      Database.SetInitializer(new MoviesInitializer());
      ModelBinders.Binders.Add(typeof(ShowOptions), new MyFlagEnumModelBinder());
    }
  }
}


執行測試將會得到和上一節一樣的結果。

 

自訂HTML Helper

最後,為了方便使用,讓我們將產生標籤的動作設計成HTML Helper,產生適用於套用Bootstrap樣式的標籤來顯示列舉資料。此外,預設CheckBox會在網頁中顯示列舉值,如Day、Night,但實際上想顯示在網頁中的文字可能不相同,我們可以一併用HTML Helper,搭配Display Attribute來處理這個問題。首先修改ShowOptions定義,讓每一個列舉值上方套用Display Attribute,設定想要呈現在檢視中的文字:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace EnumFlagMovieDemo.Models {

  [Flags]
  public enum ShowOptions {
    [Display(Name = "白天")]
    Day = 1,
    [Display(Name = "晚上")]
    Night = 2,
    [Display(Name = "平日")]
    WeekDay = 4,
    [Display(Name = "假日")]
    Holiday = 8
  }

  public class Movie {
    [Display(Name = "編號")]
    public int MovieId { get; set; }
    [Display(Name = "名稱")]
    public string Title { get; set; }
    [Display(Name = "撥放時段")]
    public ShowOptions ShowOptions { get; set; }
  }
}

 

在專案中加入一個MyFlagEnumHelper靜態類別,在類別中加入一個MyEnumCheckBox擴充方法,用來加入產生標籤的邏輯:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Web;
using System.Web.Mvc;

namespace EnumFlagMovieDemo {
  public static class MyFlagEnumHelper {
    public static IHtmlString MyEnumCheckBox<TModel, TValue>(this HtmlHelper<TModel> html,
      Expression<Func<TModel, TValue>> expression, object htmlAttributes = null) {

      var field = ExpressionHelper.GetExpressionText(expression);
      var fieldName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(field);
      var selectedValues = ModelMetadata.FromLambdaExpression(expression, html.ViewData).Model;

      IEnumerable<TValue> enumValues = Enum.GetValues(typeof(TValue)).Cast<TValue>();

      StringBuilder sb = new StringBuilder();
      foreach (var item in enumValues) {
        TagBuilder labelBuilder = new TagBuilder("label");
        if (htmlAttributes != null) {
          var attr = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
          labelBuilder.MergeAttributes(attr);
        }
        TagBuilder inputBuilder = new TagBuilder("input");
        long enumValue = Convert.ToInt64(item);
        long value = Convert.ToInt64(selectedValues);

        if ((value & enumValue) == enumValue) {
          inputBuilder.Attributes["checked"] = "checked";
        }
        inputBuilder.Attributes["type"] = "checkbox";
        inputBuilder.Attributes["value"] = item.ToString();
        inputBuilder.Attributes["name"] = fieldName;
        var attributes = (DisplayAttribute[])item.GetType().GetField(
            Convert.ToString(item)).GetCustomAttributes(typeof(DisplayAttribute), false);
        labelBuilder.InnerHtml = inputBuilder + (attributes.Length > 0 ? attributes[0].Name : Convert.ToString(item));
        sb.Append(labelBuilder.ToString());
      }
      return new MvcHtmlString(sb.ToString());
    }
  }
}

 

後續只要修改Edit檢視,叫用Html.MyEnumCheckBox來產生標籤:

@model EnumFlagMovieDemo.Models.Movie
@using EnumFlagMovieDemo.Models
@{
  Layout = null;
}

<!DOCTYPE html>

<html>
<head>
  <meta name="viewport" content="width=device-width" />
  <title>Edit</title>
</head>
<body>
  @using (Html.BeginForm()) {
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
      <h4>Movie</h4>
      <hr />
      @Html.ValidationSummary(true, "", new { @class = "text-danger" })
      @Html.HiddenFor(model => model.MovieId)

      <div class="form-group">
        @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
          @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        @Html.LabelFor(model => model.ShowOptions, htmlAttributes: new { @class = "control-label col-md-2" })
        <div class="col-md-10">
          <div>
            @Html.MyEnumCheckBox(model => model.ShowOptions,htmlAttributes: new { @class = "checkbox-inline" })
          </div>
          @Html.ValidationMessageFor(model => model.ShowOptions, "", new { @class = "text-danger" })
        </div>
      </div>

      <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          <input type="submit" value="Save" class="btn btn-default" />
        </div>
      </div>
    </div>
  }

  <div>
    @Html.ActionLink("Back to List", "Index")
  </div>
</body>
</html>

 

Edit檢視最後的執行畫面參考如下:

clip_image022

圖 11

產生出的HTML標籤適用於Bootstrap,參考以下程式碼:

<label class="checkbox-inline">
  <input name="ShowOptions" type="checkbox" value="Day">
  白天
</label>
<label class="checkbox-inline">
  <input checked="checked" name="ShowOptions" type="checkbox" value="Night">
  晚上
</label>
<label class="checkbox-inline">
  <input name="ShowOptions" type="checkbox" value="WeekDay">
  平日
</label>
<label class="checkbox-inline">
  <input checked="checked" name="ShowOptions" type="checkbox" value="Holiday">
  假日
</label>

Tags:

.NET Magazine國際中文電子雜誌 | ASP.NET MVC | C# | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List