2017年8月23日 星期三

C# 好用的 Log 紀錄 Class

這是一個用來記錄 Log的Class,只需要設定 filePath 大致上就可以運作了

public static class TraceLog
    {
        private static object _lockLog = new object();
        private static int _numberOfLines = 0;

        public static string filePath = @"C:\MyService_LOG_DIR";
        public static string serviceName = "MyService";        // 服務名稱
        public static int maxLines = 65536;      // Log 行數限制
        public static int traceLevel = 3;        // value is 1~9

        public static void Write(int pTraceLevel, string pSource, string message)
        {
            if (pTraceLevel <= traceLevel)
                Write(pSource + ":" + message);
        }
        public static void Write(string format, params object[] message)
        {
            Write(string.Format(format, message));
        }
        public static void Write(string message)
        {
            if (string.IsNullOrEmpty(filePath))
            {
                filePath = Directory.GetCurrentDirectory();
            }

            // 選擇檔案名稱
            string filename = filePath + string.Format("\\{0:yyyy}\\{0:MM}\\{1}.{0:yyyy-MM-dd}.txt",
                                                                    DateTime.Now,
                                                                    serviceName);
            FileInfo finfo = new FileInfo(filename);

            if (finfo.Directory.Exists == false)
            {
                finfo.Directory.Create();
            }

            if (_numberOfLines == 0)
                if (File.Exists(filename))
                    _numberOfLines = File.ReadAllLines(filename).Length;
                else
                    _numberOfLines = 0;
            if (_numberOfLines >= maxLines)
            {
                // 檔案夾搬移
                for (int i = 10; i > 0; i--)
                {
                    string currFile = filename.Substring(0, filename.Length - 4) + "." + i.ToString() + ".txt";
                    if (File.Exists(currFile))
                    {
                        if (i == 10)
                        {
                            File.Delete(currFile);
                            continue;
                        }

                        FileInfo f = new FileInfo(currFile);
                        f.MoveTo(currFile.Substring(0, filename.Length - 4) + "." + (i + 1).ToString() + ".txt");
                    }
                }

                // 重新計算行數
                finfo.MoveTo(filename.Substring(0, filename.Length - 4) + ".1.txt");
                _numberOfLines = 0;
            }


            // 訊息格式化
            string writeString = string.Format("{0:yyyy/MM/dd HH:mm:ss.ff} {1} {2}",
                                                DateTime.Now,
                                                serviceName + "." + System.Threading.Thread.CurrentThread.ManagedThreadId,
                                                message) + Environment.NewLine;
            // 寫Log前,先Lock
            lock (_lockLog)
            {
                File.AppendAllText(filename, writeString, Encoding.Unicode);
            }
            _numberOfLines++;
        }
    }

Windows 服務

適用於 vs2012

Step 1. 檔案>>新增>>專案,選擇 [Windows服務]

Step 2. 點選 Service1.cs的 Design模式,按右鍵 -> Add Installer (加入安裝程式)

Step 3. 點選 serviceInstaller1,這裡需要做一些設定
1) DisplayName 就是服務要顯示的名稱
2) Description 服務描述
3) ServiceName 服務的唯一名稱
4) StartType 啓動方式,初始值爲Manual (手動),當然要設定Automatic (自動) 呀
5) DelayedAutoStart 爲開機之後是否延遲啓動,設定為False


Step 4. 點選 serviceProcessInstaller1
設定只要改一個,Account 這裡決定服務的帳號與權限,我們可以設定爲LocalSystem (就是最大權限)


Step 5. 點選 Service1.cs 相關初始程式碼
裏頭主要包含了服務 OnStart,以及 OnStop,也就是服務開啟與結束所執行的動作
以下範例中的 TraceLog 是小太陽自行撰寫的紀錄Log Class,這個Class在這裡
        public Service1()
        {
            InitializeComponent();
            this.AutoLog = false;   // 因為沒有使用,所以關閉
        }
        protected override void OnStart(string[] args)
        {
            TraceLog.Write("Service OnStart");
        }
        protected override void OnStop()
        {
            TraceLog.Write("Service OnStop");
        }

Step 6. 接下來就是對程式進行編譯 [F6]

Step 7. 開啟 cmd視窗,必須要是[適用於 VS2012 的開發人員命令提示字元]
安裝服務
C:\MyService\bin\Debug>InstallUtil MyService.exe
解安裝服務
C:\MyService\bin\Debug>InstallUtil /u MyService.exe
開啟服務,這邊輸入的是 Step3 所設定的ServiceName
C:\MyService\bin\Debug>net start MyService
停止服務
C:\MyService\bin\Debug>net stop MyService



例外狀況:
Issue 1. 如果 InstallUtil 出現 「不是有效的 Win32 應用程式」,那是因為安裝的環境的問題
1) 請確認編譯時,專案屬性中的 平台目標設為 x86(或Any CPU);目標 Framework 設為 .Net Framework4 以下
2) 確認機器有安裝所需的.Net Framework 版本

Issue 2. 如果沒有 Visual Studio 命令提示字元,必須使用cmd 到指定目錄下執行
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319>InstallUtil.exe "C:\MyService\bin\Debug\MyService.exe"
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319>net start MyService
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319>net stop MyService
C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319>InstallUtil.exe /u "C:\MyService\bin\Debug\MyService.exe"







2017年6月1日 星期四

Angular2 與 C# WebAPI 一同部署到 IIS 上

花一些時間來撰寫包版筆記,因為中間遇到太多問題,折騰了不少時間呀...

首先,來實作 Angular2 部署到IIS 的部分
Step 1. 使用CMD 到專案根目錄,輸入下面這段,即可將 TypeScript 編譯成 .js 檔
ng build -prod

編譯完成後,在專案的根目錄會出現一個 dist 資料夾,內容如下:












Step 2. 將整個資料夾搬到 IIS 目錄下,並設定IIS
1) 右鍵點選站台 >> 新增網站
2) 站台名稱:輸入站台名稱
3) 實體路徑:選擇剛剛編譯的 dist 資料夾
4) 按下確定即完成部署


作到這個步驟已經完成了 Angular2 的部署了,但如果你有使用到 WebAPI 的話,必須要將WebAPI 與網站部署在一起,否則會有 Failed to load resource: the server responded with a status of 404 (Not Found) 這樣的錯誤出現。接著開始部署 WebAPI 的部分
Step 1. 在IIS 點選剛剛部署好的站台名稱,按右鍵選則新增應用程式
1) 別名:輸入WebAPI名稱
(這個名稱必須要跟Angular2所呼叫的路徑相同,例如前端呼叫的路徑是http://localhost/KyleAPI/api/values,其中KyleAPI就是應用程式的別名,而 api 是預設的route 路徑,values 就是 controller)
2) 實體路徑:選擇 WebAPI 編譯好的資料夾位子
3) 按下確定即完成

最後,部署完的結構如下:





Step 2. 之後,可以在 vs2012 直接發行來更新WebAPI,設定如下:
1) 右鍵點選方案總管的專案名稱,選擇發佈
2) 新增設定檔,並給予一個設定檔的名稱
3) 服務URL:輸入伺服器的IP
4) 網站/發行:輸入IIS部屬的位置,如上例為 test/KyleAPI
5) 使用者名稱/密碼:伺服器的帳密
6) 按下驗證連線,檢查是否連線成功
7) 如果沒有問題,之後即可直接透過發行來進行 WebAPI更新

上面已完成 Angular2 與 C# WebAPI 一同部署到 IIS 上,但實際上,我們還會遇到一些不預期的錯誤。
Issue 1. 因為小太陽的公司裡有些電腦不可以連外網,只能連結內部網路,有些需靠網路的 css 與 js 會有找不到的狀況。如下為 chrome console 所顯示的錯誤訊息:
http://10.56.xxx.xxx/assets/css/bootstrap-theme.min.css.map 404 (Not Found) 
http://10.56.xxx.xxx/assets/css/bootstrap.min.css.map 404 (Not Found) 
檢查了一下Angular2 根目錄的 index.html,發現有些 css 是直接透過外部網路連結
  <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css">
  <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
  <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
  <script type="text/javascript" src="http://netdna.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
解法就是將這些 css 與 js 抓回來,放到 \src\assets\css 與 \src\assets\js 裡面,並更改連結位置,再進行 ng build ,這時我們的 css 與 js 檔都是由 assets 所提供,就算不能連外網也可以正常瀏覽了。

Issue 2. 出現「Uncaught TypeError: Cannot read property 'apply' of undefined」的錯誤,這個錯誤折騰了小太陽很久,因為在網路上找到的討論串,只要將 zone.js 降版到 0.8.5、@angular/core 版本為 4.0.3 就不會出現這樣的錯誤,打開CMD 到專案根目錄輸入下面這兩段,就可以將 zone.js 與 angular/core 安裝為指定版本(連 package.json 也會一併更改)
   npm install @angular/core@4.0.3 --save
   npm install zone.js@0.8.5 --save
但是在公司裡還是有一些電腦會出現錯誤,後來才想到會不會是瀏覽器版本的問題,原來,公司舊電腦 xp/2003 等,所安裝的 chrome 版本是 38.0 版,由於有些 Javascript 函式不足,也會造成上面同樣的錯誤訊息。
之後試著將 Chrome 更新到最新版,卻又碰到另一個錯誤而無法安裝「安裝失敗,系統不支援您的 windows 版本」,原來Google Chrome 已停止支援 Windows XP 和 Windows Vista,而最後的封印版本為 49.0.2623.112 m。
去下載 Google Chrome v49.0.2623.112 Offline Installer (32-bit) 來進行安裝,之後就一切恢復正常了。



2017年5月26日 星期五

C# WebAPI 與 Angular2 透過 Http.post 傳遞參數做資料新增

之前一篇 提到,透過 http.get 來進行資料的 Query,Query 參數主要是透過網址列來傳遞。
而為了符合 RESTful 架構風格,因此在新增資料時,我們使用的是 http.post


承接上一篇的例子,在這邊我們仍以 ValuesController.cs 作為例子
Step 1. 同樣的步驟,我們在必須先建一個類別來接收 http.post 傳遞過來的參數,這邊我們命名為 MyData
    public class MyData
    {
        public string fab { get; set; }
        public string shop { get; set; }
        public string date { get; set; }
        public string qty { get; set; }
    }

Step 2. 將 POST預設的 code 標註起來,因為傳遞的參數不是一的字串(string),而是剛剛建立的 MyData 類別。
Http.post 就是從Post Body 取得參數,前面加的是[FromBody]。function 裡的內容就是透過 SQL 語法去將資料存到資料庫裡。
        //// POST api/values
        //public void Post([FromBody]string value)
        //{
        //}
        // POST api/values
        public void Post([FromBody]MyData value)
        {
            Console.Write(value);
        }

前端 Angular2的部分也很簡單,幾乎只是將原本的 get 改成 post 再多傳遞一個變數"body"
Step 3. 我們在網頁上多加一個按鈕,讓這個按鈕觸發 save()
save(){
        let options = new RequestOptions();
        let headers = new Headers({ 'Content-Type': 'application/json',
        'Accept': 'q=0.8;application/json;q=0.9' });
        options.headers = headers;
        let body = {
                'fab': 'Fab01',
                'shop': 'Shop01',
                'date': '2017/05/26',
                'qty': 100
        }

        let url = '/api/values/';
        this._http.post(url,body, options)
                .subscribe(next => console.log('ok'));
}


如此,即完成 Http.post 的實作。
這裡可以透過 Chrome 瀏覽器,打開 F12 編輯視窗,選擇 Network 可以看到傳送出去的http body如下:
Request Payload:
{ "fab": "Fab01", "shop": "Shop01", "date": "2017/05/26", "qty": 100 }
另外也可以 Fiddler 來查看 

2017年4月25日 星期二

建立 C# WebAPI 與 Angular2 做 Http.get 溝通

因為這個部分是小太陽一開始學習 Angular2 首要想完成的地方,中間當然遇到了許多難關,一一的克服後,撰寫這篇文章希望對大家有所幫助!
本範例是前端網頁傳遞多個Query參數,到後端Web API 搜尋資料庫後,將 DataTable 回傳到前端的範例。

Step 1. 建立 C# Web API (vs2012)
1) 新增專案 >> Web >> ASP.NET MVC 4 WEB 應用程式
2) 範本選取 Web API

Web API專案完成建立後,會預設建立一個 ValuesController.cs,我們以此Controller 來進行範例。
Step 2. 因為要傳遞多個參數,所以會用一個類別將參數接起來,所以這邊會建一個 QueryParams 類別,用來接參數。
    public class QueryParams
    {
        public string fab { get; set; }
        public string shop { get; set; }
        public string date { get; set; }    //  YYYY/MM/DD - YYYY/MM/DD
    }

Step 3.  Http.get 是透過 URL 來傳遞參數,所以在接參數的前面加一個 [FromUri],如果是 Http.post 就是從Post Body 取得參數,前面加的是[FromBody]。本範例就不著墨在DBConnector上面了,小太陽用的是 Oracle.ManagedDataAccess。
        // GET api/values
        //public IEnumerable<string> Get()
        //{
        //    return new string[] { "value1", "value2" };
        //}
        public DataTable Get([FromUri]QueryParams queryParams)
        {
            DBConnector db = new DBConnector("MyDB");
            string strSQL = @"select '{0}', '{1}', '{2}' from dual";
            strSQL = string.Format(strSQL, queryParams.fab
                                         , queryParams.shop
                                         , queryParams.date);
            string errorStr = "";
            DataTable dt = db.selectSQL(strSQL, out errorStr);
            return dt;
        }

接下來是前端 Angular2的部分,假設你已經透過這篇建好專案了 ng2 建立新專案 
並且解決 Cross Domain問題 解決 ng2 的 Cross Domain 問題
Step 4. 那麼我們開始在 app.component.ts 來呼叫 Http.get 吧
import { Component, OnInit } from '@angular/core';
import {URLSearchParams,  Headers,   Http,   RequestOptions} from '@angular/http';
@Component({
  selector: 'app-root',
  templateUrl: `
  <h1>
  <pre>{{data|async|json}}</pre>
</h1>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{
  data: any;
  constructor(private http: Http){}
  ngOnInit(){
    let options = new RequestOptions();
    let headers = new Headers({ 'Content-Type': 'application/json',
                                     'Accept': 'q=0.8;application/json;q=0.9' });
    options.headers = headers;
    let params: URLSearchParams = new URLSearchParams();
    params.set('fab', 'myFab');
    params.set('shop', 'myShop');
    params.set('date', '2017/04/24');
    options.search = params;

    let url = '/api/values/';
    this.data = this.http.get(url,options).map(response => response.json());
  }
}


如果是使用 Chrome 瀏覽器,打開 F12 編輯視窗,選擇 Network 可以看到傳送出去的網址如下:
Request URL:
http://localhost:4200/api/values/?fab=myFab&shop=myShop&date=2017/04/24

這時候在Web API 中設中斷點就可以看到傳進來的參數。
而Angular2 的畫面也會將回傳回來的 DataTable轉為 json放到 data 變數裡,下面是最後網頁呈現的結果:
[
  {
    "'MYFAB'": "myFab",
    "'MYSHOP'": "myShop",
    "'2017/04/24'": "2017/04/24"
  }
]

2017年4月17日 星期一

利用 Moment.js 將日期轉換為字串

Step 1. 安裝 moment 套件
npm install moment

Step 2. 在 TypeScript 中 import moment 套件
import * as moment from 'moment';

Step 3. 將日期格做轉為字串語法如下
var mDate = moment();
var today = mDate.format("MM/DD/YYYY");   // 轉為 MM/DD/YYYY 字串格式
var day7 = mDate.add(7,'days').format("MM/DD/YYYY");     // 加7天

2017年4月5日 星期三

解決 ng2 的 Cross Domain 問題

當我們在開發環境使用 Angular2 要去後端 Web API 取資料時,出現以下錯誤
No 'Access-Control-Allow-Origin' header is present on the requested resource.

這是因為後端的 Web API 沒有支援 Cross Origin Resource Sharing (CORS)
這時,我們可以在Client端設定,透過 Proxy 轉發網址

Step 1. 於根目錄(與package.json同層) 建立 proxy.config.json 檔案,內容如下
(其中 KyleAPI 是Web API 的名稱,target 指的是 Visual Studio 執行後產生 IIS Express 的站台IP)
{
  "/KyleAPI": {
    "target": "http://localhost:47496",
    "secure": false
  }
}

Step 2. 修改package.json檔案,將npm 指令中的 start 加上proxy-config,並指向proxy.config.json 檔案
"start": "ng serve",
↓↓↓
"start": "ng serve --proxy-config proxy.config.json",

Step 3. 重新啟動 npm start,並在程式中的 url 只要改成下面這段即可
    //let url = 'http://localhost:47496/KyleAPI/api/values/';
    let url = '/KyleAPI/api/values/';
這時我們的 proxy 設定會自動將 /KyleAPI/api/values/ 網址轉為 http://localhost:47496/KyleAPI/api/values/


當你將開發的 Angular2 與 Web API 部署到 IIS 上之後,請把他們放在一起(詳見這一篇),這時候就不需要這個設定了