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

【茶包射手日記】時好時壞的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分鐘就存一個版本,不知是否有變動內容才會觸發)

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

【笨問題】無法輸入英文小寫字母

$
0
0

開了一份新PowerPoint範本,發現標題打不出英文小寫。

第一直覺是鍵盤卡住或輸入法故障,但狂按Shift、Caps Lock、切換輸入法都無法解決。接著換到IE地址列測試,大小寫切換是正常的,判定為PowerPoint設定問題。

最後找到原因,原來字型設定區有個【全部大寫】項目,這個範本將標題字型的全部大寫設為啟用:

又學到一招。


HTML轉PDF - 使用Pechkin套件

$
0
0

剛好跟人討論到HTML轉PDF需求,便對工具進行簡單評估以備不時之需。

網路上比較多人推的是WkHtmlToPdf,如果是用.NET開發,已經有人包成NuGet套件,直接搜尋pechkin就可找到,它有兩個版本: Pechkin適用單執行緒,如要非同步執行請選用Pechkin.Synchronized。

安裝NuGet套件後,相關Unmanage DLL也會一併下載並加入專案,不用額外安裝HkHtmlToPdf就可開始寫程式,十分方便。但由於Unmanaged部分為32位元,記得要將專案目標平台切成x86。

參考Pechkin作者在GitHub的FAQ,我寫了一個簡單範例,分別將Google新聞首頁及自己產生的HTML轉成PDF:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Pechkin;
 
namespace Html2Pdf
{
class Program
    {
staticvoid Main(string[] args)
        {
            var config = new GlobalConfig();
            var pechkin = new SimplePechkin(config);
            ObjectConfig oc = new ObjectConfig();
            oc.SetPrintBackground(true)
                .SetLoadImages(true)
                .SetPageUri("http://news.google.com.tw/");
byte[] pdf = pechkin.Convert(oc);
            File.WriteAllBytes("d:\\temp\\google-news.pdf", pdf);
 
string html = @"
<html><head><style>
body { background: #ccc; }
div { 
width: 200px; height: 100px; border: 1px solid red; border-radius: 5px;
margin: 20px; padding: 4px; text-align: center;
}
</style></head>
<body>
<div>Jeffrey</div>
<script>document.write('<span>Generated by JavaScript</span>');</script>
</body></html>
";
            pdf = pechkin.Convert(oc, html);
            File.WriteAllBytes("d:\\temp\\myhtml.pdf", pdf);
        }
 
    }
}

實測效果挺不錯,跟實際網站所看到的很接近: (但不完全相同)

在自製HTML測試中,我使用<script>docuemt.write(…)</script>在網頁中插入內容,在PDF中也正確顯示。

但在某些狀況下,產生的PDF會與實際網頁差異頗大(例如: 我的Blog首頁轉PDF後排版就亂了),有可能是網頁的某些Style寫法或複雜JavaScript超出WkHtmlToPdf預期,也有可能是元件的Bug,感覺眉角還很多,留待實際遇到時再深究。

KO範例24 – Kendo輸入欄位唯讀切換

$
0
0

knockout.js內建enable繫結,可透過ViewModel的布林值切換<input>、<textarea>及<select>啟用與否產生唯讀效果。工作專案中有些輸入欄位採用的Knockout-Kendo所繫結的Kendo DatePicker及NumericTextBox就不在enable支援之列。為此,我寫了一個自訂繫結kendoEnable,支援DatePciker、NumericTextBox(視需要可再擴充),也能繫結特定屬性切換唯讀:

//由特定值的true/false決定啟用或停用Kendo DatePicker及NumericTextBox
    ko.bindingHandlers["kendoEnable"] = {
        update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var $inp = $(element);
var kendoObject = $inp.data("kendoDatePicker") || $inp.data("kendoNumericTextBox");
var val = ko.utils.unwrapObservable(valueAccessor());
if (val) {
                kendoObject.enable();
                $inp.removeClass("a-readonly");
            }
else {
                kendoObject.readonly();
                $inp.addClass("a-readonly");
            }
        }
    };

操作示範如下:

線上展示(JS Bin)

[KO系列]

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

同時開啟兩個Excel檔案於多螢幕並列顯示

$
0
0

Excel有個討厭特性,開啟多份Excel檔案時不像Word每個文件一個視窗,而是預設開在同一視窗,每次只能顯示其中一個檔案,如果要左右並陳對照,可以取消個檔案的最大化,在同一個視窗中並列,只是這麼做不能善用多螢幕的優勢,雖然可以將視窗拉大橫跨兩個螢幕,但有螢幕解析不同,以及無法使用快捷鍵調整視窗尺寸位置的問題,用起來不甚順手,還是覺得像Word一樣各自成為獨立視窗操作比較方便。

平日上班使用Excel 2010,有個解決方案不要直接點選Excel澢,而由程式集的Exce捷徑l開啟新l視窗,點選兩次就會產生兩個獨立的Excel視窗,再透過各自的【檔案/開啟舊檔】選取不同Excel檔開啟,就能實現同時開啟兩個Excel檔並列顯示。(但會有些限制,例如: 工作表不能在兩個Excel檔間複製或搬移)

先啟動Excel再開啟Excel檔有點繁瑣,網路上有人找到另一種解法: 修改或增加Excel檔Shell指令,允許Excel檔在新的Excel視窗開啟,操作起來如下圖:

要增加Shell指令,需修改Registry。將以下的內容存成.reg,再點選執行即可為Excel檔新增"在新視窗開啟"選項。注意: 實際EXCEL.EXE安裝路徑可能有所差異,請自行依實際位置修改調整。

Windows Registry Editor Version 5.00
 
[HKEY_CLASSES_ROOT\Excel.Sheet.8\shell\Open_in_New_Excel_Instance]
@="在新視窗開啟(&W)"
 
[HKEY_CLASSES_ROOT\Excel.Sheet.8\shell\Open_in_New_Excel_Instance\command]
@="\"C:\\Program Files (x86)\\Microsoft Office\\Office14\\EXCEL.EXE\" \"%1\""
 
[HKEY_CLASSES_ROOT\Excel.Sheet.12\shell\Open_in_New_Excel_Instance]
@="在新視窗開啟(&W)"
 
[HKEY_CLASSES_ROOT\Excel.Sheet.12\shell\Open_in_New_Excel_Instance\command]
@="\"C:\\Program Files (x86)\\Microsoft Office\\Office14\\EXCEL.EXE\" \"%1\""

以上範例中,Excel.Sheet.8為xls檔、Excel.Sheet.12為xlsx檔,如要為xlsm等格式也加上指令,可自行比照設定,但實務上設定xls及xlsx應該就夠用。

PS: 終於,Excel 2013起改為一個檔案一個視窗(SDI模式),要在多螢幕下兩個檔案並列對照方便多了。

Kendo UI內建圖示清單

$
0
0

Kendo UI內建了一組圖示,若網頁已參照kendo.common.min.css以及kendo.default.min.css(或其他主題),只需加入SPAN元素並指定k-icon及k-i-*樣式,就可在網頁插入Kendo UI圖示,例如:
<span class="k-icon k-i-calendar"></span>

沒有在Kendo UI網站找到像Bootstrap一樣的圖示清單,便決定自己做一張,有需要的朋友請自行參考:

這裡有JSBin線上版(不同主題配合的圖示不同,可修改kendo.*.min.css檢視結果),以下則是產生清單HTML的小程式:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
 
namespace ShowKendoIcons
{
class Program
    {
staticvoid Main(string[] args)
        {
string css = File.ReadAllText("d:\\kendo.common.min.css");
            List<string> list = new List<string>();
foreach (Match m in Regex.Matches(css, "[.]k-i-[a-z-]+"))
            {
string c = m.Value;
if (list.Contains(c)) continue;
                list.Add(c);
            }
            StringBuilder sb = new StringBuilder();
foreach (string s in list.OrderBy(o => o))
            {
                sb.AppendFormat(
"<div><span class='t'>{0}</span><span class='i k-icon {1}'></span></div>\n",
                    s, s.TrimStart('.'));
            }
            File.WriteAllText("d:\\kendo.html", sb.ToString());
        }
    }
}

KO範例25 – 狀態切換鈕

$
0
0

專案遇到的小需求,如上圖所示,某UI有四種操作模式,規格要求提供四個按鈕,用來切換模式。當模式啟用時,該按鈕要呈現反白,方便使用者掌握模式狀態。

這個需求用Knockout不難實作,利用click繫結將四顆按鈕的點擊事件都對應到setMode函數,由於四鈕共用一個函數,我們需要由event.target識別按鈕來源,模式值被藏在data-mode Attribute。切換背景色的工作則交給css繫結,比對mode()是否與該按鈕對應模式相符,決定要不要加上樣式"on"。搞定收工~

<!DOCTYPEhtml>
<html>
<head>
<scriptsrc="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js">
</script>
<meta charset=utf-8 />
<title>KO範例25 - 狀態切換鈕</title>
<style>
    .on { background-color: blue; color: white; }
</style>
</head>
<body>
<div>
<input type="button" value="依編號" data-mode="N"
    data-bind="css: { on: mode()=='N' }, click: chgMode" />
<input type="button" value="依名稱" data-mode="M"
    data-bind="css: { on: mode()=='M' }, click: chgMode" />
<input type="button" value="依類別" data-mode="T"
    data-bind="css: { on: mode()=='T' }, click: chgMode" />
<input type="button" value="依廠牌" data-mode="B"
    data-bind="css: { on: mode()=='B' }, click: chgMode" />
</div>
<div>
    Mode = <span data-bind="text: mode"></span>
</div>
<script>
function myViewModel() {
var self = this;
      self.mode = ko.observable("N");
      self.chgMode = function(data, event) {
//可由event.target取得觸發事件的來源元素
        self.mode(event.target.getAttribute("data-mode"));
      };
    }
    ko.applyBindings(new myViewModel());
</script>
</body>
</html>

線上展示

[KO系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post
Viewing all 2458 articles
Browse latest View live


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