0%

有感於自己是個廢物工程師,開了LeetCode帳號好幾個月一題都還沒刷,所以決定寫一系列紀錄自己刷LeetCode的過程跟解題方法,希望幾年後回頭能看見自己的進步,廢話不多說直接進入題目。

#題目

You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Example

Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Definition for singly-linked list.
* public class ListNode {
* public int val;
* public ListNode next;
* public ListNode(int x) { val = x; }
* }
*/
public class Solution {
public ListNode AddTwoNumbers(ListNode l1, ListNode l2) {

}
}

#解題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Solution
{
public ListNode AddTwoNumbers(ListNode l1, ListNode l2)
{
ListNode Result = new ListNode(0);

ListNode CN1 = l1;
ListNode CN2 = l2;

ListNode RCN = Result;
bool Flag = true;
while (Flag)
{
var Sum = RCN.val + GetNodeValue(CN1) + GetNodeValue(CN2);

RCN.val = Sum % 10;

if (Sum > 9)
RCN.next = new ListNode(1);

CN1 = CN1 == null ? null : CN1.next;
CN2 = CN2 == null ? null : CN2.next;


Flag = (CN1 != null || CN2 != null);

if (Flag && RCN.next == null)
RCN.next = new ListNode(0);

RCN = RCN.next;
}

return Result;
}

private int GetNodeValue(ListNode node)
{
return node == null ? 0 : node.val;
}
}

Unit Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
[TestClass]
public class SolutionTests
{
[TestMethod]
public void AddTwoNumbersTest_輸入L1為342_L2為456_應得到807()
{
//arrange
var l1 = new ListNode(2);
l1.next = new ListNode(4);
l1.next.next = new ListNode(3);

var l2 = new ListNode(5);
l2.next = new ListNode(6);
l2.next.next = new ListNode(4);

var sut = new Solution();

var expected = new ListNode(7);
expected.next = new ListNode(0);
expected.next.next = new ListNode(8);
//act
var actual = sut.AddTwoNumbers(l1,l2);

//assert
actual.Should().BeEquivalentTo(expected);
}

[TestMethod]
public void AddTwoNumbersTest_輸入L1為42_L2為708_應得到750()
{
//arrange
var l1 = new ListNode(2);
l1.next = new ListNode(4);

var l2 = new ListNode(8);
l2.next = new ListNode(0);
l2.next.next = new ListNode(7);

var sut = new Solution();

var expected = new ListNode(0);
expected.next = new ListNode(5);
expected.next.next = new ListNode(7);
//act
var actual = sut.AddTwoNumbers(l1, l2);

//assert
actual.Should().BeEquivalentTo(expected);
}

[TestMethod]
public void AddTwoNumbersTest_輸入L1為1_L2為99_應得到100()
{
//arrange
var l1 = new ListNode(1);

var l2 = new ListNode(9);
l2.next = new ListNode(9);

var sut = new Solution();

var expected = new ListNode(0);
expected.next = new ListNode(0);
expected.next.next = new ListNode(1);
//act
var actual = sut.AddTwoNumbers(l1, l2);

//assert
actual.Should().BeEquivalentTo(expected);
}
}

#分析結果

雖然過了但是效率不佳,執行完LeetCode全部的測試總共花費 176 ms只贏過38.84%的提交範例,所以試試看優化它

/images/20180615/leetcode/1.jpg

移除三元運算子

基本上三元運算子比較耗效能,所以優先改進它

原本

1
2
3
4
private int GetNodeValue(ListNode node)
{
return node == null ? 0 : node.val;
}

修改後

1
2
3
4
5
6
7
8
9
private int GetNodeValue(ListNode node)
{
if (node == null)
{
return 0;
}

return node.val;
}

原本

1
2
CN1 = CN1 == null ? null : CN1.next;
CN2 = CN2 == null ? null : CN2.next;

修改後

1
2
3
4
5
if (Node1Cursor != null)
Node1Cursor = Node1Cursor.next;

if (Node2Cursor != null)
Node2Cursor = Node2Cursor.next;

測試結果

/images/20180615/leetcode/2.jpg

簡直是三元運算子定生死….

優化可讀性

乾淨的程式碼,閱讀起來應該像是幾何證明般

Uncle Bob

所以試著讓變數命名更貼近意義一點

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Solution
{
public ListNode AddTwoNumbers(ListNode l1, ListNode l2)
{
ListNode Result = new ListNode(0);

ListNode Node1Cursor = l1;
ListNode Node2Cursor = l2;

ListNode Head = Result;
bool NeedNextRun = true;
while (NeedNextRun)
{
var Sum = Head.val + GetNodeValue(Node1Cursor) + GetNodeValue(Node2Cursor);

Head.val = Sum % 10;

if (Sum > 9)
Head.next = new ListNode(1);


if (Node1Cursor != null)
Node1Cursor = Node1Cursor.next;

if (Node2Cursor != null)
Node2Cursor = Node2Cursor.next;

NeedNextRun = (Node1Cursor != null || Node2Cursor != null);

if (NeedNextRun && Head.next == null)
Head.next = new ListNode(0);

Head = Head.next;
}

return Result;
}

private int GetNodeValue(ListNode node)
{
if (node == null)
{
return 0;
}

return node.val;
}
}

也因為有寫單元測試的關係,上述的調整過程中都沒讓程式壞掉,最後希望藉由這樣的練習能讓自己TDD與重構的技巧再更純熟一些

Demo範例 :Git位置

#什麼是DataAnnotations

DataAnnotations是.Net Framework 3.5之後提供的一個命名空間,裡面包含了一些基本的驗證Attribute,同時也提供客製化的方法,希望開發人員能透過簡單的加上Attribute即達到驗證的效果。

1
2
3
4
5
6
7
8
public class UserSignUpParameter
{
/// <summary>
/// 帳號
/// </summary>
[Required]
public string Account { get; set; }
}

#如何使用

我們透過修改先前的範例,嘗試將UserSignUpParameter的驗證從FluentValidation改成用MVC預設提供的DataAnnotations來達成,從修改中學習他是如何運作的。

重新審視一下需求

帳號

  • 必填
  • 必須包含@

密碼

  • 必填
  • 不得小於6個字元

RequiredAttribute

RequiredAttribute為DataAnnotations預設提供的驗證方式,目標是驗證該欄位是否為Null或Empty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//
// 摘要:
// 指定資料欄位值為必要。
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class RequiredAttribute : ValidationAttribute
{
//
// 摘要:
// 初始化 System.ComponentModel.DataAnnotations.RequiredAttribute 類別的新執行個體。
public RequiredAttribute();

//
// 摘要:
// 取得或設定值,指出是否允許空字串。
//
// 傳回:
// true 如果允許空字串。否則, false。 預設值是 false。
public bool AllowEmptyStrings { get; set; }

//
// 摘要:
// 檢查必要的資料欄位的值不是空的。
//
// 參數:
// value:
// 要驗證的資料欄位值。
//
// 傳回:
// true 如果驗證成功。否則, false。
//
// 例外狀況:
// T:System.ComponentModel.DataAnnotations.ValidationException:
// 資料欄位值為 null。
public override bool IsValid(object value);
}

從上述的程式碼可以看出它有提供AllowEmptyStrings的屬性可以設定,意義如同字面意思,可以EmptyString

AllowEmptyStrings

我們先將Account掛上Required,並且指定屬性AllowEmptyStrings為True,執行看看

繼承自Attribute的類別,在掛Attribute時可以省略後綴詞,所以可以看到程式碼只寫Required而不是RequiredAttribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// Parameter UserSignUp
/// </summary>
public class UserSignUpParameter
{
/// <summary>
/// 帳號
/// </summary>
[Required(AllowEmptyStrings =true)]
public string Account { get; set; }

/// <summary>
/// 密碼
/// </summary>
public string Password { get; set; }
}

當Account傳入為Null時

/images/20180615/1.png

ModelState.IsValidate為False,在Values.Errors底下可以找到錯誤訊息Account 欄位是必要項。

/images/20180615/2.png

接著改傳入空格

/images/20180615/3.png

通過驗證

/images/20180615/4.png

ErrorMessage

如果想修改錯誤的回傳訊息時,可以透過ErrorMessage來處理

1
2
3
4
5
/// <summary>
/// 帳號
/// </summary>
[Required(ErrorMessage = "帳號為必填欄位")]
public string Account { get; set; }

或是

1
2
3
4
5
/// <summary>
/// 帳號
/// </summary>
[Required(ErrorMessage = "{0}為必填欄位")]
public string Account { get; set; }

{0}的位置系統會自動帶入欄位名稱

/images/20180615/5.png

介紹完RequiredAttribute的基本用法後,目前程式碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Parameter UserSignUp
/// </summary>
public class UserSignUpParameter
{
/// <summary>
/// 帳號
/// </summary>
[Required(ErrorMessage = "{0}為必填欄位")]
public string Account { get; set; }

/// <summary>
/// 密碼.
/// </summary>
[Required(ErrorMessage = "{0}為必填欄位")]
public string Password { get; set; }
}

MinLengthAttribute

MinLengthAttribute是用來檢查屬性的最小字數,與MaxLengthAttribute為剛好相反的一組

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//
// 摘要:
// 指定屬性中所允許之陣列或字串資料的最大長度。
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class MaxLengthAttribute : ValidationAttribute
{
//
// 摘要:
// 初始化 System.ComponentModel.DataAnnotations.MaxLengthAttribute 類別的新執行個體。
public MaxLengthAttribute();
//
// 摘要:
// 初始化的新執行個體 System.ComponentModel.DataAnnotations.MaxLengthAttribute 類別根據 length
// 參數。
//
// 參數:
// length:
// 陣列或字串資料的最大容許長度。
public MaxLengthAttribute(int length);

//
// 摘要:
// 取得陣列或字串資料所容許的最大長度。
//
// 傳回:
// 陣列或字串資料的最大容許長度。
public int Length { get; }

//
// 摘要:
// 將格式套用到指定的錯誤訊息
//
// 參數:
// name:
// 要包含在格式化字串中的名稱。
//
// 傳回:
// 描述可接受之最大長度的當地語系化字串。
public override string FormatErrorMessage(string name);
//
// 摘要:
// 判斷指定的物件是否有效
//
// 參數:
// value:
// 要驗證的物件。
//
// 傳回:
// 如果此值為 null 或是小於或等於指定的最大長度,則為 true,否則為 false。
//
// 例外狀況:
// T:System.InvalidOperationException:
// 長度為零或小於 –1。
public override bool IsValid(object value);
}

密碼最少應為6個字元設定方式,當然它同時也提供修改ErrorMessage的方法

{0}的位置系統會自動帶入欄位名稱

{1}的位置系統會帶入所設定的最小字元數字

1
2
3
4
5
/// <summary>
/// 密碼.
/// </summary>
[MinLength(6, ErrorMessage = "{0}不得低於{1}個字元")]
public string Password { get; set; }

/images/20180615/6.png

/images/20180615/7.png

RegularExpression

Account還有一個需求是必須包含@字元,這時候可以透過預設提供的RegularExpression來達成需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//
// 摘要:
// 指定 ASP.NET 動態資料的資料欄位值必須符合指定的規則運算式。
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class RegularExpressionAttribute : ValidationAttribute
{
//
// 摘要:
// 初始化 System.ComponentModel.DataAnnotations.RegularExpressionAttribute 類別的新執行個體。
//
// 參數:
// pattern:
// 用來驗證資料欄位值的規則運算式。
//
// 例外狀況:
// T:System.ArgumentNullException:
// pattern 為 null。
public RegularExpressionAttribute(string pattern);

//
// 摘要:
// 取得規則運算式模式。
//
// 傳回:
// 要比對模式。
public string Pattern { get; }
//
// 摘要:
// 取得或設定執行單一比對作業,直到作業逾時之前的時間 (以毫秒為單位)。
//
// 傳回:
// 執行單一比對作業的時間 (以毫秒為單位)。
public int MatchTimeoutInMilliseconds { get; set; }

//
// 摘要:
// 格式化要顯示在規則運算式驗證失敗時的錯誤訊息。
//
// 參數:
// name:
// 造成驗證失敗的欄位名稱。
//
// 傳回:
// 格式化的錯誤訊息。
public override string FormatErrorMessage(string name);
//
// 摘要:
// 會檢查使用者輸入的值是否符合規則運算式模式。
//
// 參數:
// value:
// 要驗證的資料欄位值。
//
// 傳回:
// true 如果驗證成功。否則, false。
//
// 例外狀況:
// T:System.ComponentModel.DataAnnotations.ValidationException:
// 資料欄位值不符合規則運算式模式。
public override bool IsValid(object value);
}

可以透過寫正規表示法的方式來驗證想驗證的屬性,一樣有ErrorMessage可供使用

1
2
3
4
5
/// <summary>
/// 帳號
/// </summary>
[RegularExpression("[@]+",ErrorMessage ="{0}必須包含@字元")]
public string Account { get; set; }

/images/20180615/8.png

/images/20180615/9.png

其它Attribute

DataAnnotations還有提供一些預設的Attribute可以使用,篇幅有限就不一一介紹,有興趣的可以參考以下文章

#客製

如果碰到一個驗證需要在多個地方使用,且預設沒有提供的話,這時候就可以透過自製驗證Attribute來解決,我們以**字元需包含@**為例

繼承ValidationAttribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MouseCharactersAttribute: ValidationAttribute
{
/// <summary>
/// Returns true if ... is valid.
/// </summary>
/// <param name="value">要驗證之物件的值。</param>
/// <returns>
/// <see langword="true" /> 指定的值是否有效。否則, <see langword="false" />
/// </returns>
public override bool IsValid(object value)
{
return base.IsValid(value);
}
}

Override IsValid Method

1
2
3
4
5
6
7
8
9
10
11
public override bool IsValid(object value)
{
if (value == null)
{
return false;
}

var stringValue = value as string;

return stringValue.Contains("@");
}

將原本的RegularExpression改成我們客製的MouseCharacters

1
2
3
4
5
/// <summary>
/// 帳號
/// </summary>
[MouseCharacters(ErrorMessage ="{0}必須包含@字元")]
public string Account { get; set; }

實測會發現結果是一樣的

/images/20180615/10.png

#比較FluentValidation

DataAnnotations其實使用起來相當方便,且只要知道如何客製ValidationAttribute基本上大部分情境都能解決,加上套用Attribute的方式算是相當親民的寫法。

但我本身基於單一職責的原則下,還是喜歡將驗證邏輯等方法分離到不同的Class管理,而這也正是FluentValidation套件的特性,不過這是見仁見智,就像大部分Pattern一樣,永遠不會有最好的解法,只要能花最小的成本解決需求,同時盡量不犧牲維護及擴充性,那就稱得上是好方法了。

何謂Counting Sort

是一種排序的演算法,特色是不需要比較數字間的大小,而是透過計算在Array中的Index的位置來達到排序的效果,限制是必須先知道數字的範圍以及數字組的個數,屬於線性排序 O(n) 。

案例一

Suppose you have an array of 1000 integers. The integers are in random order, but you know each of the integers is between 1 and 5000 (inclusive). In addition, each number appears only once in the array. Assume that you can access each element of the array only once. Describe an algorithm to sort it.

假設你有個數字陣列其中包含1000個數字,且這些數字來自1~5000之間不重複,請用一組演算法來達成只讀取數字一次並排序完成。

解題

欲排序的陣列

1
2
3
4
//隨機取1000個
int Amount = 1000;
//隨機從1~5000中取出不重複的1000個數字
int[] IntPool = Enumerable.Range(1, 5000).OrderBy(x => Guid.NewGuid()).Take(Amount).ToArray();

準備一個的數字陣列,空間為5000

1
int[] IndexArray = new int[5000];

計算哪些位置應該存在數字

1
2
3
4
foreach (var element in IntPool)
{
IndexArray[ element -1 ] ++;
}

將IndexArray中值為1的挑出來

1
2
3
4
5
6
7
8
9
10
11
12
int[] Result = new int[1000];
var Index = 0;
for (int i = 0; i < IndexArray.Length; i++)
{
if (IndexArray[i] > 0)
{
Result[Index] = i + 1;
Index++;
}
}

//此時Result即為排序後的結果

案例二

假設你有個數字陣列其中包含1000個數字,這些數字來自1~5000之間且可能會重複,如何用Counting Sort達成排序

解題

欲排序的陣列

1
2
3
4
5
6
Random Rnd = new Random();
int[] IntPool =new int[1000];
for (int i = 0; i < 1000; i++)
{
IntPool[i] = Rnd.Next(1,5001);
}

找出最大值與最小值

1
2
3
4
5
6
7
8
9
10
int Min = IntPool[0];
int Max = IntPool[0];
for (int i = 1; i < IntPool.Length; i++)
{
if (IntPool[i] > Max)
Max = IntPool[i];

if (IntPool[i] < Min)
Min = IntPool[i];
}

準備一個能容納最大範圍的IndexArray

1
int[] IndexArray = new int[Max - Min +1];

計算每個數字在IndexArray中的相對位置,並統計每個位置重複的數字數

1
2
3
4
5
foreach (var element in IntPool)
{
//統計每個位置有幾個重複的數字
IndexArray[element - Min] ++;
}

計算每個位置累積的數字個數
這邊是我想比較久的地方,所以畫了一個圖希望能幫助理解,假設我們算出來後陣列長這樣,表示Index 0放著2個數字,Index 2有1個數字,Index 4有1個數字以此類推

所以如果我們將每個位置的數字與前一格相加,可以得到該Index實際上已經排了幾個數字,

計算到達該位子時,實際上總共已經有幾個數字

1
2
3
4
for (int i = 1; i < IndexArray.Length; i++)
{
IndexArray[i] = IndexArray[i] + IndexArray[i - 1];
}

排序

1
2
3
4
5
6
7
8
9
10
int[] Result = new int[1000];
foreach (var element in IntPool)
{
var TotalCount = IndexArray[element - Min];
//第幾個數字-1,其實就表示他要放在Result的Index位置
Result[TotalCount- 1] = element;
IndexArray[element - Min] --;
}

//此時Result即為排序後的結果

Demo範例 :Git位置

上一篇簡介了Controller如何安排程式邏輯流程、驗證參數,卻也發現了驗證參數導致程式寫得冗長不易維護,這一篇要介紹【FluentValidation】這個套件來解決這個問題。

安裝



設定

把之前的程式碼稍作整理,將Account與Password新建一個類型(Class)來封裝起來,原本Controller裡面的程式整理一下

自定義驗證Class

建立一個Class命名為UserSignUpParameterValidation

將UserSignUpParameter掛上Validator Attribute,意思是UserSignUpParameter請幫我用UserSignUpParameterValidation裡面的邏輯來驗證

帳號密碼不能為空值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/// <summary>
/// Validation UserSignUpParameter
/// </summary>
/// <seealso cref="FluentValidation.AbstractValidator{HelloDotNetMVC.Parameters.UserSignUpParameter}" />
public class UserSignUpParameterValidation:AbstractValidator<UserSignUpParameter>
{
public UserSignUpParameterValidation()
{
//AbstractValidator為FluentValidation提供的抽象類別
//目的是讓使用者可以透過繼承這個抽象類別後,實作自己的驗證邏輯
//而泛型T, AbstractValidator<T> ,則帶入你想驗證的類別
//所以這邊帶入UserSignUpParameter,這個我們剛剛製作的Class

//Account
RuleFor(x => x.Account)
.NotEmpty()
.WithMessage("帳號不能為Empty")
.NotNull()
.WithMessage("帳號不能為Null");

//Password
RuleFor(x=>x.Password)
.NotEmpty()
.WithMessage("密碼不能為Empty")
.NotNull()
.WithMessage("密碼不能為Null");
}
}

RuleFor是FluentValidation提供的方法,意思是我為Account這個屬性建立驗證的Rule,而Rule分別是,NotEmpty(不能為空值)、NotNull(不能為Null),而緊接在每個驗證邏輯下面的WithMessage則是表示,當發生它上面的錯誤時,錯誤訊息請回傳這行。

接著我們可以將Controller的這塊驗證邏輯拿掉,交給UserSignUpParameterValidation專責處理就好。

帳號必須包含@字元,Password必須大於6個字

因為判斷字元@並不像判斷空值與Null一樣,官方有實做好的方法,必須自己透過Must來定義,這邊提供兩種寫法給讀者參考

上面是匿名方法的寫法,但如果你對於匿名委派還不是很熟悉,也可以用比較簡單的方式寫

[![](https://4.bp.blogspot.com/-KmJSyZdiGb0/WvAWiccBedI/AAAAAAAAIvw/17gYP6xPzgIUFII1ptocXolNHkBXfGeegCLcBGAs/s400/11.png)](https://4.bp.blogspot.com/-KmJSyZdiGb0/WvAWiccBedI/AAAAAAAAIvw/17gYP6xPzgIUFII1ptocXolNHkBXfGeegCLcBGAs/s1600/11.png)
先寫好驗證的方法,然後在Must中帶入方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// Validation UserSignUpParameter
/// </summary>
/// <seealso cref="FluentValidation.AbstractValidator{HelloDotNetMVC.Parameters.UserSignUpParameter}" />
public class UserSignUpParameterValidation:AbstractValidator<UserSignUpParameter>
{
public UserSignUpParameterValidation()
{
//AbstractValidator為FluentValidation提供的抽象類別
//目的是讓使用者可以透過繼承這個抽象類別後,實作自己的驗證邏輯
//而泛型T, AbstractValidator<T> ,則帶入你想驗證的類別
//所以這邊帶入UserSignUpParameter,這個我們剛剛製作的Class

//Account
RuleFor(x => x.Account)
.NotEmpty()
.WithMessage("帳號不能為Empty")
.NotNull()
.WithMessage("帳號不能為Null")
.Must(IncludeAccountKeyword)
.WithMessage("帳號不符合格式");

//Password
RuleFor(x=>x.Password)
.NotEmpty()
.WithMessage("密碼不能為Empty")
.NotNull()
.WithMessage("密碼不能為Null")
.MinimumLength(7)
.WithMessage("密碼必須大於6個字元");
}

/// <summary>
/// Accounts 必須包含@.
/// </summary>
/// <param name="account">The account.</param>
/// <returns></returns>
private bool IncludeAccountKeyword(string account)
{
if (!string.IsNullOrWhiteSpace(account))
{
return account.Contains("@");
}

return false;
}
}

整理後的Controller

透過ModelState得知驗證結果

ModelState是本來MVC就有提供的參數驗證方式,而使用方法這邊提供幾篇文章給有興趣的讀者參考
What is the ModelState? - ASP.NET MVC Demystified
保哥 : ASP.NET MVC 開發心得分享 (28):深入了解 ModelState 內部細節

而因為開發習慣的關係,我習慣用FluentValidation的方式來取代原本ModelState驗證方法與設定方式,接著我們在Global.asax中呼叫FluentValidationModelValidatorProvider.Configure()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[HttpPost]
public ActionResult SignUp(UserSignUpParameter parameter)
{
if (!ModelState.IsValid)
{
//IsValida為False時,表示驗證參數不過
//取出第一筆錯誤訊息回傳
var Error = ModelState.Values.SelectMany(x => x.Errors).First();
TempData["Message"] = Error.ErrorMessage;
return RedirectToAction("signup", "user");
}

//這邊沒有搬到FluentValidation去驗證的原因是
//使用者存在與否比較屬於服務層的事情,通常需要讀取資料庫才能判斷
//開發上習慣盡量讓驗證參數越乾淨簡單越好,而不是在裡面呼叫很多外部服務(例如資料庫)後做驗證
//這會讓職責過於複雜
if (parameter.Account != "steven@mymail.com")
{
TempData["Message"] = "註冊成功!!";
//註冊成功,導到首頁
return RedirectToAction("index", "home");

}
else
{
TempData["Message"] = "帳號已經存在";
return RedirectToAction("signup", "user");
}
}

接著執行程式,驗證看看是否之前的驗證邏輯都還是正常運作。


比較一下最一開始與套用FluentValidation後的程式碼,應該可以明顯感受到差異,這不僅僅只是讓程式變乾淨,更重要的是驗證參數的職責被分離到UserSignUpParameterValidation這個類別中,以後要改參數驗證的邏輯只要到這調整即可,不用再擔心Controller會被不小心改壞。

延伸閱讀

FluentValidation : 官方文件、各種API使用方法
FluentValidation : 擴充驗證方法
FluentValidation : 繼承父類別的驗證

Demo範例 : Git位置

前面幾篇解說了Route設定,讓我們的程式能順利找到對應的Controller來執行,那今天要來談談Controller幫我們做些什麼?
我們通常會在Controller這層就將使用者的參數驗證完畢,並且依據傳入的參數,找到對應的商業邏輯去執行,並且回傳結果給使用者知道,已註冊頁面作為實際應用來解說。

接著我們來實作上述的內容,但因為只是MVC初學,所以我們還不加入分層、資料庫溝通之類比較深的內容,只專注在Controller怎麼做。

 

建立Controller

 
我們先建立一個UserController,專門處理User相關的服務跟邏輯

接著寫一個SignUp的Action,這邊有看到我們掛HttpGet的Attribute,只是指定呼叫動詞必須是Get的方式才會執行到這個Action

如果不知道Http動詞的話可以參考 : HTTP請求方法

[![](https://1.bp.blogspot.com/-KA_M1a5AopU/WuCDeRZvUuI/AAAAAAAAIrI/C66BwZf0wvw7Z2PDngjof-IgAzSwUE3bwCEwYBhgL/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-25%2B%25E4%25B8%258B%25E5%258D%25889.32.27.png)](https://1.bp.blogspot.com/-KA_M1a5AopU/WuCDeRZvUuI/AAAAAAAAIrI/C66BwZf0wvw7Z2PDngjof-IgAzSwUE3bwCEwYBhgL/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-25%2B%25E4%25B8%258B%25E5%258D%25889.32.27.png)

 

建立View

 
而這個Action沒有任何內容,只有回傳一個View而已,接著我們來實作出回傳的頁面,在這個Action中點擊右鍵 > 新增檢視




接著直接執行,應該就能看到我們剛剛做出來的SignUp頁面


而這邊網址是 /user/signup,我覺得不夠直覺,所以到RouteConfig改一下

接著把頁面簡單做起來



寫一個Action來接收傳過來的資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
[HttpPost]
public ActionResult SignUp(string account,string password)
{
bool Result = false;
//帳號密碼都不能為空值
if (!string.IsNullOrWhiteSpace(account) &amp;&amp;
!string.IsNullOrWhiteSpace(password))
{
//帳號必須要有@字元
//密碼必須大於六個字元
if (account.Contains("@") &amp;&amp; password.Length &gt; 6)
{
//我們判斷是否有註冊過的帳號,因為還沒有連結資料庫
//所以先假定steven@mymail.com被註冊過
if (account != "steven@mymail.com")
{
TempData["Message"] = "註冊成功!!";
Result = true;
}
else
{
TempData["Message"] = "帳號已經存在";
}
}
else
{
TempData["Message"] = "帳號密碼不符合格式";
}
}
else
{
TempData["Message"] = "帳號密碼不能為空值";
}

if (Result)
{
//註冊成功,導到首頁
return RedirectToAction("index", "home");
}
else
{
return RedirectToAction("signup", "user");
}
}

這邊注意到我們的Attribute就下了HttpPost,表示只有Post可以呼叫到這個Action,而我們在Form那邊也設定了方法用Post


接著你可能會注意到裡面有用到TempData,通常TempData是用來跨Action傳遞資料用的,底層其實是將資料存在Session之中,而且你只要取用過一次裡面的值就會被清掉。 而因為我們這個Action只是在處理註冊的相關邏輯,執行完後可以看到最後回傳的都是RedirectToAction,也就是導到我們指定的Action去回傳頁面,所以會跨兩個Action以上,存在TempData是個簡單的處理方式。

接著執行看看你會發現,好像一切有照著我們的邏輯在執行,但唯獨訊息不會顯示出來,因為我們雖然將TempData之中,卻沒有寫顯示訊息的那一段,通常這一段邏輯我們會放在共用的_Layout裡面。


因為還沒解說View的關係,這邊就先照著寫,之後會解說到。再次執行就會發現訊息會正確S顯示出來了。


上述的Controller撰寫方式,就是通常我們在Controller做的工作,驗證參數 、 依據使用者輸入的值執行對應的邏輯 、最後回傳結果。但你應該也會發現,整個Action邏輯裡面光是驗證參數就佔了大半的篇幅,這往往會讓程式碼複雜度提高,閱讀變得困難,這部分我們會在下一篇講解該如何把這類的邏輯分隔出去,讓程式碼更好維護美觀一些。另外Action其實還可以回傳很多種結果,前面範例用到了

View : 回傳頁面
RedirectToAction :回傳導頁結果

底層還支援了一些回傳方式,靈活應用就可以達成大部分的功能了。詳細參考: MSDN

學生時期學資料結構跟演算法時,每次看到厚厚的課本加上一堆用C語言寫的範例,雖然都有修過,但說真的不知道它可以拿來做什麼? 直到出了社會開始寫一些專案要調教效能時,才發現原來以前學的是這麼厲害的東西阿。

開始前想先推薦一下這本書【**演算法圖鑑:26種演算法 + 7種資料結構,人工智慧、數據分析、邏輯思考的原理和應用全圖解**】,作者用簡單的圖解方式帶領讀者瞭解艱澀的資料結構與演算法的歷程,雖然要實際應用在專案中還需要一些內化,但已經比我以前的課本好多了(拭淚),對這方面有興趣的非常推薦買這本書來看看


圖片出處: http://www.books.com.tw/products/0010771263

案例

公司有派送Coupon券的需求,而條件是該券不能與過往中的任何一張重複,所以在產生Coupon券代碼後,最好跟以前的做一下比對來確保沒有發生重複的情形,而Coupon券為英數字12個字元格式,區分大小寫,比對方法想過以下幾種方式

1. 寫入時,用SQL語法的方式要求DB先搜尋確定沒有再寫

  • 優點 : 寫法簡單
  • 缺點 : 耗掉DB效能,當要寫入的筆數一多時,DB會出現效能瓶頸

2. 將全部的Coupon券撈出來後,進行比對,確定沒有再進行寫入

  • 優點 : 因為是將資料撈出DB再從Application端做比對,所以對DB負擔較小
  • 缺點 : 進行字串比對時該如何有效率執行是個問題,尤其是字串比對,如果處理方式不佳,也一樣會在Application端產生效能瓶頸

這邊我採取方式2並搭配雜湊表來解決,而為何要用雜湊表以下慢慢說明
產生Coupon券的程式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// <summary>
/// 隨機產生Couopn
/// </summary>
/// <param name="number">幾位數</param>
/// <returns></returns>
public string CreateNewCode(int number)
{
string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
string[] allCharArray = allChar.Split(',');
string randomCode = string.Empty;

Random rand = new Random();
int temp = -1;
for (int i = 0; i < number; i++)
{
if (temp != -1)
rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

int t = rand.Next(62);
if (temp != -1 && temp == t)
return CreateNewCode(number);

temp = t;
randomCode += allCharArray[t];
}
return randomCode;
}

執行方式

List

如果你是寫C#的,那要比對是否有一樣的東西存在List中最快的方式就是用Any()這個方法,而我們知道List儲存方式實際是這樣

記憶體位置放置資料內容,而每個節點會記錄下一筆資料的記憶體位置在哪,所以List裡面的資料未必是一個相連的記憶體區段,但它只要知道開頭那筆資料,就可以依序將資料逐筆讀取出來。

換言之,如果要搜尋一個列表中是否有相同的資料存在,必須用線性的方式搜尋,也就是逐筆檢查,從第一筆開始每筆拿出來看看,直到比對到為止,最佳的狀況是第一筆就是你要比對的資料,最差,就是最後一筆才是你要的資料,而且搜尋的成本會隨著資料的增長而遞增。

1萬筆資料搜尋效能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
int 產生的資料筆數 = 10000;

void Main()
{
//準備要用來搜尋的資料
var Pools = CreateSearchPool();

//透過Stopwatch來看看實際搜尋要花費的時間
Stopwatch sw = new System.Diagnostics.Stopwatch();

Random rnd = new Random();

//總共花費的時間
double TotalTime = 0;

//搜尋一百次
for (int i = 0; i < 100; i++)
{
//動態從產生的資料母體中抽一筆作為我們要搜尋的目標
var RandomIndex = rnd.Next(0, 產生的資料筆數 -1);

//取出要搜尋的字
var Target = Pools[RandomIndex];

//碼表歸零
sw.Reset();
//碼表開始計時
sw.Start();

//透過Any方式對List做搜尋
var Result = Pools.Any(x=> x == Target);

//搜尋結束,碼錶停止
sw.Stop();

//將時間加上這次搜尋花費的時間,為毫秒
TotalTime += sw.Elapsed.TotalMilliseconds;
}

//算出平均每次搜尋,耗費的秒數
(TotalTime /100).Dump();
}

//建立要搜尋的母體
private List<string> CreateSearchPool()
{
List<string> Pool = new List<string>();
for (int i = 0; i < 產生的資料筆數; i++)
{
//動態新增Coupon資料
var Code = CreateNewCode(12);
//丟進我們要用來搜尋Pool
Pool.Add(Code);
}

return Pool;
}

// 隨機產生Couopn券代碼
public string CreateNewCode(int number)
{
string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
string[] allCharArray = allChar.Split(',');
string randomCode = string.Empty;

Random rand = new Random();
int temp = -1;
for (int i = 0; i < number; i++)
{
if (temp != -1)
rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

int t = rand.Next(62);
if (temp != -1 && temp == t)
return CreateNewCode(number);

temp = t;
randomCode += allCharArray[t];
}
return randomCode;
}

實際算出來的平均搜尋時間為 0.083978(毫秒)

100萬筆資料搜尋效能

將搜尋母體放大100倍,也就是從100萬筆資料中隨機抽樣搜尋100次,結果為8.540132(毫秒)

可以觀察到搜尋效率隨著資料量的增長快速遞減,而一百萬筆對於Coupon券來說其實不多,如果你的會員數有一萬人,每個人發到第100張時,總共發出去的數量就達到這個等級了 ,相信很多店商平台所產出的Coupon數遠遠大於這個量。

雜湊表

雜湊表與List的最大差異是它非線性搜尋,它將所有要放入的資料先進行雜湊的方式算出一個值後,依據算出來的值放到對應的記憶體位置去,搜尋時也是先將要搜尋的值進行雜湊運算,算出對應位置,直接取出該記憶體的資料進行比對

特點是它非線性搜尋,也就是說它不需要抓到第一筆資料後,依序依據指標,往下找下一筆資料,即便不是還是要每筆遍尋才能知道結果,雜湊表的好處就在於,你要搜尋時,就已經知道該去哪找了。

而可能會有一個問題,那如果經過運算後,兩筆資料要儲存的地方一樣呢?這時候就是發生所謂的碰撞,一張好的雜湊表理論上要盡量避免碰撞發生,但現實中難以避免,所以進階的用法就是將相同位置內再放入List來存入更多筆資料。

這邊可能會有一個疑問是,那跟我一開始用List有什麼差別 ?

如果我們相信資料是平均分佈,那雜湊結果理論上也會平均分佈,但就如前面提的,現實中實在難以避免碰撞的發生,所以即便真的發生碰撞,我們也能確定List中的資料絕對不會有很多筆,多到導致效能瓶頸的發生。所以雜湊表的陣列該開出幾格來就是需要經過考量的,如果你有數百萬筆的資料,只開出100格,那最平均的結果就是每一格裡面會有1萬筆的資料,這顯然不理想。

1萬筆資料搜尋效能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
int 產生的資料筆數 = 10000;
int 雜湊表格數 = 1000;
void Main()
{
//準備要用來搜尋的資料
var Pools = CreateSearchPool();

//透過Stopwatch來看看實際搜尋要花費的時間
Stopwatch sw = new System.Diagnostics.Stopwatch();//引用stopwatch物件

Random rnd = new Random();

//總共花費的時間
double TotalTime = 0;

//搜尋一百次
for (int i = 0; i < 100; i++)
{
//動態從產生的資料母體中抽一筆作為我們要搜尋的目標
var RandomIndex = rnd.Next(0, 產生的資料筆數 - 1);

//取出要搜尋的字
var Target = AllCode[RandomIndex];

//碼表歸零
sw.Reset();
//碼表開始計時
sw.Start();

//取得Hash後應該存放的位置
var HashPosition = GetHashPosition(Target);

//從陣列中取出該筆資料
var PositionData = Pools[HashPosition];

//如果有資料
if (PositionData != null)
{
//檢查這個List是否存在相同的Coupon代碼
var Result = PositionData.Any(x => x == Target);
}
//碼錶停止
sw.Stop();

//將時間加上這次搜尋花費的時間,為毫秒
TotalTime += sw.Elapsed.TotalMilliseconds;
}

//算出平均每次搜尋,耗費的秒數
(TotalTime / 100).Dump();
}

List<string> AllCode =new List<string>();
private List<string>[] CreateSearchPool()
{
List<string>[] SearchPool = new List<string>[雜湊表格數];
for (int i = 0; i < 產生的資料筆數; i++)
{
var Code = CreateNewCode(12);
AllCode.Add(Code);

var p = GetHashPosition(Code);
var PositionData = SearchPool[p];
if (PositionData == null)
{
PositionData = new List<string>();
SearchPool[p] = PositionData;
}

PositionData.Add(Code);
}

return SearchPool;
}

/// <summary>
/// 隨機產生Couopn
/// </summary>
/// <param name="number">幾位數</param>
/// <returns></returns>
public string CreateNewCode(int number)
{
string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
string[] allCharArray = allChar.Split(',');
string randomCode = string.Empty;

Random rand = new Random();
int temp = -1;
for (int i = 0; i < number; i++)
{
if (temp != -1)
rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

int t = rand.Next(62);
if (temp != -1 && temp == t)
return CreateNewCode(number);

temp = t;
randomCode += allCharArray[t];
}
return randomCode;
}

SHA256 sha256 = new SHA256CryptoServiceProvider();
//取得Hash後應該存放的位置
private int GetHashPosition(string code)
{
var ByteArray = sha256.ComputeHash(Encoding.Default.GetBytes(code));
var IntResult = BitConverter.ToInt32(ByteArray, 0);

//轉正
IntResult = Math.Abs(IntResult);
//除格子數,餘數就是這筆資料該放的位置
return IntResult % 雜湊表格數;
}

實際算出來的平均搜尋時間為 0.004124(毫秒)

100萬筆資料搜尋效能

一樣將數字放大到100萬筆,實際算出來的平均搜尋時間為 0.033612(毫秒),可以發現即便搜尋筆數擴張了100倍,效率並沒有完全等比遞減

當然各種資料型態跟搜尋狀況不同,可能適用的資料結構與演算法也會略有不同要取捨,雜湊法也並非沒有缺點,例如在製作表時比較耗時,所以適合用在資料變動不大的情境,先將表做起來後放到快取去更新維護都是一些優化的方法,以上提供給大家參考。

上一篇寫了Route比對的邏輯,這次來點更進階的應用,讓我們幫Route比對加上一些些限制。

假設今天About的頁面,他是依據網址帶入的 ID取得對應的會員資料,回傳結果,而ID必定為數字,如果不是數字就不要進到程式碼,直接擋掉該如何做?

1
2
3
//我們希望的網址,最後的ID一定要為數字
/home/about/1

加上Constraints限制,而限制的方法用正規表示法來表達,以上述的只能為數字為例

[![](https://1.bp.blogspot.com/-DDzFIwOxsvQ/WsTtsAb1bgI/AAAAAAAAInA/pJdFwqPGRdIZ6LKQ-EgiXc2xem8FcQ4QACLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.22.03.png)](https://1.bp.blogspot.com/-DDzFIwOxsvQ/WsTtsAb1bgI/AAAAAAAAInA/pJdFwqPGRdIZ6LKQ-EgiXc2xem8FcQ4QACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.22.03.png)
constraints的部分,我們把ID限制在只能出現數字,(如果對於正規表示法不熟悉的話,推薦可以翻翻這本書,就算記不起來拿來當工具書也很實用 [處理大數據的必備美工刀 - 全支援中文的正規表示法精解](https://www.tenlong.com.tw/products/9789863759539))

接著執行網站試試看 /home/about/123

[![](https://2.bp.blogspot.com/-5v8kKEHK7oQ/WsTve5DgK7I/AAAAAAAAInM/qCpoatJcmjED_wL1aP5zPUEnHuiVDzA1QCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.29.45.png)](https://2.bp.blogspot.com/-5v8kKEHK7oQ/WsTve5DgK7I/AAAAAAAAInM/qCpoatJcmjED_wL1aP5zPUEnHuiVDzA1QCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.29.45.png)
看起來沒問題,接著我們執行/home/about/Steven,這邊請記得把Default那組Route註解起來,如果還記得上篇Route比對方法的話,這組雖然會因為Steven不是數字而被About那組Route擋掉,但依然符合Default的萬用Route比對規則,而正確執行,為了測試請先註解掉Default那組。
[![](https://3.bp.blogspot.com/-97qSMhzIPOE/WsTwIiBOlMI/AAAAAAAAInU/Xb90Cy9gzJYsGtOuzXkPSL6bWfQszpr1ACLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.32.39.png)](https://3.bp.blogspot.com/-97qSMhzIPOE/WsTwIiBOlMI/AAAAAAAAInU/Xb90Cy9gzJYsGtOuzXkPSL6bWfQszpr1ACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.32.39.png)

如我們預期的,因為Steven不是數字的原因而被擋掉了,Constraints算是一個可以把Route用得更靈活的技巧,雖然需要懂的正規表示法,但我覺得這兩項學習投資很划算,正規表示法到很多地方都很萬用。

自訂更複雜的Constraints

我們再來出個更刁的要求,假設你老闆就叫Steven,而且他不希望跟別人一樣,每個人都是打ID查資料顯示太一般,他偏偏要只有輸入Steven也要能進到About頁時該怎麼辦?
當然也可以用正規表示法硬做,但可能會讓Constraint寫得很醜難維護,所以這次改用實作IRouteConstraint的方式來完成這個需求
建立一個StevenBossConstraint的Class
[![](https://1.bp.blogspot.com/-lzkbxUNTjPs/WsTyHmO8DrI/AAAAAAAAInk/Z69_UKTgzw4Yiduel0QPOKVnIq9uNLP1ACLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.39.46.png)](https://1.bp.blogspot.com/-lzkbxUNTjPs/WsTyHmO8DrI/AAAAAAAAInk/Z69_UKTgzw4Yiduel0QPOKVnIq9uNLP1ACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.39.46.png)
實作以下內容
[![](https://3.bp.blogspot.com/-mCMOFMxoiQs/WsT0UDLnh5I/AAAAAAAAIn0/w7PzqK5RsyEabGIx9azbwzT2CZj6VlN6gCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.48.47.png)](https://3.bp.blogspot.com/-mCMOFMxoiQs/WsT0UDLnh5I/AAAAAAAAIn0/w7PzqK5RsyEabGIx9azbwzT2CZj6VlN6gCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.48.47.png)

將原本設定ID的Constrainte改成我們寫的StevenBossConstraint

[![](https://3.bp.blogspot.com/-SXNFt3bhpRE/WsT1HLb4yzI/AAAAAAAAIn8/x04sB1B-ymwBSfdvYbCOrn4EcNjTaoFAwCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.51.38.png)](https://3.bp.blogspot.com/-SXNFt3bhpRE/WsT1HLb4yzI/AAAAAAAAIn8/x04sB1B-ymwBSfdvYbCOrn4EcNjTaoFAwCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.51.38.png)

執行後就會發現,數字跟Steven都可以通過Route的檢查,但你打Tom或是Tim之類的其他非數字參數,都會被擋掉

[![](https://2.bp.blogspot.com/-QOP-fMjmh1M/WsT1zhvaGSI/AAAAAAAAIoM/8GxzEl2aKKY0V2sSKZEo9zXqNg2qI7KJQCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.56.40.png)](https://2.bp.blogspot.com/-QOP-fMjmh1M/WsT1zhvaGSI/AAAAAAAAIoM/8GxzEl2aKKY0V2sSKZEo9zXqNg2qI7KJQCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.56.40.png)
[![](https://2.bp.blogspot.com/-dV8vMrVM9rQ/WsT1z2-D7uI/AAAAAAAAIoQ/TJAfdgiSc_cu36opnnSktdBx9eULS0tXwCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.56.29.png)](https://2.bp.blogspot.com/-dV8vMrVM9rQ/WsT1z2-D7uI/AAAAAAAAIoQ/TJAfdgiSc_cu36opnnSktdBx9eULS0tXwCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.56.29.png)
[![](https://3.bp.blogspot.com/-rAGzQD3L_c8/WsT1zmOtcFI/AAAAAAAAIoI/O7utdb1IfIUkMAK013X5TQwoo-QOvQ-pACLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.56.47.png)](https://3.bp.blogspot.com/-rAGzQD3L_c8/WsT1zmOtcFI/AAAAAAAAIoI/O7utdb1IfIUkMAK013X5TQwoo-QOvQ-pACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-04%2B%25E4%25B8%258B%25E5%258D%258811.56.47.png)

透過小工具來幫助偵錯Route設定

可以透過Nuget來安裝Route Debugger工具,他們告訴我們目前網頁之所以能夠顯示,是因為走了哪一條Route規則,這在初期我對這些設定還不熟悉時幫助非常的大
[![](https://1.bp.blogspot.com/-OF3Lz23taZc/WsT2n5WMicI/AAAAAAAAIoc/yvgdxPJ_Dj84sOOWiZaf5KDe4m47nuFOQCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-05%2B%25E4%25B8%258A%25E5%258D%258812.00.15.png)](https://1.bp.blogspot.com/-OF3Lz23taZc/WsT2n5WMicI/AAAAAAAAIoc/yvgdxPJ_Dj84sOOWiZaf5KDe4m47nuFOQCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-05%2B%25E4%25B8%258A%25E5%258D%258812.00.15.png)
安裝這組套件
[![](https://2.bp.blogspot.com/-MY03_784a74/WsT5eh-6T7I/AAAAAAAAIoo/fgvoj60WvfARLWeyjMfRHi_9onyYPdCmACLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-05%2B%25E4%25B8%258A%25E5%258D%258812.10.10.png)](https://2.bp.blogspot.com/-MY03_784a74/WsT5eh-6T7I/AAAAAAAAIoo/fgvoj60WvfARLWeyjMfRHi_9onyYPdCmACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-05%2B%25E4%25B8%258A%25E5%258D%258812.10.10.png)

接著重新執行網站就可在看到詳細解說

[![](https://1.bp.blogspot.com/-_GBLWNdFSoA/WsT51ZoPPkI/AAAAAAAAIos/A4wI4Ml7U9U1QjmMwGcIPe0LQCpVbra2QCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-05%2B%25E4%25B8%258A%25E5%258D%258812.13.24.png)](https://1.bp.blogspot.com/-_GBLWNdFSoA/WsT51ZoPPkI/AAAAAAAAIos/A4wI4Ml7U9U1QjmMwGcIPe0LQCpVbra2QCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-04-05%2B%25E4%25B8%258A%25E5%258D%258812.13.24.png)

相信剛開始要改Route的使用者來說,會是相當有幫助的工具喔!!

什麼是Route 在現實生活中比較接近於郵差與地圖的關係,你將想要送的東西交給郵差,郵差依據包裹上的地址,透過地圖找到目的地然後投遞。

而對應到程式中[網址]就是你包裹要送達的目的地址,Route就是[地圖],而網址的[參數]就是你要投遞的包裹

[![](https://4.bp.blogspot.com/-v_OpLGzjdtw/Wq0MHMSi2mI/AAAAAAAAIko/7mcxcoPLkpUWExDOSn657rGCjMfUtiJSgCLcBGAs/s400/1.jpg)](https://4.bp.blogspot.com/-v_OpLGzjdtw/Wq0MHMSi2mI/AAAAAAAAIko/7mcxcoPLkpUWExDOSn657rGCjMfUtiJSgCLcBGAs/s1600/1.jpg)
接著我們來看看專案內的Route在哪邊設定,又它是如何透過網址知道該把[包裹]送去哪支程式的,這邊先把我們之前的專案執行起來,並且點擊[關於]
[![](https://1.bp.blogspot.com/-ryN1Z3Mp3SI/Wq0MU9PT8mI/AAAAAAAAIks/ONCnMFBGRhYG0YWmLnZUV__5jGS5gpBBgCLcBGAs/s400/2.png)](https://1.bp.blogspot.com/-ryN1Z3Mp3SI/Wq0MU9PT8mI/AAAAAAAAIks/ONCnMFBGRhYG0YWmLnZUV__5jGS5gpBBgCLcBGAs/s1600/2.png)
可以看到關於頁的網址為 /Home/About
[![](https://4.bp.blogspot.com/-F1fHYAInGic/Wq0MkAgp4uI/AAAAAAAAIkw/PN3nKltrc5wH8Lkb9Lk5_6flzLCy80GOACLcBGAs/s400/3.png)](https://4.bp.blogspot.com/-F1fHYAInGic/Wq0MkAgp4uI/AAAAAAAAIkw/PN3nKltrc5wH8Lkb9Lk5_6flzLCy80GOACLcBGAs/s1600/3.png)
接著我們在HomeControllerAbout下中斷點後再點擊一次網頁的[關於]按鈕,會發現程式停在我們下中斷點的地方了? 為什麼知道程式會跑到這邊呢 ? 這一切都是因為有Route指路的關係
[![](https://1.bp.blogspot.com/-LFueIPKzOYI/Wq0MwArDFLI/AAAAAAAAIk4/gJdWivFruxcZoCKaGNAfYamVnM8JEO3igCLcBGAs/s400/4.png)](https://1.bp.blogspot.com/-LFueIPKzOYI/Wq0MwArDFLI/AAAAAAAAIk4/gJdWivFruxcZoCKaGNAfYamVnM8JEO3igCLcBGAs/s1600/4.png)

RouteConfig

Route的規則是可以自己制訂的,而制定的地方就在App_Start資料夾底下的RouteConfig裡面
[![](https://2.bp.blogspot.com/-empHw_-1tig/Wq0M_p2kL-I/AAAAAAAAIlA/XQjsx0UahlsjJyqijDxTtSa7hmnkJVm5ACLcBGAs/s400/5.png)](https://2.bp.blogspot.com/-empHw_-1tig/Wq0M_p2kL-I/AAAAAAAAIlA/XQjsx0UahlsjJyqijDxTtSa7hmnkJVm5ACLcBGAs/s1600/5.png)
[![](https://3.bp.blogspot.com/-r4pEpAts3NA/Wq0NBNPYBKI/AAAAAAAAIlE/rUslCqFTfUEvvPRAG5cWKdDvL8S_CgGGQCLcBGAs/s400/6.png)](https://3.bp.blogspot.com/-r4pEpAts3NA/Wq0NBNPYBKI/AAAAAAAAIlE/rUslCqFTfUEvvPRAG5cWKdDvL8S_CgGGQCLcBGAs/s1600/6.png)
讓我們來解讀這段程式碼,首先routes.MapRoute就是註冊地址的方法,裡面有幾個參數分別為
Name : 你對於這個Route的命名
Url : 網址條件,當網址符合這個條件特徵時,就會依據這個Route的指示去找對應執行的程式碼
defaults : 參數的預設值
相信光是講解參數的意義對於要理解Route還有相當的距離,讓我們直接來實戰理解 /Home/About這串網址如何對應到Route的吧,剛剛有說,routes.MapRouteUrl參數是用來判斷網址特徵是否相符,如果相符就會被這串Route捕捉到,但是這兩個怎麼看都不像啊
真實網址 /Home/About
Route Url :  {controller}/{action}/{id}
首先必須先知道一件事情,當Route Url的網址用{ }包起來的時候,代表他是個變數,什麼是變數?變數是一個表示值,它可以是任意的值,而{}裡面的字就是變數的名稱,以上述例子為例
我們有一個變數叫做controller,它可以是任意的值
我們有一個變數叫做action,它可以是任意得值
我們有一個變數叫做id,它也可以是任意的值
那套入/Home/About會變成什麼結果?
我們有一個變數叫做controller,它現在的值是Home
我們有一個變數叫做action,它現在的值是About
我們有一個變數叫做id,它現在沒有值
Route的制定中,**controlleraction****為保留字**,意思是告訴他要執行哪個controllerAction,依據這邊的規定,所以它知道要去執行Home這個Controller,裡面一個叫做AboutAction
[![](https://3.bp.blogspot.com/-lPmkR6MJKWs/Wq0NMR-XeJI/AAAAAAAAIlM/NuECRMJtFww8ve031wVUqj-dnCa6K4FQgCLcBGAs/s400/7.png)](https://3.bp.blogspot.com/-lPmkR6MJKWs/Wq0NMR-XeJI/AAAAAAAAIlM/NuECRMJtFww8ve031wVUqj-dnCa6K4FQgCLcBGAs/s1600/7.png)

在Controller裡面寫的方法我們稱之為Action,而他通常都會回傳ActionResult

再讓我們看一個例子 [首頁]
首頁的網址為 : http://localhost:54004/
[![](https://1.bp.blogspot.com/-l2A_55syPtg/Wq0NzWKrZFI/AAAAAAAAIlg/NYnq1QMfQBUv9_fsyYczIqUSy4BW4ZHEQCLcBGAs/s400/8.png)](https://1.bp.blogspot.com/-l2A_55syPtg/Wq0NzWKrZFI/AAAAAAAAIlg/NYnq1QMfQBUv9_fsyYczIqUSy4BW4ZHEQCLcBGAs/s1600/8.png)
所以首頁網址等於  
首頁網址 /
Route Url :  {controller}/{action}/{id}
我們有一個變數叫做controller,它現在沒有值
我們有一個變數叫做action,它現在沒有值
我們有一個變數叫做id,它現在沒有值
什麼都沒有,那程式到底要執行哪邊? 這時候Default終於派上用場了,Default的意思就是,當沒有值時,請用我設定的值代替吧
[![](https://1.bp.blogspot.com/-TDgD-ND409Y/Wq0OX06AV7I/AAAAAAAAImc/CSNnXFgp6jkv3ajpzFLwlQuWyC8t7rBUgCEwYBhgL/s400/9.png)](https://1.bp.blogspot.com/-TDgD-ND409Y/Wq0OX06AV7I/AAAAAAAAImc/CSNnXFgp6jkv3ajpzFLwlQuWyC8t7rBUgCEwYBhgL/s1600/9.png)
所以
controller為空值,所以用Default的值代替,所以它的值為Home
action為空值,所以用Default的值代替,所以它的值為Index
id為空直,但Default設定它為UrlParameter.Optional,意思是它是選擇性的,如果沒有沒關係
所以這個Route設定又完美符合了,所以依據條件,我們去HomeControllerIndex下中斷點,試試看進入首頁時是不是會停在這邊
[![](https://2.bp.blogspot.com/-3I8rJaSlSzI/Wq0OT3PebrI/AAAAAAAAIlo/gtyIS1YVuD83WLbI_Fh1huvHCyqCFcwUgCEwYBhgL/s400/10.png)](https://2.bp.blogspot.com/-3I8rJaSlSzI/Wq0OT3PebrI/AAAAAAAAIlo/gtyIS1YVuD83WLbI_Fh1huvHCyqCFcwUgCEwYBhgL/s1600/10.png)
[![](https://3.bp.blogspot.com/-koCX4c7IthY/Wq0OT1iZPOI/AAAAAAAAIls/8Crncjqy5b0piO1OdxXPkaPVzgxpD2pzwCEwYBhgL/s400/11.png)](https://3.bp.blogspot.com/-koCX4c7IthY/Wq0OT1iZPOI/AAAAAAAAIls/8Crncjqy5b0piO1OdxXPkaPVzgxpD2pzwCEwYBhgL/s1600/11.png)
的確,他依據規則跑到了HomeControllerIndex Action了。
那舉一反三一下,換句話說首頁網址也可以是 /Home/Index 摟,試試看,你會發現它的確走到一樣的中斷點
[![](https://2.bp.blogspot.com/-PGK33fEJg-Q/Wq0OT4Pa-7I/AAAAAAAAImo/_9AR8OUt1Iga3IytGl1LuqwTxbsmNeWkwCEwYBhgL/s400/12.png)](https://2.bp.blogspot.com/-PGK33fEJg-Q/Wq0OT4Pa-7I/AAAAAAAAImo/_9AR8OUt1Iga3IytGl1LuqwTxbsmNeWkwCEwYBhgL/s1600/12.png)
你可能會說,那這樣Route還有什麼好學的,這個寫法幾乎萬用了啊? 的確在MVC預設專案中的這個Route設定我們通常稱它為萬用Route,但也因為它幾乎萬用,所以不好管理,所以正常來說都還是會制定自己的Route規則,好方便管理,這個之後慢慢寫專案碰到多了會更有感觸。
接著我們來試著修改一下程式,讓我們的{id}能派上用場吧!!
在剛剛的案例中,因為id設定是Optional所以都沒派上用場,來試試看他怎麼用吧,首先我們先在HomeControllerAbout Action中加入參數id,並且把回傳的訊息改成Your id is (你帶進來的值)
[![](https://3.bp.blogspot.com/-FLI7MLnnMfw/Wq0OUuGGE3I/AAAAAAAAImk/nXWFNqHfTN8NUtq0rDJzxIB1jou_4DAdwCEwYBhgL/s400/13.png)](https://3.bp.blogspot.com/-FLI7MLnnMfw/Wq0OUuGGE3I/AAAAAAAAImk/nXWFNqHfTN8NUtq0rDJzxIB1jou_4DAdwCEwYBhgL/s1600/13.png)
接著我們重新執行一次程式,並且把網址改成/Home/About/Steven
[![](https://2.bp.blogspot.com/-Zks5ZE30New/Wq0OUxoT8HI/AAAAAAAAIms/WxfowIPPItUnVcXErJ1H2FQPfHTWr58RgCEwYBhgL/s400/14.png)](https://2.bp.blogspot.com/-Zks5ZE30New/Wq0OUxoT8HI/AAAAAAAAIms/WxfowIPPItUnVcXErJ1H2FQPfHTWr58RgCEwYBhgL/s1600/14.png)
再練習一次Route比對
我們有一個變數叫做controller,它現在的值是Home
我們有一個變數叫做action,它現在的值是About
我們有一個變數叫做id,它現在的值是Steven
讓接著看中斷點,把滑鼠移到id那的參數會看到,Id依據我們下的網址,以Steven帶進來了
[![](https://3.bp.blogspot.com/-h6ZLvBxZDRE/Wq0OVDhncjI/AAAAAAAAImo/IL83Kj55bx4EQb6yZg5uz7jkxi9Poyq1QCEwYBhgL/s400/15.png)](https://3.bp.blogspot.com/-h6ZLvBxZDRE/Wq0OVDhncjI/AAAAAAAAImo/IL83Kj55bx4EQb6yZg5uz7jkxi9Poyq1QCEwYBhgL/s1600/15.png)
接著看網頁呈現的成果
[![](https://3.bp.blogspot.com/-4scpot89dZU/Wq0OVopL--I/AAAAAAAAImg/sdGZ1pxWLIMBO-ftiDpWhDA3l2f1tz7XwCEwYBhgL/s400/16.png)](https://3.bp.blogspot.com/-4scpot89dZU/Wq0OVopL--I/AAAAAAAAImg/sdGZ1pxWLIMBO-ftiDpWhDA3l2f1tz7XwCEwYBhgL/s1600/16.png)

練習制定一個屬於我們的Route

看了兩個範例,我們也來制定一個Route規則給自己用吧,而且我們不要用變數的方式。
[![](https://3.bp.blogspot.com/-cOBHu8_QRGw/Wq0OV84UZ2I/AAAAAAAAImo/GhiHQFpeFGkvHHlIju0NPcUDtmSpF9OmQCEwYBhgL/s400/17.png)](https://3.bp.blogspot.com/-cOBHu8_QRGw/Wq0OV84UZ2I/AAAAAAAAImo/GhiHQFpeFGkvHHlIju0NPcUDtmSpF9OmQCEwYBhgL/s1600/17.png)
我們希望有個網址為/tellMe/whoAreYou/{name},且name跟之前的id一樣是個變數,可以由你自己打喜歡的文字來決定,寫完後來試試看這個route可不可行
[![](https://1.bp.blogspot.com/-DviuNDcFiHg/Wq0OWIgZMfI/AAAAAAAAImk/uDokBOMNij0mSGMLcLZsxjp63J7qOdKDwCEwYBhgL/s400/18.png)](https://1.bp.blogspot.com/-DviuNDcFiHg/Wq0OWIgZMfI/AAAAAAAAImk/uDokBOMNij0mSGMLcLZsxjp63J7qOdKDwCEwYBhgL/s1600/18.png)
結果竟然錯了,來看看少了什麼
網址 /tellMe/whoAreYou/steven
Route Url :  /tellMe/whoAreYou/{name}
比對tellMe,符合
比對whoAreYou,符合
比對Name這個變數,現在的值為Steven
然後呢? 有沒有發現即便這邊都比對正確,符合route規則,但我們沒告訴他Controller是誰,Action是誰,程式當然不知道該去執行誰,所以再來改一下,因為我們還沒學到如何建立ControllerAction,所以先用現成的, 導到homeControllerContact
[![](https://1.bp.blogspot.com/-qH5gM8_e5zA/Wq0OWghkjZI/AAAAAAAAImo/3gENq49UAPUaqw-ybuBoWEy2tlBNdeM6wCEwYBhgL/s400/19.png)](https://1.bp.blogspot.com/-qH5gM8_e5zA/Wq0OWghkjZI/AAAAAAAAImo/3gENq49UAPUaqw-ybuBoWEy2tlBNdeM6wCEwYBhgL/s1600/19.png)
[![](https://2.bp.blogspot.com/-dSlvDEn3NLw/Wq0OW9IOoBI/AAAAAAAAIms/uPwgRJ_nwzkB6qAltJkNWfcSZGUu6h44wCEwYBhgL/s400/20.png)](https://2.bp.blogspot.com/-dSlvDEn3NLw/Wq0OW9IOoBI/AAAAAAAAIms/uPwgRJ_nwzkB6qAltJkNWfcSZGUu6h44wCEwYBhgL/s1600/20.png)
讓我們再試試看剛剛的網址
[![](https://1.bp.blogspot.com/-Pm-1-hAQ2Xc/Wq0OXEQrCpI/AAAAAAAAImo/7jBfE4xT3w8Y4QRu_iDmqrxtYcEk-8AgACEwYBhgL/s400/21.png)](https://1.bp.blogspot.com/-Pm-1-hAQ2Xc/Wq0OXEQrCpI/AAAAAAAAImo/7jBfE4xT3w8Y4QRu_iDmqrxtYcEk-8AgACEwYBhgL/s1600/21.png)
這次不會壞掉了,且正確的導到HomeControllerContact Action去,接著試試不帶{Name}會怎麼樣
[![](https://2.bp.blogspot.com/-qulCDWyyTIs/Wq0OXeD8fFI/AAAAAAAAIms/3CVn_ntnPms8AvxipLFd34dYGInhrSN_wCEwYBhgL/s400/22.png)](https://2.bp.blogspot.com/-qulCDWyyTIs/Wq0OXeD8fFI/AAAAAAAAIms/3CVn_ntnPms8AvxipLFd34dYGInhrSN_wCEwYBhgL/s1600/22.png)
變成找不到資源,為什麼?因為我們的Name沒有設定為UrlParameter.Optional,所以是必帶的參數,這邊要特別注意!!! 之前教很多新人Route時很多人都卡在這邊
**Route**比對時必須完全符合才會被捕捉到
所以回頭審視我們的Route註冊目前有兩個規則
1. tellMe/whoAreYou/{name}
2\. {controller}/{action}/{id}
而我們帶的網址為 /tellMe/whoAreYou ,比對的規則將會是
由上到下,所以先比對了 1\. **tellMe/whoAreYou/{name}** 的規則 
tellMe,符合
whoAreYou,符合
Route要求要有Name,且不是Optional,所以必帶,”不符合”
這段Route會被跳過,執行比對2\. **{controller}/{action}/{id} **的規則
ControllertellMe,符合
ActionWhoAreYou,符合
Id為空值,因為為Optional,符合
所以實際程式去找的是TellMeController裡面的WhoAreYou這個Action,因為找不到這支程式所以顯示錯誤,而不是去執行規則1. HomeControllerContact這個Action
這是新手常常會犯的觀念錯誤,所以特別舉這個例子希望能夠較清楚的比較跟釐清,而Route因為還有很多種設定,避免一次太多造成混亂,所以先到這邊。試著將上述的觀念釐清,對於之後撰寫MVC Route時會很有幫助

如果你本來跟我一樣是寫Dot Net Web的工程師,那你應該是從Web Form開始的,第一次接觸MVC時,應該心中都會浮現「什麼是MVC ?」

其實Wiki已經解釋得很清楚了,就不重複贅述這些定義,有興趣可以自己看看,而我比較想提的是這跟原本開發Web Form,或是說你是寫別語言但沒使用MVC這個架構時的差別。

還記得以前在接手前輩專案時,因為每個人開發習慣不盡相同,對於各種用途的類別可能都有一套自己的歸類方式,所以剛開始總是需要花大把時間在熟悉前輩定義的架構, 如果專案一多,常常切換專案時都會有一種混亂感。

如果遇到是初心工程師,可能會有那種一個Function幾百行的,基本上要改個東西都要找很久之外,還很怕改錯,而這些不僅僅造成維護的成本增加之外,還讓接手別人專案時總是滿滿恨意!!

而MVC這個架構就是在解決這個問題,他將系統分成三大部分 Model 、View、 Controller, 而這三大部分分別管理著

Model : 資料的管理(例如與資料庫的溝通) , 演算法邏輯(商業邏輯) , 物件結構定義

View : 呈現給使用者看、操作的介面

Controller : 依據傳入的資料該怎麼運作、程式流程的控制、該回傳給使用者什麼資料等
[![](https://3.bp.blogspot.com/-ByxeEs_OPjo/Wo7SEU86Q2I/AAAAAAAAIkM/ILz4hUYI59gdspAfW_MxUekeTfa2ZaWCQCEwYBhgL/s640/2.jpg)](https://3.bp.blogspot.com/-ByxeEs_OPjo/Wo7SEU86Q2I/AAAAAAAAIkM/ILz4hUYI59gdspAfW_MxUekeTfa2ZaWCQCEwYBhgL/s1600/2.jpg)
出處 https://helloacm.com/model-view-controller-explained-in-c/

優點ㄧ : 讓習慣代替配置

因為大家對於MVC有相同的認識,且Dot Net MVC更把這些定義落實到專案上,在不改動底層運作的情況下,屬於Controller的Class就該放到Controllers的資料夾、屬於頁面呈現的就放到Views資料夾,且預設也有一個Models的資料夾給你放Model,有了這些規範後,讓習慣來代替配置,你接手別人的MVC專案後, 所有人的開發方式基本上都會依照這個規範去落實,降低維護的成本。

[![](https://4.bp.blogspot.com/-4aTMTqC7vZE/Wo7SSOsXq7I/AAAAAAAAIkY/aQAQWR1nxkATcDPTD2xx4nwxLUOqQYI-gCEwYBhgL/s400/1.png)](https://4.bp.blogspot.com/-4aTMTqC7vZE/Wo7SSOsXq7I/AAAAAAAAIkY/aQAQWR1nxkATcDPTD2xx4nwxLUOqQYI-gCEwYBhgL/s1600/1.png)
Dot Net MVC預設新建專案就會有這些資料夾

優點二 : 關注點分離

如果今天是在處理使用者操作介面,那就專心的套版,將Controller傳回來的資料看要怎麼擺放,又如果在處理資料流傳入的參數驗證,那就在Controller處理完,不用去管會不會影響到View的呈現, 如果是跟資料庫的溝通,就在Model裡將它實作好,不需擔心是否參數有空值或Null, 因為那些該是在Controller處理掉的。

相較於以前的Web Form開發方式,因為UserControl介於頁面跟Code Behind的事件之間,常常耦合度太強,一改兩邊都會動到,需要較嚴謹的開發規範才能避免耦合問題,而且我記憶最深的就是UserControl間的生命週期,那個互相攪在一起要改還真要命…

Web Form有其快速且便利性,以上並非說Dot Net MVC能完全取代Web Form,
更想表達的是,這是當我從開發Web Form兩年然後跳到MVC目前5年多,
它所帶給我的感受與改變,希望透過這些講述能更清楚MVC是否是你該投入資源學習的架構

優點三 : 前端更自由

在Dot Net MVC中,不再有User Control這類的元件存在,所以前端會更加的自由,無論是在JS或是CSS的運用上,再也不會有元件Render時幫你加上一堆多餘的Tag或是Class。

目前經手過的一些專案,有些都已經徹底前後端分離,
後端只有負責處理好API或是ViewModel(傳給View的資料通常定義出來的Class都會叫ViewModel,這之後會再提到),
吐一個空的頁面載入指定JS,剩下就讓前端去處理介面,彼此分工更精細,互相耦合的程度也降低許多。

這篇花比較多篇幅在解說MVC是什麼,又Dot Net MVC在開發上能帶來什麼改變跟好處, 下一篇預計就要開始講Router 與 Controller之間的關係。

年前朋友討論是否寫Dot Net Mvc的簡單教學文章,害我過年都在思考該怎麼寫這東西…可惡(果然是個心裡容不下待辦事項的人啊)。

認真的想了後覺得這真是個非常難的主題,一來是網路上已經有很多資源,我也不是什麼大神,很怕誤人子弟。二來雖然以前曾經幫忙教育訓練過新進同事,但面對面的溝通且依照每個人程度不同給予討論,跟用文章教會一個不知道對象是誰,且博大精深的框架與語言,這個差距實在太大,所以最後只好一直將目標縮小再縮小,希望這系列文如果看完時能達到以下目標我就滿足了

**目標對象 **

1.你有一點程式語言的底子,但沒接觸過Dot Net MVC
2.寫過Dot Net MVC,但還不是很熟悉它怎麼應用

希望達成目標

1.能夠比較清楚的理解Dot Net MVC怎麼運作
2.知道了一些小東西,或許能改善目前你開發上困擾

那就先從工具開始吧

Visual Studio Community : 微軟官網載點

開發Dot Net的工程師,應該9成9的人都是使用Visual Studio,它不僅號稱地表最強編輯器,微軟更佛心的提供了免費版本給大家使用,否則最基礎的一套Visual Studio的價格應該也會讓初學者非常卻步。

當然既然是免費版本,功能上一定會有些陽春,但對於只是入門學習Dot Net而言已經是相當強大的工具了。

[![](https://4.bp.blogspot.com/-AThY6IIeOMU/Wozqfrbkj8I/AAAAAAAAIhM/unPhEYOMwi4GocjIIbH3sQLsG6zFLwLXgCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258A%25E5%258D%258811.41.26.png)](https://4.bp.blogspot.com/-AThY6IIeOMU/Wozqfrbkj8I/AAAAAAAAIhM/unPhEYOMwi4GocjIIbH3sQLsG6zFLwLXgCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258A%25E5%258D%258811.41.26.png)
功能比較(來源: [https://www.visualstudio.com/zh-hant/vs/compare/](https://www.visualstudio.com/zh-hant/vs/compare/))
[![](https://2.bp.blogspot.com/-IAZ842DU7bE/WozrAMFvS5I/AAAAAAAAIhU/hXxvWOr48VIim3dJ2gxOVdllEfdLsvspgCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258A%25E5%258D%258811.43.35.png)](https://2.bp.blogspot.com/-IAZ842DU7bE/WozrAMFvS5I/AAAAAAAAIhU/hXxvWOr48VIim3dJ2gxOVdllEfdLsvspgCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258A%25E5%258D%258811.43.35.png)
價格表(來源:[https://www.visualstudio.com/zh-hant/vs/pricing/](https://www.visualstudio.com/zh-hant/vs/pricing/))

下載完之後將它安裝起來

[![](https://4.bp.blogspot.com/-l_GogekWtuo/Wo0NeymgUGI/AAAAAAAAIhk/a9KRtEucnA8ZAofXpTfM1WKc0Cn1-VSeACLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25882.10.47.png)](https://4.bp.blogspot.com/-l_GogekWtuo/Wo0NeymgUGI/AAAAAAAAIhk/a9KRtEucnA8ZAofXpTfM1WKc0Cn1-VSeACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25882.10.47.png)
選擇Community安裝
[![](https://4.bp.blogspot.com/-VxH0BFcP1N4/Wo0OAeM_n8I/AAAAAAAAIhs/XbpBzTLtFQsEQwdPNDVF0V1-yghRDhHMgCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25882.13.01.png)](https://4.bp.blogspot.com/-VxH0BFcP1N4/Wo0OAeM_n8I/AAAAAAAAIhs/XbpBzTLtFQsEQwdPNDVF0V1-yghRDhHMgCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25882.13.01.png)
勾選ASP.NET與網頁程式開發
[![](https://1.bp.blogspot.com/-YT3wMLUPuxQ/Wo0OS6i817I/AAAAAAAAIhw/cJGnL6jonT0Kf9XxbKzB9k_5qT6vdB16wCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25882.14.13.png)](https://1.bp.blogspot.com/-YT3wMLUPuxQ/Wo0OS6i817I/AAAAAAAAIhw/cJGnL6jonT0Kf9XxbKzB9k_5qT6vdB16wCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25882.14.13.png)
等它安裝完
[![](https://2.bp.blogspot.com/-mb-CXRobkYs/Wo0ZC8U4VJI/AAAAAAAAIiE/hVP-yUd9UtMsSVpt3iVTjyRgksp825iLwCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.00.10.png)](https://2.bp.blogspot.com/-mb-CXRobkYs/Wo0ZC8U4VJI/AAAAAAAAIiE/hVP-yUd9UtMsSVpt3iVTjyRgksp825iLwCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.00.10.png)
安裝完之後打開應該會看到這個畫面

因為寫程式大部分時間都需要一直長盯著螢幕,所以螢幕底色如果是淺色系通常到下午我眼睛都會很酸,所以Visual Studio也很貼心了提供修改視窗色系的功能,例如我截圖就是深色系。
調整方式如下
工具 > 選項 > 色彩佈景主題

** **** ****建立第一個Web專案**
[![](https://3.bp.blogspot.com/-8NkEiv-Pdy0/Wo0atMIeTBI/AAAAAAAAIiY/Xqtt1Nt4vdYMutyRHnF6SK0ywGJw6IBVwCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.07.17.png)](https://3.bp.blogspot.com/-8NkEiv-Pdy0/Wo0atMIeTBI/AAAAAAAAIiY/Xqtt1Nt4vdYMutyRHnF6SK0ywGJw6IBVwCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.07.17.png)
檔案 > 新增 > 專案
[![](https://3.bp.blogspot.com/-52w4Z2LQrMo/Wo0bFfQ42ZI/AAAAAAAAIic/W7IdEeXSIqIh3rG2CmSN4slAT6Qu1qkzgCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.08.51.png)](https://3.bp.blogspot.com/-52w4Z2LQrMo/Wo0bFfQ42ZI/AAAAAAAAIic/W7IdEeXSIqIh3rG2CmSN4slAT6Qu1qkzgCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.08.51.png)
選擇Web > Web應用程式(.NET Framework) 。 專案名稱與放的位置依照自己喜好即可
[![](https://3.bp.blogspot.com/-sG9_1osHZzM/Wo0bo1ny1kI/AAAAAAAAIio/mRKCMTpCjp461LAbSFCK6EzS02y1Z02KQCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.11.14.png)](https://3.bp.blogspot.com/-sG9_1osHZzM/Wo0bo1ny1kI/AAAAAAAAIio/mRKCMTpCjp461LAbSFCK6EzS02y1Z02KQCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.11.14.png)
選擇MVC
建立完成之後應該可以看到這個畫面
[![](https://3.bp.blogspot.com/-bgNV43jpPbY/Wo0cBzZrZPI/AAAAAAAAIis/U152KITJzwkkk3aMigyzBB7fAyCPS0pIACLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.12.56.png)](https://3.bp.blogspot.com/-bgNV43jpPbY/Wo0cBzZrZPI/AAAAAAAAIis/U152KITJzwkkk3aMigyzBB7fAyCPS0pIACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.12.56.png)

如果看不到方案總管,可以透過檢視> 方案總管來打開

右邊就是我們寫程式要放的地方,之後介紹Dot Net MVC時會慢慢帶到,但今天還不需要寫到程式,先直接將程式執行起來,看看預設建立好的網站是長什麼樣

[![](https://2.bp.blogspot.com/-rsiatvazfV0/Wo0dW96VDgI/AAAAAAAAIi0/62hUceI6RWI5zmuuQrkAE9MP5q0ZXAjaQCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.17.44.png)](https://2.bp.blogspot.com/-rsiatvazfV0/Wo0dW96VDgI/AAAAAAAAIi0/62hUceI6RWI5zmuuQrkAE9MP5q0ZXAjaQCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.17.44.png)
因為這是網站程式, 點選執行旁邊的向下箭頭後,可以依照個人喜好選擇習慣用的瀏覽器來瀏覽,我個人是習慣用Google Chrome,所以以下用Chrome做示範,選好後按下綠色的箭頭執行。

執行起來之後應該可以看到一個很專業的診斷工具面板,分別顯示目前佔了CPU幾%,用了多少記憶體…等等

[![](https://2.bp.blogspot.com/-02yTU2prJZY/Wo0ekE8DgvI/AAAAAAAAIi8/8BiY3LezXhUhV7QMK8XeKO9oy3RG4jKOQCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.22.30.png)](https://2.bp.blogspot.com/-02yTU2prJZY/Wo0ekE8DgvI/AAAAAAAAIi8/8BiY3LezXhUhV7QMK8XeKO9oy3RG4jKOQCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.22.30.png)

而這時候你選擇的瀏覽器應該也會開啟,然後看到目前網站的成果

[![](https://2.bp.blogspot.com/-Xw28Lnnj1vI/Wo0euDl5nmI/AAAAAAAAIjA/hj1eQKYS0jcS8goj3FDv0yngDCHDYPmVACLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.24.20.png)](https://2.bp.blogspot.com/-Xw28Lnnj1vI/Wo0euDl5nmI/AAAAAAAAIjA/hj1eQKYS0jcS8goj3FDv0yngDCHDYPmVACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.24.20.png)

可以隨意的點擊網站按鈕逛逛玩玩後,現在來簡單講一下該如何追蹤偵錯

偵錯是門大學問,常常我們寫的程式編譯時都沒有錯誤,但執行結果卻不如我們預期那般時,一步一步偵錯就會是我們發現問題的好朋友

我們以關於這個按鈕為例,究竟點擊關於這個按鈕時,程式是如何執行,讓我們可以看到畫面的

[![](https://3.bp.blogspot.com/--aluBIC6nMg/Wo0gH9fbBYI/AAAAAAAAIjQ/BaKOSBTJacIOl9_W7YAz8lqi-9Fx5C0FQCLcBGAs/s640/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.29.58.png)](https://3.bp.blogspot.com/--aluBIC6nMg/Wo0gH9fbBYI/AAAAAAAAIjQ/BaKOSBTJacIOl9_W7YAz8lqi-9Fx5C0FQCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.29.58.png)

因為我們還不知道目前整個程式的運作原理,所以依照下面的指示這樣做

  1. 找到HomeController,並且打開它

    [![](https://4.bp.blogspot.com/-kCJKeJELsx0/Wo0gxIiQthI/AAAAAAAAIjY/FPc1xN8m0oQzv5tfWhG8hf649M2gp4jUACLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.31.50.png)](https://4.bp.blogspot.com/-kCJKeJELsx0/Wo0gxIiQthI/AAAAAAAAIjY/FPc1xN8m0oQzv5tfWhG8hf649M2gp4jUACLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.31.50.png)
  2. 找到第18行的地方,點擊那行的最左邊邊框,會產生一個紅色的原點,我們稱之為中斷點

    [![](https://2.bp.blogspot.com/-LIvuofL8J2g/Wo0ha-LzXzI/AAAAAAAAIjg/GrPyIZE-hss6nVqf2b6CgYKIhaXTjpfNgCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.34.22.png)](https://2.bp.blogspot.com/-LIvuofL8J2g/Wo0ha-LzXzI/AAAAAAAAIjg/GrPyIZE-hss6nVqf2b6CgYKIhaXTjpfNgCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.34.22.png)

    中斷點的意思就是,當程式執行到這行時會停下來讓你知道,並且等待你的指示

  3. 接著再回去剛剛的網頁點擊關於,你會發現,視窗會跳到你剛剛下得中斷點位置,而網頁會卡住,並沒有顯示關於的頁面


    那是因為程式還沒執行完,所以還沒辦法顯示頁面資訊4. 按下F11,每按一次F11,你會看到程式往下走一行,藉此來觀察程式如何運行,並且是否符合我們預期的結果

    [![](https://4.bp.blogspot.com/-2hTcGcR7bAg/Wo0mas9EleI/AAAAAAAAIj0/H7T3H2WKSO0yF_lGqHIcrR-CxhuraR1egCLcBGAs/s400/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.56.54.png)](https://4.bp.blogspot.com/-2hTcGcR7bAg/Wo0mas9EleI/AAAAAAAAIj0/H7T3H2WKSO0yF_lGqHIcrR-CxhuraR1egCLcBGAs/s1600/%25E8%259E%25A2%25E5%25B9%2595%25E5%25BF%25AB%25E7%2585%25A7%2B2018-02-21%2B%25E4%25B8%258B%25E5%258D%25883.56.54.png)
  4. 當然,我們不必每次進入中斷點後都一步一步將它執行完,如果已經找到哪邊有問題,只要再按下繼續,程式就會自動往下跑,直到碰到下一個中斷點為止


    當然以上所提的,都是非常非常基礎的Visual Studio(之後文章都簡稱為VS)操作,跟日常比較會運用到的情境,VS之所以被人稱為地表最強編輯器,想當然耳不可能只有這樣而已,但這都是要慢慢開發中去體會跟累積的。相信如果真的用久了,開始使用擴充輔助套件之類的,就會知道它的強大之處。

下一篇來提何謂MVC,知道後應就能理解為何會知道關於這個按鈕點擊後,會停在我們今天下的中斷點了。