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

【茶包射手日記】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>

【茶包射手日記】時好時壞的SSRS報表訂閱

$
0
0

案情如下:

SSRS 2008的某份報表,每天有三次訂閱排程寄送報表給使用者。怪異的是排程時好時壞,有時一天成功一次、有時成功兩次,三次都成功或失敗的情況也有。失敗會隨機出現在早、中、晚,毫無規則可言。

對SSRS訂閱功能研究有限,算是從頭摸起。首先在訂閱管理只看到"The delivery extension for this subscription could not be loaded"的失敗訊息,進一步檢視ReportServer Log(位置在C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQL2008\Reporting Services\Logfiles\ReportServerService__時間.log)找到詳細的錯誤訊息。

在Log中,發現每次訂閱前會有類以下的記錄代表排程開始:

schedule!WindowsService_26!550!06/04/2013-08:50:01:: Creating Time based subscription notification for subscription: d3ea8ffe-0d5c-47ae-8736-f3c52b9f278d
而隨後有錯誤原因:
library!WindowsService_26!550!06/04/2013-08:50:03:: e ERROR: Throwing Microsoft.ReportingServices.Diagnostics.Utilities.ServerConfigurationErrorException: Email Provider has no server or pickup directory specified, Microsoft.ReportingServices.Diagnostics.Utilities.ServerConfigurationErrorException: The report server has encountered a configuration error. ;
extensionfactory!WindowsService_26!550!06/04/2013-08:50:03:: e ERROR: Exception caught instantiating Report Server Email report server extension: Microsoft.ReportingServices.Diagnostics.Utilities.ServerConfigurationErrorException: The report server has encountered a configuration error. .
notification!WindowsService_26!550!06/04/2013-08:50:03:: e ERROR: Extension Report Server Email did not load, extension factory returned null

由訊息推測是缺少SMTP Server設定導致,但發現疑點: 當天的Log中,我只找到早上的排程記錄,但午、晚的訂閱也有執行記錄。我這才理解到,線上環境有三台SSRS Server,而SSRS訂閱會自動實現Load Balance(負載平衡),嗯,這可解釋成功失敗隨機分佈,碰上沒設好的主機就失敗、輪到設定無誤的主機就成功。檢查SMTP設定後,才發覺案情不如想像單純,三台SSRS的設定檔都有問題,理論上寄送應該全部失敗,那麼成功派送的記錄是哪裡冒出來的? 莫非,還有神祕的3又1/4台?

訂閱管理網頁無法看出由哪一台SSRS主機寄送,我想到的方法是直接查詢SSRS資料表,在[ReportServerDB].[dbo].[ExecutionLogStorage]中查到訂閱發送記錄,而由InstanceName(機器名稱)追出,默默寄出的報表,來自一台系統搬遷後遺留的舊主機,由於暫未停機,便自動自發加入營運陣容。

真相大白,將三台SSRS的SMTP資料設好,正準備開香檳,無奈一波未平一波又起。訂閱寄送時出現不一樣的錯誤訊息:
ERROR: Throwing Microsoft.ReportingServices.Diagnostics.Utilities.ServerConfigurationErrorException: AuthzInitializeContextFromSid: Win32 error: 5, Microsoft.ReportingServices.Diagnostics.Utilities.ServerConfigurationErrorException: The report server has encountered a configuration error. ;

由關鍵字,很快找到微軟KB(http://support.microsoft.com/kb/842423/en-us),指出這與SSRS服務帳號有關。經實測確實有"只寄連結的訂閱成功,傳送附件的訂閱失敗"的現象,驗證確為KB所指狀況。在調整系統帳號後,問題正式解除。(原本被設為Local Servce導致錯誤,改為Local System或Domain Account均可行,但使用Domain Account較安全,是建議的做法)

Outlook農曆日期有誤

$
0
0

聽到有人討論,才發現Outlook今年(2013)的農曆日期有誤:

6/12明明是五月初五端午節,Outlook的顯示卻是五月初四。

研究後發現,這是Outlook與官方採用的萬年曆版本不一致造成。我國現行農曆曆書採用南京紫金山天文台萬年曆,今年農曆四月是小月為29天,故6/8應是五月初一而非Outlook所顯示的四月三十;而清朝制訂的萬年曆曆書則主張今年農曆四月是大月30天,因而產生了一天的差異。(參考: 今年端午節到底是哪天?)

查了一下,Outlook的各版本都有此問題,而Outlook 2013已有Hotfix可修正,其他版本的修正則暫無所獲,提醒大家在參考應用時要留意。

  • When you use the Chinese lunar calendar in Outlook 2013 or in an earlier version of Outlook, the Gregorian date is displayed as an incorrect lunar date. For example, the Gregorian date June 8, 2013 is displayed as lunar date 4/30 instead of 5/1.

趕緊檢查一下.NET的農曆計算,還好還好,日期正確,Pass!!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace TestTwCalender
{
class Program
    {
staticvoid Main(string[] args)
        {
            System.Globalization.TaiwanLunisolarCalendar tlc =
new System.Globalization.TaiwanLunisolarCalendar();
            DateTime d = new DateTime(2013, 6, 12);
            Console.Write("{0:yyyy/MM/dd}是農曆{1}月{2}日",
                d, tlc.GetMonth(d), tlc.GetDayOfMonth(d));
            Console.Read();
        }
    }
}
2013/06/12是農曆5月5日

【延伸閱讀】


Bootstrap!

$
0
0

Bootstrap是近來紅透半邊天的網頁設計無敵懶人包,號稱是網頁攻城獅的救星,連我這種先天不足後天失調,美感殘缺到可以領殘障手冊的設計麻瓜,只要下載安裝CSS及JS檔,照著範例三兩下就可以打造出質感頗佳的網頁,猶如流浪漢忽然能拉小提琴般令人稱奇,要說化腐朽為神奇,莫此為甚。(還沒見識過Bootstrap的朋友,可以看這篇介紹Bootstrap網站上有頗為詳細的示範與教學,好消息是MVP Bruce已將全站翻成正體中文版,要入手的同學切勿錯過。另外Bootstrap網站也有人翻成簡體中文版,例如: Bootstrap中文網)

在NuGet搜尋一下bootstrap,由下載項目及次數不難想像其熱門程度!

儘管Bootstrap火紅已久,真正讓我把Bootstrap當成"緊急又重要需立即學會"項目,卻是上週TechEd 2013宣告的一則消息 --  未來ASP.NET專案範本將會以Bootstrap為鍋底基底!

【延伸閱讀】

既然Bootstrap跟jQuery一樣被採納成為ASP.NET的標準配備,不懂不會就輸在起跑點了! 而我很快聯想到的議題,便是與我常用的套件組—Kendo UI的相容性,查了文章發現擔心是多餘的,2013.1.319版裡已經有個kendo.bootstrap.min.css,換裝之後,Kendo UI就跟Bootstrap融為一體囉! (見最下方的數字欄位及日期選擇器)

這樣就可以放心向Bootstrap邁進囉!!

附上完整程式碼: (請使用NuGet下載Bootstrap及KendoUIWeb)

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Kendo UI + Bootstrap</title>
<linkhref="Content/kendo/2013.1.319/kendo.common.min.css"rel="stylesheet"/>
<linkhref="Content/kendo/2013.1.319/kendo.bootstrap.min.css"rel="stylesheet"/>
<linkhref="Content/bootstrap.min.css"rel="stylesheet"/>
<scriptsrc="Scripts/jquery-1.9.1.min.js"></script>
<script src="Scripts/bootstrap.min.js"></script>
<script src="Scripts/kendo/2013.1.319/kendo.web.min.js"></script>
</head>
<body>
<div class="navbar">
<div class="navbar-inner">
<div class="container">
<!-- brand classis from bootstrap.css -->
<a class="brand" href="#">My Brand</a>
<div class="nav-collapse">
<ul class="nav">
<li class="active"><a href="#">Home</a></li>
<li class="dropdown">
<a href="#"class="dropdown-toggle" data-toggle="dropdown">
                                Dropdown <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#">Action 1</a></li>
<li><a href="#">Action 2</a></li>
<li class="divider"></li>
<li class="nav-header">Header</li>
<li><a href="#">Separated action</a></li>
</ul>
</li>
</ul>
<form class="navbar-search pull-left">
<input type="text"class="search-query" placeholder="Search">
</form>
 
</div>
<!-- /.nav-collapse -->
</div>
</div>
<!-- /navbar-inner -->
</div>
<!-- /navbar -->
 
 
<div style="width: 500px; margin-top: 10px;">
<table id="kGrid">
<thead>
<tr>
<th data-field="make">Car Make</th>
<th data-field="model">Car Model</th>
<th data-field="year">Year</th>
<th data-field="category">Category</th>
<th data-field="airconditioner">Air Conditioner</th>
</tr>
</thead>
<tbody>
<tr>
<td>Volvo</td>
<td>S60</td>
<td>2010</td>
<td>Saloon</td>
<td>Yes</td>
</tr>
<tr>
<td>Audi</td>
<td>A4</td>
<td>2002</td>
<td>Saloon</td>
<td>Yes</td>
</tr>
<tr>
<td>BMW</td>
<td>535d</td>
<td>2006</td>
<td>Saloon</td>
<td>Yes</td>
</tr>
<tr>
<td>BMW</td>
<td>320d</td>
<td>2006</td>
<td>Saloon</td>
<td>No</td>
</tr>
</tbody>
</table>
</div>
<div class="btn-group" style="margin: 9px 0;">
<button class="btn">Left</button>
<button class="btn">Middle</button>
<button class="btn">Right</button>
</div>
<input type="text" id="kNumText" />
<input type="text" id="kDatePicker" />
<script>
        $("#kDatePicker").kendoDatePicker();
        $("#kNumText").kendoNumericTextBox();
        $("#kGrid").kendoGrid({ height: 150 });
</script>
</body>
</html>

【笨問題】遠端桌面的鋸齒字

$
0
0

一直以來,使用遠端桌面連上筆電的Windows 8,畫面就像下圖,文字是帶著鋸齒的…

不知怎麼的,沒多想認定這是Windows 8支援遠端桌面的限制,就這麼將就用了好久。(現在回想,是鬼迷心竅吧!)

前幾天福至心靈才開始起疑,ClearType技術出來很久了,怎麼可能在Windows 8遇到遠端桌面就破功? 細查才發現是自己豬頭,明明選項裡就可以調,由於我的Client被設成低頻寬,許多改善操作體驗的特效都被關閉,包含字型平滑化(Font smoothing)。

將連線速度調到LAN(10Mbps以上),預設所有選項都會啟用。

重新連線。嘖! 字型圓滑順眼,這才是Windows 8應有的表現,我竟笨了這麼久。

【茶包射手日記】ReportViewer在ModalDialog中無法列印

$
0
0

發現以showModalDialog()顯示ReportViewer網頁,按下列印按鈕會彈出錯誤:

嘗試取得目前的視窗時發生錯誤。
錯誤: 發生錯誤,無法完成操作 8007f305。

同一個ReportViewer網頁只要不用Modal Dialog方式開啟就不會出錯。在Microsoft Connect上找到報案記錄,證實為Bug且短期內不會修正。研究發現後找到幾種繞道方法:

  1. 使用【Ctrl + P】按鍵取代點選列印圖示,就能避開錯誤順利列印,很神奇的解法,但在使用者都學會密技前,客訴是免不了滴。
  2. 考慮以window.open()取代window.showModalDialog(),如果一定要用強制對話框,可改用Block UIKendo UI WindowjQuery UI Dialog…等解決方案。

遇到的案例因使用ModalDialog + ReportViewer的網頁數頗多,最後決定來點奇技淫巧偷雞: 在公用JS Library中加入一段程式,把window.showModalDialog換上修改版,由URL檔名發覺內含ReportViewer時,改用Kendo Window開啟,否則維持Modal Dialog開啟,如此報告網頁通通不用修改囉!

var _showModalDialog = window.showModalDialog;
        window.showModalDialog = function (url, argument, options) {
//判斷是否為ReportViewer網頁...略...
if (urlIsReportViewer)
                showUrlInKendoWindow(url); //改用Kendo Window顯示
else
                _showModalDialog(url, argument, options);
        }

連內建函數都可以用一行指令輕易換掉,很少語言可以這麼彈性,玩出這麼多花樣,JavaScript真是神奇的語言~

CODE-透過程式執行T4範本

$
0
0

最近在開發自動化套件,想在自己寫的程式產生器中借用T4產生Code。

典型T4應用多發生於專案編輯階段,透過存檔動作或PowerShell Script產生程式碼。簡單嘗試後,發現T4早已設想周到,在程式中用T4產生文字是小菜一碟,透過Runtime Text Template(執行階段文字範本)即可輕鬆達成, MSDN有篇詳細說明可以參考。

我做了一個簡單範例,從PTT取得文章清單後,透過T4輸出成文字檔。

首先,在專案中新增一個Runtime Text Template: (在General分類下,直接用關鍵字"template"過濾比較快)

與傳統T4範本不同,每個Runtime T4範本都附加了同名的cs檔案,定義同名的類別物件(在本例中RTT4.tt範本,對應的類別名稱即為RTT4),如此便可在程式碼中建立範本物件RTT4,呼叫其.TransformText()方法就能獲得轉換結果。

使用Runtime T4時,呼叫端常需傳遞參數物件給T4範本,以達到動態變更產生內容的彈性。要實現此點,可透過T4類別為partial class的特性,在專案另外加入類別名稱相同partial class檔案(即上圖中的RTT4Code.cs),於其中另外定義內部欄位、屬性,並透過建構式接收參數。

範例先要定義文章資料物件:

publicclass Post 
    {
publicstring Date;
publicstring Author;
publicstring Subject;
    }

接著在partial class加入接收List<Post>的建構式,並宣告一個內部欄位posts,用以儲存文章清單。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace RuntimeT4
{
partialclass RTT4
    {
private List<Post> posts;
public RTT4(List<Post> posts)
        {
this.posts = posts;
        }
    }
}

T4範本如下,注意其中的this.posts,就是我們在partial class中所定義的內部欄位。

<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<# 
int i = 1;
foreach (var p in this.posts) 
{ #>
<#= i++ #>.<#= p.Date #> / <#= p.Author #> / <#= p.Subject #>
<# } #>

呼叫端程式如下(結果抓資料部分比主體多出許多 orz),它會抓回PTT笨版第一頁,從HTML取出文章列表交由RTT4範本執行,.TransformText()後可得到一個字串,即為產出結果。

class Program
    {
staticvoid Main(string[] args)
        {
//連上PTT網站取回文章資料
              List<Post> posts = GetPTTPosts();
//將文章資料當成參數傳給T4
            RTT4 t = new RTT4(posts);
//執行TransformText()即可取得結果
    Console.WriteLine(t.TransformText());
            Console.Read();
        }
//取得PTT文章清單
static List<Post> GetPTTPosts()
        {
            WebClient wc = new WebClient();
            wc.Encoding = Encoding.GetEncoding(950);
string h = wc.DownloadString(
"http://www.ptt.cc/bbs/StupidClown/index1.html");
            List<Post> posts = new List<Post>();
string date = null, author = null, subject = null;
            Regex re = new Regex(">(?<t>[^<]+?)<");
            Func<string, string> getInnerText = 
                s => re.Match(s).Groups["t"].Value;
foreach (string line in h.Split('\n'))
            {
if (line.StartsWith("<td width=\"50\">"))
                    date = getInnerText(line);
elseif (line.StartsWith("<td width=\"120\">"))
                    author = getInnerText(line);
elseif (line.StartsWith("<td width=\"500\">"))
                {
                    subject = getInnerText(line);
                    posts.Add(new Post()
                    {
                        Date = date,
                        Author = author,
                        Subject = subject
                    });
                }
 
            }
return posts;
        }

執行結果:

1. 4/23 / vivianJ / 我大概是唯一一個被"阿"過的女生吧
2. 4/25 / purinfocus / 小護士回憶錄2
3. 4/27 / EchoMary / 話說今天期中考..
4. 5/05 / wubaiwife / 是任性還是笨﹖
5. 5/22 / m0630821 / [閒聊] 油表上的E是?
6. 6/13 / hohoholalala / 去郵局
7. 6/22 / cynthia730 / 語無倫次
8. 6/22 / chachalee / 你真的是我媽嗎?
9. 6/25 / superlubu / [動畫] 三人成虎大鬧台北第七集  慘劇
10. 8/04 / keikolin / [耍笨] 我終於當媽了
11. 8/05 / utou / [動畫]笨蛋的故事
12. 9/12 / okaoka0709 / [轉錄]Re: [火大] 我的姓真的很特別嗎
13. 9/25 / Liska / [童年] 中秋節烤肉= =|||
14.10/03 / JKH945 / [生日+笨夢] 19歲最後一個夢
15.10/28 / winniechi / [耍笨] 類疊的用法
16.11/10 / olivetrees / 我弟應該算是最早的詐騙集團
17.11/10 / keikoYAMADA / 古蹟修護系
18.12/12 / glenmarlboro / 突然想到一件笨事
19.12/14 / QQQWERT / [耍笨] 其實...應該很多人做過這種事
20.12/18 / Chucky9527 / [耍笨] 我的朋友的褲子拉鍊忘了關

學會這招,就能在自己的程式中輕鬆使用T4生Code,以後就不必用StringBuilder硬兜囉!

本日口號: T4好威呀!

2013海山馬拉松~

$
0
0

毫無期待的第九馬。

與賽事本身無關,是自己在三月三連馬後進行歲修,原本希望透過休養讓右腳足底筋膜炎徹底解決,4月報名時未料到恢復期比想像長,目前雖已可跑上十多K無大礙,但尚未完全痊瘉操不得,未處於最佳狀況加上已屆酷暑,天氣預報氣溫高達34度,勢必倍加艱苦。所以這次的願望很小: 以身體狀況為先,苗頭不對棄賽無妨,若行有餘力,無傷完賽就好。

清晨五點多,天色就已經很亮,夏至剛過,今天想必很"夏天"! (抖)

全馬一千人、半馬五百,人工計時,是場小而美的賽事。全馬的路線由板橋浮洲橋沿河濱跑到永和折返,回到浮洲橋後再一路跑到三峽再折返,最後再浮洲橋。

一開始的20K還算中規中矩,速度維持在六分速到六分半之間,兩小時多跑完半馬,但時間到了八點多,太陽已轉為大火強微波,隨著里程拉長,開始擔心足底筋膜不宜再操。心一橫,決定墮落到底: 心中無成績,眼中剩補給,餘下3個半小時就專心當步兵吃東西吧!

 

大會的補給辦得挺好,一路礦泉水、舒路供應充足,還有西瓜、香蕉、小蕃茄,途中還遇到神祕補給站,出現冰啤酒跟冰蠻牛, 揪甘心A~ 11點多看到機車緊急送來冰塊供跑友消暑,我只想說: "暗~ 排A,你真有心"。

一路跑到中正橋折返,有股衝動想過橋到公館直接搭236回家。但想到衣保袋還寄在浮洲橋,只能乖乖跑回去拿。

 

回家整理照片,發現無意拍到愛上馬拉松的高志明大哥正迎面而來,另外,今天這場也是邪惡帝國的百馬,百馬團聲勢浩大,但關門前夕才從容抵達,速度很親民呀~

 

河濱公園的阿勃勒開得燦爛,十分好看。在三峽段看到人工溼地附近的圓錐形山丘,想起五年前的左岸大迷航

就這樣,在毒辣日頭下於河濱漫步,一站吃過一站,這個水站大啖蕃茄,走到下個水站狂飲舒跑,再趕去下站啃西瓜,恥力滿點~ orz

哥參加的不是馬拉松,
是河濱炭烤人肉趴兼補給站吃到飽沙拉吧!

既然吃到飽,就要撐到"關門"才甘心~ 一路走到底臨關門才臨時抱個佛腳,趕在關門倒數三分鐘前跑回終點。有趣的是,我拿到的是576名次卡,代表還有40%的跑友還沒回來? 今天的溫度真是可怕。

  

最後一定要說一下,終點提供的冰仙草、冰豆漿好好喝,歷經35度高溫烘烤後喝到冰豆漿,千金不換!

DateTime時區與比較

$
0
0

發現自己對.NET DateTime時區及比對的概念有點模糊,特實測並整理筆記備忘。

首先,.NET的DateTime型別包含時區觀念,DateTime.Kind記錄了時區類別,共分為Unspecified(不指定)、Utc(世界標準時間)以及Local(本地時間)三種。但兩個DateTime在比較時不會自動轉換時區,所以本地時間台北早上8點的DateTime,與UTC當日凌晨0點的DateTime物件直接比較不相等(雖然它們實質上相等),但只需透過ToUniversalTime()ToLocalTime()轉換成統一基準就相等了。

以下的例子利用JSON轉換產生四組時間,00:00 UTC、00:00 Unspecified、08:00 台北時區、00:00 台北時區,分別檢視其Kind、ToUniversalTime()及ToLocalTime()結果,並兩兩進行比較。

staticvoid Main(string[] args)
        {
string j1 = "\"2012-12-21T00:00:00Z\""; //00:00 UTC
string j2 = "\"2012-12-21T00:00:00\"";  //00:00 不指定時區
    string j3 = "\"2012-12-21T08:00:00+0800\""; //08:00 TPE
string j4 = "\"2012-12-21T00:00:00+0800\""; //00:00 TPE
            DateTime d1 = JsonConvert.DeserializeObject<DateTime>(j1);
            DateTime d2 = JsonConvert.DeserializeObject<DateTime>(j2);
            DateTime d3 = JsonConvert.DeserializeObject<DateTime>(j3);
            DateTime d4 = JsonConvert.DeserializeObject<DateTime>(j4);
            Func<DateTime, string> inspect = (d) =>
            {
returnstring.Format(
"{0} toString=[{1:MM-dd HH:mm}] toUTC=[{2:MM-dd HH:mm}] toLocal=[{3:MM-dd HH:mm}]",
                    d.Kind.ToString().PadRight(11), 
                    d, d.ToUniversalTime(), d.ToLocalTime());
            };
            Console.WriteLine("d1:{0}", inspect(d1));
            Console.WriteLine("d2:{0}", inspect(d2));
            Console.WriteLine("d3:{0}", inspect(d3));
            Console.WriteLine("d4:{0}", inspect(d4));
            Console.WriteLine("d1 == d2 ? {0}", d1.CompareTo(d2) == 0);
            Console.WriteLine("d1 == d3 ? {0}", d1.CompareTo(d3) == 0);
            Console.WriteLine("d1 == d4 ? {0}", d1.CompareTo(d4) == 0);
            Console.WriteLine("d2 == d3 ? {0}", d2.CompareTo(d3) == 0);
            Console.WriteLine("d2 == d4 ? {0}", d2.CompareTo(d4) == 0);
            Console.WriteLine("d3 == d4 ? {0}", d3.CompareTo(d4) == 0);
            Console.Read();
        }

結果如下:

d1:Utc         toString=[12-21 00:00] toUTC=[12-21 00:00] toLocal=[12-21 08:00]
d2:Unspecified toString=[12-21 00:00] toUTC=[12-20 16:00] toLocal=[12-21 08:00]
d3:Local       toString=[12-21 08:00] toUTC=[12-21 00:00] toLocal=[12-21 08:00]
d4:Local       toString=[12-21 00:00] toUTC=[12-20 16:00] toLocal=[12-21 00:00]
d1 == d2 ? True
d1 == d3 ? False
d1 == d4 ? True
d2 == d3 ? False
d2 == d4 ? True
d3 == d4 ? False

最後補充兩點:

  1. Unspecified很有趣(但也可能變成陷阱),ToUniversalTime()時被視為本地時間(台北時區)減去8小時,ToLocalTime()時被視為UTC時間加上8小時。
  2. 比較結果相等的有d1 == d2, d1 == d4, d2 == d4,原則上,ToString()的結果一致就相等,不管時區。

JSON日期轉換的時區陷阱

$
0
0

在使用Kendo UI DatePicker時,出現選好日期送至後端卻變成前一天的狀況。

以下程式可重現問題,kendoDatePicker所選日期透過.value()可得到一個JavaScript Date物件,JSON.stringify()後傳至Server端,使用Json.NET還原回DateTime後,以ToString("yyyy-MM-dd HH:mm:ss")方式傳回Client端alert顯示。

<%@ Page Language="C#" %>
<%@ Import Namespace="Newtonsoft.Json" %>
 
<!DOCTYPEhtml>
 
<scriptrunat="server">
protectedvoid Page_Load(object sender, EventArgs e)
    {
if (Request["m"] == "post")
        {
var p = Request["d"];
var nd = JsonConvert.DeserializeObject<DateTime>(p);
            Response.Write(string.Format("{0}->{1:yyyy-MM-dd HH:mm:ss}", p, nd));
            Response.End();
        }
    }
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title></title>
<link href="../Content/kendo/2013.1.319/kendo.common.min.css" rel="stylesheet" />
<link href="../Content/kendo/2013.1.319/kendo.bootstrap.min.css" rel="stylesheet" />
</head>
<body>
<form id="form1" runat="server">
<input data-bind="kendoDatePicker: { value: TheDate, format: 'yyyy-MM-dd' }" />
<br />
<span data-bind="text: TheDate"></span>
<input type="button" data-bind="click: post" value="POST" />
</form>
<script src="../Scripts/jquery-1.9.1.min.js"></script>
<script src="../Scripts/kendo/2013.1.319/kendo.web.min.js"></script>
<script src="../Scripts/knockout-2.2.1.js"></script>
<script src="../Scripts/knockout-kendo.min.js"></script>
<script>
function myViewModel() {
var self = this;
            self.TheDate = ko.observable("2012-12-21");
            self.post = function () {
                $.post("",
                    { m: "post", d: JSON.stringify(self.TheDate()) },
function (r) {
                        alert("Result = " + r);
                    });
            };
        }
var vm = new myViewModel();
        ko.applyBindings(vm);
</script>
</body>
</html>

測試結果如下:

明明選了12/22日,但傳到.NET端ToString後卻是12/21日! 問題出在12/22的本地時間在JSON.stringify時被轉成UTC,12/22凌晨0點減去8小時,於是.NET端得到 DateTimeKind = UTC的DateTime -- 12/21 16:00 UTC。

依據Telerik RD的說法,kendo.stringify跟JSON.stringify一樣,會將本地時間轉換成UTC時間,而kendoDatePicker .value()傳回的是JavaScript Date物件時區則會以本地時間為準,JSON轉成UTC後,若.NET處理時沒轉回本地時間或UTC時間,就會出問題。

知道原委,我理解到這個問題與Kendo UI無關,而是JSON具有全球化觀點,.NET端沒跟上造成的。在一個全球化網站,傳送時間需反應使用者所在時區,Server端才能精準掌握真正時點,但前提是.NET端應將來自各地的時間一律轉為UTC時間或本地時間才合理,直接ToString()看到的是當地時間,忽略時區差異便會衍生問題。

因此,我們可以重塑一個與Kendo UI無關的精簡範例:

<%@ Page Language="C#" %>
<%@ Import Namespace="Newtonsoft.Json" %>
 
<!DOCTYPEhtml>
 
<scriptrunat="server">
protectedvoid Page_Load(object sender, EventArgs e)
    {
if (Request["m"] == "post")
        {
var p = Request["d"];
var nd = JsonConvert.DeserializeObject<DateTime>(p);
            Response.Write(string.Format("{0}->{1:yyyy-MM-dd HH:mm:ss}", p, nd));
            Response.End();
        }
    }
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<span id="sTime"></span>
</form>
<script src="../Scripts/jquery-1.9.1.min.js"></script>
<script>
var d = new Date();
        $("#sTime").text(d.toString());
        $.post("",
            { m: "post", d: JSON.stringify(d) },
function (r) {
                alert("Result = " + r);
            });
</script>
</body>
</html>

在早上8:00以前,將new Date()經JSON轉換後送到.NET,還原回DateTime再ToString(),看到的日期會是前一天!!

面對這個問題有兩個解決方向:

  1. 在Server端落實全球化概念,所有來自Client端的JSON時間,一律轉為UTC保存,顯示呈現時再視需求決定時區。
  2. 如果只是本土小公司使用的內網系統,所有Client端座落在方圓100公尺內,只因為用了JSON就要在系統推行全球化有點小題大作。而且,Server未必能配合修改,此時就要考慮由Client端解決。

要從Client端解決,我想到的做法是讓JSON.stringify()忽略時區差異,轉成"2013-06-22T07:18:48"(最後不加Z或+0800,對應成.NET DateTime相當於Kind = Unspecified)。實作技巧是偷偷將Date.prototype.toISOString()改成我們自訂的版本:

<script>
//將原本的函數保留起來,必要時可以換回去
var _toIsoDate = Date.prototype.toISOString;
//借用kendo.toString做出Unspecified Kind的ISO8601格式
        Date.prototype.toISOString = function () {
return kendo.toString(this, "yyyy-MM-ddTHH:mm:ss");
        };
 
function myViewModel() {
var self = this;
            self.TheDate = ko.observable("2012-12-21");
//...以下略...

重新評估後,改寫.toJSON()只會針對JSON轉換調整邏輯,較置換.toISOString()更符合目的,感謝Kuo-Chun Su提醒。

<script>
//借用kendo.toString做出Unspecified Kind的ISO8601格式
        Date.prototype.toJSON = function () {
return kendo.toString(this, "yyyy-MM-ddTHH:mm:ss");
        };
 
function myViewModel() {
var self = this;

如此,應該就能避開惱人的JSON日期時差問題囉~


CODE-將CHAR(1)欄位轉換為列舉型別

$
0
0

工作上常遇到的需求:

旗標性質欄位在資料庫被定義成CHAR(1),用單一字元代表不同意義,例如:  1=新增、2=修改、3=刪除、A=同意、R=同意、W=撤回、C=取消。針對這類旗標,UI常會使用下拉選單或Radio Button列出選項讓使用者選取;而在顯示時需將資料庫讀到的"A"轉成"同意"方便理解。

實作時,我偏好在ViewModel將這種欄位屬性定義成.NET列舉(Enum)型別,UI上直接用列舉項目當作下拉選單選項,利用每個列舉項目都有對應int值的特性,直接將各項目對應成要存入的字元,例如:

publicenum MyEnum
    {
新增 = 1,
修改 = 2,
刪除 = 3,
同意 = (int)'A',
否決 = (int)'R',
撤回 = (int)'W',
取消 = (int)'C'
    }

如此便能很方便地在列舉型別與CHAR(1)字元間互轉--如果有一個好用的工具函數的話!

以下就是我心中好用的工具函數:

//參考: http://bit.ly/16yoItk
/// <summary>
/// 將列舉值轉為列舉型別
/// </summary>
/// <typeparam name="T">列舉型別</typeparam>
/// <param name="value">列舉值字串</param>
/// <returns></returns>
publicstatic T GetEnum<T>(stringvalue) where T : struct, IConvertible
        {
if (!typeof(T).IsEnum) 
thrownew ArgumentException("T must be an enumerated type");
int n;
//若非數字時且為單一字母,將其轉為CHAR
if (!int.TryParse(value, out n) && value.Length == 1)
value = ((int)value[0]).ToString();
return ((T)Enum.Parse(typeof(T), value));
        }
/// <summary>
/// 將列舉型別轉為列舉值,可為數字或字元(ex: 1='1',65='A')
/// </summary>
/// <typeparam name="T">列舉型別</typeparam>
/// <typeparam name="R">傳回型別,限int, char或string</typeparam>
/// <param name="enumVal">列舉參數</param>
/// <returns></returns>
publicstatic R GetEnumValue<T, R>(T enumVal) where T : struct, IConvertible
        {
if (!typeof(T).IsEnum) 
thrownew ArgumentException("T must be an enumerated type");
            Type resType = typeof(R);
if (resType != typeof(int) && resType != typeof(char) 
&& resType != typeof(string))
thrownew ArgumentException("R must be int, char or string");
int n = enumVal.ToInt32(null);
//R is int時,直接傳回數字
if (resType == typeof(int))
return (R)Convert.ChangeType(n, resType);
//否則轉為Char後傳回(小於10則直接傳數字字元)
char c = n < 10 ? n.ToString()[0] : (char)n;
return (R)Convert.ChangeType(c, resType);
        }

實地測試:

protectedvoid Page_Load(object sender, EventArgs e)
    {
foreach (MyEnum me in Enum.GetValues(typeof(MyEnum)))
        {
//針對每個列舉項目取得int及string
            Response.Write(string.Format("<li>{0} / {1} / {2}",
                me, Common.GetEnumValue<MyEnum, int>(me),
                Common.GetEnumValue<MyEnum, string>(me)));
        }
foreach (char c in"123ARWC")
        {
//將單一字元轉為列舉
            Response.Write(string.Format("<li>{0} -&gt; {1}",
                c, Common.GetEnum<MyEnum>(c.ToString())));
        }
        Response.End();
    }

測試成功!!

新增 / 1 / 1 
修改 / 2 / 2 
刪除 / 3 / 3 
同意 / 65 / A 
取消 / 67 / C 
否決 / 82 / R 
撤回 / 87 / W 
1 -> 新增 
2 -> 修改 
3 -> 刪除 
A -> 同意 
R -> 否決 
W -> 撤回 
C -> 取消

Web Site專案SQL Server Compact 4.0元件的手動部署

$
0
0

SQL Server Compact 4.0是輕量級的內嵌式資料庫,不需要安裝成系統服務,只需引用相關DLL,載入DB檔案,用起來跟SQL Server幾乎一樣,也支援Entity Framework,很適合小量資料、少數使用者的小型應用。手邊有個Web Site專案,第一次試用了SQL CE,本機測試一切正常,部署到正式伺服器卻彈出找不到System.Data.SqlServerCe的錯誤訊息:

Could not load file or assembly 'System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91' or one of its dependencies. The system cannot find the file specified.

由錯誤訊息不難看出是少了相關組件,猜想在伺服器上安裝SQL CE就可解決,但我就是想挑戰只靠複製檔案完成部署,才不愧SQL CE輕巧可攜本色。網路上找到不少文章,但資訊有些零散,MSDN雖提到私下部署及集中部署,但並沒有Web Site部署的完整攻略,加上還有些小眉角(後來在天空垃圾場撿到一篇文章,提及EF放在Class Library會導致部署失敗,懷疑就是我踩到的地雷),故整理筆記備忘。

  1. 複製相關檔案:
    * 將C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v4.0\Desktop\System.Data.SqlServerCe.dll 複製到~\bin
    * 將C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v4.0\Desktop\System.Data.SqlServerCe.Entity\System.Data.SqlServerCe.Entity.dll 複製到~\bin
    * 將C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v4.0\Private\x86\*
    複製到~bin\x86
    * 將C:\Program Files (x86)\Microsoft SQL Server Compact Edition\v4.0\Private\amd64\*複製到~bin\amd64
    注意: System.Data.SqlServerCe.dll 及 System.Data.SqlServerCe.Entity.dll 要用Desktop目錄的版本而不能直接用Private目錄的版本;Private目錄下的x86與amd64內容Unmanaged程式庫記得也要一併複製到bin目錄下(所以bin下應該要有x86及amd64兩個子目錄)
  2. 加入DbProvider設定:
    伺服器未安裝SQL CE,.NET會不認得Microsoft SQL Server Compact Data Provider(癥狀為出現The specified store provider cannot be found in the configuration, or is not valid訊息),如果不想更動machine.config,在web.config加以下設定即可:
    <system.data>
      <DbProviderFactories>
        <remove invariant="System.Data.SqlServerCe.4.0" />
        <add name="Microsoft SQL Server Compact Data Provider 4.0" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" />
      </DbProviderFactories>
    </system.data>

完成以上兩項設定,應該就能解除【使用Copy檔案完成SQL CE部署】成就囉~

【參考資料】

KO範例23 – 單選或多選兩用Checkbox清單

$
0
0

利用Checkbox模擬Radio清單的互斥選項是我常用的UI風格,之前曾用jQuery實作過,現在網頁都搬到Knockout的場子,少不了也要重現相同功能,順便考驗KO的能耐。(為何不直接用下拉選單就好? 這裡有一個好理由)

廢話不多說,直覺用以下Demo定義規則! (PS: 寫得太順手,不小心連多選的版本都寫進去 XD)

我最開始的想法是開發一個自訂繫結,將ko.observableArray轉成選項,並將勾選結果反應給ko.observable,而選項繫結時還需要像下拉選單一樣指定Text及Value…  想來想去,幾乎就是重寫一組跟<select>的options、optionsText、optionsValue、value一樣的繫組,那那那,何不直接寄生在<select>上? 把<option>轉成<input type="checkbox">,再把<select>本體藏起來,出乎順利地便完成了上述展示的效果。

使用方法很簡單,在一般的<select> data-bind後方再多加一個xorChkValue參數指向繫結對象就OK了:
(被選取項目的文字預設會變成藍色,如需修改可透過xorChkColor指定)

<select data-bind="options: categories, optionsText: 't', optionsValue: 'v', value: category, xorChkValue: category, xorChkColor: 'brown'"></select>

多選時一樣是加xorChkValue,但繫結對象要是ko.observableArray,記得<select>要加上multiple並改用selectedOptions取代value:

<select data-bind="options: categories, optionsText: 't', optionsValue: 'v', selectedOptions: selCatgs, xorChkValue: selCatgs" multiple>
</select>

完整程式碼如下,線上展示這回我放在JS Bin,有興趣的朋友可以試玩看看,發現問題請再回饋給我。

<!DOCTYPEhtml>
<html>
<head>
<scriptsrc="http://ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js"></script>
<script>
//互斥點選的checkbox
        ko.bindingHandlers.xorChkValue = {
            update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var $elem = $(element); //應為select
var val = valueAccessor();
var settings = allBindingsAccessor();
//檢查是否為多選
var multiple = "push"in val;
//支援自訂顏色
var color = settings.xorChkColor || "blue";
//取得<select>後方的元素
var $next = $elem.next();
var $container;
//取後方已有容器元素,清空即可
if ($next.hasClass("xor-checks")) {
                    $container = $next;
                    $container.empty();
                }
else { //否則建立容器
                    $container = $("<span class='xor-checks'></span>");
//加入對label及checkbox的點擊行為
                    $container.on("click", "input,label", function () {
//點擊label時透過prev()找到checkbox
var inp = this;
if (this.tagName.toLowerCase() === "label") {
                            inp = $(this).prev()[0];
                            inp.checked = !inp.checked; //切換選取
                        }
                        console.log(inp.checked);
if (multiple) { //多選時, 視狀態決定新增或移除
if (inp.checked) {
if ($.inArray(inp.value, val()) == -1)
                                    val.push(inp.value);
                            }
else {
                                val.remove(inp.value);
                            }
                        }
else { //單選
                            inp.checked = true;
                            val(inp.value);
                        }
                    });
                }
                $elem.find("option").each(function () {
var $cbx = $("<span><input type='checkbox' /><label /></span>");
varchecked =
                        multiple ? $.inArray(this.value, val()) != -1 :
                        val() == this.value;
                    $cbx.find("input").val(this.value).prop("checked", checked);
                    $cbx.find("label").text(this.text).css("color", checked ? color : "");
                    $container.append($cbx);
                });
                $elem.after($container);
                $elem.hide();
            },
        }
 
var c = 1;
function myViewModel() {
var self = this;
            self.categories = ko.observableArray();
            self.category = ko.observable("D");
            self.selCatgs = ko.observableArray(["D", "T"]);
            self.selCatgsText = ko.computed(function () {
return JSON.stringify(self.selCatgs());
            });
            self.addOption = function () {
                self.categories.push({ t: "Extra-" + c, v: c });
                c++;
            };
        }
var vm = new myViewModel();
        vm.categories.push({ t: "Desktop", v: "D" });
        vm.categories.push({ t: "Phone", v: "P" });
        vm.categories.push({ t: "Tablet", v: "T" });
        vm.categories.push({ t: "TV", v: "V" });
        $(function () {
            ko.applyBindings(vm);
        });
</script>
<metacharset="utf-8"/>
<title>KO範例23 – 單選或多選兩用Checkbox清單</title>
</head>
<bodystyle="padding: 24px">
<inputtype='button'data-bind="click: addOption"value="Add Option"/>
<br/>
單選: 
<selectdata-bind="options: categories, optionsText: 't', optionsValue: 'v', value: category, xorChkValue: category, xorChkColor: 'brown'"></select>
<br/>
<spandata-bind="text: category"></span>
<br/>
多選:
<selectdata-bind="options: categories, optionsText: 't', optionsValue: 'v', selectedOptions: selCatgs, xorChkValue: selCatgs"multiple>
</select>
<br/>
<spandata-bind="text: selCatgsText"></span>
</body>
</html>

[KO系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post

使用Excel維護多國語系字串資源檔

$
0
0

針對多國語系,.NET提供了不錯的解決方案 -- 透過.resx資源檔定義字串,透過ResourceManager或Visual Studio自動產生對應的類別[*.Designer.cs]取用。要新增語系支援,只需增加該語系的resx檔,提供各項目對應的文字,配合CultureInfo切換就能輕易切換語系顯示。(延伸閱讀: 逐步解說:使用資源進行 ASP.NET 的當地語系化)

像是以下這個例子:

這個例子也剛好突顯維護多國語系常見的困擾。Message.resx中有四個項目,Message.zh-CN.resx只有兩則。在開發過程,隨著新介面出現就需要定義新的字串項目,此時得在Message.resx加一筆,在Message.zh-CN.resx也加一筆。支援語系一多,同樣的編輯操作得重複N次(還不包含翻譯工作),擺明了要逼某個暴躁無耐性中年程序員走上絕路...

在我心中,理想的多國語系資料維護模式應該要像這樣:

用Excel來管理,每一個項目的內容依語系並列,一眼就能看出各語系的翻譯對照關係。

正體中文要翻成簡體還可直接用Excel搞定,豈不快哉?

最美妙的部分是 -- 文件是Excel格式,可直接丟給具有Domain Know-how的User翻譯校對,以使用者觀點調校出最適當的用語。

完成結果會自動轉回resx!

很棒吧!

網路上有不少現成的解決方案: ResxManagerresx2xlsRESX to XLS conversionXHEO RESX Translator... 有些甚至整合了自動翻譯(但實務上還是得經人工潤稿才不會貽笑大方),可見大家都有類似需求。由於我還有進一步整合需求,加上評估程式碼並不難寫,就捲了袖子,花了不到兩小時寫出以下小程式讓美夢成真。(謎: 快承認你根本是忍不住手癢吧!)

程式碼如下,有興趣的朋友請自取。使用時請將boo.resx, boo.en-US.resx, boo.zh-CN.resx等放在同一目錄下,使用ConvResxToExcel(resx所在目錄, "boo")轉出boo.xlsx(Excel格式如先前的圖例),修改後可用ConvExcelToResx(xlsx路徑)再轉回多個resx。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel.Design;
using System.IO;
using System.Linq;
using System.Resources;
using System.Text;
using System.Text.RegularExpressions;
using ClosedXML.Excel;
 
namespace ResxConv
{
class Program
    {
staticvoid Main(string[] args)
        {
string path = @".";
string pattern = "message";
ConvResxToExcel(path, pattern);
            ConvExcelToResx(@"FixedMessage.xlsx");
        }
 
publicclass ResxStrings
        {
publicstring Key;
publicstring Comment;
public Dictionary<string, string> Strings =
new Dictionary<string, string>();
        }
//REF: http://msdn.microsoft.com/en-us/library/system.resources.resxdatanode.aspx
privatestaticvoid ConvExcelToResx(string xlsxPath)
        {
            var list = new List<ResxStrings>();
            var langs = new List<string>();
using (XLWorkbook wb = new XLWorkbook(xlsxPath))
            {
                var sht = wb.Worksheets.First();
int col = 3;
while (!sht.Cell(1, col).IsEmpty())
                {
                    langs.Add(sht.Cell(1, col).Value.ToString());
                    col++;
                }
int row = 2;
while (!sht.Cell(row, 1).IsEmpty())
                {
                    ResxStrings data = new ResxStrings()
                    {
                        Key = sht.Cell(row, 1).Value.ToString(),
                        Comment = sht.Cell(row, 2).Value.ToString()
                    };
for (int i = 0; i < langs.Count; i++)
                        data.Strings.Add(langs[i], 
                            sht.Cell(row, i + 3).Value.ToString());
                    list.Add(data);
                    row++;
                }
            }
//Gen resx
string path = Path.GetDirectoryName(xlsxPath);
string pattern = Path.GetFileNameWithoutExtension(xlsxPath);
foreach (string lang in langs)
            {
string resxPath = Path.Combine(path,
                    pattern + (lang != "DEFAULT" ? "." + lang : string.Empty) + ".resx");
using (ResXResourceWriter rsxw = new ResXResourceWriter(resxPath))
                {
foreach (var data in list)
                    {
                        ResXDataNode node = new ResXDataNode(data.Key, data.Strings[lang]);
                        node.Comment = data.Comment;
                        rsxw.AddResource(node);
                    }
                    rsxw.Generate();
                    rsxw.Close();
                }
            }
 
        }
 
privatestaticvoid ConvResxToExcel(string path, string pattern)
        {
            OrderedDictionary dict = new OrderedDictionary();
            List<string> langs = new List<string>();
foreach (string file in Directory.GetFiles(path, pattern + ".*resx"))
            {
string lang = "DEFAULT";
                var m = Regex.Match(file, pattern + "[.](?<l>.+)[.]resx", RegexOptions.IgnoreCase);
if (m.Success) lang = m.Groups["l"].Value;
                langs.Add(lang);
using (ResXResourceReader rsxr = new ResXResourceReader(file))
                {
                    rsxr.UseResXDataNodes = true;
                    ResourceSet rs = new ResourceSet(rsxr);
foreach (DictionaryEntry entry in rs)
                    {
string key = (string)entry.Key;
                        var node = (ResXDataNode)entry.Value;
if (!dict.Contains(key))
                        {
                            dict.Add(key,
new ResxStrings() { Key = key, Comment = node.Comment });
                        }
                        var data = (ResxStrings)dict[key];
stringvalue = (string)node.GetValue((ITypeResolutionService)null);
                        data.Strings.Add(lang, value);
                    }
                }
            }
//Export to Excel
using (XLWorkbook wb = new XLWorkbook())
            {
//Excel
                var sht = wb.Worksheets.Add("Resx List");
                sht.Cell(1, 1).Value = "Key";
                sht.Cell(1, 2).Value = "Comment";
for (int i = 0; i < langs.Count; i++)
                    sht.Cell(1, i + 3).Value = langs[i];
//HTML
                StringBuilder sb = new StringBuilder();
                sb.AppendFormat(@"
<html>
<head>
<title>{0} RESX</title>
<style>
    table {{ border-collapse:collapse;  }}
    td,th {{ border: 1px solid gray; padding: 6px; font-size: 9pt; }}
</style>
</head>
", pattern);
                sb.Append("<body><table><tr><th>Key</th><th>Comment</th>");
foreach (string lang in langs)
                    sb.AppendFormat("<th>{0}</th>", lang);
                sb.AppendLine("</tr>");
 
int row = 2;
foreach (DictionaryEntry de in dict)
                {
//Excel
                    sht.Cell(row, 1).Value = de.Key;
                    var data = (ResxStrings)de.Value;
                    sht.Cell(row, 2).Value = data.Comment;
for (int i = 0; i < langs.Count; i++)
                    {
string lang = langs[i];
if (data.Strings.ContainsKey(lang))
                            sht.Cell(row, i + 3).Value = data.Strings[lang];
                    }
                    row++;
//HTML
                    sb.AppendFormat("<tr><td>{0}</td>", de.Key);
                    sb.AppendFormat("<td>{0}</td>", data.Comment);
for (int i = 0; i < langs.Count; i++)
                    {
string lang = langs[i];
                        sb.AppendFormat("<td>{0}</td>",
                            data.Strings.ContainsKey(lang) ? data.Strings[lang] : string.Empty);
                    }
                    sb.AppendLine("</tr>");
                }
//Excel
                sht.Column(1).AdjustToContents();
                sht.Column(1).Width += 2;
                wb.SaveAs(Path.Combine(path, pattern + ".xlsx"));
//HTML
                sb.AppendLine("</table></html>");
                File.WriteAllText(Path.Combine(path, pattern + ".html"), sb.ToString());
            }
        }
    }
}

Office自動儲存,我要輕輕為你唱首歌

$
0
0

過去只知道Office的自動儲存功能能在當機時救回部分未儲存內容。最近才發現,連自己手殘誤砍,自動儲存也能讓你少搥幾下心肝。

故事是這樣: 我在Excel編輯資料,由文件末端複製了一大段空白列,準備插入中段後輸入資料,明明該用【插入複製的儲存格】,卻鬼迷心竅按成【貼上】而不自覺,就這麼把幾十行打好的資料給蓋過了。等要插入的資料打完,立刻存檔求心安,接著向下找後半部的資料繼續改... 才發現資料不 見 了!!

趕緊先將後來中段新增資料複製到另一個Excel,再狂按Ctrl+Z想復原到用空白列覆蓋前的狀態,但回上一步的步數有限,Ctrl+Z按到極限也只到中段剛開始輸入資料,回不到貼上空白列前。這下可好,臉也綠了心也涼了,向幾十行化為輕煙的文字喊聲安心上路,準備乖乖重打。

重打前想先另存新檔,卻驚喜地看到一線生機:

Excel提示有個03:12的自動儲存,推估時間點在覆蓋空白列之前! 用顫抖的手點開文件: 哇~ 真的是覆蓋前的版本,順利救出資料,省下重打苦工。不得不說,自動儲存功能真是太貼心了~

【後記】

經過這次事件,我才找了一下自動儲存的設定,在選項中可以設定自動儲存的間隔(不過我發現並沒有很精準地每10分鐘就存一個版本,不知是否有變動內容才會觸發)

另外,在自動回復檔案位置,各版本備份是採一個一個檔案方式儲存,如有需求也可自行取出應用。

Viewing all 2311 articles
Browse latest View live