July 2009 - 文章

【雛型】Docx套版列印功能試作

在我的程式開發生涯中,套版輸出指定格式的報表/表單一直是揮之不去的煩人差事,沒什麼營養,偏偏在每個案子裡幾乎都像小強一樣冒出來。

面對這類需求,轉成網頁是下策,因為列印時排版格式常會亂到一塌糊塗,鮮少讓人滿意。在經驗裡,Reporting Service是不錯的選擇(而且免費)。

但有些報表如確認書、通知書,在格式上並非Gird格式,跟Reporting Service最擅長的表格呈現有點差距,數量一多,要將User提供的Word檔一一轉成Reporting Service報表便成了苦差事,尤其某些文件被要求必須模仿到跟原始樣版分毫不差,常為了一兩公釐急死大丈夫。(有時,所謂原有樣版不過是某個User信手捻來的作品,並非官頒公訂,也沒人說過排版必須完全相符,但是你也知道的,各地風俗民情不同,尤其是澳洲...)

當以上情境發生,直接把User提供Word樣版裡文字換一換,照樣生出個Word檔來,在我看來是最簡便直覺省工的解法。

依我所知,動態產生Word要在ASP.NET上實現,有幾種解法,個人評估分析如下:

  1. 利用Office Automation(就如同VBA操作Word/Excel Object Model的做法)
    在Server端(ASP.NET)使用Office Automation技巧有些額外的風險,我自己有過不好的經驗,依MS KB的說法也不建議。
  2. 產出HTML後標註ContentType,讓Word/Excel開啟
    先前曾貼文介紹過這種做法,但逼真度與真實doc/xls有段差距,例如: doc沒法精確分頁、xls不支援多Worksheet... 等等。
  3. 使用3rd Party元件直接產生Office文件
    剛才提到的MS KB裡就有些建議的元件廠商,另外還有一家元件廠商ASPOSE的名字也常被提到。只是,元件的價格通常不便宜。
  4. docx/xlsx OpenXML
    Office 2007在檔案格式上做了大改革,揚離了過去的獨有二進位格式,變成公開的標準,而docx/xlsx,其實是ZIP起來的一堆XML及資源檔。這麼一來,修改XML也比以前搞封閉的二進位檔案格式簡單多了,也因為標準公開,可用的技術資源也多。
    使用docx/xlsx固然便捷,缺點是產出的docx/xlsx Office 2003/2000無法直接開啟,所幸微軟提供了免費的Word Viewer可以用來檢視,另外也有Microsoft Office Word、Excel 及 PowerPoint 2007 檔案格式相容性套件能將docx/xlsx轉存為doc/xls。在我看來,這點是可被容忍的缺陷。因此,我決定設法試做OpenXML的解決方案。

雖然我們可以自行將docx Rename成.zip,解壓縮後取出XML修改再存回壓縮回去,但MS提供了更好的支援服務: (SDK文件裡的範例寫得簡單明瞭,讓我一下子就上手!! 哦哦~~ Open XML Format SDK,我要輕輕為你唱首歌)

因為我想做的套版只是更換特定字串,用1.0的寫法也不算太鳥,加上打算用來Production環境,不想承擔CTP的風險。所以我決定現階段先用1.0 SDK正式版來實作,日後有更複雜運用時再改版。

今天只花了幾個小時,還真的把雛型做出來了:

1.先用Word 2007存一個docx檔案,把其中要動態更換的文字用[$keyName$]的格式標起來。(這種[$...$]的格我還為它取了個名字--Parser Tag,從ASP時代,我就一直用它來玩範本套表的把戲)

2.寫一小段程式,把[$keyName$]要更換的文字轉成Dictionary<string, string>,連同範本的檔案路徑一起傳給套表輔助元件DocxHelper,傳回的byte[]就是套表好的docx檔案內容。

    protected void btnExport_Click(object sender, EventArgs e)
    {
        string template = Server.MapPath("Notice.docx");
 
        Dictionary<string, string> dct = new Dictionary<string, string>()
            { { "today", DateTime.Today.ToString("yyyy-MM-dd") },
              { "name", txtName.Text },
              { "addr", txtAddr.Text },
              { "amount", txtAmount.Text } };
 
        Response.Clear();
        Response.ContentType = "application/octet-stream";
        Response.AddHeader("content-disposition", "attachment;filename=Notice.docx");
        Response.BinaryWrite(
            Darkthread.OpenXml.DocxHelper.MakeDocx(
                Server.MapPath("Notice.docx"), 
                dct)
        );
        Response.End();
    }

3.實際跑起來,按下【匯出DOCX】,TextBox裡的文字就會被填進下載的docx中,很棒吧!!

天哪,我真的中了大樂透了嗎? 不會吧? 莫非有人在耍我? (謎之聲: 不就是你自己嗎? 找時間去看一下精神科好唄?)

我打算在手邊的小案子試行這個新元件,陸續蒐集一些問題跟情境,待元件較為成熟後,到時再推出懶人包跟大家分享。

邊做邊學 jQeury教學勘誤表-1

文章埋藏各種錯誤,並以"野火燒不盡春風吹又生"之姿向世人展現旺盛的生命力,向來是敝人寫作時的一大特色,但隱隱以為這是我邁向專業作家路上的絆腳石呀! (長嘆)

即便交稿前看了又看,讀了又讀,每回總是會遺留幾個可恥的錯誤在文章中(恨)。我有點想效法SQL指令保險栓裡提到的副艦長、武器官複核模式加強校稿,不過人家核武幾十年也用不了一次且跟地球存亡有關,才值得這般慎重。像我這樣三五不時寫些無關痛癢小文章也妄想當艦長,感覺像連尿尿也規定要副艦長拿出第二把鑰匙並恭請武器官按密碼才能開廁所門一般小題大做,除了被別人不屑不理外,最後應該會以膀胱發炎收場吧?

對不起,我離題惹。(跳一下)

以下是網友Jim反應教學文章中有兩處錯誤,在此公告更正並已通知微軟幫忙校正,在此感謝熱心指正!!! 各位網友如發現還有謬誤之處,敬請見諒並不吝指正。

邊做邊學 jQeury 系列5 -jQuery 的樣式、屬性、欄位內容存取語法

原文: 在使用時選取同一群 radio(可透過 #name) 以 val() 取得被選取 radio 的值。
修改: 在使用時選取同一群 radio再加上:checked條件以 val() 取得被選取 radio 的值

範例: 改為input:checked

邊做邊學 jQuery 系列6-jQuery 網頁元素操控

mozila應改為mozilla

Posted 29 July 2009 09:15 AMJeffrey | 6 comment(s)
Filed under:
【茶包射手專欄】WCF傳回DataTable時發生錯誤

被一個WCF問題卡住好一陣子。

在專案開發過程中,我測試了WinForm Call WCF傳回DataTable的做法,卻一直得到一個錯誤訊息。
(註: DataTable序列化後體積頗為可觀,在網路上傳輸並不是有效率的做法,我用WCF直接傳回DataTable是在開發初期先驗證可行性,打算後續再做效能改良。這傳說中黑董事長"先研究不傷身體,再講求效能"的開發理念!!)

"An error occurred while receiving the HTTP response to httq://localhost/MyWCFSvc/WCFDataHelper.svc. This could be due to the service endpoint binding not using the HTTP protocol. This could also be due to an HTTP request context being aborted by the server (possibly due to the service shutting down). See server logs for more details."

由這個訊息研判,我高度懷疑跟WCF設定成使用Windows認證有關,查了許久,看起來我IIS端的設定沒有問題(上回使用相同設定就成功),WinForm也加了身份宣告如下:
client = new MyWCF.WCFDataHelperClient();
clisnt.ClientCredentials.Windows.ClientCredential =
    new NetworkCredential("myServer\\demo", "****");

當密碼故意給錯時,會傳回不一樣的訊息:

The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate,NTLM'.

這樣推估起來,那個錯誤是在身份認證完成後才傳回的。

一籌莫展之際,回歸最原始的方法,建立對照組。我另外建一個WCF,再把Method一個一個加上去。開始有些新發現:

  1. 走Windows認證呼叫WCF傳回"HelloWorld"的測試是OK的。表示不是Windows認證問題。
  2. 加了一個傳回Dictionary<string, string>的Method,也順利得到結果。(小插曲,發現VS2008在WinForm app.config加入的WCF Client binding maxBufferSize/maxReceivedMessageSize屬性太小,只有64KB,我很大氣地放大到16MB)
  3. 加入傳回DataTable的Method。噹! 踢到鐵板,傳回一開始那個HTTP Error訊息。

DataTable有什麼特別呢? 在腦中快速翻日記,想起一件事,之前用DataTable.WriteXml時,若未設TableName會得到"Cannot serialize the DataTable. DataTable name is not set."的錯誤訊息。在WCF傳回DataTable,肯定也經過序列化,極有可能是凶手就躲在裡面。

為DataTable加上TableName,糾纏多時的宿疾,就這麼不藥而瘉。

【心得】要使用WCF傳送DataTable時,請記得加上TableName。不過,無法序列化為什麼要傳回彷彿WCF不存在的錯誤訊息,實在是太機車了,劣劣劣劣。

小心駛得萬年船--SQL指令保險栓

手動對資料庫下指令是一件恐怖的事,稍一操作不慎,就有可能把整個系統給毁了。

理論上,吾人應該極力避免手工更動資料這等可恥行徑。只要系統考慮得夠周詳,預先料想到所有可能出現的詭異狀況,一一提供相關的介面,經過程式邏輯檢查後才對資料進行處置或修正,不可能出現需要手動改資料的狀況。這是一個好的系統應有的嚴謹度!!

好,官冕堂皇的屁話說完了,現在來聊聊怎麼做好這件"可恥的事"? (道德感強烈者或軟體工程基本教義派請略過本文)

當我們萬不得已,必須使用T-SQL指令直接對資料庫進行操作時,由於指令不受到任何既有程式邏輯的保護,條件設定稍有不當,很有可能本來只要更新一筆,變成更新1000筆;想刪除某個人的資料卻把整個表都清除。因此,除了操作前要反覆檢查指令外,再三提醒自己小心謹慎外,還有一些小技巧:

  1. 請同事幫忙檢核指令
    每個人都有盲點,在緊急狀況下尤其更會因情緒、壓力而失誤。因此,針對重大更動指令,可仿效核武啟用程序,艦長的發射指令需要經過副艦長、武器官的檢核後才允許被執行。以減少個人疏忽可能引發的災難。
  2. 用begin tran保留反悔機會
    在SQL Sever Management Studio/Query Analyzer上的指令一經執行,就像射出去的箭,想抓都抓不回來。利用begin tran宣告成Transaction,最後的commit tran加上Remark,可以防止手滑按下執行時發生不測。
  3. 分段執行並觀察更動筆數
    如果更動指令有多段,可分段選取及執行, 並留意每段指令更動筆數是否如預期,若有不吻合的狀況,請立刻用rollback tran取消。
  4. UPDATE/DELETE前的範圍檢查
    利用以下技巧,可以在UPDATE, DELETE前先檢查WHERE條件的範圍是否符合預期。另外,用Remark將有殺傷力的部分包裝起來,必須要明確選取才能執行,可防止誤跑傷人。
  5. WHERE條件不嫌多
    在更新刪除資料時,若WHERE條件不是Primay Key,不妨多加幾項比對條件。例如: 除了比對姓名外,順便檢查生日、加入會員日期等,很多時候,某些欄位的唯一性並不如我們想像。
  6. 設定手動commit
    SQL Server Management Studio可以預設成每次執行都自動包成Transaction,且不自動Commit。設定後,每次跑完指令都要多下commit tran才算數,較為麻煩,但本著流汗總比流血好的哲學,也是可以考慮。
  7. 快速表格備份
    忘了說,還有一招: SELECT * INTO BackupTableName FROM TableName可以快速把表格內容複製一份保留下來,更新後可用來比對或修復資料,非常好用。

PS: 以上技巧,部分習自小熊子的獨門心法,特此鳴謝!

【茶包射手專欄】又見SSRS無法列印問題

這幾天又零星傳出災情,部分使用者在Windows Update後,回報原本的Reporting Service列印功能無法使用,按列印時出現以下訊息:

Unable to load client print control.
無法載入用戶端列印控制項

這是老問題了。觀察了網頁封包,確認問題出在RSClientPrint版本還是舊的FA91...這組。

httq://server/ReportServer/Reserved.ReportViewerWebControl.axd?ExecutionID=...&ControlID=...&Culture=127&UICulture=9&ReportStack=1&OpType=PrintHtml

<OBJECT ID="RSClientPrint" CLASSID="CLSID:FA91DF8D-53AB-455D-AB20-F2F023E498D3" CODEBASE="/ReportServer/Reserved.ReportViewerWebControl.axd?ExecutionID=...略...&amp;OpType=PrintCab#Version=2005,090,3042,00" VIEWASTEXT>

依照以前的經驗,安裝了SQL 2005 SP1 GDRReport Viewer SP1。本以為會藥到病除,但不知什麼原因,Microsoft.ReportingServices.Diagnostics.dll, ReportingServicesNativeServer.dll, ReportingServicesService.exe, ReportingServicesWebServer.dll, RSClientPrint.cab五個檔案日期並沒有更新成2008/8/5版本。

今天沒氣力去查出更新失效的原因,決定用霸王硬上弓法,從其他正常主機上Copy了這五個檔案的2008/8/5版,複製到C:\Program Files\Microsoft SQL Server\MSSQL.2\Reporting Services\ReportServer\bin下,RSClientPrint版本便被強迫更新了:

<OBJECT ID="RSClientPrint" CLASSID="CLSID:41861299-EAB2-4DCC-986C-802AE12AC499" CODEBASE="/ReportServer/Reserved.ReportViewerWebControl.axd?ExecutionID=...&amp;OpType=PrintCab#Version=2005,090,3073,00" VIEWASTEXT></OBJECT>

雖然手法有點粗暴(沒辦法,我叫小賀),但問題解決囉~~

【延伸閱讀】

MEMO-取回Oracle Procedure Ref Cursor

[MEMO系列是老人家備忘用途的貼文,可能沒什麼營養,大家請姑且看之或逕行忽略。]

好久沒跟ORACLE纏綿惹,這陣子都在跟SQL Server廝混。這幾天接手另一個連線ORACLE的專案,在呼叫ORACLE Procedure透過Ref Cursor傳回結果時,腦中已不太記得精確寫法,只記得Ref Cursor的值可以直接用來Fill DataTable或轉成OracleDataReader,胡亂湊出程式碼,系統卻一直傳回以下錯誤:

ORA-06550: line 1, column 7:.PLS-00221: 'MyProc' is not a procedure or is undefined.

OracleCommand cmd = new OracleCommand 
{ 
    CommandText = "MyProc", 
    CommandType = CommandType.StoredProcedure 
};
cmd.Parameters.Add("p_arg1", OracleType.VarChar).Value = "blah";
cmd.Parameters.Add("p_arg2", OracleType.VarChar).Value = "blahbooboo";
dt = MyUtil.GetDataTable(cmd, cnnString);

在錯誤訊息的導引下,拼了老命張大眼睛檢查,Procedure名稱確定沒有拼錯,也確認該Procedure存在於資料庫中。雖然有點殺雞用牛刀,但我還是用茶包射手的手法解決這個問題(其實是太久沒射茶包技癢),祭出了Microsoft Network Monitor查看封包,發現我的程式組合出以下的PL/SQL送去執行:

begin
MyProc(p_arg1=>:p_arg1, p_arg2=>:p_arg2);
end;

這段PL/SQL錯在沒有接回Procedure所傳回的Ref Cursor,而在ORACLE上執行的確就會傳回找不到MyProc這個Procedure的錯誤訊息。

找出問題後一切好說,原來就是自己老糊塗沒搞清楚Ref Cursor的接回方法,怪不得人。參考微軟KB,加上OracleType.Cursor參數,問題解決!

OracleCommand cmd = new OracleCommand 
{ 
    CommandText = "MyProc", 
    CommandType = CommandType.StoredProcedure 
};
cmd.Parameters.Add("p_refcur", OracleType.Cursor).Direction 
    = ParameterDirection.ReturnValue;
cmd.Parameters.Add("p_arg1", OracleType.VarChar).Value = "blah";
cmd.Parameters.Add("p_arg2", OracleType.VarChar).Value = "blahbooboo";
dt = MyUtil.GetDataTable(cmd, cnnString);
Posted 23 July 2009 01:27 PMJeffrey | no comments
Filed under: , ,
2009/07/22 09:40:29 日全食
  1. 21世紀計會發生224次日食現象,其中有77次「日偏食」,72次「日環食」,68次「日全食」及7次「混合型日食」(日食過程中環食及全食交替出現) 。
  2. 西元2009年7月22日日全食後,下次日全食 將發生於西元2010年7月12日,可見全食地區為南太平洋及南美洲南部陸地。
  3. 臺灣上次日全食是出現在西元1941年9月21日(北海岸-基隆-濱海公路北段及馬祖)。
  4. 臺灣未來日全食將發生於西元2070年4月11日(墾丁、蘭嶼) 。
  5. 西元2012年5月21日臺灣將可見到日環食現象,包括基隆、 臺北、桃園、新竹及苗栗等局部地區均在環食帶內。

資料來源: 中央氣象局網頁

上一次是68年前,下一次在61年後。

跟宇宙久遠到難以想像的時空相比,100年不過是短暫瞬間,卻可能已超出一個人一輩子所能涵蓋的最大極限。難得一見的日全食奇景,值得用力把它記錄下來...

(以下照片可點選檢視原始尺寸)

   

臨時只找到3.5" 1.44MB Floppy充當克難濾光片,卻搞出與眾不同的色彩效果。無法掌握克服的折射(反射?),卻在鏡頭上映出令人驚喜的對影...

各位同學,聖杯柳~~~~

【攝影資訊】Canon 10D + 28-135mm變焦鏡 + 7-11贈品包裝黑色不透光塑膠袋 + 3.5" Floppy碟片 + 橡皮筋固定,以上照片由黑暗工作室資深美女助理拍攝。

Posted 23 July 2009 01:43 AMJeffrey | 3 comment(s)
Filed under:
TIPS-C# 3.0的Dictionary元素簡式宣告法

為了找記憶中依稀存在的C# 3.0 Dictionary元素簡式宣告法,耗了我快五分鐘,下定決心把它寫成一篇KB,以拯救中老年人日益衰退的記憶力。

public static void Test()
{
    //要宣告固定元素的陣列,我們都知道可以簡寫成
    string[] strAry = { "A", "B", "C", "D" };
 
    //但要宣告固定元素Dictionary,傳統上只能一步一腳印
    Dictionary<string, string> dctTradWay = 
        new Dictionary<string, string>();
    dctTradWay.Add("Key1", "AAAA");
    dctTradWay.Add("Key2", "BBBB");
    dctTradWay.Add("Key3", "CCCC");
    dctTradWay.Add("Key4", "DDDD");
 
    //C# 3.0起,強化了Collection Initializer,所以...
    Dictionary<string, string> dctNewWay =
        new Dictionary<string, string>()
        {
            {"Key1", "AAAA"}, {"Key2", "BBBB"},
            {"Key3", "CCCC"}, {"Key4", "DDDD"}
        };
    
    //很酷吧!
}
Posted 21 July 2009 04:10 PMJeffrey | 7 comment(s)
Filed under: , ,
LINQ to SQL,說好的更新呢?

自從學會LINQ to SQL一行資料庫更新法,它便成為我專案裡常用的技巧。對於彈性要求較高、嚴謹性要求較低的複雜資料,我還喜歡借重SQL 2005起新增的XML資料型別作為儲存欄位。透過LINQ to SQL對應,Xml欄位會變成System.Xml.Linq.XElement Class,XElement在建立與操作上文件又比.NET 2.0時代XmlDocument、XmlElement的做法便捷許多。

例如: 我手上有一個簡單的XmlStore資料表。

CREATE TABLE [dbo].[XmlStore](
	[DocId] [varchar](16) NOT NULL,
	[XmlContent] [xml] NULL,
	[ModDateTime] [datetime] NOT NULL,
 CONSTRAINT [PK_XmlStore] PRIMARY KEY CLUSTERED 
(
	[DocId] ASC
)

用以下的程式片段三兩下就塞進一筆新資料。

        PlaygroundDataClassesDataContext db =
            new PlaygroundDataClassesDataContext();
        db.Log = new DebuggerWriter();
        XmlStore xs = new XmlStore()
        {
            DocId = "Dummy",
            XmlContent = new XElement("Root",
                new XElement("CodeName", "Darkthread")
                ),
            ModDateTime = DateTime.Now
        };
        db.XmlStores.InsertOnSubmit(xs);
        db.SubmitChanges();

用上回介紹過的DebuggerWriter,我們可以觀察到它轉化成的T-SQL

INSERT INTO [dbo].[XmlStore]([DocId], [XmlContent], [ModDateTime])
VALUES (@p0, @p1, @p2)
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [Dummy]
-- @p1: Input Xml (Size = 50; Prec = 0; Scale = 0) [<Root>
  <CodeName>Darkthread</CodeName>
</Root>]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:11:03]

接著,用一行更新法, 試圖更新XmlContent的內容。

        PlaygroundDataClassesDataContext db =
            new PlaygroundDataClassesDataContext();
        db.Log = new DebuggerWriter();
        var xs = (from o in db.XmlStores
                  where o.DocId == "Dummy"
                  select o).Single();
        XElement xe = xs.XmlContent;
        xe.Add(new XElement("Language", "C#"));
        xs.ModDateTime = DateTime.Now;
        db.SubmitChanges();

噹! 踢到鐵板~~~

依據DebuggerWriter截錄的UPDATE T-SQL,它只更新了ModDateTime,並未更新XmlDocument。

SELECT [t0].[DocId], [t0].[XmlContent], [t0].[ModDateTime]
FROM [dbo].[XmlStore] AS [t0]
WHERE [t0].[DocId] = @p0
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [Dummy]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30729.1

UPDATE [dbo].[XmlStore]
SET [ModDateTime] = @p2
WHERE ([DocId] = @p0) AND ([ModDateTime] = @p1)
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [Dummy]
-- @p1: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:11:03]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:12:52]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.30729.1

我知道LINQ to SQL會聰明地察覺哪些欄位有更動,只UPDATE必要的欄位,不會傻呼呼地所有欄位重新設定一次。我想,先前的寫法只改變XElement的內容,在LINQ to SQL的比對邏輯上並未被視為資料有異動。為了一探究竟,追進dbml對應的designer.cs,找到以下這段邏輯:

    [Column(Storage="_XmlContent", DbType="Xml", 
        UpdateCheck=UpdateCheck.Never)]
    public System.Xml.Linq.XElement XmlContent
    {
        get
        {
            return this._XmlContent;
        }
        set
        {
            if ((this._XmlContent != value))
            {
                this.OnXmlContentChanging(value);
                this.SendPropertyChanging();
                this._XmlContent = value;
                this.SendPropertyChanged("XmlContent");
                this.OnXmlContentChanged();
            }
        }
    }

由這段程式來看,它用!=做新舊值比對,我取出XmlConent做修改,根本未執行到set段,自然不會觸發SendPropertyChanged等相關邏輯。

依此方向,我做了幾個實驗:

【測試1】

XElement xe = xs.XmlContent;
xe.Add(new XElement("Language", "C#"));
xs.XmlContent = xe;

無效!! 我想是因為對Reference Type來說,自始至終都只有一個Instance,xs.XmlConent與xe,this._XmlConent與value也就保持恆等。

【測試2】

XElement xe = xs.XmlContent;
xe.Add(new XElement("Language", "C#"));
xs.XmlContent = null;
xs.XmlContent = xe;

還是無效! 由Line-by-Line Debug,設定null再設回來的過程中,我偵測到this.SendPropertyChanged("XmlContent");被執行,但我猜想底一層的比對可能會回歸到如測試1 Reference Type自己等於自己的狀況,所以功敗垂成。

最後我測試成功的版本如下: (我找不到XElement有Clone() Method,所以用轉為XML字串再重新建成另一個XElement的做法。)

XElement xe = xs.XmlContent;
xe.Add(new XElement("Language", "C#"));
xs.XmlContent = XElement.Parse(xe.ToString());

令人感動的結果出現!

UPDATE [dbo].[XmlStore]
SET [XmlContent] = @p2, [ModDateTime] = @p3
WHERE ([DocId] = @p0) AND ([ModDateTime] = @p1)
-- @p0: Input VarChar (Size = 5; Prec = 0; Scale = 0) [Dummy]
-- @p1: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:22:39]
-- @p2: Input Xml (Size = 77; Prec = 0; Scale = 0) [<Root>
  <CodeName>Darkthread</CodeName>
  <Language>C#</Language>
</Root>]
-- @p3: Input DateTime (Size = 0; Prec = 0; Scale = 0) [2009/7/19 上午 01:24:51]

【結論】

在LINQ to SQL中如要更新XElement資料,可用xs.XmlContent = XElement.Parse(newXml);讓資料被判定為有變動,以觸發資料更新。

Posted 19 July 2009 06:00 PMJeffrey | 4 comment(s)
Filed under: ,
CODE-用jQuery實作<select>選項上下移動

剛好有個需求要讓<select>裡的選項可以上下移動,落到本山人手上,想當然爾是用jQuery打死。

除了做了向上、向下鈕外,我還順手加上按【alt-向上】/【alt-向下】移動選項的快速鍵,沒想到程式碼比想像中還簡潔,忍不住貼文讚嘆一番!

嘖嘖嘖,短短兩個API串接: $opt.next().after($opt)就做出了<option>向下移動的效果。記得以往用純Javascript寫,還得判斷是否為最後一個,若是就不能下移;然後上下位置交換得用options[index]搞半天。不得不要再次讚嘆jQuery的神奇!

<html>
<head>
<script type='text/javascript' 
src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.js'></script>
<script type='text/javascript'>
$(function() {
$("#btnMoveUp,#btnMoveDown").click(function() {
  var $opt = $("#selList option:selected:first");
  if (!$opt.length) return;
  if (this.id == "btnMoveUp") $opt.prev().before($opt);
  else $opt.next().after($opt);
});
//按Alt加上下鍵也可以移動
$("#selList").keydown(function(evt) {
  if (!evt.altKey) return;
  var k = evt.which;
  if (k == 38) { $("#btnMoveUp").click(); return false; }
  else if (k == 40) { $("#btnMoveDown").click(); return false; }
});
});
</script>
</head><body>
<select id='selList' size='7' style='width: 100px'>
<option>Item 1</option>
<option>Item 2</option>
<option>Item 3</option>
<option>Item 4</option>
<option>Item 5</option>
</select><br />
<input type="button" value="▲" id="btnMoveUp" title="快速鍵: alt+向上" />
<input type="button" value="▼" id="btnMoveDown" title="快速鍵: alt+向下"/>
</body>
</html>

以上程式範例可以另存成HTML動手玩看看,或將<body>、$(function() { ... })裡的內容分別貼進Mini jQuery Lab裡Body HTML及Script區,也能馬上體驗!

TIPS-設定WCF使用Windows認證(補遺)

上次介紹過如何設定WCF使用Windows認證,今天處理一個WCF部署時,如法泡製卻一直撞壁... 呼叫MyDataService.svc時始終彈出:

Security settings for this service require 'Anonymous' Authentication but it is not enabled for the IIS application that hosts this service.

比對與上回的WCF設定差異主要在於用的是basicHttpBinding,而不是上回說的webHttpBinding,所以我依經驗將web.config修改如下,但看來行不通。

<system.serviceModel>
  <bindings>
    <basicHttpBinding>
      <binding name="BasicHttpEndpointBinding">
        <security mode="TransportCredentialOnly">
          <transport clientCredentialType="Windows" />
        </security>
      </binding>
    </basicHttpBinding>
  </bindings>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
    <behaviors>
      <serviceBehaviors>
        <behavior name="MyDataServiceBehavior">
      <serviceMetadata httpGetEnabled="true"/>
      <serviceDebug includeExceptionDetailInFaults="false"/>
          <dataContractSerializer maxItemsInObjectGraph="2147483647"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service
        behaviorConfiguration="MyDataServiceBehavior" 
        name="MyDataService">
        <endpoint address="" binding="basicHttpBinding"
        bindingConfiguration="BasicHttpEndpointBinding"
        name="BasicHttpEndpoint" contract="IMyDataService">
        </endpoint>              
        <endpoint address="mex" binding="mexHttpBinding" 
        contract="IMetadataExchange"/>
    </service>
  </services>
</system.serviceModel>

摸索加Google一陣子,找到一篇文章,提到mexHttpBinding預設會使用匿名存取,應該是導致前述問題的元凶,解決之道是比照basicHttpBinding也設成Windows驗證。

我評估部署的環境不需提供開發者WCF的Metadata,直接將<endpoint address="mex" ... />切除(其實是想偷懶),錯誤立刻消失,問題排除。

從LINQ to SQL的"一行更新法"聊起

我喜歡LINQ to SQL的簡潔,就拿更新資料庫某筆資料這件事來說,你可以忘記SqlConnection、丟掉SqlCommand、抛下SqlParameter,就搞定整個更新動作,對寫慣ADO.NET的人來說,實在是件不可思議的事。

像下面這個例子,寫一段LINQ配上Single()取得資料物件,重新指定值,然後SubmitChanges()就完成了ID=2 Player資料的CreateTime欄位更新。有種袖子都還沒捲起來,敵人就忽然自已暴斃的莫名爽快。

    protected void Page_Load(object sender, EventArgs e)
    {
        PlaygroundDataClassesDataContext db = 
            new PlaygroundDataClassesDataContext();
        db.Log = new DebuggerWriter();
        (from o in db.Players where o.ID == 2 select o)
            .Single().CreateTime = DateTime.Now;
        db.SubmitChanges();
    }

上回既然提過DebuggerWriter,我們就不該只知其然不知其所以然,來看看背後發生什麼事吧!

這裡發生了兩個SQL動作,在Single()時會SELECT取回ID=2的資料,而SubmitChages()則進行UPDATE動作。值得一提的是,在UPDATE時,除了ID=2,它還會一併比較Name及CreateTime,用意是確認資料讀出到更新這段期間資料沒有被其他人更動過,防止多人同時更新資料庫時發生彼此資料互相覆寫衝突。

要印證它的保護效果,我們可以在db.SubmitChanges()上設定中斷點,讓VS2008跑偵錯模式停在該中斷點上,此時另外開SQL Server Management Studio執行T-SQL更新ID=2的資料:

UPDATE Player SET CreateTime = '2009-07-08 12:34:56'
WHERE ID = 2

接著好戲上場,在VS2008按下F10執行db.SubmitChanges(),會發生以下例外:

System.Data.Linq.ChangeConflictException: Row not found or changed. (中文版為: 資料列找不到,或者已變更。)

這個實驗證實了LINQ to SQL的確有提供資料庫更新時的衝突管理,然而要如何妥善處理更新衝突是門學問,保哥有篇文章做了進一步的剖析,推薦給大家參考。

當然,如果效能不是最優先考量,我還有一招。以前介紹過可在LINQ to SQL上使用的TransactionScope大法,在Single()到SubmitChanges()間對資料上鎖,防止別人擅動,就不會有衝突的問題(但想其他要更新資料的人會被卡住,直到這一方的UPDATE完成為止)。在此還是要善盡提醒之責,包TransactionScope的方法較適合更新頻率不高、鎖定時間不長、使用人數不多的情境,採用前宜審慎評估,不然有可能大幅拖累系統整體效能。

    protected void Page_Load(object sender, EventArgs e)
    {
        using (System.Transactions.TransactionScope tx
            = new System.Transactions.TransactionScope())
        {
            PlaygroundDataClassesDataContext db =
                new PlaygroundDataClassesDataContext();
            db.Log = new DebuggerWriter();
            (from o in db.Players where o.ID == 2 select o)
                .Single().CreateTime = DateTime.Now;
            db.SubmitChanges();
        }
    }
在ASP.NET中觀察LINQ to SQL所產生的T-SQL語法

接連在好幾個小專案裡用了LINQ to SQL,慢慢掌握要領,煎、煮、炒、炸查詢、新增、修改、刪除,各種料理操作都已能手到擒來,就愈發感受到它的便利性。

說穿了,LINQ to SQL只不過是ORM的一種具體實踐,並無深奧學問,之所以用來得心應手、讓人驚豔,不外乎是在與Visual Studio 2008整合深度上佔了優勢。以一個開發者的角度而言,我不在乎這對其他解決方案是否公允? 也不關心這類綁標圖利是否會有爭議? 給我方便的開發工具,其餘免談。

過去曾用ADO/ADO.NET開發過很長一段時間,在效能議題上下過一些功夫。切換到LINQ to SQL後,完全不沾SqlConnection、SqlCommand、SqlParameter就能搞定與資料庫相關的大小事,固然讓人心曠神怡;但過去對效能斤斤計較,換到LINQ,我還是常常懷疑LINQ所自動轉譯成的T-SQL到底長得什麼德性,會不會荒腔走板、效能低落?

要解除疑慮,最直接有效的方法就是檢查LINQ to SQL所產出的T-SQL語法,沒有什麼比眼見為憑更具說服力了! System.Data.Linq.DataContext類別有個屬性叫Log,我們可以接上一個TextWriter,DataContext會在執行T-SQL指令時,輸出實際使用的T-SQL語法、參數細節,提供極佳的觀察與偵錯資訊。

不過,如MSDN文件所示,能找到的Log應用範例幾乎都是接上Console.Out適用於Console Application,如果我們是在網頁中執行,想要如同System.Diagnositcs.Debug.WriteLine一般輸出在VS2008的偵錯輸出視窗,該怎麼做呢?

我找到了Kris Vadermotten寫的DebuggerWriter類別,可以滿足以上的需求:

using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
 
    /// <summary>
    /// Original by Kris Vadermotten: http://www.u2u.info/Blogs/Kris/Lists/Posts/Post.aspx?ID=11 <br />
    /// Remixed by Jeffrey Lee, 2009-07-11 http://blog.darkthread.net<br />
    /// Implements a <see cref="TextWriter"/> for writing information to the debugger log. 
    /// </summary>
    /// <seealso cref="Debugger.Log"/>
    public class DebuggerWriter : TextWriter
    {
        private bool isOpen;
        private static UnicodeEncoding encoding =
            new UnicodeEncoding(false, false);
        public int Level { get; private set; }
        public string Category { get; private set; }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="DebuggerWriter"/> class.
        /// </summary>
        public DebuggerWriter()
            : this(0, Debugger.DefaultCategory) { }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="DebuggerWriter"/> class with the specified level and category.
        /// </summary>
        /// <param name="level">A description of the importance of the messages.</param>
        /// <param name="category">The category of the messages.</param>
        public DebuggerWriter(int level, string category)
            : this(level, category, CultureInfo.CurrentCulture) { }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="DebuggerWriter"/> class with the specified level, category and format provider.
        /// </summary>
        /// <param name="level">A description of the importance of the messages.</param>
        /// <param name="category">The category of the messages.</param>
        /// <param name="formatProvider">An <see cref="IFormatProvider"/> object that controls formatting.</param>
        public DebuggerWriter(int level, string category, IFormatProvider formatProvider)
            : base(formatProvider)
        {
            Level = level;
            Category = category;
            this.isOpen = true;
        }
 
        protected override void Dispose(bool disposing)
        {
            isOpen = false;
            base.Dispose(disposing);
        }
 
        public override void Write(char value)
        {
            if (!isOpen)
                throw new ObjectDisposedException(null);
            Debugger.Log(Level, Category, value.ToString());
        }
 
        public override void Write(string value)
        {
            if (!isOpen)
                throw new ObjectDisposedException(null);
            if (value != null)
                Debugger.Log(Level, Category, value);
        }
 
        public override void Write(char[] buffer, int index, int count)
        {
            if (!isOpen)
                throw new ObjectDisposedException(null);
            if (buffer == null || index < 0 || count < 0 || buffer.Length - index < count)
                base.Write(buffer, index, count); // delegate throw exception to base class
            Debugger.Log(Level, Category, new string(buffer, index, count));
        }
 
        public override Encoding Encoding
        {
            get { return encoding; }
        }
 
    }

將以上的DebuggerWriter.cs放入App_Code,然後youDataContext.Log = new DebuggerWriter(),設定Breakpoint,再一列一列Debug,你就可以觀察到何時LINQ to SQL會執行什麼樣的T-SQL指令,甚至包含參數值等細節,很犀利吧?

 

以上的範例,其實會觸發兩次SQL查詢,若要再精簡,可以改寫成:

var q = (from o in db.Players
        where o.ID < 20
        select o).ToList<Player>();

大家實際動手玩玩便知。

【2009-07-12補充】艾小克提供可以整合在VS2008裡,在開發階段檢視查詢所對應T-SQL並可直接試連DB做查詢的工具一枚,十分實用,一併列出供大家參考。

【2009-07-13補充】本草綱目有記載,若要查Query對應的CommandText,可以用DataContext.GetCommand,Insert/Update/Delete的部分則還沒看到Log法的替代方案,如有線報,歡迎提供。

與大海重逢

昨天早上上班出門,乍見天氣好到嚇人,藍天白雲,空氣清澄,指南群山歷歷在目,這是標準登高望達的絕佳時機,不禁讓我憶起從木柵看海的日子。無奈掛在肩頭上的五斗米狠心地將我拉回現實,乖乖忘卻攻頂,趕緊專心Coding。

7/10(五) 08:01 AM 為了抒發心情,我在噗浪上感嘆: "為了不辜負上天賜給我們這麼好的天氣,理應殺到猴山岳樂逍遙,而不是窩在辦公室蹲苦牢..." (有噗友安慰我,他在放無薪假,會盡力"代替"我樂逍遙的,就甘心A... orz)

7/11(六) 08:43 AM 我真的在猴山岳上,再一次從木柵與東海遠遠相望...

一張照片上同時出現圓山飯店跟航行中的船,很奇妙吧!

PS: 在山頂拍照時,另一位山友大叔說,今天的展望只有80分,昨天更美,有95分。
大景現於天,未必有空閒,有空又有閒,偏偏相機不在手邊,恰巧相機掛胸前,稍稍一出鎚,一樣沒有好照片... 在無數次與大景失之交臂後,聞此言已能無動於衷,瞬間釋懷... 我想我快得道了。

Posted 11 July 2009 01:11 PMJeffrey | 2 comment(s)
Filed under:
jQuery自動完成懶人包

前陣子在專案中用了jQuery AutoComplete Plugin,感覺甚好,我想將來很可能會繼續用在其他專案上,索性做了一個範例懶人包以備未來不時之需,利人也利已。(我預估自己大概下個月就會忘記這次是怎麼寫出來的,所以這個懶人包對我來說也粉重要)

放了一個線上的範例網頁讓大家試敲,你可以輸入電子類股的股票代號、中文/英文股票名稱,網頁會提供提示,選取後會在左邊填入股票代號、右邊填入中文名稱。如下圖剖析,其實它是用<ul><li>去建構清單,而我特別框出selectValue,extra,會呼應到稍後提到findValue()函數中取值的邏輯。

程式的簡單說明如下。

前端HTML中主要透過$("...").autocomplete()的方式在Textbox掛上自動完成功能:
(註: 原版的jquery.autocomplete.js有中文相容的問題,所以我用的是對岸網友修改過的版本,我自己則加了一個noCache參數。文件上說cacheLength設為1就不會做Cache,但我觀察結果似乎不然,也許是版本不同所致。當資料筆數很多時,Cache在Script Side的做法就會變得不合適,因此我加了一個參數鋸箭解決Cache問題。)

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>jQuery自動輸入完成懶人包</title>
    <script src="jquery-1.3.2.js" type="text/javascript"></script>
    <script src="jquery.autocomplete.js" type="text/javascript"></script>
    <link href="jquery.autocomplete.css" rel="stylesheet" type="text/css" />
    <style type="text/css">
    input 
    {
        width: 60px;
        margin-left: 5px;
    }
    </style>
    <script type="text/javascript">
        $(function() {
//選項說明: http://docs.jquery.com/Plugins/Autocomplete/autocomplete#url_or_dataoptions
            $("#txtSymbol").autocomplete("ACDataSrc.aspx",
            {
                delay: 10,
                width: 120,
                minChars: 1, //至少輸入幾個字元才開始給提示?
                matchSubset: false,
                matchContains: false,
                cacheLength: 0, 
                noCache: true, //黑暗版自訂參數,每次都重新連後端查詢(適用總資料筆數很多時)
                onItemSelect: findValue,
                onFindValue: findValue,
                formatItem: function(row) {
                    return "<div style='height:12px'><div style='float:left'>" + row[0] +
                            "</div><div style='float:right;padding-right:5px;'>" +
                            row[1] + "/" + row[2] + "</div></div>";
                },
                autoFill: false,
                mustMatch: true //是否允許輸入提示清單上沒有的值?
            });
            function findValue(li) {
                if (li == null) return alert("No match!");
                $("#txtSymbol").val(li.extra[0]);
                $("#txtCName").val(li.extra[1]);
            }
        });
    </script>
</head>
<body>
<input type="text" id="txtSymbol" />
<input type="text" id="txtCName" readonly="readonly" style="background-color: #cccccc;" />
</body>
</html>

另外,我寫了一隻ACDataSrc.aspx負責餵資料: (為力求單純,股票資料是用字串列出股票清單,實務上應該會以資料庫查詢取代之)

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.IO;
 
public partial class jQueryAutoComp_ACDataSrc : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //使用者目前輸入的文字預設以q傳入
        string q = Request["q"] ?? "";
        if (q.Length > 0)
        {
            DataTable t = getStockData();
            DataView dv = new DataView(t);
            //利用LIKE做查詢
            dv.RowFilter = "Key LIKE '" + q.Replace("'", "''") + "%'";
            dv.Sort = "Key, Symbol";
            List<string> lst = new List<string>();
            lst.Add("");
            foreach (DataRowView drv in dv)
            {
                DataRow r = drv.Row;
                //組裝出前端要用的欄位
                lst.Add(string.Format("{0}|{1}|{2}", r["key"], r["symbol"], r["cname"]));
                if (lst.Count >= 10) break;
            }
            //每筆資料間以換行分隔
            Response.Write(string.Join("\n", lst.ToArray()));
        }
    }
 
    private DataTable getStockData()
    {
        #region 股票代號
        string rawData = @"
1435    中福    C.F.C.Y.CORP.
1437    勤益    GTM
...省略...
8249    菱光    CSI  
9912    偉聯    AIC";
        #endregion
 
        //如果資料量未多到誇張,將DataTable Cached住
        string CACHE_KEY = "StkTable";
        if (Cache[CACHE_KEY] == null)
        {
            DataTable t = new DataTable();
            t.Columns.Add("Key", typeof(string));
            t.Columns.Add("Symbol", typeof(string));
            t.Columns.Add("CName", typeof(string));
            t.Columns.Add("EName", typeof(string));
            //測試時由字串取得資料,實務上會去查DB
            StringReader sr = new StringReader(rawData);
            string line = null;
            while ((line = sr.ReadLine()) != null)
            {
                string[] p = line.Split('\t');
                if (p.Length != 3) continue;
                //分別以Symbol, CName, EName作Key
                //輸入中英文及代號都可以查
                t.Rows.Add(p[0], p[0], p[1], p[2]);
                t.Rows.Add(p[1], p[0], p[1], p[2]);
                t.Rows.Add(p[2], p[0], p[1], p[2]);
            }
            //放入Cache,保存兩小時
            Cache.Add(CACHE_KEY, t, null, DateTime.Now.AddHours(2),
                System.Web.Caching.Cache.NoSlidingExpiration,
                System.Web.Caching.CacheItemPriority.Normal, null);
        }
        return Cache[CACHE_KEY] as DataTable;
    }
}

實作細節請大家看程式碼自行揣摩,應沒什麼深奧的學問在裡面。如果有什麼對程式架構或寫法方面的指教,歡迎提出來大家切磋。

想抓整包回去玩的朋友,請按這裡下載。

更多文章 下一頁 »

搜尋

Go

<July 2009>
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678
 
RSS
【工商服務】
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


BlogLook Score and Rank

Syndication