2017/08/30

[C#] 透過 JSON 進行資料前後端傳遞

透過 JSON 字串進行資料前後端傳遞已是非常普遍的作法。此作法好處在於可跨網域進行資料交換。這邊將記錄後端資料透過 Ajax 傳遞給前端、及兩種前端透過 Ajax 方式傳遞給後端的作法。

首先,先定義一下所需要的 Model 架構:
//C#: Model
public class Movie
{
    public int ID { get; set; }

    [StringLength(60, MinimumLength = 3)]
    [Required]
    public string Title { get; set; }

    [Display(Name = "Release Date")]
    [DataType(DataType.Date)]
    public DateTime ReleaseDate { get; set; }

    [Range(1, 100)]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
    [Required]
    [StringLength(30)]
    public string Genre { get; set; }

    [RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
    [StringLength(5)]
    [Required]
    public string Rating { get; set; }
}

1. 後端資料傳遞給前端

後端傳遞給前端的作法還滿單純的,只要建立一個回傳型別為
JsonResult
的 Action,並回傳一個 JSON 物件即可
//C#: Controller
public JsonResult GetMovies()
{
    List<Movie> lstMovie = new List(){
        new Movie
        {
            Title = "When Harry Met Sally",
            ReleaseDate = DateTime.Parse("1989-1-11"),
            Genre = "Romantic Comedy",
            Price = 7.99M
        },
        new Movie
        {
            Title = "Ghostbusters ",
            ReleaseDate = DateTime.Parse("1984-3-13"),
            Genre = "Comedy",
            Price = 8.99M
        },
        new Movie
        {
            Title = "Ghostbusters 2",
            ReleaseDate = DateTime.Parse("1986-2-23"),
            Genre = "Comedy",
            Price = 9.99M
        },
        new Movie
        {
            Title = "Rio Bravo",
            ReleaseDate = DateTime.Parse("1959-4-15"),
            Genre = "Western",
            Price = 3.99M
        }
    };

    return Json(new { MoviesList = JsonConvert.SerializeObject(lstMovie) });
}
前端接收到資料後,即可透過
data.PropertyName
方式取得資料。
//C#: View (JavaScript)
function GetMovies() {
    $.post(
        '@Url.Action("GetMovies")',
        '',
        function (data) {
            alert(typeof data.moviesList);  //return "string"
            var lst = JSON.parse(data.moviesList);
            alert(typeof lst); //return "object"
            for (var i = 0; i < lst.length; i++) {
                alert('Title: ' + lst[i].Title);
            }
        },
        'json');
}
然而由於範例中,Action 取得的是一個清單 (List<T>),因此在回傳時須將 List<T> 透過
JsonConvert.SerializeObject()
方式將清單內容序列化成字串,待前端取得資料後,再使用 JavaScript 的
JSON.parse()
將已序列字串的內容再次反序列回 JSON 物件。


2. 前端資料傳遞給後端

前端資料傳遞到後端有兩種方式,第一種是直接將 JSON 物件序列化後以字串方式傳遞給 Server 端,另一種是不產生 JSON,而是產生矩陣 (Array) 物件,並以「參數」的方式將矩陣方式傳遞給 Server 端。

2.1 使用 JSON

此種方式最主要的關鍵在於資料傳遞至 Server 端前,先將 JSON 物件透過
JSON.stringify()
方法轉換成序列化的字串。
//C#: View (JavaScript)
//透過已 stringifly 的字串,將資料傳至 Server 後端
function AddMovie_PassString() {

    var movie = [
        //Movie 1
        {
            "ID": 1,
            "Title": "movie title 1",
            "ReleaseDate": "2017-08-15",
            "Price" : 15.89,
            "Genre": "Comedy",
            "Rating": "4.5"
        },
        //Movie 2
        {
            "ID": 2,
            "Title": "movie title 2",
            "ReleaseDate": "2017-08-15",
            "Price": 14.99,
            "Genre": "Comedy",
            "Rating": "3.5"
        }];

    //Post data to server
    $.post(
        '@Url.Action("AddMovies_ByPassString")',
        { "dataPost": JSON.stringify(movie) },
        function (data) {
            //Do something here...
        },
        'json'
    );
}


當 Server 端接收到字串後,再透過
JsonConvert.DeserializeObject()
方式轉型成 List<T> 型別物件。
//C#: Controller
public JsonResult AddMovies_ByPassString(string dataPost)
{
    List lst = JsonConvert.DeserializeObject<List<Movie>>(dataPost);
    return Json(new { Result = true, Msg = "OK" });
}

2.2 使用 Array

上述 2.1 透過 JSON 物件麻煩處在於資料傳遞至後端時需要進行序列化的轉換,但若透過陣列作為參數進行傳遞,再搭配 MVC 的 DataBinding 的機制,資訊取得後並不需要任何型別轉換即可立即使用,似乎更加適合懶人的我。
//C#: View (JavaScript)

//透過已陣列物件,將資料傳至 Server 後端
function AddMovie_PassArray() {

    var data_array = [];
    data_array.push({
        "ID": 3,
        "Title": "movie by array 1",
        "ReleaseDate": "2017-08-15",
        "Price": 18.99,
        "Genre": "Comedy",
        "Rating": "4.5"
    });
    data_array.push({
        "ID": 4,
        "Title": "movie by array 2",
        "ReleaseDate": "2017-08-15",
        "Price": 21.98,
        "Genre": "Comedy",
        "Rating": "3"
    });

    //Post datas to server
    $.post(
        '@Url.Action("AddMovies_ByArrayObj")',
        { "dataPost": data_array },
        function (result) {
            //Do something here...
        },
        "json"
    );
}

//C#: Controller
public JsonResult AddMovies_ByArrayObj(IEnumerable dataPost)
{
    return Json(new { Result = true, Msg = "OK" });
}

2017/05/28

透過 Dapper 建立導覽屬性

使用過 Entity Framework 的開發者應該很難回到弱型別的 DataReader 或 DataTable 取值方式吧。因為目前有些專案使用傳統 WebForm 開發,而資料的讀取方式也採用傳統的弱型別方式存取,因此免不了手殘打錯字造成頁面噴掉的情形發生。

還好有了 Dapper,讓在舊的專案開發架構中,可以漸進導入強型別方式與資料庫進行溝通,再也不用擔心資料欄位 Key 錯或是型別給錯這種問題發生 (呃…至少在偵錯過程前,Visual Studio 會派出紅色毛毛蟲提示啦)。不過畢竟 Dapper 只是個替代方案,仍無法完整取代完整 ORM 框架的 Entity Framework,因此目前還沒辦法像 EF 一樣,透過
include()
的方式將關聯的類別資料一同載入以建立完整的導覽屬性。若要使用導覽屬性,目前得要透過「手動後製」方式完成囉。

首先,先定義我們的資料情境:


我們的目的是要將目前既有的所有課程以清單方式列出,而每筆課程資訊 (Course) 中,可一併取出該課程授課老師 (Teacher) 資訊及修課學生 (Student) 清單,而每個修課學生資訊中,還要取出該學生所住的地區資訊。完整的類別定義如下:

public class CourseStudent
{
    public int CID { get; set; }
    public int SID { get; set; }
    public virtual Course CourseClass { get; set; }
    public virtual IList<student> StudentClass { get; set; }
}

public class Course
{
    public int CID { get; set; }
    public int TID { get; set; }
    public string CourseName { get; set; }
    public virtual Teacher TeacherClass { get; set; }
    public virtual IList<student> StudentClass { get; set; }
}

public class Teacher
{
    public int TID { get; set; }
    public string Name { get; set; }
    public string Gender { get; set; }
}

public class Student
{
    public int SID { get; set; }
    public string Name { get; set; }
    public string Gender { get; set; }
    public int LID { get; set; }
    public virtual Location LocationClass { get; set; }
}

public class Location
{
    public int LID { get; set; }
    public string Name { get; set; }
}

接著要如何透過 Dapper 將完整的資訊放到 Course 類別呢? 以下展示兩種方式。

方法一

將所有資訊透過
QueryMultiple
將所需的所有資訊全部取出,再透過後製方式,建立導覽屬性:

//使用 QueryMultiple 將所需的資料全部取出
var results = conn.QueryMultiple(@"SELECT * FROM Course;
                                   SELECT * FROM Course c LEFT JOIN Teacher t on c.TID = T.TID;
                                   SELECT * FROM CourseStudent cs LEFT JOIN Student s ON cs.SID=s.SID;
                                   SELECT * FROM Location;
                                   SELECT * FROM Student;");
//依照順序讀取資料
var courses = results.Read<course>();
var teachers = results.Read<teacher>();
var course_students = results.Read<coursestudent>();
var locations = results.Read<location>();
var students = results.Read<student>();

//進行後製 => 手動建立導覽屬性
foreach (var course in courses)
{
    //取得這筆課程的 Teacher 資訊
    course.TeacherClass = teachers.Where(x => x.TID == course.TID).FirstOrDefault();
    //取得這筆課程的修課學生清單
    course.StudentClass = students.Where(s => (course_students.Where(x => x.CID == course.CID)
                                                               .Select(x => x.SID)).Contains(s.SID)).ToList();
    //將 Location 資訊一筆筆寫至每個 Student 資訊內
    foreach (var s in course.StudentClass)
    {
        s.LocationClass = locations.Where(l => l.LID == s.LID).FirstOrDefault();
    }
}

方法二

透過一個 SQL 將所有資訊全部取出,透過指定
splitOn
方式拆解取得的內容以自動建立導覽屬性:

var lookup = new Dictionary<int, Course>();
conn.Query<Course, Student, Location, Course>(@"
                SELECT c.*, s.*, l.*
                FROM Course c
                LEFT JOIN CourseStudent cs ON cs.CID = c.CID
                LEFT JOIN Student s ON cs.SID = s.SID
                LEFT JOIN Location l ON s.LID = l.LID
                ", (c, s, l) =>
{
    Course course;
    if (!lookup.TryGetValue(c.CID, out course))
    {
        lookup.Add(c.CID, course = c);
    }

    if (s != null && l != null)
    {
        s.LocationClass = l;
    }
    if (s!=null && course.StudentClass == null)
    {
        course.StudentClass = new List<student>();
    }
    if (s != null)
    {
        course.StudentClass.Add(s);
    }
    
    return course;
}, splitOn: "SID, LID").AsQueryable();
var resultList = lookup.Values;

這裡值得一提的是
splitOn
的用法。
splitOn
是告訴 Dapper 要將哪個欄位當作資料切割的起始點。預設的切割點為 ID 或 id。假設我們的切割點非預設,就必須要告訴 Dapper 切割點是哪個欄位 (多欄位以逗號分隔)。若取得的欄位如下:

OrderId | OrderDate | OrderOwerId | MemberId | MemberName | ProductId | ProductName

假設我們將 MemberId 及 ProductId 作為分割點,這樣 Dapper 就會依照分割點的欄位,將資料切割為三個物件 (Object)

目前只知道有這兩種方法,期待還有其他更方便的方法!


參考資料:
  1. Correct use of Multimapping in Dapper
  2. How do I map lists of nested objects with Dapper 
  3. Mapping Dapper Query to a Collection of Objects (which itself has a couple of Collections)
  4. Dapper簡明教程

2017/05/01

SQL Server Error 15404 解決方法

因為系統因素重新安裝 SQL Server,將 DB 附掛或重建後準備瀏覽「資料庫圖表」,發現出現這個錯誤訊息:
Error 15404 - Could not obtain information about Windows NT group/user '***\Administrator', error code 0x54b. 

解決方法其實還滿簡單的。只要把資料庫的 DB Owner 重設給 "sa",問題就解決了。
要如何知道目前各個 DB 的擁有者呢? 先執行以下 SQL:
select name, suser_sname(owner_sid) from sys.databases

接下來,透過以下語法將擁有者不屬於 sa 的資料庫重設給 sa:
ALTER AUTHORIZATION ON DATABASE::[DB_NAME] TO [sa]

完畢!


參考來源:
Could not obtain information about Windows NT group/user .......error code 0x534... 

2017/02/14

建立 TypeScript 開發環境

使用地表最強的 IDE Visual Studio 開發 TypeScript 不是件難事,尤其是 VS2015 以後已內建 TypeScript 環境,想要開發相關程式碼,只要打開 VS 直接編輯儲存即可。不過 VS 畢竟是開發大型專案的開發工具,若只是單純想要寫些 TypeScript 檔案,免費且跨平台的編輯工具,像 Sublime 及微軟的 Visual Studio Code,便是個好選擇。

若選用 Sublime, VS Code 等工具開發前端相關程式,編譯的環境除了現有套件外,就得要自己一步步的建立了。我自己依照參考的書籍及網路上的分享文嘗試,可說是百試百敗,不過試到最後,還是建立成功了,因此以下就是個紀錄文。未來環境重建時,至少還有個 Step by Step 可看。

建立前的前置作業:

1. 由於使用
npm
安裝套件,務必安裝 Node.js,並將路徑設到系統環境路徑裡
2. 簡單的編輯工具

Step.1 套件管理設定

  • 建立專案的目錄,並於命令中執行
    npm init
    初始化 Node.js 管理工具。此時將會要求建立一些專案的屬性內容,隨便填填就好。若不想填專案屬性內容,指令改下:
    npm init -y
  • 安裝 TypeScript:
    npm install typescript -g
  • 將 TypeScript 設為「運行時相依」(使用 -save):
    npm install typescript --save
  • 安裝任務管理工具 Gulp:
    npm install gulp -g
  • 於 node.js 初始化所建立的 package.json 中將 gulp 設為「運行時相依」:
    npm install gulp --save
    ,此時的設定檔大致如下:


  • 安裝編譯 TypeScript 程式碼的套件工具 gulp-typescript:
    npm install gulp-typescript --save-dev

Step.2 設定 TypeScript Definitions (Types)

  • 安裝全域性用於管理 TypeScript 中描述文件的工具 tsd (TypeScript Definitions):
    npm install tsd -g
  • 安裝完成後,執行
    tsd init
    以初始化,並會自動在專案根目錄中建立「typings」資料夾,裡面放置了許多描述文件。
  • TypeScript 1.5 以後版本支援自訂編譯設定檔,因此可在設定檔中針對不同的情況下設定不同的編譯條件 (詳細內容)。接著專案根目錄中建立 tsconfig.json 檔案,並填入以下內容:
    {
      "compilerOptions": {
      "target": "es5",
      "sourceMap": true
      }
    }
    
  • 於根目錄中建立 app (放置 .ts 檔案) 及 js (放置編譯後的 .js 檔案) 資料夾

Step.3 設定 Gulp ->快要完成了!

  • 該裝的都裝完後,接著要設定最重要的 gulp 了。我們可在 gulp 中設定許多任務 (如: 編譯 scss/sass …等),並可隨時監視檔案的變化,一有儲存即可自動進行編譯。
  • 首先,於專案根目錄中建立 gulpfile.js 檔案,並寫入以下內容:
    var gulp = require('gulp'),
        tsc = require('gulp-typescript'),
        tsProject = tsc.createProject('tsconfig.json');
    
        // path variables for files 
        var tsSource = './app/**/*.ts';
        var tsTypings = './typings/**/*.ts';
        var tsOutPath = './js/';
    
        // Main task responsible for compiling typescript files
        gulp.task('compile-ts', function() {
    
        var tsResult = gulp.src([tsSource, tsTypings])
            .pipe(tsc(tsProject));
    
        tsResult.dts.pipe(gulp.dest(tsOutPath));
        return tsResult.js.pipe(gulp.dest(tsOutPath));
    });
    
        // responsible for watching ts files to auto compile
        gulp.task('watch', function() {
        gulp.watch(tsSource, ['compile-ts']);
    });
    
    //default task which runs when you call gulp
    gulp.task('default', ['compile-ts', 'watch']);
    
  • 最後執行 gulp 指令啟動任務

  • 之後存放於 app 資料夾內的 .ts 檔案,一經儲存且編譯無誤狀況下,就會自動編譯並於 js 資料夾內產生相對應的 .js 檔了。

Node.js 指令補充

  • npm i -g typescript typings gulp
    等同於
    npm install -g typescript typings gulp
    -i
    =
    install
    ,
    -g
    : 全域性安裝意思
  • npm i -D gulp
    i -D
    =
    install --save-dev
    (縮寫)
  • --save
    會把相依套件名稱添加到 package.json 文件 dependencies 下,
    --save-dev
    則添加到 package.json 文件
    devDependencies
    下。dependencies 生產環境中需要的相依;devDependencies 為開發測試時所使用的相依,並不會實際部屬到正式環境中。

參考來源

2016/12/29

AutoMapper - 自動對應 Model

寫了一年多的 MVC 後,發現強型別真是太方便了。透過 Model Binding 的方式,真的加快了程式開發效率。不過若專案功能很龐大時,免不了多了很多為了功能而制定的 ViewModel,因此若沒有工具來協助,要將 ViewModel 取得的資料對應到 Model,真的手會打到斷啊! 還好,發現了 AutoMapper 套件,真是拯救了我啊!

假設目前有一個檔案資訊的 Model
public class FilesInfo
{
  public Guid FILE_ID { get; set; }        
  public string MODULE_ID { get; set; }
  public Guid? REF_CONTENT_ID { get; set; }
  public string FILE_TITLE { get; set; }
  public string FILE_FILENAME { get; set; }
  public string FILE_DL_FILENAME { get; set; }
  public int FILE_DL_TYPE { get; set; }
  public string FILE_PATH { get; set; }
  public string FILE_LINK { get; set; }
  public string FILE_URL { get; set; }
  public string FILE_DESCRIPTION { get; set; }
  public bool IS_ACTIVE { get; set; }
}

此時因為頁面上的功能設計關係,我不需要原來 FilesInfo 中那麼多欄位,但需要添加一個註解 (Remark) 的屬性,所以 ViewModel 會變成這樣:
public class FileInfoEditViewModel
{
  public Guid FILE_ID { get; set; }
  public string FILE_TITLE { get; set; }
  public string FILE_LINK { get; set; }
  public string FILE_URL { get; set; }
  public string FILE_DESCRIPTION { get; set; }
  public bool IS_ACTIVE { get; set; }

  //New property
  public string Remark { get; set; }
}


現在,要將自 DB 取到的 FilesInfo 資訊直接對應到 FileInfoEditViewModel 內。當然可以用傳統土法鍊鋼方式一筆筆設定,但這次直接用 AutoMapper,幾行程式就搞定啦!
var file = context.FILE_INFO.Find("id_1");
Mapper.Initialize(x => x.CreateMap());
//我比較偏好這樣: Mapper.Initialize(x => x.CreateMap()); 比較能知道由哪個 Model 對應到哪個 Model
Mapper.Initialize(x => x.CreateMap var mFileInfoEdit = Mapper.Map(file);

上面程式碼執行後,因 FilesInfo 沒有 Remark 對應 Property, 所以 Remark 獲得 NULL。若要在 Mapping 的時候直接給定預設值,可以用 ForMember 指定:
Mapper.Initialize(x => x.CreateMap()
.ForMember(dest => dest.Remark, opt => opt.MapFrom(src => DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + " 建立"))
);
model.FileInfoEdit = Mapper.Map(file);

很方便吧!

AutoMapper:
https://github.com/AutoMapper/AutoMapper/wiki

Bootstrap Modal 中,Client 端 Model Validation 失效解決辦法


問題: 在 MVC 專案中,動態產生的表單項目 (如 Textbox, Checkbox 等) 在表單送出 (Submit) 時,無法進行 Client 端驗證

解法:

#1 確認必要的 .js 是否都載入
jquery.validate.js
jquery.validate.unobtrusive.js

MVC Bundles:
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include("~/Scripts/jquery.validate*"));

#2 確認 Web.Config 是否啟用 unobtrusive validation
也就是 appSettings 區塊內的 ClientValidationEnabled 跟 UnobtrusiveJavaScriptEnabled 都要設為 true


#3 動態的表單內容產出後,需要重新綁定 unobtrusive validation
$.validator.unobtrusive.parse('FORM_ID_OR_HTML_ELEMENT');
parse 地方設定 form (頁面上所有的 form) 或是指定的表單 ID
$.get(
'@Url.Action("GetContent", "RenderSource")', { act: 'view' },
  function (data) {
    $('#ContentContainer').html(data);
    //rebind unobtrusive validation
    $.validator.unobtrusive.parse('form');
});

在表單送出前,透過 $('表單ID').valid() 即可知道 Client 端驗證是否正確
$('form#FormID').valid();


參考內容
  1. 謙卑式jQuery檢核範例-動態變更檢核設定 
  2. MVC client side validation not working with modal or Bootstrap model 
  3. Unobtrusive validation not working with dynamic content 
  4. Unobtrusive validation not working on Ajax loaded partial view

移除 Windows 10 快速存取 (Quick Access) 清單

Windows 10 的快速存取 (Quick Access) 雖然很方便,但長久使用上仍有許多問題。
最近發現釘選某個資料夾,結果卻發現那個資料夾路徑或是屬性關係,
導致無法直接開啟目的資料夾
更扯的是,也沒辦法將那個釘選項目移掉 (沒錯! 滑鼠右鍵「取消釘選」沒有任何作用)
這時,只能靠 Google 大神找方法囉。

主要有兩種方法。
第一種是直接到設定的資料夾中直接刪除
在檔案總管中打上以下位置,直接到「最近使用」的資料夾
%AppData%\Microsoft\Windows\Recent\AutomaticDestinations
接著,將這個檔案刪掉:
f01b4d95cf55d32a.automaticDestinations-ms
輕鬆把快速存取釘選的項目全部刪除囉!

第二種,直接下指令吧
cmd.exe /c del "%AppData%\Microsoft\Windows\Recent\AutomaticDestinations\f01b4d95cf55d32a.automaticDestinations-ms"
執行後,快速存取清單也完全清除囉!

參考來源
Fix Quick access Pinned Shortcuts Stuck or Not Working Correctly in Windows 10