Quantcast
Channel: 黑暗執行緒
Viewing all 2458 articles
Browse latest View live

【茶包射手日記】Excel xlsm複製工作表到xls時出現1004錯誤

$
0
0

幫忙修改某個Excel VBA,透過巨集要將xslm的某個工作表(Worksheet)複製到另一個xls活頁簿(Workbook),執行Sheets("TheSheet").Copy Before:=AnothorXLS.Sheets("Boo"),卻出現以下錯誤:

執行階段錯誤 '1004':

Excel無法將工作表插入目的地活頁簿,因為它包含的列和欄比來源活頁簿少。若要移動或複製資料至目的地活頁簿,您可以選取資料,然後使用[複製]與[貼上]命令將它插入另一個活頁簿的工作表。
Excel cannot insert the sheets into the destination workbook, because it contains fewer rows and columns than the source workbook. To move or copy the data to the destination workbook, you can select the data, and then use Copy and Paste commands to insert it into the sheets of another workbook.

懸疑之處在於同一組檔案使用者測試回報經運作正常,讓我一直懷疑是自己操作步驟有誤導致,而使用者遠在天邊無從目睹操作過程,就只能透過電話及Skype一直在操作過程及環境細節打轉。最後終於發現一點明顯差異: 使用者用Excel 2003,而我用Excel 2010。之前沒想到此點,是認定使用者既能開啟xlsm,應是Excel 2007+版本,忽略了可能是靠相容套件無縫整合。而挖出Excel 2003 vs Excel 2007差異,修正方向後才知是是我Excel江湖經驗不足,不然早該抓出問題。

所謂的目的地列與欄比較少,指的是Excel 2007+支援1,048,576列x16,384欄,而Excel 2003只支援65,536列x256欄,所以將xlsm(Excel 2007格式)的工作表複製到xls(Excel 2003格式)時,就註定要滿出來而報錯~ (參考)

知道問題來源,要解決只是小菜一碟。將xls改存成xlsx,問題豁然而解~


拒當臉書廣告免費代言人

$
0
0

接獲通報:

我,具備鋼鐵般意志的程式魔人兼馬拉松鐵漢,竟在臉書當起某化妝品廣告的代言人。

這... 為了生活我可以忍,但是侮辱... 等一下,不對,我根本沒收到錢呀!

查詢之下才知這是FB所謂的"社交廣告",依據官方網站說明:

社交廣告是在廣告商要推廣的訊息旁,搭配你執行過的社交動作一起出現,如你對某個專頁說讚的動作。而廣告對象會是你確認過的朋友,且受到隱私設定的規範。

回想並查詢,我沒有對該粉絲團按過讚,但查看該粉絲團時常分享趣味照片,是不是某次針對朋友轉分享的其中一則PO文按過讚就不得而知。不過沒關係,透過隱私設定,我們可以拒接這類"通告":

在帳號設定介面找到【廣告】項目:

找到【廣告&朋友】項目,只需要把顯示對象設成"破喉嚨"的好朋友--"沒有人"先生,就OK囉!

經實測,調完顯示對象請線民再次重新整理FB網頁,該則社交廣告立刻消失,換上另一位FB朋友接了某量販店通告出來按讚代言... (有種抓交替的感覺 XD)

Outlook,不要偷換我的雙引號!

$
0
0

不知大家有沒有遇過這個問題,在Outlook寫信時,寫了一小段程式範例。對方收到後,由信中複製程式碼,貼成程式執行卻是壞的,仔細一看,問題出在標示字串的雙引號通通被換掉了!!

例如以上範例,明明我們輸入的是",Outlook卻自做聰明改成“,敲完雙引號立刻按Ctrl + Z可以把它改回來,但只要一時不察沒改正就會出問題。

這是Word的智慧引號功能,用意是貼心地讓引號更美觀,但套用在程式碼上卻會造成困擾。原以為Outlook借用Word核心當編輯器,設定也共用。在Word停用智慧引號,Outlook卻不受影響,細查才知Outlook有自己的獨立設定。

以Outlook 2010為例,在 【Outlook選項 / 郵件】 有個 【編輯器選項】:

【校訂 / 自動校正選項】提供了跟Word相同的設定選項:

取消後就可以停用Outlook的智慧引號功能,恣意忘情地在信中敲Code囉~ (謎: 這傢伙沒救了!)

【延伸閱讀】

JavaScript端的JSON日期轉換

$
0
0

討論JSON日期轉換已不是第一次[12],但過去多半聚焦在JavaScript與.NET間的格式轉換。近來戰場移到Knockout MVVM,卻發現即使只在JavaScript端,也有日期轉換的小眉角。

在JavaScript的Date型別,經過JSON.stringify(),會轉成ISO 8601格式(yyyy-MM-ddTHH:mm:ss.fffZ);有趣的是,將這個字串用JSON.parse()解析,得到的是字串,而不會還原成當初的Date型別。

將ISO 8601格式還原回Date的需求,要透過JSON.parse()的Reviver函數實現。範例如下:

var d = new Date();
//預設Date會轉為yyyy-MM-ddTHH:mm:ss.fffZ ISO 8601格式
var t = JSON.stringify(d);
console.log(t);
//parse時,ISO 8601格式並不會被轉成Date,而是被視為字串
console.log(typeof (JSON.parse(t)));
//透過reviver提供ISO 8601字串轉Data的功能
//REF: http://msdn.microsoft.com/zh-tw/library/ie/cc836466(v=vs.94).aspx
var dateReviver = function (key, value) {
var a;
if (typeof value === 'string') {
        a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
returnnew Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]));
        }
    }
return value;
};
var result = JSON.parse(t, dateReviver);
console.log("type=" + typeof (result) + ", value=" + result.toString());
//同場加映: 排除特定屬性及修改屬性值
var obj = {
    boo: "BOO",
    foo: "FOO",
    blah: "BLAH"
};
t = JSON.stringify(obj);
console.log(t);
//客製Reviver,忽略boo,並在foo的內容後方加上"*"
var myReviver = function (key, value) {
//傳回null或undefined會刪除該屬性
if (key == "boo") return undefined;
if (key == "foo") return value + "*";
return value;
};
result = JSON.parse(t, myReviver);
console.log(JSON.stringify(result));

線上展示: http://jsfiddle.net/darkthread/8yC8m/

由執行結果可以看到Date在JSON.stringify()後被轉成"2013-05-10T14:07:46.761Z",但JSON.parse()的型別卻是string不是Date。接著我們宣告一個dateReviver函數,偵測當輸入字串格式為ISO 8601時,則拆解其中的年、月、日、時、分、秒還原回Date。另外,順道示範Reviver可以依屬性名稱執行不同邏輯(由key參數判斷),以及忽略某個屬性(return undefined)的能力。

透過.NET程式操作Excel的注意事項

$
0
0

【個案】某支開啟Excel進行作業的.NET排程程式,定期排程執行時遇到錯誤,留下一堆無主excel.exe。

用以下程式示範:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Office.Interop.Excel;
 
namespace RunExcelBadWay
{
class Program
    {
staticvoid Main(string[] args)
        {
try
            {
                Application excel = new Application();
                var wrkBook = excel.Workbooks.Add();
                var sheet = wrkBook.Sheets.Add();
                sheet.Cells(1, 1).Value = "ABC";
                wrkBook.SaveAs(@"d:\" + Guid.NewGuid() + ".xlsx");
                wrkBook.Close();
                excel.Workbooks.Close();
                excel.Quit();
                Console.WriteLine("Done");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error:" + ex.Message);
            }
        }
    }
}

程式貌似正常,執行結果無誤。但做個小實驗,把存檔路徑改成wrkBook.SaveAs(@"d:\PathNotThere\" + Guid.NewGuid() + ".xlsx");,故意指向不存在的目錄,接著連續執行程式12次,打開Task Manager:

哦哦,冒出12個桌面看不到摸不著的背景EXCEL.EXE,記憶體用量卻持續跳動,是會繼續吃CPU吃RAM的僵屍嗎?

Excel屬於Unmanaged程式,所佔用資源不會在.NET程式結束時自動回收。過去處理其他如FileStream、DbConnection等Unmanaged資源,用using即可搞定,但Excel Application未實做IDisposable介面,無法用using打發。在處理上,我們需要加入finally區段明確清理及關閉Excel,確保任何情況下Excel程式都會確實關閉釋放資源。更進一步,可參考MSDN Code Sample建議:

... Clean up the unmanaged COM resource. To get Excel terminated rightly, we need to call Marshal.FinalReleaseComObject() on each COM object we used ...

意思是程式過程使用過的所有COM+物件,包含Workbook、Worksheet、Range、Cell(註: 連Cell都要,挺麻煩的)... 結束前都建議使用Marshal.FinalReleaseComObject()清掉COM+物件的Reference Counter,確保Excel程式能順利關閉。

修改的範例如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Office.Interop.Excel;
 
namespace RunExcel
{
class Program
    {
staticvoid Main(string[] args)
        {
            Application excel = null;
            _Workbook wrkBook = null;
            dynamic sheet = null;
try
            {
                excel = new Application();
                wrkBook = excel.Workbooks.Add();
                sheet = wrkBook.Sheets.Add();
                sheet.Cells(1, 1).Value = "ABC";
                wrkBook.SaveAs(@"d:\PathNotThere\" + Guid.NewGuid() + ".xlsx");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error:" + ex.Message);
            }
            finally
            {
              //注意: Excel是Unmanaged程式,要妥善結束才能乾淨不留痕跡
                //否則,很容易留下一堆excel.exe在記憶體中
                //所有用過的COM+物件都要使用Marshal.FinalReleaseComObject清掉
                //COM+物件的Reference Counter,以利結束物件回收記憶體
                if (sheet != null)
                {
                    Marshal.FinalReleaseComObject(sheet);
                }
                if (wrkBook != null)
                {
                    wrkBook.Close(false); //忽略尚未存檔內容,避免跳出提示卡住
                    Marshal.FinalReleaseComObject(wrkBook);
                }
                if (excel != null)
                {
                    excel.Workbooks.Close();
                    excel.Quit();
                    Marshal.FinalReleaseComObject(excel);
                }
            }
            Console.WriteLine("Done");
        }
    }
}

由於必須逐一釋放用過的Excel物件,感覺頗麻煩,如果沒有特別需求,我會建議改用OpenXML SDK、EPPlus、ClosedXML等純.NET程式庫[參考12]更輕巧單純些。

呼叫Excel的程式無法以排程方式執行

$
0
0

某支呼叫Excel的轉檔程式(做法如前文所述),直接執行正常,移入Windows 2008排程執行,卻發生錯誤:

Task Scheduler successfully completed task "\Run Excel" , instance "{2b987743-dfa2-4883-b7ee-0f26b1534c64}" , action "X:\Temp\RunExcel.exe" with return code 3762504530.

經過實驗,發現將安全選項改為"Run only when user is logged on"可避開問題,切換到"Run whether user is logged on or not"則會產生錯誤。

使用錯誤碼爬文,在MSDN論壇找到一則密技:

手動建個"c:\windows\syswow64\config\systemprofile\desktop"資料夾,就醬!

很多深受Excel無法在排桯執行所苦的網友證言,此技神奇無比一帖見效! 深入搜查找到另一篇MS RD的心得,歸納問題只發生在Windows 2008以Service方式執行Excel Automation時(在Windwos 2003上不會),推測可能因Excel嘗試在Desktop資料夾寫入暫存檔而引發錯誤!

在問題主機加入desktop資料夾,問題迎刃而解! 但是試著在我的本機模擬同一情境,加desktop卻不管用。調整程式,把Exception寫成Log,再配合事件檢視器,整理得到以下線索:

  1. 排程仍出現return code 3762504530錯誤
  2. 程式本身發生的Exception為
    System.UnauthorizedAccessException: Retrieving the COM class factory for component with CLSID {00024500-0000-0000-C000-000000000046} failed due to the following error: 80070005 Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)).
       at System.Runtime.Remoting.RemotingServices.AllocateUninitializedObject(RuntimeType objectType) ...後面省略... 
  3. 事件檢視器有一則系統錯誤: The machine-default permission settings do not grant Local Activation permission for the COM Server application with CLSID
    {00024500-0000-0000-C000-000000000046}
    and APPID Unavailable  to the user JEFFBOX\jeffrey SID (S-1-5-21-1234…8303-1000) from address LocalHost (Using LRPC). This security permission can be modified using the Component Services administrative tool.
    其中JEFFBOX\jeffrey是執行排程的身分。

事件檢視器訊息很清楚地提出是DCOM預設權限(machine-default permission settings)問題,開啟DCOMCNFG,授予jeffrey帳號"Local Activation"權限。

終於在本機上也成功以排程呼叫Excel。

基於好奇,把desktop資料夾刪除,故意引發錯誤,留下Exception訊息當關鍵字供未來參考:
System.Runtime.InteropServices.COMException (0x800A03EC): Exception from HRESULT: 0x800A03EC
   at Microsoft.Office.Interop.Excel._Workbook.SaveAs(Object Filename, Object FileFormat, Object Password, Object WriteResPassword, Object ReadOnlyRecommended, Object CreateBackup, XlSaveAsAccessMode AccessMode, Object ConflictResolution, Object AddToMru, Object TextCodepage, Object TextVisualLayout, Object Local)

最後一個疑問,為什麼設成Run only when user is logged on可以過關,推測是如此讓Excel從當下登入者環境中執行(如同gelis在[元件服務]的[安全性]與[權限]驗證模型一文中所提到的"互動式使用者"選項),狀況較單純無切換成其他執行身分(systemprofile)的需求,但需留意權限範圍無法預期(視當時登入的是誰,權限就有多大)以及無入登入時會掛點的副作用。

【延伸閱讀】

Coding4Fun: 讓Windows桌面凍結的.NET程式

$
0
0

在Huan-Lin學習筆記看到一篇文章 -- C# 學習筆記:多執行緒 (3) - 優先順序。用.NET跑多執行緒對我不算陌生,但卻一直沒注意到.NET的Process跟Thread的Priority是可以調整的。.NET執行緒預設的優先權是Normal,而Windows核心層次的作業(例如滑鼠、鍵盤訊息處理)多半能擁有更高的優先權,無怪乎過去就算寫.NET程式如何搾乾CPU,Windows的基本操作仍能維持順暢。

學到這招讓我動起邪惡的念頭 -- 來寫支可以凍結Windows桌面的.NET程式吧!

做法很簡單,Process及Thread的優先權都拉到最高,一口氣開八條Thread用空轉的while迴圈吃光CPU。為了比較差異,高優先權模式可透過參數決定是否啟動。程式如下:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
 
namespace SystemHang
{
class Program
    {
staticvoid Main(string[] args)
        {
//由傳入參數決定要不要讓Windows卡卡
            bool freeze = (args.Length > 0 && args[0] == "freeze");
            var p = Process.GetCurrentProcess();
//卡卡模式時,將Process優先等級拉到RealTime
//註: 實務上幾乎沒有需要設定RealTime的情境(叔叔有練過,小朋友不要學)
if (freeze)
                p.PriorityClass = ProcessPriorityClass.RealTime;
//建立八條Thread,就算i7有四核八緒的也要趕盡殺絕
              Thread[] threads = new Thread[8];
//使用ManualResetEvent做執行緒同步
              ManualResetEvent[] waits = new ManualResetEvent[threads.Length];
//設定刑求時間10秒
              DateTime timeUp = DateTime.Now.AddSeconds(10);
for (int i = 0; i < threads.Length; i++)
            {
                var wait = waits[i] = new ManualResetEvent(false);
                threads[i] = new Thread(() =>
                {
//於時限內形成空迴圈,讓條件比對耗盡CPU資源
while (DateTime.Now.CompareTo(timeUp) < 0)
                    {
//什麼都不做,讓迴圈快速轉動
                    }
//通知執行完畢
                    wait.Set();
                });
//卡卡模式時,指定執行緒優先等級為Highest
if (freeze)
                    threads[i].Priority = ThreadPriority.Highest;
//啟動執行緒
                threads[i].Start();
            }
//等待所有執行緒執行完畢
            WaitHandle.WaitAll(waits);
            Console.WriteLine("Done!");
        }
    }
}

實測結果如下:

程式跑了兩次,執行期間8顆CPU負載全滿,差別在於第一次使用預設優先權,滑鼠還能自由移動,下方的Up Time也如常累計;而第二次將優先權提高後,滑鼠很難移動,而下方的Handles、Threads、Processes、Up Time等統計停止更新,CPU的負載曲線圖表也不動,直到程式結束才恢復。

無聊的小實驗,可以看出優先權高低造成的影響,也由此可知: 除非你有很好的理由,沒事別提高Thread優先權,只會惹人嫌。

使用OpenSSL建立CA及簽發SSL憑證

$
0
0

之前曾討論過WebClient存取使用自我簽署憑證的SSL連結,會發生以下錯誤:

The underlying connection was closed: Could not establish trust relationship for SSL/TLS secure channel. / 基礎連接已關閉: 無法為 SSL/TLS 安全通道建立信任關係。

需透過ServerCertificateValidationCallback克服。最近又遇到類似情境,但Client程式是別人寫的,修改有困難。決定嘗試另一種做法: 自己搞一個CA,讓Windows信任該CA,如此該CA簽發的憑證直接被視為有效,就可避免前述問題。

Windows Server有Active Directory 憑證服務,缺點是綁了AD權限,不想在正式伺服器亂擺測試用途資料,為此架測試用AD又有點為了喝牛奶養牛,故決定使用獨立軟體工具來處理,OpenSSL,自然是首選。

第一步是下載與安裝OpenSSL。身為C語言麻瓜,沒興趣也沒信心挑戰在Windows平台編譯OpenSSL,當機立斷衝到OpenSSL for Windows下載編譯好的版本。網站的版本很多,我選擇了Win32 OpenSSL v1.0.1e(OpenSSL作者建議開發者安裝的版本),但安裝前要先裝Visual C++ 2008 Redistributables

安裝完成後,在安裝目錄(我的例子是X:\Tools\OpenSSL)的bin目錄下,可以找到openssl.cfg,修改其中CA相關設定:

dir        = ./PEM/darkCA  (要放CA資料的目徑,要手動建目錄及檔案,後面會提)
default_days    = 3650    (預設365,改成十年比較省事)
(以下的預設值請自行依個人狀況調整)
countryName            = TW
countryName_default        = TW
stateOrProvinceName        = Taiwan
stateOrProvinceName_default    = Taiwan
localityName            = Taipei
0.organizationName        = Darkthread
0.organizationName_default    = Darkthread
organizationalUnitName        = HQ
commonName            = darkthread.net (具有識別性的名稱,FQDN也是好選擇)
emailAddress            = nobody@ mail.com.tw

接著準備CA專用資料夾:

    1. 在bin/PEM下建立CA專屬的資料夾,本範例為darkCA
    2. 建立bin/PEM/darkCA/private,放私鑰用
    3. 建立bin/PEM/darkCA/newcerts,放簽發的憑證
    4. 建立一個文字檔案bin/PEM/darkCA/serial(無附檔名),在其中寫入1000,或者可以用echo 1000 > serial建立,該數字被用來產生簽發憑證流水號
    5. 建立一個全空的文字檔bin/PEM/darkCA/index.txt,保存簽發憑證記錄用

下一步,開啟CMD.EXE準備動手。為了省去每次執行工具都要指定openssl.cfg的麻煩,先設定目錄變數
set OPENSSL_CONF=x:\tools\openssl\bin\openssl.cfg

使用以下指令建立CA憑證,參數剛才在openssl.cfg都寫好了,除了PEM密碼,其餘幾乎都可以按Enter用預設值。

X:\Tools\OpenSSL\bin>openssl req -new -x509 -days 3650 -extensions v3_ca -keyout
PEM/darkCA/private/cakey.pem -out PEM/darkCA/cacert.pem
Loading 'screen' into random state - done
Generating a 1024 bit RSA private key
..........++++++
.......++++++
writing new private key to 'PEM/darkCA/private/cakey.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be incorporated into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
TW [TW]:
Taiwan [Taiwan]:
Taipei []:
Darkthread [Darkthread]:
HQ []:
darkthread.net []:
nobody @mail.com.tw []:

此時,bin/PEM/darkCA/cacert.pem就建好了,可以開始用它簽發SSL憑證給IIS囉!

取得IIS憑證要求檔(產生步驟請參考保哥的文章),再用openssl產生SSL憑證: (註: openssl.cfg中預設憑證要求檔所輸入的國家、州省、組織名稱必須與CA一致,此點可修改openssl.cfg的[ policy_match ]設定放寬)

X:\Tools\OpenSSL\bin>openssl ca -out iisCert.pem -in webRequest.txt
Using configuration from x:\tools\openssl\bin\openssl.cfg
Loading 'screen' into random state - done
Enter pass phrase for ./PEM/darkCA/private/cakey.pem:
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 4096 (0x1000)
        Validity
            Not Before: May 15 04:13:22 2013 GMT
            Not After : May 13 04:13:22 2023 GMT
        Subject:
            countryName               = TW
            stateOrProvinceName       = Taiwan
            organizationName          = Darkthread
            organizationalUnitName    = HQ
            commonName                = localhost
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            Netscape Comment:
                OpenSSL Generated Certificate
            X509v3 Subject Key Identifier:
                C3:8A:CD:2D:69:0C:2F:B3:1C:68:3E:31:11:8A:EA:9B:01:13:74:3A
            X509v3 Authority Key Identifier:
                keyid:72:F8:85:B1:D0:42:EF:EB:3D:C3:2E:B5:C1:FD:E8:AA:B0:52:91:A1

Certificate is to be certified until May 13 04:13:22 2023 GMT (3650 days)
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated

接著依前述文章的第4步說明,將bin/PEM/darkCA/newcerts/iisCert.pem當成CA送回結果進行"完成憑證"作業,我們這個地下CA所簽發的憑證就在IIS裝好了。不過此時使用SSL連線還是會彈出警告,原因是我們的電腦尚未信任地下CA的根憑證。

開啟IE的網際網路選項,依圖中的(1)到(4),選取【憑證】/ 【受信任的根憑證單位】 / 【匯入】,再指向bin/PEM/darkCA/cacert.pem,按下一步開始匯入。

由於匯入根憑證非同兒戲,Windows會再次詢問確認: "你願意嗎?" "Yes, I do." (提醒: 匯入任何根憑證前請確認憑證來源安全無虞,以免埋下禍害)

完成後,原先用OpenSSL簽發的憑證就會視為有效,但憑證Cache會影響結果,請重新開機或使用第一張圖的【清除SSL狀態】鈕(紅色標示區)清除再做測試。此時可發現,IE已不再跳出任何警告,WebClient取存時也不再出錯囉~

【資安提醒】

經以上操作,自訂CA簽發的憑證都會被Windows直接判定有效,有可能惡意程式也藉此通過檢查獲得更大權限,為避免遭到誤用危險安全,請務必保管好自訂CA憑證及密碼並不要輕信來路不明的憑證。

【延伸參考】Creating an SSL Certificate of Authority


【茶包射手日記】長不高的IFrame

$
0
0

接獲報案,又出現IFrame無法佔滿網頁的問題:

原本以為是小菜一碟,這是五年前就知道的超級老哏,只需加上html,body { height: 100% }立刻藥到病除,檢視原始碼後卻讓我大吃一驚跌坐在地,網頁早就加上height: 100%,莫非茶包又變種了?

<!DOCTYPEhtml>
<html>
<head>
<title>Frame Height Issue, AGAIN!</title>
<style>
    html,body { height: 100%; }
</style>
</head>
<body>
<form>
<div>Frame Height Issue, AGAIN!</div>
<IFRAMEframeborder="0"width="100%"height="90%"
src="http://blog.darkthread.net"></IFRAME>
</form>
</body>
</html>

實際個案的網頁比範例複雜很多,而且是ASP.NET內嵌ReportViewer的情境。將ASP.NET網頁存成HTML,用消去法一點一點移去網頁元素,終於在移掉<form>時,<iframe>瞬間由黃子佼變姚明。這才恍然大悟,一直以為<form>是個虛元素不需考量高度,但由測試結果來看卻不然,為<form>也加上style="height: 100%",問題便排除了!

進一步測試,這個問題只出現在IE跟Firefox,Chrome跟Safari不會發生。

【結論】當<iframe>被內嵌於<from>,在IE及Firefox會出現無法佔滿網頁高度的問題,需在<form>加上height: 100% CSS設定。

我的JSDC 2013筆記

$
0
0

JSDC是JavaScript Developer Conference的縮寫,是台灣JavaScript開發者的年度盛會,今年5/18-19兩天舉辦,沒有親自參加,但FB、噗浪上的資訊如潮水般湧入,許多講者幾乎是一講完就把投影片上傳到Slide ShareSpeaker Deck,然後我開始陷入: "哇! 這個我想看、那個我得了解"的焦躁中,一口氣吞下超量資訊,就像:

JSDC的議程很多很廣,聲明一下,筆記偏向我的個人觀點,以自己JS程度能理解駕御及與目前學習方向接近的議題為主,某些議題過去有寫文章提過的筆記就會簡略些,請大家以投影片為主。完整的議程資訊則可在JSDC懶人包找到。

  1. 保哥 - JavaScript開發實戰 投影片 / 錄影
    IE, Chrome的效能工具、JavaScript效能及語法陷阱
  2. Lawrance – 愛料理網站前端開發經驗 投影片
    Bootstrap、RWD、CoffeeScript、jQuery、Backbon.js、SASS/SCSS、Require.js(造成部署慢後來未用)
    從Client追蹤滑鼠移動與點擊(CrazyEgg) / Google Analytics,將使用者行為作為網站改善依據
    新功能上線時自動跳出指示與教學(intro.js, bootstro.js)
    Mobile Tips: Request數要少、圖片要小、放棄特效省去CSS/JS下載
    JSON API很有用,前後端開發關注點分離
  3. ant - 前端工程師面對的資安挑戰投影片
    2nd SQL Injection - 轉送到後端在後端爆炸(後端內網多半鋼板薄防護差,改不動可以考慮中間插入DB當DMZ)
    DOM XSS – 直接在Client端引爆與後端無關
    HttpOnly Cookie (document.cookie取不到)
    限定HTTP POST並不能有效防範CSRF,至少要加上Form Token,小心Click Jacking
    CDN要小心CDN被人攻破掛木馬(個人認為用Microsoft或Google的CND還出事就是命了)
    ** Filter Input + Escape Output + One Time Token **
    【同場加映】ant去年的講題: JavaScript的著作權問題,我的簡略心得是JavaScript授權問題比想像複雜。簡單來說,若使用的程式庫採MIT/BSD/Apache授權問題較少,但要小心打包壓縮後未保留著作聲明及原始檔連結的議題,使用CDN當成來源也是不錯的解決方案。
  4. fillano (費大公) - Easier Async - Flow Control 的原理、應用、實作及展望投影片
    Promise是當今主流(ex: jQuery Deferred)
  5. josephj(蔣定宇,啊嗚) - F2E(前端工程),軟體工業的鍥石投影片
    Frontend Engineer 前端工程師!! 2001起Yahoo首先定出F2E職位,兩年內擴充到600人。
    工程師不管呈現 + 設計師不管做法 + 瀏覽器/JS/CSS日益複雜(近年又多了Mobile) => F2E展現價值,將設計與技術完美結合
    ** 沒Github還能算是工程師嗎? **
  6. 大澤木小鐵 - 透視JavaScript的MVC/MVP/MVVM投影片
    巡覽MV* Pattern,MVVM的代表以Knockout為例
  7. 流浪小風 – Rock with Knockout.js 投影片
    Knockout.js Rocks!!! 不解釋
  8. 小惡魔 - 你不可不知的前端開發工具投影片
    CoffeeScript - 統一Coding風格、可透過JSLint測試
    Compass – CSS產生器,寫一行生出跨瀏覽器用的N行、簡化原本複雜的CSS寫法。SCSS跟CSS一樣、SASS對設計師而言很難。
    Require.js管理JavaScript載入時的相依性。
  9. 閃光洽 - 生在幸福的JS年代投影片
    細數跨瀏覽器的血淚、瀏覽器是史上最複雜的測試環境(CSS/UI/UX/AJAX/WebSocket/Worker/IndexesDB/Camera API…)
    幸福的年代: 當代瀏覽器支援度相當完全、市面工具/套件與解決方案相當多元、IDE支援越來越完整
    ** 門檻是用來跨越的,不是用來絆倒的 **
  10. josephj - 關於JavaScript品質,我想說的是投影片
    (PS: 愛跑步的工程師最帥氣~)
    在Yahoo!學到的教訓: F2E放在公用Pool、缺乏訓練/一致的開發規範
    miiiCasa: 當上F2E部門主管,迫於時程,大量Copy & Paste
    ** 數小時寫出的程式,要花數週的時間去維護它 **
    代碼抽象的原則: DRY(重複時用抽象化解決)、YAGNI(抽象化要快+簡單,不要耗費精力)、Rule of 3(出現3次再進行抽象化)
    JS檔不要超過500行、一行不要超過100個字元 => 抽離成模組
    善用try..catch、window.onerror –> 利用虛image?error_info在Log留下記錄,分析工具Sentry
    Esprima – JS Code品質工具,將程式碼轉成AST(描述語意),再轉成統一規範的程式碼
    CI整合!! 每次CheckIn都檢查 + 測試。現成的CI伺服器 CodeShip、CloudBees、Travis,可搭Github。
    文學編程(Literate Programming) - 寫文章 -> 變成程式碼

【茶包射手日記】WP模擬器偵測不到硬體虛擬化支援

$
0
0

使用模擬器測試Windows Phone專案時,忽然出現電腦不支援硬體虛擬化的錯誤訊息。想起筆電前陣子觸控版故障,曾試著將BIOS重設回預設值(註: UEFI的VAIO筆電要進入BIOS,不要傻傻地在開機時從F1按到F12,正確做法是確實開機後,改按【ASSIST】鈕開機),應是BIOS虛擬化支援選項又還原為停用所致。進入BIOS重新啟用虛擬化支援,卻發現訊息依舊。

很好,又得換上射手裝出任務了~

開啟cmd命令視窗,執行systeminfo發現異常,韌體啟用虛擬化的項目為"否"。

Hyper-V 需求: VM 監視器模式擴充: 是
                      韌體中已啟用虛擬化: 否
                      第二層位址轉譯: 是
                      具有資料執行防止: 是

印象中,安裝Hyper-V後會影響虛擬化支援的判定。決定將Hyper-V移除驗證一番,移除完重新開機,再使用systeminfo測試,果然該選項變成"是":

Hyper-V 需求: VM 監視器模式擴充: 是
                      韌體中已啟用虛擬化: 是 
                      第二層位址轉譯: 是
                      具有資料執行防止: 是

但是,我記得WP模擬器要Hyper-V才能跑,果不其然,移除後VS2012警示錯誤:

重新安裝Hyper-V,WP模擬器就可以運作了,再測一次systeminfo,結果為:

Hyper-V 需求: 偵測到 Hypervisor。將不會顯示 Hyper-V 所需的功能。

原來,這才是Hyper-V已啟用且可正常運作時的訊息! 至於先前已安裝Hyper-V卻可以看到四項細項,推測是前陣子重設BIOS設定導致。

結論,下回應先確定systeminfo訊息為"偵測到 Hypervisor",如有狀況,可以試著移除Hyper-V再重裝。

遠端桌面連線無法儲存帳號密碼

$
0
0

遠端桌面連線程式允許我們儲存帳號密碼,省去每次登入都要重敲密碼的麻煩。

有時候,即使儲存好帳號密碼,每次登入還是會跳出以下錯誤,得重敲密碼才能登入:

Your system administrator does not allow the use of saved credentials to logon to the remote computer […] because its identity is not fully verified. Please enter new credentials.
您的系統管理員不允許使用已儲存的認證來登入 […]。請輸入新的認證。

原來,當被登入主機與登入客戶端主機間沒有網域信任關係時,會改採NTLM方式認證,而本機群組原則預設不接受NTLM認證預儲密碼。

修改方式是開啟【本機群組原則編輯器】(找不到的話可以在執行輸入gpedit.msc),依下步驟操作:

  1. 找到 電腦設定 / 系統管理範本 / 系統 / 認證委派(Credentials Delegation)
  2. 找到 允許在僅使用NTML的伺服器驗證時委派已儲存的認證(Allow Delegating Saved Credentials with NTLM-only Server Authentication)
  3. 點選 已啟用(Enabled)
  4. 按下 顯示(Show)
  5. 加入 TERMSRV/*
  6. 關閉編輯器,執行gpupdate.exe /force 強制更新原則設定

這樣子,下回遠端登入時就不必再輸入密碼囉~

參考資料: Windows 7 Remote Desktop Connection Save Credentials not working

【茶包射手日記】被Non-Existent Process佔用的TCP Port

$
0
0

某支服務程式監聽9000、12000、12001三個TCP Port提供服務,要啟動時出現該Port已被佔用的錯誤訊息。

這是很常見的情境,既然Port被現有程式佔用,最簡單的做法就是netstat –oa找出LISTEN該Port程式的Process ID,結束該程序即可。但這回情況遠比想像複雜...

netstat查出這三個Port被一支PID=5300的程序所佔用(應是先前當掉的同一服務程式),但由Task Manager或Process Explorer卻找不到PID=5300的程序,既然找不到程序就無從砍起,不把程序砍掉,Port就會一直被佔用著,重開機或重啟網卡是最簡便的解決手段,卻限於主機上仍有其他服務不能中斷而不可行。

束手無策之際,改用TCP View檢查發現異常之處: PID=5300程序,除了聽9000, 12000, 12001三個Port外,還有一些殘留的連線(狀態為CLOSE_WAIT),而其Process Name標示為<non-existent>,代表該Porcess已經不存在,雖然按右鍵選單可以選擇終結程序,但試著關閉不存在的程序,想當然於事無補。就這樣再度陷入焦著,狀況很明確 -- 只要關掉不存在的程序就能解決問題,問題是關不掉。

爬文爬了很久,一堆文章建議用pskill、Process Explorer刪掉程序即可,我想他們肯定沒見過這枚看得到打不到的BOSS級茶包。終於,在serverfault找到一篇很棒的討論回應為我解惑:

What may be happening is that your process had a TCP port open when it crashed or otherwise exited without explicitly closing it. Normally the OS cleans up these sorts of things, but only when the process record goes away. While the process may not appear to be running any more, there is at least one thing that can keep a record of it around, in order to prevent reuse of its PID. This is the existence of a child process that is not detached from the parent.

文中提到一個重點,當Process開啟TCP Port後未正常結束就當掉,理論上OS會幫忙善後歸還其佔用資源,前題是 -- Process的PID記錄已自OS中移除。若該程序啟動了子程序(Child-Process),程序終止後子程序繼續存活,子程序的Parent PID仍指向該程序,對OS來說,該PID仍有意義,其佔用的TCP Port便不能任意處置。這充分解釋我遇到的狀況,很明顯的,找出並終結已成為孤兒(Orphaned)的子程序,讓PID 5300徹底安息,這一切就能解套。

使用Process Explorer開始對Process進行盤查,很快發現有一個dw20.exe(Dr.Watson,當機善後程式)行跡可疑,點開一看,BINGO!!! Parent就指向5300,推論是5300 Process當機後啟動,卻不知為何一直沒有自行結束才惹出事端。

將Dr.Watson就地正法(Kill Process),原本PID 5300佔用的所有TCP Port瞬間被釋放,Case Closed!

我的GitHub首航 - jQuery版台灣地區地址輸入輔助器

$
0
0

一個多月前重翻Silverlight版,寫成純jQuery版地址輸入輔助器,當時提到近期會以Open Source方式釋出。薑薑薑薑~ 終於,我也有作品在偉大的GitHub航道載浮載沈乘風破浪了!

過去有過使用CodePlex分享開源專案的經驗。近幾年來,GitHub憑藉著符合社群交流特性的P2P運作模式,漸漸成為開源專案的最主要集散中心(jQuery, PHP, Perl, Ruby, Ruby on Rai, Mono, YUI… 全在上面),聲勢如日中天,就連Visual Studio也納入Git支援。更有種說法: 這年頭找程式設計師除了要看應徵者的Blog、Twitter、Plurk跟Facebook,還要順道看看他在GitHub的活動軌跡,預先了解對方對軟體開發的熱情、興趣專長、甚至程式碼風格。

GitHub,儼然已成為開發者的準社交平台,一個用程式碼PO文、用Star按讚、用Fork轉分享的另類臉書。

廉頗老矣,無力效法熱血青年在開源社群縱橫,但分享的精神還是有的;沒本事學高手牛人們砌牆蓋屋造橋舖路,幸好種棵小樹美化環境還在能力所及,有興趣的朋友就加減參考吧!

各位觀眾,jQuery版台灣地區地址輸入輔助元件 on GitHub! https://github.com/darkthread/twAddrHelper

【茶包射手日記】ImageButton+UpdatePanel+IE10=ASP.NET錯誤

$
0
0

系統Log顯示,某個運作多年的網站最近冒出以下錯誤:

System.Web.HttpUnhandledException: Exception of type 'System.Web.HttpUnhandledException' was thrown. ---> System.FormatException: Input string was not in a correct format.
   at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
   at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
   at System.Web.UI.WebControls.ImageButton.LoadPostData(String postDataKey, NameValueCollection postCollection)
   at System.Web.UI.WebControls.ImageButton.System.Web.UI.IPostBackDataHandler.LoadPostData(String postDataKey, NameValueCollection postCollection)

程式久未更動,忽然出錯令人狐疑,後來找出關鍵點,當使用IE10瀏覽時才發生問題!

爬文佐以實驗,整理出以下心得:

  1. 發生條件: 1) 使用UpdatePanel包住ImageButton的ASP.NET網頁,2) IE10使用標準模式檢視(切成相容模式可避開)
  2. 遇到相同狀況的開發者不少,該問題在MS Connect已累計超過50票
  3. 問題根源為AJAX Client Library在IE10標準模式以UpdatePanel執行ImageButton送出,X、Y座標參數出現小數點,但ASP.NET PostBack接收時只接受整數
  4. ASP.NET 3.5/4.0都有此問題,ASP.NET 4.5則已修正

理論上只需調整AJAX Client Library或ASP.NET解讀X/Y值的邏輯即可解決,而目前已有Hotfix:

  • 2783780 Hotfix rollup 2783780 is available for the .NET Framework 2.0 SP2 in Windows Server 2003 SP2 and Windows XP SP3
  • 2784147 Hotfix rollup 2784147 is available for the .NET Framework 2.0 SP2 in Windows 7 and Windows Server 2008 R2
  • 2783767 Hotfix rollup 2783767 is available for the .NET Framework 4

安裝後一帖見效,問題排除。


再探文件套表 - Word套表與轉存PDF

$
0
0

需求如下:

有多份要遞交客戶的文件,由於格式與內容經常要微調,故規劃以Word檔形式由使用者自行編排修改。執行時由程式套版查詢資料庫後置換其中欄位,並以PDF格式輸出。

Word套版這事兒已是老生常談,但這回的特殊需求是必須轉成PDF格式。原本盤算用OpenXML SDK處理套版,再用第三方元件將Word轉成PDF,研究後發現Word內建的轉存PDF功能出奇的簡單,而Word本身的搜尋取代功能拿來處理套版也綽綽有餘,拍板定案 -- 就用Office Automation吧!

套版加轉PDF的程式碼如下:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Office.Interop.Word;
 
namespace WordToPdfService
{
publicclass PdfConverter : IDisposable
    {
private Application wordApp = null;
 
public PdfConverter()
        {
            wordApp = new Application();
            wordApp.Visible = false;
        }
 
publicbyte[] GetPdf(string templateFile, Dictionary<string, string> fields)
        {
object filePath = templateFile;
//檔案先寫入系統暫存目錄
object outFile =
                Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".pdf");
            Document doc = null;
try
            {
object readOnly = true;
                doc = wordApp.Documents.Open(FileName: ref filePath, ReadOnly: ref readOnly);
                doc.Activate();
                Stopwatch sw = new Stopwatch();
                sw.Start();
//REF: http://bit.ly/Z9G5zg
                Range tmpRange = doc.Content;
                tmpRange.Find.Replacement.Highlight = 0; //去除醒目提示(Highlight)
                tmpRange.Find.Wrap = WdFindWrap.wdFindContinue;
object replaceAll = WdReplace.wdReplaceAll;
foreach (string key in fields.Keys)
                {
                    tmpRange.Find.Text = "[$$" + key + "$$]";
                    tmpRange.Find.Replacement.Text = fields[key];
                    tmpRange.Find.Execute(Replace: ref replaceAll);
                }
                sw.Stop();
                Debug.WriteLine("Replaced in {0:N0}ms", sw.ElapsedMilliseconds);
//釋放Range COM+                
                Marshal.FinalReleaseComObject(tmpRange);
                tmpRange = null;
//存成PDF檔案
object fileFormat = WdSaveFormat.wdFormatPDF;
                doc.SaveAs2(FileName: ref outFile, FileFormat: ref fileFormat);
//關閉Word檔
object dontSave = WdSaveOptions.wdDoNotSaveChanges;
                ((_Document)doc).Close(ref dontSave);
            }
finally
            {
//確保Document COM+釋放
if (doc != null) 
                    Marshal.FinalReleaseComObject(doc);
                doc = null;
            }
//讀取PDF檔,並將暫存檔刪除
byte[] buff = File.ReadAllBytes(outFile.ToString());
            File.Delete(outFile.ToString());
return buff;
        }
 
publicvoid Dispose()
        {
//確實關閉Word Application
try
            {
object dontSave = WdSaveOptions.wdDoNotSaveChanges;
                ((_Application)wordApp).Quit(ref dontSave);
            }
finally
            {
                Marshal.FinalReleaseComObject(wordApp);
            }
        }
    }
}

程式碼不複雜,只有幾個小地方要補充:

  1. Word活在Unmanaged世界,故使用完畢要確實用Marshal.FinalReleaseComObject釋放資源,並明確結束應用程式(Excel也有相同議題),否則.NET程式結束時,將無法自動清除佔用的Unmanaged資源。我寫了一個PdfConverter類別並實作IDisposable,在其中建立一個Word Applicatoin物件,並在IDispose()時確實結束它。如此,當外界透過using方式使用PdfConverter,可有效降低程式結束後殘留Word應用程式的風險。
  2. Word方法接受的參數都是傳址物件,故即便是true/false,也要先object flag = true,再以ref flag方式傳入,不能直接傳true/false。而.NET 4.0的具名參數在此大顯神威,讓我們在呼叫Word方法時只需傳入指定的參數項目,不用填入一堆missing。
  3. 要置換的欄位以Dictionary<string, string>方式傳入,程式一一取其Key,組成[$$KeyName$$]後搜尋文件中出現的地方並置換成Value值(但保留其字型、大小、顏色等設定),達到套表的目的。
  4. 實務上維護套表範本時,多期望在動態置換欄位處加上標示,以便能在檢視文件時能"一望即知"(看到這詞我就想趕一下羚羊)哪些地方的內容是動態的。套版程式允許為欄位加上Word的醒目提示(Highlight),在置換文件時會一併將醒目提示清除。

接著用個實例做測試,範本文件如下: (謎之聲: 奴才知道主子很想中樂透,但容奴才說兩句: 這張怎麼看都像詐騙信!)

建立PdfConverter物件,指定範本路徑,再傳入Dictionary<string, string>欄位資料,就能生出PDF檔囉!

            Dictionary<string, string> fields = new Dictionary<string, string>();
            fields.Add("Seq", "32767");
            fields.Add("LetterDate", DateTime.Today.ToString("yyyy年M月d日"));
            fields.Add("Name", "黑暗執行緒");
            fields.Add("Date", new DateTime(2012,12,21).ToString("yyyy年M月d日"));
            fields.Add("Amount", int.MaxValue.ToString("N0"));
            fields.Add("TelNo", "0800092000");
            fields.Add("AgentName", "林志玲");
            fields.Add("AgentTitle", "副理");
//使用using確保Word資源被釋放
using (var cvtr = new PdfConverter())
            {
                var buff =
                    cvtr.GetPdf(Path.Combine(
                        System.AppDomain.CurrentDomain.BaseDirectory,
"templates\\notice.docx"), fields);
                File.WriteAllBytes("d:\\Temp\\" + Guid.NewGuid() + ".pdf", buff);
            }
            Console.WriteLine("Done");
            Console.ReadLine();

產生結果如下: (謎之聲: 很好,這下子確定是詐騙無誤了!)

【後記】

以前述範本為例實測,套表約0.1秒,存PDF約0.9秒,但整個過程(含啟動Word Application及結束)卻要4秒。因此 ---不建議把前述範例整個搬進網頁執行,每個Web Request自己開啟一份Word Application在太過奢華,資源利用不符經濟效益且效能欠佳;在Web Application中設法建立共用機制,啟動多份Word Appliation消化套版轉檔需求是一種解法,但會有執行身分(ASP.NET多半會用權限較低的帳號執行)及程序生命週期的問題要傷腦筋。

而我想到的另一種做法是改採Console Application或Windows Service方式執行,開啟指定數量的PdfConverter(意味著只會開啟指定數量的Word Application,理論上與CPU核心數目相同時可達到最大產能)組成Pool,提供介面接收轉換需求,由Pool中的PdfConverter分擔處理,應該可以達到較佳的運作效率。如此可視為獨立的服務程式,可任意指定執行身分,管理監控方便,還能提供套表轉檔服務給Web以外的其他系統使用,算是不錯的解決方案。

不用IIS也能執行ASP.NET Web API

$
0
0

在某些情境,桌面環境執行的程式(Console、Windows Form、WPF… 等)也需要提供API管道供外界呼叫,例如: 先前提到的Word轉PDF服務、ERP UI接受外部(如Excel VBA)匯入資料... 等等。

設計API管道時有不少選擇: DDE、Anonymous Pipe/Named Pipe、Socket... 都可行。對轉行寫桌面程式的ASP.NET開發者來說,還有一個溫馨的好選擇 -- 在桌面程式專案裡寫ASP.NET Web API吧!!

是的,即使沒有IIS,ASP.NET Web API也能照跑不誤,在Windows Form、WPF可以繼續用同一招打天下,對跨界寫桌面程式的ASP.NET開發人員,實在是一大福音。

以下使用Console Application專案做個簡單示範。建好新專案後,透過NuGet Packages Manager尋找self host,可以找到"Microsoft ASP.NET Web API Self Host"套件,二話不說立刻安裝。

ASP.NET Web API Self Host由多個組件構成,但不用擔心,NuGet會自動一一下載安裝好。

安裝完成後,我們要在主程式中加幾行程式,啟動一個小小的Http Server。

第一步要先透過HttpSelfHostConfiguratio宣告提供Web API的URL。由於向Windows註冊特定的TCP Port需要權限,有兩種做法: 以管理者身分執行Visual Studio及應用程式,或是透過netsh http add urlacl url=http://+:port_number/ user=machine\username指令授權。依"永遠只授與足以執行的最小權限"的資安原則,用netsh授權雖然手續較麻煩,但比讓整個應用程式都具有管理者權限安全。

接著,使用Routes.MapHttpRoute()指定MVC必備的路由設定,就可使用這組設定值宣告一個HttpSelfHostServer並啟動。由於會動用到網路資源,建議使用using HttpSelfHostServer的寫法,確保結束時會透過Dispose()釋放相關資源。

加上一段迴圈,直到使用者輸入exit才結束HttpSelfHostServer。在這段期間,HttpSelfHostServer便能接收HTTP請求,找到適當的Controller提供服務。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Http;
using System.Web.Http.SelfHost;
 
namespace SelfHostWebApi
{
class Program
    {
staticvoid Main(string[] args)
        {
//指定聆聽的URL
            var config = new HttpSelfHostConfiguration("http://localhost:32767");
 
//注意: 在Vista, Win7/8,預設需以管理者權限執行才能繫結到指定URL,否則要透過以下指令授權
//開放授權 netsh http add urlacl url=http://+:32767/ user=machine\username
//移除權限 netsh http delete urlacl url=http://+:32767/
 
//設定路由
              config.Routes.MapHttpRoute("API", "{controller}/{action}/{id}", 
new { id = RouteParameter.Optional });
//設定Self-Host Server,由於會使用到網路資源,用using確保會Dispose()加以釋放
            using (var httpServer = new HttpSelfHostServer(config))
            {
//OpenAsync()屬非同步呼叫,加上Wait()則等待開啟完成才往下執行
                   httpServer.OpenAsync().Wait();
                Console.WriteLine("Web API host started...");
//輸入exit按Enter結束httpServer
                string line = null;
do
                {
                    line = Console.ReadLine();
                }
while (line != "exit");
//結束連線
                   httpServer.CloseAsync().Wait();
            }
        }
    }
}

Console Application專案沒有Models、Controllers、Views資料夾,要如何加入Web API Controller讓人有些茫然,此時讓我們回歸ASP.NET MVC的"Convension over Configuration"(以慣例取代設定)原則: 在專案中新增一個名為BlahController的類別並繼承ApiController,Self Host自然會依著類別名稱認出它,並在有人呼叫http:// localhost:32767/Blah時派它上場。

為了測試,我宣告了一個很沒營養的Date方法傳回日期字串,標註[HttpGet]是為方便用瀏覽器輸入URL就能直接看結果(否則預設只接受POST,需要寫JavaScript才能測試)。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Http;
 
namespace SelfHostWebApi
{
publicclass BlahController : ApiController
    {
        [HttpGet]
publicstring Date()
        {
return DateTime.Today.ToString("yyyy/MM/dd");
        }
    }
}

實際執行結果如下:

不會寫Socket、不懂Named Pipe,居然也能寫出具有API整合功能的桌面程式~ 衝著這點,讓我們一起呼喊: ASP.NET Web API 好威呀!

【範例】呼叫Self-Hosted ASP.NET Web API

$
0
0

前一篇文章提到不靠IIS在Console/WinForm/WPF程式裡也可以執行ASP.NET Web API,接著我們更深入一點,談談Client端如何傳遞資料給ASP.NET Web API。

在ASP.NET Web API的傳統應用,Client端多是網頁,故常見範例是透過HTML Form、JavaScript、AJAX傳送參數資料給Web API;而在Self-Hosted ASP.NET Web API情境,由於Web API常被用於系統整合,呼叫端五花八門,.NET程式、VBScript、Excel VBA、Java... 都有可能,所幸Web API建構在HTTP協定之上,不管平台為何,都不難找到可用的HTTP Client元件或函式庫。

本文將示範我自己常用的兩種平台: .NET Client及Excel VBA。

首先,我們改寫前文範例,加上接受前端傳入Player物件新增資料的Insert() Action。由於ASP.NET MVC的ModelBinder已具備將JSON字串轉為Model物件的能力,我們也沒什麼好客氣的,直接宣告Player物件當成Insert()的輸入參數,JSON字串轉物件的工作就丟給ASP.NET MVC底層傷腦筋。

BlahController.cs改寫如下,程式碼很單純,唯一的小手腳是要捕捉例外,產生自訂錯誤訊息的HttpResponseMessage,再以其為基礎拋出HttpResponseException,理由容後說明。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http;
using Newtonsoft.Json;
 
namespace SelfHostWebApi
{
publicclass BlahController : ApiController
    {
//宣告Model類別承接前端傳入資料
publicclass Player
        {
publicint Id;
publicstring Name;
public DateTime RegDate;
publicint Score;
        }
        [HttpPost]
publicstring Insert(Player player)
        {
try
            {
//輸出資料,驗證已正確收到
                Console.WriteLine("Id: {0:0000} Name: {1}", player.Id, player.Name);
                Console.WriteLine("RegDate: {0:yyyy-MM-dd} Score: {1:N0}",
                    player.RegDate, player.Score);
return"Player [" + player.Id + "] Received";
            }
catch (Exception ex)
            {
//發生錯誤時,傳回HTTP 500及錯誤訊息
                var resp = new HttpResponseMessage()
                {
                    StatusCode = HttpStatusCode.InternalServerError,
                    Content = new StringContent(ex.Message),
                    ReasonPhrase = "Web API Error"
                };
thrownew HttpResponseException(resp);
            }
        }
 
    }
}

呼叫端的寫法很簡單,WebClient.UploadString(url, jsonString)會以jsonString為內容丟出HTTP POST請求,但有個關鍵: 必須設定ContentType為application/json,告知ModelBinder我們所POST的內容是JSON字串,ModelBinder才能正確地反序列化成Player類別。測試程式另外亂傳空字串及非JSON字串,以測試輸入錯誤時Web API的反應。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Newtonsoft.Json;
 
namespace ApiTest
{
class Program
    {
staticvoid Main(string[] args)
        {
            WebClient wc = new WebClient();
string url = "httq://localhost:32767/blah/Insert";
//由WebException中取出Response內容
            Action<string> postJson = (json) =>
            {
try
                {
//重要: 需宣告application/json,才可正確Bind到Model
                    wc.Headers.Add(HttpRequestHeader.ContentType, 
"application/json");
                    var test = wc.UploadString(url, json);
                    Console.WriteLine("Succ: " +test);
                }
catch (WebException ex)
                {
                    StreamReader sr = new StreamReader(
                        ex.Response.GetResponseStream());
                    Console.WriteLine("Error: " + sr.ReadToEnd());
                }
            };
//故意傳入無效資料進行測試
    postJson(string.Empty);
            postJson("BAD THING");
//利用匿名型別+Json.NET傳入Web API所需的Json格式
              var player = new
            {
                Id = 1,
                Name = "Jeffrey",
                RegDate = DateTime.Today,
                Score = 32767
            };
            postJson(JsonConvert.SerializeObject(player));
            Console.ReadLine();
        }
    }
}

測試結果如下:

Error: Object reference not set to an instance of an object.
Error: Object reference not set to an instance of an object.
Succ: "Player [1] Received"

當傳入空字串及非JSON字串,UpdateString()會發生WebException,而透過WebException.Response.GetResponseStream()可讀取Insert()方法在捕捉例外時透過HttpResponseMessage傳回的訊息內容。如果我們不捕捉例外,任由MVC內建機制處理,則Client會收到Exception經JSON序列化後的結果(如下所示),資訊較詳細但需反序列化才能讀取。相形之下,拋回HttpResponseException可以精準地控制傳回的錯誤訊息及提示,更容易符合專案客製需求。

Error: {"Message":"An error has occurred.","ExceptionMessage":"Object reference
not set to an instance of an object.","ExceptionType":"System.NullReferenceExcep
tion","StackTrace":"   at SelfHostWebApi.BlahController.InsertByBinding(Player p
layer) in x:\\Temp\\Lab0603\\SelfHostWebApi\\SelfHostWebApi\\BlahController.cs:l
ine 34\r\n   at lambda_method(Closure , Object , Object[] )\r\n   at System.Web.
Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass1
3.<GetExecutor>b__c(Object instance, Object[] methodParameters)\r\n   at System.
Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object
instance, Object[] arguments)\r\n   at System.Threading.Tasks.TaskHelpers.RunSy
nchronously[TResult](Func`1 func, CancellationToken cancellationToken)"}

最後補上VBA寫法:

Sub SendApiRequest(body AsString)
Dim xhr
Set xhr = CreateObject("MSXML2.ServerXMLHTTP")
Dim url AsString
    url = "httq://localhost:32767/blah/insert"
    xhr.Open "POST", url, False
    xhr.SetRequestHeader "Content-Type", "application/json"
OnErrorGoTo HttpError:
    xhr.Send body
    MsgBox xhr.responseText
ExitSub
HttpError:
    MsgBox "Error: "& Err.Description
EndSub
 
Sub Test()
    SendApiRequest "BAD THING"
    SendApiRequest "{ ""Id"":1,""Name"":""Jeffrey"", "& _
"""RegDate"":""2012-12-21"", ""Score"":32767 }"
End Sub

我的App開發夢--「國語辭典」上架囉!

$
0
0

去年底趁著BUILD大會的優惠,開開心心花錢註冊好Windows Phone開發者帳號,熱血沸騰地打算邁向偉大的App開發航道,沒想到就這麼日復一日在上班下班與日常瑣事中消磨著,一眨眼半年過去,別說App,連個屁都沒有。

五月某一天,忽然有股衝動,憶起當年身處iPhone圍攻仍堅守四行倉庫的堅持,為的不就是衝著"身為程式魔人居然不會在自己手機上寫Code將是一生的污點"? 沒想到開發帳號有了,920也已入手,幾年過去一事無成,豈不當了"人過四十只剩一張嘴"的活證。

就這麼熱血上身拼掉整個週末(話說: 寫程式這檔事還是得一鼓作氣方有所成,想一天寫個幾行聚沙成塔? 肯定是天方夜譚!!),題材則選了我一直想寫的"國語辭典",很久前就寫過Windows Form版,這回算是重新改用XAML + MVVM打造,但為了配合手機有限的CPU、記憶體及儲存空間,在資料結構及檢索方式做了許多調整,其中最具有挑戰性的部分是Tombstoning後的狀態還原(註: Windows Phone App在多工切換過程可能會從記憶體被移除,而App程式必須能在重新載入時,還原回切換前一刻的操作狀態,盡可能讓使用者無感)。由於辭典本文及索引資料都很有分量,不能一股腦往暫存區塞了事,加上還有頁面切換的議題,總之,費了好番手腳才搞定。(說不定仍有Bug,大家如果發現了請再回報給我 orz)

之前聽過不少App被退件的血淚史,提交App讓人期待又怕受傷害,幸好有前輩的經驗導引(黃忠成老師的這篇整理很值得一讀),多少能避開一些常見的地雷。即便程式寫好自己就一路Dogfooding,也邀請朋友充當Beta Tester進行小規劃封測,但仍然難減按下送件鈕那一刻的忐忑。週末送了件,依慣例需要1-5個工作日進行人工審查,週三一早收到來信,小心翼翼點開,看到"Congratulations"字樣映入眼簾,啊哈! 我終於摸到人生另一顆三角點!

   

歡迎有Windows Phone手機的朋友下載使用(今天就去買一支也成! :P),如有使用上的意見或建議,請在部落格或FB專頁留言給我,謝謝大家~

【下載】App連結或 直接在市集搜索"國語辭典"

 

【開發雜記】

  1. 同一支程式,在HD7與920上跑起來天差地遠,只差兩年的產品速度差了何止10倍(實測影片),大家的結論是--除了硬體演進,WP7到WP8的OS核心差異也是重要關鍵。
  2. 軟體上架後大約到24小時,市集資料更新完成,才能在市集搜索到。
  3. 辭典資料來自一場黑客松的成果,背後有一段非常精彩的故事,感謝社群高手們的付出與貢獻(看到高手們出招的架式,才驚覺自己還在提鞋的等級),駭客精神萬歲!!

【答客問】JavaScript修改WebForm DropDownList選項

$
0
0

【案例】

某個ASP.NET WebForm網頁,加入JavaScript動態修改欄位,送出表單時出現錯誤:
(英文版) Invalid postback or callback argument.  Event validation is enabled using <pages enableEventValidation="true"/> in configuration or <%@ Page EnableEventValidation="true" %> in a page.  For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them.  If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation.
(中文版) 無效的回傳或回呼引數。已在組態中使用 <pages enableEventValidation="true"/> 或在網頁中使用 <%@ Page EnableEventValidation="true" %> 啟用事件驗證。基於安全性理由,這項功能驗證回傳或回呼引數是來自原本呈現它們的伺服器控制項。如果資料為有效並且是必需的,請使用 ClientScriptManager.RegisterForEventValidation 方法註冊回傳或回呼資料,以進行驗證。

追查後發現問題出在JavaScript為<asp:DropDownList>動態新增了選項。

ASP.NET 2.0+為避免原本Server端管控的下拉選單被駭客加料塞入非預期值,故DropDownList會記下原有選項組合,一旦Client讓該欄位送回選項以外的值,便會觸發錯誤。解決之道是透過RegisterForEventValidation()方法向ASP.NET預告該欄位可能出現的值,如此資料送回時比對吻合就能通過驗證。

用一個範例來說明: WebForm網頁上有一個DropDownList,預先宣告C#及VB.NET兩個ListItem,另外再透過JavaScript加入Ruby及JavaScript兩個新<option>。至於Server端,我們需覆寫Render()方法,加入ClientScript.RegisterForEventValidation(),但此處只註冊JavaScript,以觀察Ruby及JavaScript兩個選項產生的結果。程式碼如下:

<%@ Page Language="C#" %>
 
<!DOCTYPEhtml>
 
<scriptrunat="server">
protectedvoid btn_Click(object sender, EventArgs e)
    {
        Response.Write(ddl.SelectedValue + " " + Request["ddl"]);
        Response.End();
    }
//為了讓Cient新增的DropDownList選項被接受,Page需覆寫Render方法
//註冊前端可能動態加入的新選項
protectedoverridevoid Render(HtmlTextWriter writer)
    {
        ClientScript.RegisterForEventValidation(
            ddl.UniqueID, "JavaScript");
base.Render(writer);
    }
 
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Test DropDownList Change in Client Side</title>
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.0.min.js"></script>
<script >
        $(function () {
//動態增加兩個新選項,注意: 選Ruby送出會出錯,選JavaScript卻OK
            $("#ddl")
            .append("<option value='Ruby' selected>Ruby</option>")
            .append("<option value='JavaScript'>JavaScript</option>");
        });
</script>
</head>
<body>
<formid="form1"runat="server">
<asp:DropDownListID="ddl"runat="server">
<asp:ListItem>C#</asp:ListItem>
<asp:ListItem>VB.NET</asp:ListItem>
</asp:DropDownList>
<asp:ButtonID="btn"runat="server"OnClick="btn_Click"Text="Submit"/>
</form>
</body>
</html>

執行結果如上圖,下拉選單會出現四個選項,選Ruby按Submit會出錯(如下圖);選C#、VB.NET、JavaScript按Submit則不會出錯。

但是還有一個問題: 選取JavaScript雖然不會出錯,Request["ddl"]也能取得選取結果--"JavaScript",但透過ddl.SelectedValue查到的卻是C#,表示DropDownList.Selected*屬性只接受Server端建立的選項,應用時需留意此一限制。

如此看來,若只是為了在Server端及Client端都能增減下拉選單選項,可以不用DropDownList,改用<select runat="server">更簡單,且後端取值應以Request["…"]為準。

<%@ Page Language="C#" %>
 
<!DOCTYPEhtml>
 
<scriptrunat="server">
protectedvoid btn_Click(object sender, EventArgs e)
    {
        Response.Write(Request["ddl"]);
        Response.End();
    }
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Test DropDownList Change in Client Side</title>
<script src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.0.min.js"></script>
<script >
        $(function () {
//動態增加兩個新選項
            $("#ddl")
            .append("<option value='Ruby' selected>Ruby</option>")
            .append("<option value='JavaScript'>JavaScript</option>");
        });
</script>
</head>
<body>
<formid="form1"runat="server">
<selectname="ddl"id="ddl"runat="server">
<option>C#</option>
<option>VB.NET</option>
</select>
<asp:ButtonID="btn"runat="server"OnClick="btn_Click"Text="Submit"/>
</form>
</body>
</html>
Viewing all 2458 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>