---
# System prepended metadata

title: MVC
tags: [Learning]

---

# MVC
###### tags: `Learning`
### View 只 Show；Model 各種處理；Controller 簡單處理

## Debug

### 查看 print 的 html 內容
![](https://i.imgur.com/M5g3k0Y.png)

![](https://i.imgur.com/e7S1cvM.png)

/------------------------------------------------------------------------/
### 使用 Toggle Device ToolBar 看不出響應式頁面，可是拉動瀏覽器可以
解:
> Add viewport meta to your head tag
> 
> `<meta name="viewport" content="width=device-width, initial-scale=1.0">`
> 
> VIEWPORT
> 
> The viewport is the user's visible area of a web page.
> 
> The viewport varies with the device, and will be smaller on a mobile phone than on a computer screen.
> 
> Before tablets and mobile phones, web pages were designed only for computer screens, and it was common for web pages to have a static design and a fixed size.
> 
> Then, when we started surfing the internet using tablets and mobile phones, fixed size web pages were too large to fit the viewport. To fix this, browsers on those devices scaled down the entire web page to fit the screen.
> 
> The responsive design using chrome dev tool. But that is not working
> 
> CTRL + F5 ( Force refresh the webpage ) and then check it.

[參考](https://stackoverflow.com/questions/46972711/responsive-design-not-working-chrome-dev-tool-but-browser-does)

---

## 使用某個 dll 出現 PInvokeStackImbalance ( MDA )

![](https://i.imgur.com/dFWrRtK.png)

### 為什麼會出現 ?
有兩個要素：
* 在 32 位元的專案 Debug 模式會出現
* dll 可能是 C++ 寫的，轉譯過來有落差 (Ex. 型別、數值)

### 該怎麼解 ?
1. 專案配置為 64 位元 ( 其實新專案預設都是 64 位元了 )
工具 -> 選項 -> 專案與方案 -> Web 專案 -> 勾選 「將 64 位元版本的 IIS Express 用於網站和專案」
![](https://i.imgur.com/Cm2S6Up.png)
![](https://i.imgur.com/VWSRwN3.png)
平台目標最好也改一下 ( 雖然不改好像也沒差，尚未知道不改的差異 )
專案 -> 屬性 -> 建置 -> 平台目標 選擇 x64
![](https://i.imgur.com/zbliIKj.png)

2. 32 位元的專案 Debug 關閉 Visual Studio 在啟動 MDA 時是否擲回例外狀況，詳情參考 " 參考-PInvokeStackImbalance MDA " 連結

3. 用 啟動但不偵錯 ( Ctrl + F5 ) 執行 

[參考-.Rdlc Report in MVC project - Managed Debugging Assistant 'PInvokeStackImbalance'](https://stackoverflow.com/questions/57384402/rdlc-report-in-mvc-project-managed-debugging-assistant-pinvokestackimbalance)

[參考-PInvokeStackImbalance MDA](https://docs.microsoft.com/zh-tw/dotnet/framework/debug-trace-profile/pinvokestackimbalance-mda)


---

## 自訂驗證
[可參考](https://ithelp.ithome.com.tw/articles/10161328)
```
// 也可以不用 partial class 來寫，繼承 IValidatableObject 並實作就可
public partial class A
{
    // prop...
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}

public partial class A : IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (StartDate == EndDate)
            yield return new ValidationResult("error", new string[] { nameof(StartDate), nameof(EndDate) });
    }
}
```

---

## ValidateAntiForgeryToken
View 加上@Html.AntiForgeryToken()、
Action 加上 ValidateAntiForgeryToken 屬性，
才會去驗證前後端表單的 Key 是否相同 ( 防偽 )
※原理是看兩個加密後的字串解碼後是否相同
![](https://i.imgur.com/o4wTOF4.png)
![](https://i.imgur.com/09gwkvZ.png)

![](https://i.imgur.com/nc1R0fx.png)



---

## 自訂全站共用的Helper
[參考資料](https://ithelp.ithome.com.tw/articles/10160481)

---

## Q&A

**Q:**
![](https://i.imgur.com/sRMDsiW.png)
**A:**
Table CustomerBanks中的那個 FK 存在著 Table Custormer 的 PK 中沒有的值。


---

**Q:**
### Map.Route若要設Area
**A:**
![](https://i.imgur.com/01MpyWH.png)


---

**Q:**
### 使用Ajax.BeginForm需要注意的
**A:**
1. Nuget 安裝此套件↓
![](https://i.imgur.com/zSxPao4.png)
2. 引入套件
2-1. BundleConfig 加上一行
![](https://i.imgur.com/RtLan66.png)
2-2. 頁面中引入套件
![](https://i.imgur.com/6gzxDM3.png)


---

**Q:**
### Html.Editor 方法加入 Html 屬性元素 

**A:**
在參數"object additionalViewData"的位置書寫
`new { htmlAttributes = new { @onchange = "this.form.submit()" } }`

![](https://i.imgur.com/hHmYqTp.png)

---

**Q:**
### Datatables 使用語言插件變成亂碼
#### 前情提要：
這個語言插件是自己另存 Json 檔案，然後 Datatables 套件會去讀取。
原本的英文模樣
![](https://i.imgur.com/YB63sNS.png)
改成中文後變亂碼
![](https://i.imgur.com/vQ3JWJ3.png)


**A:**
因為 Json 檔案的編碼存的是 Big5 (繁中)，改成 UniCode 就好啦！

#### 要怎麼將檔案改編碼後儲存呢？
1. 另存檔案
![](https://i.imgur.com/EAqaHaF.png)
2. 選擇以編碼方式儲存
![](https://i.imgur.com/udkiv7r.png)
3. 選好你要的編碼
![](https://i.imgur.com/wn4D3sd.png)

完成！結果如下↓
![](https://i.imgur.com/7GrvwDq.png)

---

## 前端取得 Model 驗證錯誤訊息
```
@foreach (var item in ViewData.ModelState)
{
        <p>@item.Key : @string.Join("", item.Value.Errors.Select(x => x.ErrorMessage))</p>
}
```

---

## Post 檔案到後端要改寫 enctype = "multipart/form-data"
※一樣可以同時Post別的資料
※原本預設是x-www-form-urlencoded，只會Post文字
![](https://i.imgur.com/pQFSbre.png)

※特別注意 BeginForm 前兩個參數給 null，不給的話他會去預設值 => 當前頁面，不能給 string.empty，會去錯地方

---

## CheckBox 接值 & View 組成

### 寫法一
![](https://i.imgur.com/MnnHvBr.png)

※若用 Helper 的 CheckBox，微軟有一個同 name 的 hidden input，value 一直都會是 false，所以如果你用上圖的寫法，會收到 {"珍珠","false"}... 必須自己排除 ( 擴充或原生方法 )。
但是　!!　如果你原本值就是 true/false，還是能夠用 Helper 的方法

### 寫法二 ( 包含 View 的組成 )

#### 前端組成
![](https://i.imgur.com/gEypomo.png)

#### 後端接值
``` 
string[] memberIds 
```


---

## Repository
使用 Repository 來跟資料庫做連結
像是這有三個 Service 其實可以把 Repository 寫成泛型，一個檔案就能解決了
![](https://i.imgur.com/eqWMf3U.png)

※特別注意其中不會有 Edit 的 Function，因為你 GetSingle 撈出 DB Model 直接改欄位資料後再 Call SaveChange就好

---

## EF DB Model 小技巧
每次 Entity Framework 都會重建 DB Model，每次加上的 Attribute 會被洗掉，其實你可以自己建立一個 MetaData，定義好 Attribute，透過 MetaData 取資料就可以不用怕每次都被洗掉。

---

## Metadata & 自訂驗證方式
用 EF 產出的 Model 不可加上 Attribute，到時候 DB 更新 Model 重抓就會遺失

```
    [MetadataType(typeof(tblDocumentsMeta))]
    public partial class tblDocuments : IValidatableObject
    {
        internal sealed class tblDocumentsMeta 
        {
            [DisplayName("收文編號")]
            public string DocumentID { get; set; }

            [Required(ErrorMessage = "收文日期 為必填 !")]
            [DisplayName("收文日期")]
            [DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}", ApplyFormatInEditMode = true)]
            public System.DateTime DocumentDate { get; set; }

            public bool IsReceived { get; set; }

            [Required(ErrorMessage = "主旨 為必填 !")]
            [DisplayName("主旨")]
            [StringLength(200, ErrorMessage = "最多只能填 200 個字 !")]
            public string Subject { get; set; }

            [DisplayName("內容")]
            public string Content { get; set; }

            [Required(ErrorMessage = "來文單位 為必填 !")]
            [DisplayName("來文單位")]
            public string FromOrg { get; set; }

            [Required(ErrorMessage = "來文字號 為必填 !")]
            [DisplayName("來文字號")]
            public string FromID { get; set; }

            [Required(ErrorMessage = "來文日期 為必填 !")]
            [DisplayName("來文日期")]
            [DisplayFormat(DataFormatString = "{0:yyyy/MM/dd}", ApplyFormatInEditMode = true)]
            public Nullable<System.DateTime> FromDate { get; set; }

            [Required(ErrorMessage = "類別 為必填 !")]
            [DisplayName("類別")]
            public Nullable<int> Class { get; set; }

            [Required(ErrorMessage = "等級 為必填 !")]
            [DisplayName("等級")]
            public string Level { get; set; }

            [Required(ErrorMessage = "歸屬單位 為必填 !")]
            [DisplayName("歸屬單位")]
            public string DepartmentTo { get; set; }

            [DisplayName("DepartmentFrom")]
            public string DepartmentFrom { get; set; }

            [DisplayName("承辦人")]
            public string CaseOfficer { get; set; }

            public bool UseStatus { get; set; }

         
        }

        #region Validate
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var data = new HRContext().tblDocuments.ToList();

            // 檢查使用者填入的來文單位、來文日期、來文單號在資料庫裏面不得重複，否則顯示錯誤提示
            if (data.Any(x => x.FromOrg == FromOrg && x.FromDate == FromDate && x.FromID == FromID))
                yield return new ValidationResult($"來文單位: {FromOrg}、來文日期: {FromDate.Value.ToString("yyyy/MM/dd")}、來文單號: {FromID}，已存在", new string[] { "FromOrg" });
        }
        #endregion
    }

```

---

## 網頁取得當前目錄

### 一般網頁取得方式
```
根目錄
HostingEnvironment.MapPath("~/") <-- ROOT
當前目錄
HostingEnvironment.MapPath(".") <-- CURRENT DIRECTORY
父目錄
HostingEnvironment.MapPath("..") <-- PARENT DIRECTORY
```

### 如果是在別的網頁嵌入的網頁，存取檔案會有路徑問題

#### 擴充 Url 方法
```
public static class UrlHelperExtension
{
    public static string Absolute(this UrlHelper url, string relativeOrAbsolute)
    {
        var uri = new Uri(relativeOrAbsolute, UriKind.RelativeOrAbsolute);
        if (uri.IsAbsoluteUri)
            return relativeOrAbsolute;

        return ResolveServerUrl(VirtualPathUtility.ToAbsolute(relativeOrAbsolute), false);
    }

    static string ResolveServerUrl(string serverUrl, bool forceHttps)
    {
        if (serverUrl.IndexOf("://") > -1)
            return serverUrl;

        string newUrl = serverUrl;
        Uri originalUri = HttpContext.Current.Request.Url;
        newUrl = (forceHttps ? "https" : originalUri.Scheme) + "://" + originalUri.Authority + newUrl;
        return newUrl;
    }
}
```
#### 使用 ( 已讀取圖片 Url 為例
```
<div class="customHeaderBg" style="background-image:url(@Url.Absolute(Path.Combine("~/Images", "PythonReport","header.jpg"))) !important">
</div>
```

#### 呈現的結果
使用前的路徑
> background-image: url(/Images/PythonReport/header.jpg)

使用後的路徑
> background-image:url(http://localhost:6388/Images/PythonReport/header.jpg)

※ P.S. 後端只要這樣就好↓
```
Path.Combine($"{HostingEnvironment.MapPath("~/")}Reports", "Report_Python_Ver2.rdlc")
```

---

## IQueryable v.s IEnumberable Where
某天用 LINQ 時，遇到了無法寫成 Function 後 Return
![](https://i.imgur.com/lKYiU7R.png)

直接對 EF 的 DBSet 下 Where 無法使用 Lambda，因為 IQueryable、IEnumberable 的 Where 不同，這時的 Where 是 IQueryable 的。

```
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
```

IQueryable、IEnumberable 的區別
![](https://i.imgur.com/xXOxkDY.png)

[Linq中 AsQueryable(), AsEnumerable()和ToList()的區別和用法
](https://www.itread01.com/content/1544347817.html)
※P.S. Leo 說過 Foreach 的時候也會執行

結論：
AsEnumberable 後的 Where 可以展開成 Function
![](https://i.imgur.com/EIYYk5M.png)

![](https://i.imgur.com/KYTby6w.png)

只知道 Expression 不可展開成 Function 
沒搞懂 Expression 是什麼XD
但不懂陳述式主體和運算式樹狀架構是什麼 = =

[運算式主體成員 | Microsoft Docs](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/statements-expressions-operators/expression-bodied-members#read-only-properties)

[運算式樹狀架構 | Microsoft Docs](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/expression-trees/)

還是看不懂XD

---

## 前端接 DateTime 資料長這樣 /Date(1529888400000)/
### 原因
這是微軟原生的 Json 套件轉出來的 Datetime 格式 ( timestamp，時間戳 )

### 解法1
前端套件 moment.js
擷取字串數字部分 ( 可用 replace 因為可以用規則表示式XD )
```
moment(1541411221345) // Mon Nov 05 2018 17:47:01 GMT+0800
moment(1541411221345).format('YYYY-MM-DD') // 2018-11-05
moment(1541411221345).format('YYYY-MM-DDTHH:mm:ss.SSS') // 2018-11-05T17:47:01.345
```
### 解法2
後端使用 Json.Net(非微軟) 的套件去轉換 Json 格式，再傳給前端
#### 普通解法
Json.net 序列化資料後，return content(json);

#### 如果是新的專案可以直接 override 改寫 Json 
```
protected override JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
{
    if (behavior == JsonRequestBehavior.DenyGet
        && string.Equals(this.Request.HttpMethod, "GET",
                            StringComparison.OrdinalIgnoreCase))
        //Call JsonResult to throw the same exception as JsonResult
        return new JsonResult();
    return new JsonNetResult()
    {
        Data = data,
        ContentType = contentType,
        ContentEncoding = contentEncoding
    };
}

    public class JsonNetResult : JsonResult
    {
        public JsonSerializerSettings SerializerSettings { get; set; }
        public Formatting Formatting { get; set; }
        public JsonNetResult()
        {
            SerializerSettings = new JsonSerializerSettings();
        }

        public override void ExecuteResult(ControllerContext context)
        {
            if (context == null)
                throw new ArgumentNullException("context");
            HttpResponseBase response = context.HttpContext.Response;
            response.ContentType =
                !string.IsNullOrEmpty(ContentType) ? ContentType : "application/json";
            if (ContentEncoding != null)
                response.ContentEncoding = ContentEncoding;
            if (Data != null)
            {
                JsonTextWriter writer = new JsonTextWriter(response.Output)
                {
                    Formatting = Formatting
                };
                JsonSerializer serializer = JsonSerializer.Create(SerializerSettings);
                serializer.Serialize(writer, Data);
                writer.Flush();
            }
        }
    }

```

---

## 神奇的 ModelBinding

![](https://i.imgur.com/Bz4zhM8.png)
前端沒有特別定義 name 為 applyViewModel 的元素
也沒有看到 name 的格式是 applyViewModel_欄位名稱，但是還抓的到資料，神奇到炸。
尤其是後面的 formViewModel，他的資料是從 Table 來的，自己轉成陣列還排序好的

接 table 資料的 model
![](https://i.imgur.com/gOM1aWC.png)

前端 name 的部分都是沒有前綴的↓
![](https://i.imgur.com/bx8JBZE.png)

育佑查的說明
![](https://i.imgur.com/W30jzcb.png)

※在 Binding 的過程中，是從排序前面的 Model 開始比對塞值，所以若 applyViewModel 和 formViewModel 有同型別同名稱的欄位，會將該欄位資料塞在 applyViewModel

---

## 統一回傳結果

1. 定義 ResultInfo
```
public class ReturnInfo
{
    #region Return Code Values

    // Customized Error Messages
    public const int RC_UNKNOWN = -1;
    public const int RC_SUCCESS = 0;
    public const int RC_EXCEPTION = 1;
    public const int RC_INVALID_ARGUMENT = 2;
    public const int RC_INVALID_DATA = 3;

    // System Error Codes and Messages
    public const int RC_SESSION_TIMEOUT = 100001;

    #endregion

    public ReturnInfo(int code, string message)
    {
        Code = code;
        Message = message;
    }

    public int Code { get; private set; }
    public string Message { get; private set; }
}
```

2. 在 Action 中回傳 Json ReturnInfo。以下範例為 Insert 資料進資料庫
![](https://i.imgur.com/Hca4Jfa.png)

3. 前端 ajax 接值，決定成功/失敗要做甚麼事情。以下範例為: 成功重新讀取頁面/失敗跳出失敗訊息
![](https://i.imgur.com/K3oW8nH.png)

這樣前端也能知道後端發生甚麼事了XD

---

## 在 View 中組出 ViewModel ( List\<Model\>, Array ) !!

### ViewModel

```
public class MailNotificationViewModel
    {
        public string MailSubject { get; set; }

        public string MailContent { get; set; }

        public string[] RecipientIds { get; set; }

        public int EventId { get; set; }

        public string EventName { get; set; }

        public DateTime EventStartTime { get; set; }

        public DateTime EventEndTime { get; set; }

        public List<MemberOfEvent> MemberOfEvents { get; set; }

        /// <summary> 此活動的會員資料 </summary>
        public class MemberOfEvent
        {
            public int MemberId { get; set; }

            public string Name { get; set; }

            public string Email { get; set; }

            /// <summary> 是否已繳費 </summary>
            public bool IsPay { get; set; }
        }
    }
```

### Controller
```
public ActionResult MailNotification(int id)
{
    // 取得活動資料
    var eventInfo = _eventService.GetEvent(id);
    // 取得已報名會員
    var memberApplyOfEvents = _memberService.GetConfirmTickets(id);

    var viewModel = new MailNotificationViewModel()
    {
        EventId = id,
        EventName = eventInfo.Name,
        EventStartTime = eventInfo.StartDate,
        EventEndTime = eventInfo.EndDate,
        MemberOfEvents = new List<MailNotificationViewModel.MemberOfEvent>()
    };

    foreach (var memberApply in memberApplyOfEvents)
    {
        viewModel.MemberOfEvents.Add(new MailNotificationViewModel.MemberOfEvent()
        {
            MemberId = memberApply.MemberId,
            Name = memberApply.ApplyFormData.FirstOrDefault(x => x.Title == "姓名")?.Value,
            Email = memberApply.ApplyFormData.FirstOrDefault(x => x.Title == "電子郵件")?.Value,
            IsPay = memberApply.PayStatus.Value
        });
    }

    return View(viewModel);
}

[HttpPost]
public ActionResult MailNotification(MailNotificationViewModel viewModel)
{
    #region 資料檢核

    if (string.IsNullOrEmpty(viewModel.MailSubject))
        ModelState.AddModelError("", "請填寫主旨");

    #endregion

    // 若資料檢核不過，回去剛剛的頁面
    if (!ModelState.IsValid)
        return View(viewModel); 

    // 寄信
    _eventService.SendMailCustomNotification(viewModel.MailSubject, HttpUtility.HtmlDecode(viewModel.MailContent), viewModel.RecipientIds, viewModel.EventId);
    // 導回列表頁
    return RedirectToAction("Index");
}
```

### View

![](https://i.imgur.com/bow08mW.png)

---

## 回傳 IDictionary 的方法

![](https://i.imgur.com/1RZJBcb.png)

### name 解析

* FeatureRoles => IDictionary 變數名稱
* EventCategoryRole => Key
* Enabled => => Value

```
<input type="radio" id="FeatureRoles[EventCategoryRole]" name="FeatureRoles[EventCategoryRole]" value="Enabled" >&nbsp;啟用</label>
```

### Html
```
<div class="input-group mb-1">
    <div class="input-group-prepend">
        <span class="input-group-text justify-content-center bg-info text-white" style="min-width:125px">活動類別管理</span>
    </div>
    <div class="form-control d-flex justify-content-center">
        <label class="mr-3"><input type="radio" id="FeatureRoles[EventCategoryRole]" name="FeatureRoles[EventCategoryRole]" value="Enabled" >&nbsp;啟用</label>
        <label class="mr-3"><input type="radio" id="FeatureRoles[EventCategoryRole]" name="FeatureRoles[EventCategoryRole]" value="Disabled">&nbsp;停用</label>
    </div>
</div>
```

---

## 可以直接回傳 Script 做 Alert、導向 !!
```
if (model == null)
    return Content("<script>alert('沒有對應的活動，將導回主頁');location.replace('" + Url.Absolute(Url.Action("Index", "Home")) + "')</script>", "text/html");
```

---

### HttpClient 呼叫 (MVC5)

```
// 建立 HttpClient 實例
using (HttpClient client = new HttpClient())
{
    // 設置超時時間為10秒
    client.Timeout = TimeSpan.FromSeconds(10);

    try
    {
        string domain = HttpContext.Request.Url.GetLeftPart(UriPartial.Authority); // Controller 層
        //string domain = HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority); // Service 層加 Current
        // 設置要調用的專案的 URL
        string baseUrl = domain + "/NewPassdown/PMManager/CheckHasDefineParts";

        // 建立 UriBuilder 實例，並設置基礎 URL
        UriBuilder builder = new UriBuilder(baseUrl);

        // 添加查詢參數
        builder.Query = System.Web.HttpUtility.ParseQueryString(builder.Query).ToString();
        builder.Query += "&pmType=不定期"; // 添加第一個參數 (必填)
        builder.Query += "&pmNo=1238771"; // 添加第二個參數 (必填)
        builder.Query += "&sn=F5-TF-PSD-PMM-240220-3793"; // 添加第三個參數 (必填)
        builder.Query += "&toolG="; // 添加第四個參數 (可為 null)

        // 取得建構後的 URL
        string url = builder.ToString();

        // 發送 GET 請求並等待回應
        Task<HttpResponseMessage> responseTask = client.GetAsync(url);

        // 等待回應完成
        responseTask.Wait();

        // 取得回應
        HttpResponseMessage response = responseTask.Result;

        // 檢查回應是否成功
        if (response.IsSuccessStatusCode)
        {
            // 讀取回應內容
            Task<string> dataTask = response.Content.ReadAsStringAsync();
            dataTask.Wait();
            string data = dataTask.Result;
            Console.WriteLine(data);

            bool result;
            if (bool.TryParse(data, out result))
            {
                // 轉換成功，result 中包含了轉換後的布林值
                Console.WriteLine(result);
            }
            else
            {
                // 轉換失敗
                Console.WriteLine(string.Format("無法將字串轉換為布林值。[data: {0}]", data));
            }
        }
        else
        {
            Console.WriteLine("請求失敗: " + response.StatusCode);
        }
    }
    catch (HttpRequestException e)
    {
        Console.WriteLine("請求發生異常: " + e.Message);
    }
}
```

---