Pages

2015-12-20

Open Live Writer首帖

發表Google Blogger上的文章,我一向都用Windows Live Writer (簡稱WLW),但它很久前微軟官方就宣告不再繼續維護,前陣子就遇到登入錯誤問題,才注意到微軟已開源此套軟件,重新命名為「Open Live Writer」(以下簡稱OLW)。首次用OLW發表文章,使用UI和WLW差不多都類似,只是它抓不到Google Blogger的文章Tag資訊,這是OLW不如WLW的地方。

Meta enough for you?

關於OLW的介紹及軟件畫面,可詳見下面此文章。
Open Live Writer 全新開源,取代 Windows Live Writer 的部落格寫作工具 – 香腸炒魷魚
https://sofree.cc/open-live-writer/

2015-12-11

為EasyUI Pagination的Nav按鈕加上文字

EasyUI Pagination分頁控件的Nav按鈕列預設只有Icon圖示,沒有顯示文字,其缺點除了Icon配色不易外,寬度太窄滑鼠不好點按。尤其在前端顯示時,通常會使用plain=true(去框)的分頁效果,不加上文字實在更不好點。
 
分頁器的Nav按鈕是使用linkbutton控件做成的,它本身就有提供text屬性來顯示文字,只是預設沒有顯示它。由於Pagination控件官方沒有提供src源碼,因此只能從minify混淆過的js程式碼加入此功能。由於加上文字又牽扯到各國語言化的問題,我懶得牽一髮動全身,因此直接改/plugins/jquery.pagination.js,若需要顯示文字時再額外include該js即可。

以下是修改步驟做法:
  1. 開啟/plugins/jquery.pagination.js檔案,搜尋「$.fn.pagination.defaults={」字串(約252行),在它的預設button屬性定義後方加入一個控制變數showNavIcon,上方parser函式也加入定義(250行)。

     
  2. 搜尋「nav:{first:」字串(約256行),加入下方紅框text屬性值,並類推修改下方First,Prev,Next,Last,Refresh共5個text屬性值。


  3. 搜尋「a.linkbutton({iconCls:」字串(約112行),加入下面紅框2處,即當showNavIcon=false時,則不顯示icon,而text文字就看步驟2的定義。


  4. 儲存js檔案,在前端html頁面的easyui.min.js後加入pagination.js來重載(override).


  5. 分頁器的設定值可加上showNavIcon值,達到控制Icon是否顯示的效果。


  6. 最後html配上重載配色的linkbutton.css,pagination.css等相關的css值,即可達到前端分頁視覺效果。

2015-12-08

檔案版號日期命名工具

一般下載的應用程式安裝檔案,我偏好在其檔名後面加上版號+日期,以利分類收藏。例如下圖編號1的檔案,它的右鍵內容含有檔案版號(編號2)、產品版號(編號3)及修改日期(編號4),最後產生為編號5的新檔名。

image

寫了一個更名工具,只要把多選的檔案或目錄拉入這視窗,就能批次完成上述的更名動作。

 App Screenshot

由於每個人命名習慣及風格不同,因此我把命名規則邏輯寫在Config.json設定檔中,以方便每個人客製化。

image

設定檔的參數設定,意義解釋如下(Ln=行號):

Ln:2  設定檔版號,若太舊版本需要刪除Config.json檔,再讓程式自動產生。
Ln:3  檔案名稱的命名格式化字串 {變數名}
Ln:4  目錄名稱的命名格式化字串
Ln:6  替換變數名稱,你也可以自己命名。
Ln:7  檔名是否轉成UTF8繁體中文? (方便中文簡體檔名轉換)
Ln:11 版號會自動抓檔案屬性的FileVersion,若要使用產品版號則設為true。
Ln:12 目錄名稱缺乏版號版號,但也可以用日期變數來當版號。
Ln:16 日期變數預設是以目前時間產生,可設定.NET日期格式化字串。
Ln:17 若是想以檔案修改日期,則UseModifiedDate設為true即可。

檔案下載: (綠化免安裝,但需要支援.NET Framework 4.6)

百度雲盤下載 (VersionRenamer_20151208.zip, 310KB)

貼心功能:

  1. 檔案版號若為空時,程序會判斷格式化字串中的{version}變數前面字元若為空白,若刪除該空白字元。
  2. 不僅檔案能命名,目錄也可以更名,支援多檔或多目錄的拉曳方式。
  3. 更名失敗時,彈出錯誤訊息視窗。

2015-11-25

MVC聯絡表單Design Pattern

以下是一個MVC前端表單的設計模式,雖是我是第1次寫MVC,但有隱約把握到MVC的分層精神,自己的心得體會給大家參考看看。

Form Submit

ASP.NET MVC5裏前端View裏,那些@Html.XXXFor(),ValidateFor()這類的標籤輸助函式我都儘量不用,因為它不是標準的HTML,還得依賴Server端的解讀。雖然它能簡化產生html碼,但其轉譯代價明顯不合算。View提供的功能應該是協助前端HTML,JS而存在,而不是去取代或扭曲它,不足之處就靠前端JS框架來補足即可。事實上證明,下一版的MVC6提出Tag Helper,它的方式比較適合一些。上面表單驗證我使用jQuery Validation來取代MVC提供的Model Validation的前端作法,但保持其後端的ModelState驗證檢查。

Controller Action的表單接受方式為下圖,其精神就是儘量把商業邏輯寫入Model裏, Controller只寫「很簡單」的頁面請求服務判斷。這也是為何MVVM(Model-View-ViewModel)模式會被提出的理由,因為HTML的JS AJAX已經明顯選擇好了服務URL,Controller已不像字面上的「控制者」角色,精確來說它只是一個請求轉貼者(Forwarder),重點應在Service Model如何處理請求及讀寫DB (with Repository Model),最後產生ViewModel帶入View中顯示而己。

Action Code

既然MVC的Controller是虛職角色,真正處理請求邏輯的Model目錄,就得依其功能分類成主要三類:「Repository, Service, ViewModel」。你可以理解各種水果(DbTable)有它們削皮處理的方式,那麼Respository就是處理單一水果讀取的角色,而把它們拼成不同的水果盤,就是Service角色,包括各種水果再切片搭配或舖盤美化。最後完整的水果盤被端出,就是所謂的ViewModel。

Model Category

很多人會關心MVC裏,SQL命令該怎麼執行?是繼續寫串接式的SQL字串,還是該用Entity化的EF LINQ查詢?若有過ORM的開發經驗,你會很清楚EF的定位是啥,我很認同某人說過「別糾結EF查詢最終轉成什麼SQL法,EF本來對於複雜查詢本來就不是它的目的」。儘管如此,很多人還是會在EF技術群裏,糾結詢問複雜的DB查詢,LINQ該怎麼寫?產生的SQL差異效能如何?我提供一個比較簡單的比喻,傳統SQL寫法就像刀子,而EF是槍,我們要完成的DB動作目的就是要殺生。槍雖然威力強,但子彈成本貴(LINQ轉成SQL),假如要殺一隻老鼠,應該用刀子簡單省錢,因為開槍也是讓子彈像刀子一樣快速射入目標體內,剝奪生命而己。然而,槍畢竟不是刀,刀子除了殺生還能削片處理細節,槍就很難辦到了(LINQ複雜JOIN語法)。

簡言之,當複雜SQL報表查詢,你應該用SQL來寫,若覺得字串相加串條件的寫法很討厭,我這兒提供一個SQL範本式Pattern,能解決SQL動態條件字串問題。以我的作法而言,會先考量查詢效能及管理維護成本,再建立一個Service Model類別來管理DB操作函式,依目的選擇EF或SQL,或兩者併用,只要有統一類別管理它們,它們就能和平共處,共造雙贏局面。

 Model Code

至於,EF Data Model的POCO Class產生方式,我建議使用「CodeFirst from existing database」的工具方式來產生POCO,雖然它缺乏像*.edmx的UI管理工具,但其在連線字串、欄位微調會比EDMX精簡很多。雖然至目前我仍以DB First為主的開發方式,但Code First的優點卻是立即採用(聽說EF7也沒有EDMX了)!

2015-11-19

MVC5 ResolveUrl in static code

ASP.NET MVC5常用Url.Content(virtualPath)來取得實際的URL,若是在static程式區塊裏,要取得頁面實際路徑的簡易方式為System.Web.VirtualPathUtility.ToAbsolute(virtualPath),然而它無法處理http://路徑(會丟出例外),因此要額外處理它:

public static string ResolveUrl(string virtualPath)
{
if (virtualPath == null)
return null;

// VirtualPathUtility can't handle the http://xxx path.
if (virtualPath.IndexOf("://") != -1)
return virtualPath;

// Resolve the ~/ tilde.
if (virtualPath.StartsWith("~"))
return System.Web.VirtualPathUtility.ToAbsolute(virtualPath);

return virtualPath;
}

2015-10-22

MVC的分頁及分組顯示

昔日WebForm有ListView控件,可以輕易分頁及分組顯示,到了MVC沒有Server控件就得自力而為。找到幾個被推薦的分頁UI控件,不過該選擇哪一項,得確認它能滿足以下的需求:

  1. 該分頁控件能持續被維護。
  2. 產生的分頁數字顯示能修改CSS樣式,配合前端網頁風格。
  3. 分頁的顯示方式、文字可以輕易調整修改。

最後選出下面的項目候選:

以MVC的精神,EasyUI分頁是最好的方式,簡單又有提供豐富JS控制函式,但其缺點就是Prev/Next等是用圖示顯示,要修改也不容易(EasyUI雖然開源,但js被minifier且混淆過)。

EasyUI Pager Style

Telerik Kendo UI最符合商業持續被維護的原則且也很美觀,但它的分頁CSS及行為控制,就比較難客製化,而且又得學習其Template Hash語法,後端AJAX綁定也是自己的物件方式,雖然都不用寫啥程式,但受制於人。

Kendo Pager Style

MvcPaging是老牌的分頁控件,不過分頁樣式有點少,要改文字須自己實作Output輸出,有點Dirty。最後覺得X.PagedList的分頁有提供屬性設定,這會比較適合簡易客製化,尤其它的顯示文字及CSS表都容易調整。

PagedList Pager Style

XPagedList Properties

具體教學及樣式,請看這篇教學文章:
mrkt 的程式學習筆記: ASP.NET MVC 資料分頁 - 使用 PagedList.Mvc
http://kevintsengtw.blogspot.tw/2013/10/aspnet-mvc-pagedlistmvc_17.html

站在客戶角度,其實對分頁顯示樣式不是很要求,只要配色能搭配整體即可。其實分頁功能自己寫也行,只是我覺得不用重複造車,遇到客戶特別要求再來研究不遲。因此我個人是建議使用EasyUI分頁即可,其次是X.PagedList。

Server端分頁控件的使用,大抵上就是調整好樣式屬性,最後輸出分頁數字,再綁定JS Click事件以AJAX取出後端的PartialView(依頁碼取出該頁資料,並透過(StaticPagedList<T>()顯示分頁),並更新分頁資料的顯示及分頁狀態。簡易分頁條件用AJAX GET方式即可,複雜的分頁+搜尋條件就用POST+JSON方式。

最後,分頁資料要分組顯示(Group),即一列顯示幾個Item的意思,例如下面圖片中的Table為4欄為一列。

Group List

使用Razor語法就是使用foreach+%餘數判斷式,即可達到分組顯示功能。

Group Razor

2015-09-30

國泰世華ATM虛擬帳號編碼

網購業者收款確認的事一向是最煩鎖的事,若有公司營業登記,可以向國泰世華申請ATM虛擬帳號,就可以發給買家匯款指定的帳號,等入帳後再使用銀行提供的即時對帳API(存取頻率最低3分鐘)作查稽。

國泰世華發行的虛擬帳號長度有11, 14及16字長,前2者可以使用臨櫃匯款的方式繳納(匯款單的帳號長度是14長)。若有限繳時間的檢查及訂單編號的綁定,則使用16字長編碼才夠。

編號原則: 16位編碼 = 企業代碼[4] + 限繳日期MMDDHH[6] + 自訂編碼[5] + 檢核查[1]

送交銀行的申請表單勾核選項如下:

ATM Apply

使用國泰ATM虛擬帳號不需要事先在銀行開立允許列表,只要符合其編碼原則文件規範即可。

ATM Encoding

發現這虛擬帳號的檢核碼規則,跟台灣身份字號的最後1碼是邏輯相同,只差別在於權數不同而己。我的程式應用是採用16位帳號+限繳日期yyymmhh+交易金額+檢核碼的驗證方式,最後2組檢核碼相加須要取餘數,這點官方文件沒提到。

分享我的國泰世華16位虛擬帳號的C#源碼下:

void Main()
{
MyVirtualAccount provider = new MyVirtualAccount();

// 限繳日期設定
DateTime expDate = new DateTime(2015, 08, 31, 12, 00, 00);

// 產生10筆16位長編碼
for (int i = 1; i <= 10; i++)
{
var s = provider.CreateAccount16(
prefix4: "9999"
, expDate: expDate
, billNo5: i.ToString().PadLeft(5, '0')
, amount: 4);

s.Dump();
}
}

///
/// 國泰世華-虛擬帳號產碼類別
///

/// Author: Tomex Ou
/// Update: 2015-08-25 13:56:30
public class MyVirtualAccount
{

///
/// Creates Len=16 Account
///

/// Customer ID (Len=4), 公司編碼="9999"
/// Expired Date (GetNextFullHour)取下個整時刻
/// Bill No (Len=5, 左補0)
/// Amount value金額
///
public string CreateAccount16(string prefix4, DateTime expDate, string billNo5, decimal amount)
{
// -------------------------------------------------------------
// Arguments Validating.
// -------------------------------------------------------------
if (prefix4.Length != 4)
throw new Exception("The length of Customer ID must be 4.");

if (billNo5.Length != 5)
throw new Exception("The length of Bill No. must be 5.");


int accCheck, amtCheck, value;

// -------------------------------------------------------------
// accCheck for Account
// -------------------------------------------------------------
string strAccount = prefix4 + this.GetNextFullHour(expDate).ToString("MMddHH") + billNo5;
int[] accWeights = { 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // Len=15
int accSum = 0;
for (int i = 0; i < accWeights.Length; i++)
{
value = (int)Char.GetNumericValue(strAccount[i]); // char '0' to int32.
accSum += (value * accWeights[i]) % 10;
}

accCheck = this.GetCheckValue(accSum);


// -------------------------------------------------------------
// amtCheck for Amount
// -------------------------------------------------------------
int[] amtWeights = { 8, 7, 6, 5, 4, 3, 2, 1 };
int[] amtIntArray = this.CreateAmountIntArray(amount, amtWeights.Length);
int amtSum = 0;
for (int i = 0; i < amtWeights.Length; i++)
{
value = amtIntArray[i];
amtSum += (value * amtWeights[i]) % 10;
}

amtCheck = this.GetCheckValue(amtSum);


// -------------------------------------------------------------
// Full check
// -------------------------------------------------------------
int fullCheck = (accCheck + amtCheck) % 10;

return strAccount + fullCheck;
}


///
/// 取得Sum值的檢核碼
///

private int GetCheckValue(int sum)
{
int d0 = sum % 10; // 個位數
return (d0 == 0) ? 0 : 10 - d0;
}



///
/// 限繳日期的下一個整點時刻
///

public DateTime GetNextFullHour(DateTime theDate)
{
double nextFullHour = Math.Ceiling(theDate.TimeOfDay.TotalHours);
return theDate.Date.AddHours(nextFullHour);
}


///
/// 金額的int陣列 (8位長)
///

private int[] CreateAmountIntArray(decimal amount, int totalWidth = 8)
{
// 1200.00 => 1200 => ["00001200"]
string strAmount = decimal.ToInt32(amount).ToString().PadLeft(totalWidth, '0');

if (strAmount.Length != 8)
throw new Exception("The length of amount must <= 8.");

int[] intArray = new int[strAmount.Length];
for (int i = 0; i < strAmount.Length; i++)
{
intArray[i] = (int)Char.GetNumericValue(strAmount[i]); // char '0' to int32
}

return intArray;
}

}

發行的虛擬帳號在轉帳時,必須符合限繳時間+交易金額+檢核碼的規則,否則轉帳時會出現錯誤,無法入帳。


ATM Error

2015-09-23

XML+LINQ片段讀取解析

讀取XML資料雖然方法各異,但第一原則還是得考慮效能問題,若是來源XML資料是很龐大,就必須使用XmlReader循序讀入,而非貪圖方便用XML DOM類別(XmlDocument, XDocument)讀入記憶體,例如「台灣3+2郵遞區號XML查詢」的檔案讀取。若是片段的XML字串,則以語法搜尋簡便為原則,以前使用XmlDocument + XPath方式讀取資料,但新式做法用XDocument + LINQ更為簡便。

以下是C#範例程式碼:

void Main()
{
string xmlString = @"



10058
臺北市
中正區
八德路1段



10079
臺北市
中正區
三元街
單全



".Trim(); // XML前面不能有空白

//Elements
try
{
// XML字串若有錯誤,在Parse()會丟出例外
var xDoc = XDocument.Parse(xmlString);

// xDoc.Root =
var items = from x in xDoc.Root.Elements("Zip32")
select new Zip32Item {
// Element(name)可能為null
Zip5 = x.Element("Zip5").Value.Trim()
, City = x.Element("City").Value.Trim()
, Area = x.Element("Area").Value.Trim()
, Road = x.Element("Road").Value.Trim()
, Scope = x.Element("Scope").Value.Trim()
};

// 列出內容
items.Dump();
}
catch (Exception ex)
{
ex.ToString().Dump();
}

}

// XML節點強型別Class宣告
public class Zip32Item
{
public string Zip5 { get; set; }
public string City { get; set; }
public string Area { get; set; }
public string Road { get; set; }
public string Scope { get; set; }
}

讀出XML結果為:


image


以上是將XML片段讀取並變成強型別Class的簡易方式。若能選擇來源格式,使用JSON字串會是更便潔好用的方式,因為它能直接反序列化為物件。

2015-09-16

微信WeChat公眾平台

微信公眾平台就是騰訊官方開放微信的訊息收發API,讓組織或企業在微信APP裏建立自己的組識平台,可以讓用戶透過微信關注及訂閱該群組,就可以查詢及推送訊息,與用戶互動。這些預先建立的圖文,可以在微信公眾平台裏的網站定義好,定好一些訊息規則,就能主動推送給用戶。例如下圖是我的一個微信作品:

WeChat App

圖文訊息除了事先定義外,微信平台也允許在收到特定訊息時就主動POST資料到指定的網址,因此可以存取自身的資料庫,再將查詢結果回信至微信APP給用戶。整個訊息流運作如下圖:(此圖來源:這兒)

WeChat Flow

微信官方企業號API文件: http://qydev.weixin.qq.com/wiki/index.php

Senparc.Weixin.MP是第三方提供的微信C#版SDK,可以簡化開發工作,而且是開源免費的,中文學習教程在這兒

2015-09-09

字串編碼Encoding CodePage/CodeName List

很多公家機關及銀行資料查詢仍是以big5為主,假如沒有歷史包袱,新的程序就儘量使用utf-8編碼吧!

// Converts big5 into utf-8 encoding.
byte[] big5Bytes = XXX();
byte[] utf8Bytes = Encoding.Convert(Encoding.GetEncoding("big5"), Encoding.UTF8, big5Bytes);
string result = Encoding.UTF8.GetString(utf8Bytes);


要產生指定的CodePage(int)及CodeName(string)的方式:

using System.Text;

void Main()
{
// For every encoding, get the property values.
foreach (EncodingInfo ei in Encoding.GetEncodings() )
{
Encoding e = ei.GetEncoding();
Console.WriteLine("{0,-6} {1}", ei.CodePage, ei.Name);
}
}

產生的列表如下:


Encoding CodePage List

37     IBM037
437    IBM437
500    IBM500
708    ASMO-708
720    DOS-720
737    ibm737
775    ibm775
850    ibm850
852    ibm852
855    IBM855
857    ibm857
858    IBM00858
860    IBM860
861    ibm861
862    DOS-862
863    IBM863
864    IBM864
865    IBM865
866    cp866
869    ibm869
870    IBM870
874    windows-874
875    cp875
932    shift_jis
936    gb2312
949    ks_c_5601-1987
950    big5
1026   IBM1026
1047   IBM01047
1140   IBM01140
1141   IBM01141
1142   IBM01142
1143   IBM01143
1144   IBM01144
1145   IBM01145
1146   IBM01146
1147   IBM01147
1148   IBM01148
1149   IBM01149
1200   utf-16
1201   utf-16BE
1250   windows-1250
1251   windows-1251
1252   Windows-1252
1253   windows-1253
1254   windows-1254
1255   windows-1255
1256   windows-1256
1257   windows-1257
1258   windows-1258
1361   Johab
10000  macintosh
10001  x-mac-japanese
10002  x-mac-chinesetrad
10003  x-mac-korean
10004  x-mac-arabic
10005  x-mac-hebrew
10006  x-mac-greek
10007  x-mac-cyrillic
10008  x-mac-chinesesimp
10010  x-mac-romanian
10017  x-mac-ukrainian
10021  x-mac-thai
10029  x-mac-ce
10079  x-mac-icelandic
10081  x-mac-turkish
10082  x-mac-croatian
12000  utf-32
12001  utf-32BE
20000  x-Chinese-CNS
20001  x-cp20001
20002  x-Chinese-Eten
20003  x-cp20003
20004  x-cp20004
20005  x-cp20005
20105  x-IA5
20106  x-IA5-German
20107  x-IA5-Swedish
20108  x-IA5-Norwegian
20127  us-ascii
20261  x-cp20261
20269  x-cp20269
20273  IBM273
20277  IBM277
20278  IBM278
20280  IBM280
20284  IBM284
20285  IBM285
20290  IBM290
20297  IBM297
20420  IBM420
20423  IBM423
20424  IBM424
20833  x-EBCDIC-KoreanExtended
20838  IBM-Thai
20866  koi8-r
20871  IBM871
20880  IBM880
20905  IBM905
20924  IBM00924
20932  EUC-JP
20936  x-cp20936
20949  x-cp20949
21025  cp1025
21866  koi8-u
28591  iso-8859-1
28592  iso-8859-2
28593  iso-8859-3
28594  iso-8859-4
28595  iso-8859-5
28596  iso-8859-6
28597  iso-8859-7
28598  iso-8859-8
28599  iso-8859-9
28603  iso-8859-13
28605  iso-8859-15
29001  x-Europa
38598  iso-8859-8-i
50220  iso-2022-jp
50221  csISO2022JP
50222  iso-2022-jp
50225  iso-2022-kr
50227  x-cp50227
51932  euc-jp
51936  EUC-CN
51949  euc-kr
52936  hz-gb-2312
54936  GB18030
57002  x-iscii-de
57003  x-iscii-be
57004  x-iscii-ta
57005  x-iscii-te
57006  x-iscii-as
57007  x-iscii-or
57008  x-iscii-ka
57009  x-iscii-ma
57010  x-iscii-gu
57011  x-iscii-pa
65000  utf-7
65001  utf-8

2015-09-05

LINQPad強制不升級

LINQPad軟件本身強迫自動升級至最新版本,沒有任何設定選項能取消AutoUpdate。依官方文件問答,若不想升級的話,必須設定啟動程式參數-noupdate,以下是建立「LINQPad NoUpdate Loader.bat」的內容:

@echo off
start /b LINQPad.exe -noupdate

若不幸已自動升級了,就算把舊版本檔案蓋回來,啟動時仍會被蓋新檔。原來LINQPad自動升級的新版本下載會放在「C:\ProgramData\LINQPad\」目錄裏,它一比對到舊版本,就會蓋檔,因此刪除此目錄就可避免強迫升級。

LINQPad Logo

2015-09-02

顯示Exception錯誤的詳細資訊

為了顯示程序Exception例外錯誤的資訊,直覺會使用ex.Message訊息,但它其實不夠詳細,以下列出三種錯誤資訊的差別。

Exception Code Snippet

發現ToString()就列出了最完整的錯誤資訊,所以內部偵錯階段,請直接使用ex.ToString()函式吧!

Exception Msg

2015-08-26

ASP.NET發送三竹簡訊服務

購物網站在對買家發送下單簡訊,可利用一些簡訊服務商的WebAPI來傳送,以三竹簡訊為例,申請流程為:

  1. 註冊公司帳號(純一編號),向業務要求開通WebAPI存取的權限,對方會提供API SDK文件,並贈送100點作測試。
    API Documents
  2. 寄發單筆簡訊是以Http GET呼叫API,Doc文件都有提供Link範例,多筆簡訊則用Http POST方式,內容以INI文件格式撰寫,但只支援Big5編碼。由於單次發量沒有很多筆,因此這多筆API POST就略過沒測試:
    Multiple Sample
  3. 為了支援多國語言,測試其Unicode簡訊函式,使用utf-8傳送。相關參數只需要填寫「必要」欄位即可,其他欄位不填。
    API Params
  4. 多國語言簡訊API沒有多筆發送POST模式,因此程式裏使用loop迴圈,每筆間隔1秒來實作群發功能。

要呼叫遠端Web API,可以使用.NET 4.0的WebClient或HttpWebRequest類別來實作,我是使用.NET 4.5以上才具有的HttpClient類別,不使用它的非同步功能。

Code Snippet

後台簡訊設定畫面:

SMS Config

2015-08-22

Web虛擬路徑轉換成實體路徑(MapPath)

要在Web環境將虛擬路徑(~/XXX/)轉換成實體磁碟Path, 一般都會使用下列4種函式作轉換:

  1. Page.MapPath()
  2. Server.MapPath()
  3. Request.MapPath()
  4. HttpContext.Current.Server.MapPath()

當把處理函式整理成Model類別時,為了減少傳入Page類別,就會使用第4種方式。然而若呼叫者(Caller)不是由Web頁面觸發的話,就會引起HttpContext.Current類別Null Reference錯誤。

因此在.NET 4.5以後版本,建議使用:

string phyPath = System.Web.Hosting.HostingEnvironment.MapPath(virPath);

這個方式即使在Global.asax中使用,也能正確解析虛擬相對路徑,是包裝Model Class中取得實體路徑較為建議的方式。

2015-08-19

NuGet Source Unreachable問題

當在VS裏使用NuGet Package Manager Console安裝套件時,有時候會出現以下錯誤:
The source at nuget.org [https://www.nuget.org/api/v2/] is unreachable. Falling back to NuGet Local Cache at XXX.

NuGet Source Unreachable

建立新的方案,則NuGet運作正常,估計某個地方出錯了。重開VS程式或可修正,但急於當下,可直接加下-Source選項指令:
PM> Install-Package bootstrap -Source nuget.org

網上討論連結:
c# - Source unreachable when using the NuGet Package Manager Console - Stack Overflow http://stackoverflow.com/questions/24025409/source-unreachable-when-using-the-nuget-package-manager-console

2015-08-12

Taipei Free臺北公眾區免費無線上網方式

使用Taipei Free 臺北公眾區免費無線上網時,無論桌機或手機連上「TPE-Free」WiFi SSID後,仍需要開啟登入網頁輸入帳密。雖然帳密可以記憶下來,但有時仍會發生無法自動登入的問題。以下提供依自己經驗摸索而來的直接上網方式。

桌機版:

1. 連上「TPE-Free」SSID.
2. 打開「http://www.gov.taipei/lp.asp?ctNode=65439&CtUnit=10400&BaseDSD=60&mp=100003」這網址,也可以在桌面設成捷徑,方便點擊。它其實就是正常登入後的「熱門活動」頁面,直接連接就會啟動帳密登入。
TPE-Free Desktop Page 
3. 成功上網。

手機版:

1. 連上「TPE-Free」SSID.
2. 安裝「Taipei Free 臺北公眾區免費無線上網」這APP, 它是官方出的,連線能力比「快速登入Wi-Fi熱點 (Taiwan)」這套APP精準。
TPE-Free Phone
3. 成功上網。

BTW,當日後全民上網成為生活中一部分時,希望市政府能把這種煩人的登入帳密方式取消。透過裝置上網連線的網卡MAC位址作資料庫Data Mining分析搜尋,足以達到資訊安全的管理底線,或者實名制+綁定裝置網卡MAC位址亦可。

2015-08-05

修改ADO.NET Entity Data Model的Namespace

當新增一個ADO.NET Entity Data Model並自動生成相關的表格Table POCO Class,雖然有指定了特定的Namespace命名空間,但你會發現生成後的Table POCO Class之命名空間,仍以是該edmx檔所在目錄路徑去命名的(project namespace + folder heirarchy)。就算手動搜尋替代掉Namespace,下一次Refresh edmx檔仍又恢復回來。

原來除了指定edmx檔的Namespace值外,還要手動修改下面兩個T4檔設定:

 image image

選定*.tt檔滑鼠右鍵,查看到Properties設定,在「Custom Tool Namespace」處寫入自訂的Namespace(預設為空白),它可自動更新其下產生的cs檔之命名空間。

image

相關討論:
c# - T4 Generation: Where does VsNamespaceSuggestion() pull from? - Stack Overflow
http://stackoverflow.com/questions/5953094/t4-generation-where-does-vsnamespacesuggestion-pull-from

2015-07-29

軟件授權序號發行

以前覺得軟件授權用序號方式很容易被破解,因此就實作硬體ID+網路啟動的方式,但站在User的角度其實輸入註冊序號是比較簡單的,至少不需要連網。一般簡單註冊序號的實作方式,就是將用戶資訊+到期時間加密寫成二進位檔。

加密內容方式應該能隨DateTime變化而內容不同,以防止被推算竄改,加密方式可以加上隨機byte陣列Salt(鹽)混淆即可。

License KeyGen

關於License授權物件的寫作,多數人大概會優先使用.NET二進化序列化技術,不過我不喜歡這類帶有強型別的序列化行為(因為序列化的前面Bytes都是描述類別不會變化,很好推算加密私鑰),最主要也因為別的程式語言不容易針對檔案作反序列化。

因此我使用「Packet封包結構與解析」文件的格式來包裝License物件,因為它有很好的擴充性,也是C/C++很好理解處理的模式。

當然,.NET軟件上的檢查註冊機制,很容易被反組譯給註解掉IL執行指令,這部分可以在程式裏加上很多空的函式來混淆別人的分析,授權檢查不要只回傳Boolean值就了事,而是在多處關鍵處去授權物件內的值作簡易計算比對,即使發現授權檢查被竄改也不要立即彈視窗警告,使用一個%機率出現怪現象即可。

最後,防守到一個程度被破解就算了,千萬不要為了授權檢查而混淆程式執行效能,做出本末倒置的事才好。畢竟,軟體上的授權檢查,都是防君子不防小人的。

2015-07-22

C# General Tree結構

.NET Framework有Stack, Queue, List, Directory等資料結構,但很少看到Tree,主要是Tree樹的形成各有其用途,很難針對專一用途實作比較通用的Library物件。在一般AP程式裏,Tree結構是代表資料階層性,往往需要在DB->UI裏使用Recursive遞迴函式去形成Tree控件結構。

為了增加轉換效率及判斷整個Tree節點的屬性(如是否有子點),我會在DB與UI控件間使用到Tree結構的物件。以前作法是直接拿UI TreeView控件來當記錄物件,但該TreeView難免比較佔記憶體,因此就興起自己寫作一個很單純的Tree資料結構的念頭。

以Tree資料物件來說,只需要TreeNode<T>及其父子點TreeNodeCollection<T>兩個類別,即可形成一個完整的Tree結構。對任一個TreeNode節點來說,要有父節點及上下的群點,以利整顆樹的節點搜尋。

Tree Structure

Tree CodeSnippet

研究過程裏,發現TreeNode節點的Depth深度值的演算法挺特別的,把它節錄出來。

public int Depth
{
get
{
// ------------------------------
// Recursive algorithm.
// ------------------------------
return (this.ParentNode == null ? -1 : this.ParentNode.Depth) + 1;

// ------------------------------
// General algorithm
// ------------------------------
//int depth = 0;
//TreeNode node = this;
//while (node.ParentNode != null)
//{
// node = node.ParentNode;
// depth++;
//}

//return depth;
}
}

2015-07-18

阿里巴巴1688的收貨地址

阿里巴巴1688網站相較淘寶而言,是屬於批發大量的購物站,其價格都更有競爭力。它的「收貨地址」維護頁面,在登入後台很不容易找到,因此作下備忘筆記。

1688 Address

2015-07-15

EF的多筆批次修改及刪除

EF v6.1.3對於執行多筆批次修改的SQL命令(UPDATE Table1 SET ItemType=2 WHERE ItemId > 5),還是得一筆一筆執行,網上很多套Update Extensions可供使用,比較有名的就是「EntityFramework.Extended」:

PM> Install-Package EntityFramework.Extended

在多筆修改部分,它使用INNER JOIN方式來綁定ID修改欄位,雖然不及原生SQL語法簡潔,但還能接受,而且它能強制更新指定欄位,比起用EF內建的Attach(entity)方式更方便,強制更新指定欄位,不需再注意Old/New Values差異可能造成的BUG地雷,。
using EntityFramework.Extensions;

var listId = new List { 1, 2, 3 };
dbContext.Entries.Where(x => listId.Contains(oldEntry.LogId)).Update(
expr => new CMS_Log {
LogType = "123",
LogLevel = 0
}
);
dbContext.SaveChanges();

SQL Update


在多筆刪除部分,EF v6已支援內建RemoveRange(entities)來多筆刪除,也可以使用外掛的Extension Update()更是簡潔。

using EntityFramework.Extensions;

dbContext.Entries
.Where(x => x.LogId > 15 && x.LogId < 20)
.Delete();
dbContext.SaveChanges();

SQL Delete


外掛Extension函式產生的SQL都有點冗長,但在不使用原生SQL Text下,就將就用吧! 總比起EF得下N筆SQL指令的方式更具高效率了。此外,EF BulkInsert大量新增操作的效能提升,又是另一話題了。

2015-07-08

WebForm更新MasterPage的推薦方式

ASP.NET下的MasterPage一般用來作前端UI框架的,真正的ContentPage才是主要寫該頁程序的地方。這兩種Page在CodeBehind的方式,長得都一樣,因此初學者很容易在各自的頁面CS裏,寫獨立的DB連線及程序(Page_Load),這樣的作法除了多浪費一倍頁面物件所需建立的時間及效能損耗下,Master與Content也不容易作互動。

頁面的執行Trigger都是決定在ContentPage裏,因此要改MasterPage上的控制項方式為:

if (this.Master != null)
{
Literal menu = this.Master.FindControl("MyControldId") as Literal;
if (menu != null)
{
menu.Text = "ABC";
}
}

這些控制項本身就在MasterPage有始初化操作,又被刷值了一次,N個控制項得FindControl()集合N次,十分冗雜又效能差的。其實MasterPage.cs裏不要去連資料庫,只需要寫個更新UI的Method函式即可:

public void Menu_UpdateText(string text, Entity items)
{
this.lblPageTitle.Text = "MasterTitle";
this.ListView1.DataSource = items;
this.ListView1.DataBind();
}
在ContentPage.cs裏,使用以下方式呼叫MasterPage內的函式進行刷新:
if (this.Master != null)
{
var masterPage = this.Master as MasterPageType;;
if (masterPage != null)
{
masterPage.Menu_UpdateText("ABC", dbItems);;
}
}
如此方式,即可既簡單又高效率地在MasterPage與ContentPage間繫結資料項了,一切由ContentPage來控制。

2015-07-04

EF的異動偵測DetectChanges機制

EF執行資料庫Update指令時,由於在UI上都會挾帶PK ItemId, 因此我們常用Attach(entity)方式來取代得多下一道Select指令的問題:

// 一般作法,得多一道DB Select指令
var entity = dbContext.Entities.Single(x => x.ItemId = 1);
entity.ItemText = "abc";
entity.ItemKey = 0;
dbContext.SaveChanges();

// Attach方式,不會下一道DB Select指令
EntityType entity = new EntityType();
entity.ItemId = 1;
dbContext.Entities.Attach(entity); // EntityState = Unchanged

entity.ItemText = "abc";
entity.ItemKey = 0;

//dbContext.Entry(entity).State = EntityState.Modified; // 此行不需寫,因為它會讓DB修改所有欄位值,而非指定的欄位,這也是一個隱藏Bug地雷
dbContext.Configuration.ValidateOnSaveEnabled = false; // 因為Entity有些欄位必填,若不避開會有Validate錯誤
//dbContext.ChangeTracker.DetectChanges(); // 不需撰寫此行,因為dbContext.Configuration.AutoDetectChangesEnabled = true (Default)會自動呼叫
dbContext.SaveChanges();

首先令人覺得好奇的是,只是修改一個很單純POCO Entity的屬性值,為何EF會知道異動了? 原來當你Attach(entity)後,EF透過DbSet宣告時virtual關鍵字,暗地把原本的POCO類別ref位址指向內部DynamicProxy類別,這Proxy為每一個屬性Property作了延伸處理,讓內部ChangingTracker記錄了每個屬性的Old, New值及IsModified狀態。可查看一下EF PropertyEntry源碼宣告,就知大概的追蹤異動處理手法。


因此,當一個Property改值時,它會比對新舊值,一旦不一樣就會把EntityState狀態由Unchanged變為Modified,並在SaveChanges()之前自動呼叫DetectChanges()來搜集每個Properties的新舊值變動狀況。在這樣的原則下,上述用Attach(entity)省下DB Select指令的寫法,就藏著一個可怕的Bug,原來只要Property的值沒有異動到,DB指令裏該欄位就不會修改到,這點是筆者尚未明白EF偵測異動原理前遇到的血淚地雷經驗。

// 調整如下,才能確保要修改的欄位值會被DB更新
EntityType entity = new EntityType();
entity.ItemId = 1;
entity.ItemKey = -1; // Entity int類型屬性,預設是0,所以要指定不可能的值造成異動
dbContext.Entities.Attach(entity); // EntityState = Unchanged

entity.ItemText = "abc"; // 字串通常不需要在Attach()前先調值,因為string預設是null,而UI取值通常是String.Empty
entity.ItemKey = 0; // 若事先沒調值,當有UI把某值改為0時,DB執行時並不會修改到此欄位,造成Bug

dbContext.Entry(entity).Property(x => x.ItemKey).IsModified = true; // 可以直接使用這方式強制某欄位要更新,只是查詢集合耗效能而己

dbContext.Configuration.ValidateOnSaveEnabled = false; // 因為Entity有些欄位必填,若不避開會有Validate錯誤
dbContext.SaveChanges();

由於EF預設下都會自動偵測異動(dbContext.Configuration.AutoDetectChangesEnabled=true),尤其在以下操作時都會自動呼叫DetectChanges()比對所有的entry集合的每一個屬性Properties的新舊值,若在大量操作時難免效能不佳,因此特殊情況時,會讓dbContext.Configuration.AutoDetectChangesEnabled=fase,完成複雜的操作,才只作一道dbContext.ChangeTracker.DetectChanges()比對異動。



  • DbSet.Find
  • DbSet.Local
  • DbSet.Remove
  • DbSet.Add
  • DbSet.Attach
  • DbContext.SaveChanges
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries

EntityState是EF執行DB指令重要的判斷依據,不了解底層原理就直接使用Attach(entity),其實隱藏了很多可能Bug錯誤。不過,或許大多數EF使用者不在乎修改資料時多下一道DB Select指令,也就不會遇到這樣的問題了。EF內部行為已為大多數DB操作行為作了合理推斷,不過每個開發者追求效能的心態層次不一樣,想自由穿梭其間,就得多閱讀理論基礎就是了。

2015-06-29

Genymotion安卓模擬器

官網:http://www.genymotion.com/

GENYMONTION因為是採用X86指令,所以模擬器飛快,不像SDK自帶的採用ARM指令, 但是GENYMONTION對純ARM指令是不支援的,例如DELPHI生成的APK。Xamarin是支持的,這個說明Xamarin生成的不是真正的原生ARM代碼。

以下是使用Genymotion一些細節及好文章: (以Genymotion v2.5為例)

  • 如何重開機模擬器?
    Reboot virtual device
  • [Android] Genymotion安裝Google Play的方法 ~ One Node
    http://onenode.blogspot.tw/2015/05/android-genymotiongoogle-play.html
  • 移動下載的Virtual Device Images?
    1. 預設下載位置是: C:\Users\[UserName]\AppData\Local\Genymobile\Genymotion\deployed\,先修改Genymotion下的VirtualBox日後下載位置:
    Change VM saving location
    2. 關閉運行中的Emulator, 移動原本的Image目錄至想要的位置上,並開啟VirtualBox,移除Virtual Mache再新增新位置即可。
    Change VM
    3. 重新開啟Genymotion,列表就能載入位置後的Virtual Device了,因為此列表是讀取VirtualBox上的VM List。
    Virtual Device List