2010年6月30日 星期三

執行Coded UI Test 出現例外:The playback failed to find the control

過程

在本機執行 Coded UI Test 好好的,改到 Test Agent 上執行,卻發生測試的例外:

Microsoft.VisualStudio.TestTools.UITest.Extension.UITestControlNotFoundException: The playback failed to find the control with the given search properties. Additional Details: 
TechnologyName:  'MSAA'
ControlType:  'Button'
Name:  'Close'
 ---> System.Runtime.InteropServices.COMException: Error HRESULT E_FAIL has been returned from a call to a COM component..

原因

錄製測試時,是使用本機 (Windows 7 En + IE 8) 來錄製的。該測試最終時會關閉瀏覽器。

Test Agent 上則是 (Windows 2003 + IE6)。故執行測試時, IE 6 上並沒有 “Close” 的 Control。因為不同文化的關係,中文 IE 上應該是「關閉」而非「Close」。

解法

目前我並不清楚碰到不同語言版本的瀏覽器時,Coded UI Test應該如何處理,但微軟的 Visual Studio Test tools 有關閉瀏覽器的 API 可直接呼叫,就省去以名稱來找 Control 的困擾

var browser = BrowserWindow.Launch(new System.Uri(this.LauchBrowserParams.Url));
browser.Close();

當然,需要手動修改程式。

2010年6月29日 星期二

Asp.Net 4.0 Xml 轉換錯誤

於執行 TFS build  的結果,找到了下面的錯誤

Error    1    The "TransformXml" task failed unexpectedly.
System.UriFormatException: Invalid URI: The URI is empty.
   at System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind)
   at System.Uri..ctor(String uriString)
   at Microsoft.Web.Publishing.Tasks.TransformXml.Execute()
   at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
   at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask(ITaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask, Boolean& taskResult)        0    0  

為什麼呢?完全看不懂。

原來,簡單地說是轉換失敗了。我的 Web.config 中,屬性的值裡的大於 > 這個字元,但這是不合法的。Visaul Studio 並不會抱怨。

例如:

<?xml version="1.0"?>
<configuration>
<appSettings>
  <add key="myKey" value="cdcdcdcdjkcjd>" /> <!-- 值裡有大於字元  -->
</appSettings>
</configuration>

而使用 MSDepoly 工具時,會使用 Xml Transform 來轉換 web.config,就會發生錯誤了。

只是,為什麼錯誤訊息是 Invalid URI 呢?

TFS 建置:Coded UI 自動測試

Coded UI test 是 Visual Studio 2010 的一大賣點,可模擬使用者操作程式。而現在我們已經將 Build Server 建置完畢,當然希望也能進行自動測試。單元測試等原本就在 TFS 2008 上即可執行,而 Coded UI test 卻因模擬使用者登入而需要更多的安裝與設定。

安裝

除了 Team Foundation Server 2010 的安裝外,也需要安裝 Build Agent。在建置的過程中,會進行 Source Control 的取得最新版本,compilatin,以及 Test。在Test 的過程,如果有 Coded UI Test,則必須安裝 Test Agent Controller 及 Test Agent。

設定

Build Server

在Build Server 上,我們必須讓 Build Agent 跑在互動的模式。執行 Team Foundation Administration Console, 點擊「Build Configuration」, 找到Build Service 的 Property。

image

先按「Stop to make change」後,開使調整設定。必須選擇「Interative Process」, 並指定適合的 Credentialsi , 按Start鍵。

image

按Start 鍵後,會跑出下面的視窗。

image

Test Controller

Test Controller 用來控制及收集 Test Agent 執行測試的結果。

執行 Microsfot Visual Studio Test Controller 2010 Configuration,出現下面的視窗。一樣地,使用相同的帳號,並按「Apply Settings」。

image

Test Agent

Test Agent 則是安裝在測試機上,用來模擬使用者在電腦上操作。通常這些測試機都會使用虛擬器,以模擬不同的使用者環境,如 Windows 7 + IE8, Windows Vista + IE7,Mac + Safari。

執行 Microsfot Visual Studio Test Controller 2010 Configuration,出現下面的視窗。一樣地,使用相同的帳號,並按「Apply Settings」。

image

需要注意的是,由於這些安裝 Test Agent 的機器需要「模擬」使用者操作 UI,故不能鎖住電腦(螢幕保護程式),本機一開機後也會直接登入。

自動部署

為了讓後續的網站測試能順利進行,我們必須讓 daily build 時自動在 IIS 上建立應用程式。
在 Build Definition 上,加入 MSBuild 的參數

/p:DeployOnBuild=True
/p:DeployTarget=MsDeployPublish
/p:MSDeployPublishMethod=InProc
/p:CreatePackageOnPublish=True
/p:DeployIisAppPath="預設的網站/EInvoice2" 
/p:MsDeployServiceUrl=localhost

image

修改測試

我測試的是一個 Web Form 的應用程式,在專案中加入一個測試專案。在測試專案中再加入一個 Code UI Test。完成錄製後,簽入到TFS後執行。哇!發生錯誤!

Test method xxx.Tests.CodedUIPermissionsTest.AddDeletePageTest threw exception:
Microsoft.VisualStudio.TestTools.UITest.Extension.PlaybackFailureException: Cannot perform 'SetProperty of Password with value "zPjUJLMG7JyDvQw78l9YYBtInZnoHu7P"' on the control. Additional Details:
TechnologyName:  'Web'
ControlType:  'Edit'
Id:  '_txtPassword'
Name:  '_txtPassword'
TagName:  'INPUT'
---> System.Runtime.InteropServices.COMException: Error HRESULT E_FAIL has been returned from a call to a COM component.

怎麼辦呢?原來當初錄製的帳號與執行測試的帳號是不同的。而為了安全,Coded UI 測試時遇到密碼欄位資料會使用原錄製帳號來加密,執行帳號當然不能解密了。

我這時找 Google 大神,也找不到解法。難道這些內容太新了嗎?看了一下測試專案中UIMap.uitest的內容,哈哈!xml 果然容易了解。將有問題的網頁,將 Encoded=”true” 改成 false, 並將內容改成明碼的密碼後,就解決了這次的問題。

<SetValueAction UIObjectName="UIMap.UIGoogleWindowsInterneWindow.UIDocument.UI_txtPasswordEdit">
  <ParameterName />
  <Value Encoded="true">vDeufj/Hr41sx0RsGlhIlhfWB2QAYMfo</Value> <!-- Encoded :是否加密-->
  <Type>String</Type>
</SetValueAction>

 

 

參考

寫出好程式的好習慣

在寫程式時,哪些習慣必須建立呢?以下列出我認為一個好的程式設計師應該養成的好習慣。

1 Test Driven Development

第一名的,就是我非常推的 TDD了。TDD不只是先寫測試再寫程式這麼簡單的規定而已,背後還包涵了一些情境。

首先,在寫測試程式時,其實就必須先要了解需求。不知道需求,當然寫不出測試。

其二,寫測試時式時,其實我就會開始想「要怎麼讓客戶端呼叫我的程式」,方法是要建立在 instance上呢?還是實作成類別的靜態方法。此時,我們就必須從由使用者的觀點來看,客戶端應該如何使用我們的程式。

其三,寫測試程式時,也會想到客戶端如何與我的程式互動?由兩者的互動關係,進而衍生出 dependent objects,dependency injections, IoC 等。而撰寫測試時,也需要開始建立  mocking object。

最後,TDD將修正我們所寫的類別,不再自行建立物件,而改由客戶端傳入,因此可得到較佳的相依性。好處是物件的生命週期可規劃在IoC一致處理,避免物件到處都是。

2 善用 Static analysis 工具

Visual Studio 中有個 Calculate Code Metrics工具,可計算程式的可維護性指數。此可維護性指數是由循環複雜度、繼承深度、類別結合程度、程式碼行數等四者組合而成。每次重構後,我也習慣來這裡看看可維護性指數是否增加了。如果可維護性指數小於10,那就代表完全沒辦法維護,這樣的程式是連當初的作者也會看不懂的。

FxCopCode analysis 又是另一種常用的功能。它將常見的程式規則寫成 coding rule 並進行分析。分析完後產生如同編譯的錯誤或警告,讓開發人員可以得到提醒並進行修正。

3 使用開發輔助工具

使用良好的開發輔助工具,會讓我們寫程式時事半功倍。Visual Studio 已經內建了許多工具,如 Refactor,Code Snippet等。第三方工具如 CodeRush, ReSharper 等更提供強大的功能,幫助我在搜尋、重構、程式碼分析等地方。

不得不提一下ReSharperCode Analysis功能,可直接在編輯區中背景地指出程式哪裡需要改進。就好像請了一個超強的大師,在身邊耳提面命,不斷地提出修正。久而久之,程式功力當然會進步。

image

4 不要太多的預設計 (Big Design Up Front)

在開發程式時,僅需要符合當下的需求就好。不要預想未來「可能」的需求為何?效能問題?應用程式架構怎樣分才能符合「未來」的需求。

要知道未來不見得發生這些預想的事。做這些預設計不但浪費時間,並可能造成未來程式難以重構,成本自然增加不少。

之前我也犯下不少這樣的錯誤。其中一項頗令我汗顏。當時我將某網站設計成三層式(3-tier)的架構,其中應用程式層(application tier)使用了 remoting的分散式架構。當時想:未來系統一定拆開到不同的伺服器,以符合高擴展性的需求,這些設計未來一定會用到。結果,開發兩年後,不但沒有發生,更糟的是網站還交給另給一組人維護。因為我做了多餘的設計,而這個設計已過了5年還沒發生當時預想的狀況,新進人員都會問當時為何這樣設計?造成交接、維護上的困擾。

5 奧坎剃刀原則 (Occam’s razor)

Occam’s razor 說明了下面的原則:
     假如一個問題有很多種解決辦法,選最簡單的那個

造成系統複雜的原因,其實還可再細分成兩種類型:本質複雜及意外複雜。

本質複雜(essential complexity)

本質上的複雜,是該問題本質上就比較複雜,我們應該採取科學上的方法讓它變簡單。常見科學方法,再分成兩種。

一個是將大而複雜的問題,拆解成多個簡單的小問題,進而一個個解決並得到整體行為。例如微積分,有限元素法。專案管理中的WBS 也是採用了這類的方法。這類方法有個缺點:小問題的總體行為等同於原大問題的行為嗎?

另一種是承認該大而複雜的問題難以拆解成小而簡單的問題。因此使用統計學的方法來統計大而複雜問題的行為。缺點是相當費時秏力,方法不正確,有時反而會得到相反的結果。

意外複雜(accidental complexity)

問題本質性的複雜外,有時候(也是常常)是我們採取的解決方法錯了,反而使得問題更複雜,稱為意外複雜。例如上述我所犯的預設計問題。此時,正確的方法則是將這些意外複雜因素移除掉。

本質複雜與意外複雜有時難以分辨,相當依賴設計人員的經驗。經典常見的意外複雜原因,如下。

  • 我們實作了自己的 Web/Persistence/Messaging/Caching 的framework, 因為找不到比較好用的!!
  • 買整個工具包,即使我們只用到10%。
  • 為了效能,我們將所有的商業邏輯寫到 Stored procedure。(常見吧!)
  • 我們不寫單元測試,因為我們已經花了很多時間在 debug。(真是天才)

這些理由看起來頭頭是道,似乎解決了問題,卻都使用問題更意外複雜。

6 學新的技術

為何要發明新技術?新技術往往是用來解決以前難以解決的問題,進而使用更簡單的方式來解決。以下是一些新的技術,您學會了嗎?
•Reflection
•Regular expressions
•Dependency injection
•Lambda expressions
Extension methods

雖然使用新技術解決問題往往更加容易,但並非一定要採用這些新技術才能解決問題。因此許多「上班族」不願花時間學習,只願使用已經習慣的舊技術來解決。這樣的心態,稱為舒適區(Comfort zone)。這裡不適合討論心理學,話題就此打住。

7 獨立思考

並非所有新技術都能存活下來,故並非所有新技術都值得學習。那要如何分辨呢?

相同的道理,網路上的訊息已經多到「知識爆炸」也難以形容,但我們還是要學習/吸取新知。那要學習哪些知識呢?使用什麼方式學習較有效率呢?

答案就是獨立思考,不要隨廣告/行銷等手法矇騙。要常常想這樣做,比較快嗎?比較有效率嗎?沒有其他方法了嗎?沒有其他觀點了嗎?

結論

寫著寫著,就跳脫程式寫作的範圍了。要如何寫出好程式,其實與個人的思考習慣有密切關係。希望這一篇能對大家有幫助。

後記:感謝保哥提醒,應為 Extension Methods

2010年6月24日 星期四

TPL 學習日記(6): 等待任務1:等待單一任務

等待任務執行完畢後,再接著執行其他的工作,是相當常見的需求。以下介紹幾種常見的需求。

等待某一任務完成

等待某一任務完成後,接著再執行另一任務。這是最基本的工作了。

static void Main(string[] args)
{
  var tokenSource = new CancellationTokenSource();
  var task1 = createTask(tokenSource.Token);
  task1.Start();
  Console.WriteLine("等待執行完畢,或任務取消,或任務發生 Exception");
  task1.Wait();
  Console.WriteLine("執行完畢"); // task1 會執行完畢

  var task2 = createTask(tokenSource.Token);
  task2.Start();
  Console.WriteLine("等待執行完畢(只等3秒),或任務取消,或任務發生 Exception");
  bool completed = task2.Wait(3000);
  Console.WriteLine("執行完畢:{0}", completed); //task2 仍然繼續執行,只是不等了

  var task3 = createTask(tokenSource.Token);
  task3.Start();
  Console.WriteLine("等待執行完畢(只等3秒),或任務取消,或任務發生 Exception");
  completed = task3.Wait(3000, tokenSource.Token);
  Console.WriteLine("執行完畢:{0}, 是否取消:{1}", completed, task3.IsCanceled);
}

執行結果如下

等待執行完畢,或任務取消,或任務發生 Exception
Task Id 1: 0
Task Id 1: 1
Task Id 1: 2
Task Id 1: 3
Task Id 1: 4
執行完畢
等待執行完畢(只等3秒),或任務取消,或任務發生 Exception
Task Id 2: 0
Task Id 2: 1
Task Id 2: 2
執行完畢:False
等待執行完畢(只等3秒),或任務取消,或任務發生 Exception
Task Id 3: 0
Task Id 2: 3
Task Id 3: 1
Task Id 2: 4
Task Id 3: 2
執行完畢:False, 是否取消:False
Task Id 3: 3
Press any key to continue . . .

Task 有 Wait 的方法可以用來等待該任務完成的條件。
方法 說明
Wait() 等待執行完畢
Wait(Int32) 等待執行完畢,但只等一定的時間(微秒)
Wait(Int32, CancellationToken) 等待執行完畢,但只等一定的時間(微秒),或任務取消。

需要注意的是使用 int 當等待時間,當等待時間到達時,就不再等待而程式繼續向下執行,原本執行的任務依然繼續執行。由輸出中的 Task Id 來看,2 與 3 會交互執行一段時間。

2010年6月22日 星期二

ASP.NET 4: Application domain resource management

以往,兩個 Web Application 放在同一個 Application Pool 時,其中一個非常吃資源,往往會影響到另一個Web Application。因此,兩個Web Application 都看起來不正常,常常令人無法了解誰是禍首,也引起一番爭論。

常見的建議,是將這兩個應用程式分開到不同的 Application Pool,再來看看誰是罪魁。這樣一來,勢必有一個Web Application 必須換Application Pool,也就等於應用程式重起啟動,會讓線上使用者跳腳。

以前,這是原罪,一定要經過這樣的痛楚才能得到是非的原因。在 ASP.NET 4 則新增了一項功能,可以在同一個 Application Pool 的 ASP.NET 4 Applications,也可以找到分別的效能計數器。這項功能稱 Application domain resource management,簡寫為 ARM

設定

ARM 預設為非啟動,必須手動修改 C:\Windows\Microsoft.NET\Framework\v4.0.30319\Aspnet.config下,新增一行設定appDomainResourceMonitoring ,如下

<appDomainResourceMonitoring enabled="true"/>
設定完畢後,記得將該 Application Pool 重啟,才會讀取這個新的設定。

效能計數器

在 ASP.NET Apps v4.0 下,會比 V2.0 多了 % Managed Processor Time(estimated) 及 Managed Memory Used(estimated) 這兩組計數器,其下可再細分出每個應用程式的效能。

image

結論

經過這樣的設計,找效能殺手的應用程式,果然比以前來的有效率多了。但為什麼不預設就是啟動的呢?還得手動地加上設定?我猜這也會造成效能上的issue 吧。

REF

http://www.asp.net/learn/whitepapers/aspnet4

What's New in ASP.NET 4 and Visual Web Developer

2010年6月20日 星期日

ASP.NET 4: Routing

雖然已經使用一段時間的 ASP.NET MVC,但畢竟之前使用 ASP.NET WebForm技術寫的程式多到數不清,無法拋棄,但仍然想要使用 MVC 的 Routing 啊。

經由 ASP.NET MVC 的激勵,在 ASP.NET 4.0 WebForm,特地加上了一組新的 Routing 的API,以支援愈來愈多的 SEO 需求。

單一網頁的 Routing

最簡單的Routing,就是簡單地把一個 Url 對應到一個 WebForm 的 aspx。以下就先簡單地建立一個 WebForm Application。

  1. 建立一個 Web Application,並指定名稱寫 TestRoutes
  2. 在 Global.asax.cs 中,修改 Application_Start 如下
void Application_Start(object sender, EventArgs e)
    {
      System.Web.Routing.RouteTable.Routes.MapPageRoute("about", "About", "~/About.aspx");
    }

測試一下網頁,http://localhost:1295/About.aspxhttp://localhost:1295/About 的結果是相同的。

這樣,我們就簡單地將某一 Request 對應到單一的網頁。

單一參數改成 Routing

建立舊有的網頁

我們先新增一個舊 Style 的 WebForm 網頁 Tag.aspx,並指定 Master Page 如下。

image image

網頁的內容如下

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Tag.aspx.cs" Inherits="TestRoutes.Tag" %>
<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
  指定 Tag 為 
  <asp:Label Text="text" runat="server" ID="_Tag" />
</asp:Content>

而 CodeBehind 程式如下

using System;

namespace TestRoutes
{
  public partial class Tag : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      _Tag.Text = Request["Tag"];
    }
  }
}

測試一下網頁並加上參數 ?Tag=MVC,結果如預期地顯示如下

image

以上是最簡單不過的 WebForm 程式。現在呢,我想要讓 Url 可以讓搜尋引擎更方便地來搜尋(SEO),故想讓 Url 可以可使用 /Tags/MVC 來達到相同的效果。

改成 Routing 型式

第一步,仍然是在 Global.asax.cs 的 Application_Start 上修改 RouteTable,如下

void Application_Start(object sender, EventArgs e)
    {
      System.Web.Routing.RouteTable.Routes.MapPageRoute("about", "About", "~/About.aspx");
      System.Web.Routing.RouteTable.Routes.MapPageRoute("tag", "Tags/{Tag}", "~/Tag.aspx");
    }
接下來,修改 Tag.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
  //_Tag.Text = Request["Tag"];
  string routeValue = (string)Page.RouteData.Values["Tag"];
  _Tag.Text = routeValue;
}

測試一下網頁 http://localhost:1295/Tags/MVC 果然和之前的結果是一樣的。

image

兩者兼顧

回過頭來測試一下,什麼? http://localhost:1295/Tag.aspx?tag=MVC 反而不能 work? 這非常讓客戶不能接受。我們希望即使SEO 化後,原來的 WebForm 存取方式也能正常。故改 code behind 如下。

protected void Page_Load(object sender, EventArgs e)
{
  _Tag.Text = TagValue;
}

private string TagValue
{
  get
  {
    string fromQuery = Request["Tag"];
    if (string.IsNullOrEmpty(fromQuery))
    {
      string routeValue = (string)Page.RouteData.Values["Tag"];
      return routeValue;
    }
    else
      return fromQuery;
  }
}

經過這次修改後,兩者都可以正常運作了。缺點呢?一看就知道,為了取得使用者要求的 Tag,並且能適用兩種Url 存取方式,必須大動土木地修改 Global.asax.cs 與每個網頁,實在很不人道,而且程式變的更難維護了。之後有空再來解決這一個問題。

使用 RouteValue expression builders

如果在每一頁的 aspx都需要在 code behind 上使用 Page.RouteData來讀取 route value,也太浪費時間了。因此 ASP.NET 4 介紹了一個新的 RouteValue expression builders 來顯示。範例如下

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Tag.aspx.cs" Inherits="TestRoutes.Tag" %>
<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
  指定 Tag 為 
  <asp:Label Text="text" runat="server" ID="_Tag" />
  <br />
  使用 RouteValue expression builders方式,得到指定 Tag 為
  <asp:Label runat="server" Text="<%$ RouteValue:Tag %>" />
</asp:Content>

簡單地使用 <%$ RouteValue:routeKey%> 語法,就可以讀取 RouteData 了。

製作 RESTFul的連結

要寫出 RESTFul 的連結其實很簡單,只是需要遵循以下RouteValue expression的規則

<%$RouteUrl:RouteName=yourRouteName,yourRouteKey=yourRoutKeyValue%>

image

其中yourRoutName 及 yourRouteKey 是在 Global.asax.cs 中註冊的 RouteTable,yourRouteKeyValue 即是要查詢的值。

舉例來說,若要在首頁(default.aspx)增加一個連結,可以使用如下的設定

<asp:HyperLink NavigateUrl="<%$RouteUrl:RouteName=tag,Tag=MVC %>" runat="server" Text="MVC" />

結論

在 WebForm 中使用這些 Routing 雖然不是難事,但與 MVC 相比起來,相對來說還是較難使用。在 ASP.NET MVC 中,使用 Html Helper 的 ActionLink 是非常直覺的事啊!而 WebForm 卻要記這個RouteValue expression。

範例下載

2010年6月18日 星期五

MSBuild 與 DevEnv 的建置結果不相等

使用 TFS Build 了一陣子,發現了 MSBuild 的結果未必與 Visual Studio 2005 建置的結果不同。

故事是這樣的:我使用了下列的指令 rebuild 了一個 solution。

msbuild mySolution.sln /t:rebuild

將該solution 下的某個 webform 專案結果拿到 CAT.NET 下掃描,結果會出現「並未將物件參考設定為物件的執行個體」的錯誤訊息。

image

怎麼會呢?看起來是 CAT.NET 的 bug。

追了好久,改用Visual Studio 2005 內建的指令 devenv 來建置

DevEnv MySolution.sln /rebuild "Release"

建置結果竟然可以了!

只能說 Visual Studio 2005 與 msbuild 的結果可能不一樣。有沒有人知道差別到底在哪裡呢?

在VS2008, VS2010 是否也是相同的狀況呢? 還沒有測試呢!

TPL 學習日記(5): 等待一段時間

有時候,任務的執行需要等待一段時間後再繼續進行。
下面的程式,彷效進出貨。進貨及出貨各需2.5秒,如果使用者按 Stop 鍵,程式就會「停止」。

Canecl2.xaml
<Window x:Class="TPL_CancelAnApp.Cancel2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Cancel2" Height="350" Width="525">
    <Grid>
    <StackPanel >
      <TextBlock x:Name="txtTotal" />
      <Button x:Name="btnStart" Click="btnStart_Click">Start</Button>
      <Button x:Name="btnStop" Click="btnStop_Click" Content="Stop"></Button>
      <TextBlock Name="txtStatus" />
    </StackPanel>
  </Grid>
</Window>

 

Cancel2.xaml.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace TPL_CancelAnApp
{
  public partial class Cancel2 : Window
  {
    //建立 CancellationTokenSource
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    Task task;
    public Cancel2()
    {
      InitializeComponent();
      InitialTask();
    }

    private void InitialTask()
    {
      CancellationToken token = tokenSource.Token;
      task = new Task(() =>
      {
        long total = 0;
        while (total < int.MaxValue)
        {
          total++;
          Action<string> action = new Action<string>(s => txtTotal.Text = s);
          txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString() + " 進貨");
          Thread.Sleep(2500);
          txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString() + " 出貨");
          Thread.Sleep(2500);
          token.ThrowIfCancellationRequested();
        };
      }, token);
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      txtStatus.Text = "進行中";
      task.Start();
      btnStart.IsEnabled = false;
    }

    private void btnStop_Click(object sender, RoutedEventArgs e)
    {
      txtStatus.Text = "使用者要求停止";
      tokenSource.Cancel();
    }
  }
}

上面的程式是可以執行的。但有個問題,當使用者按 Stop 鍵,程式並不會立即停止,而是需要等到進貨及出貨完成(各2.5秒)後, 才到token.ThrowIfCancellationRequested這一行決定是否取消。因此,一定會出貨完。 有沒有方法可以立即停止呢?

更新

問題出在 Thread.Sleep(2500),是無論如何都會「睡」個 2.5 秒,與目前的 Task 無關。

此時,我們需要使用 CancellationToken.WaitHandle.WaitOne(2500) 來模擬了。程式會等待取消 2.5 秒,並回傳是否有被取消。

程式如下

private void InitialTask()
{
  CancellationToken token = tokenSource.Token;
  task = new Task(() =>
  {
    long total = 0;
    while (total < int.MaxValue)
    {
      total++;
      Action<string> action = new Action<string>(s => txtTotal.Text = s);
      txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString() + " 進貨");
      //Thread.Sleep(2500);
      bool isCanceled = false;
      isCanceled = token.WaitHandle.WaitOne(2500);
      if (isCanceled)
        token.ThrowIfCancellationRequested();
      txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString() + " 出貨");
      //Thread.Sleep(2500);
      isCanceled = token.WaitHandle.WaitOne(2500);
      if (isCanceled)
      token.ThrowIfCancellationRequested();
    };
  }, token);
}
這樣修改後,按了Stop 鍵就會立即停止了。 範例程式下載

2010年6月15日 星期二

TPL 學習日記(4): 取消多個任務

接下來看比較複雜的範例。

取消多個任務

多個任務在不同時間開始執行,但需要在同一個時間結束。以下是畫面。

image

需求

當我們按上面的 Start 鍵,就開始第一個計數器。當按下面的 Start 鍵,就開始第二個計數器。而按 Stop All 鍵時,兩個計數器同時停止。

在 Task 的初始時,指定同一個 CancelTokenSource 的 CancelToken,就可以使用同一個CancelTokenSource 來 Cancel 多個 Task

CancelMutilpleTasks.xaml
<Window x:Class="TPL_CancelAnApp.CancelMutilpleTasks"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
  <Grid>
    <StackPanel >
      <StackPanel>
        <TextBlock x:Name="txtTotal1" />
        <Button x:Name="btnStart1" Click="btnStart_Click">Start</Button>
      </StackPanel>
      <StackPanel>
        <TextBlock x:Name="txtTotal2" />
        <Button x:Name="btnStart2" Click="btnStart_Click">Start</Button>
      </StackPanel>
      <Button x:Name="btnStop" Click="btnStop_Click">Stop All</Button>
      <TextBlock Name="txtStatus" />
    </StackPanel>
  </Grid>
</Window>
CancelMutilpleTasks.xaml.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using System.Windows.Controls;

namespace TPL_CancelAnApp
{
  public partial class CancelMutilpleTasks : Window
  {
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    Task[] tasks = new Task[2];
    public CancelMutilpleTasks()
    {
      InitializeComponent();
      InitialTask();
    }

    private void InitialTask()
    {
      CancellationToken token = tokenSource.Token;
      //建立第一個 task
      tasks[0] = new Task(() =>
      {
        long total = 0;
        while (total < 1000)
        {
          total++;
          Thread.Sleep(2);
          Action<string> action = new Action<string>(s => txtTotal1.Text = s);
          txtTotal1.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString());
          token.ThrowIfCancellationRequested();
        };
      }, token);
      //建立第二個 task
      tasks[1] = new Task(() =>
      {
        long total = 0;
        while (total < 1000)
        {
          total++;
          Thread.Sleep(2);
          Action<string> action = new Action<string>(s => txtTotal2.Text = s);
          txtTotal2.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString());
          token.ThrowIfCancellationRequested();
        };
      }, token);

      Task.Factory.StartNew(() =>
      {
        token.WaitHandle.WaitOne();
        txtStatus.Dispatcher.BeginInvoke(new Action(() => txtStatus.Text = "使用者要求停止。"));
      });
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      txtStatus.Text = "進行中";
      Button button = sender as Button;
      int index = int.Parse(button.Name.Substring(button.Name.Length - 1));
      tasks[index-1].Start(); //啟動指定的任務
    }

    private void btnStop_Click(object sender, RoutedEventArgs e)
    {
      btnStart2.IsEnabled = btnStart1.IsEnabled = false;
      tokenSource.Cancel(); //發出 Cencel 的訊號
    }
  }
}

範例下載

TPL 學習日記(3): 監看 task 的 Cancel

任務被 Cancel 後,我們需要一些進行一些處理,例如 UI 的顯示,使用者事件的記錄等。以下程式由上一次的範例繼續演變下去。

要監看任務是否被取消,有下列的方法。

方法1:使用Polling (輪詢)

這個方法即上次的演示,是在 Task body 內的迴圈中使用 tokenSource.IsCancellationRequested 來得到是否任務被取消。

方法2:使用Delegate

將程式重構,在建構子時建立 task,並在使用者按「start」鍵時啟始這個任務。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace TPL_CancelAnApp
{
  public partial class MainWindow : Window
  {
    //建立 CancellationTokenSource
    CancellationTokenSource tokenSource = new CancellationTokenSource();
    Task task;
    public MainWindow()
    {
      InitializeComponent();
      InitialTask();
    }

    private void InitialTask()
    {
      task = new Task(() =>
      {
        long total = 0;
        while (total < 1000)
        {
          total++;
          Thread.Sleep(2);
          Action<string> action = new Action<string>(s => txtTotal.Text = s);
          txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString());
          //當收到要求取消任務時,我就終止任務
          tokenSource.Token.ThrowIfCancellationRequested();
        };
      }, tokenSource.Token); //具有這個 Token 的來源可以取消這個任務

      tokenSource.Token.Register(() => txtStatus.Text = "使用者要求停止。");
    }

    private void btnStart_Click(object sender, RoutedEventArgs e)
    {
      txtStatus.Text = "進行中";
      task.Start();
    }

    private void btnStop_Click(object sender, RoutedEventArgs e)
    {
      btnStart.IsEnabled = false;
      tokenSource.Cancel();
    }
  }
}
注意到在 InitialTask() 中,我增加了一行 tokenSource.Token.Register(…,這一行註冊了一個 delegate,當任務被取消時,會回過頭來呼叫該 delegate。
範例下載

方法3:使用Wait Handle

WaitHandle 是多執行緒程式中常用的物件,在TPL 中也不例外。在下例中,我建立了一個 task2,並且等到 task的Cancel呼叫後才會執行。

private void InitialTask()
{
  CancellationToken token = tokenSource.Token;
  task = new Task(() =>
  {
    long total = 0;
    …
  }, token); //具有這個 Token 的來源可以取消這個任務

  Task task2 = new Task(() =>
  {
    token.WaitHandle.WaitOne(); //直到 tokenSource.Cancel() 被呼叫後才會往下執行
    txtStatus.Dispatcher.BeginInvoke(new Action(() => txtStatus.Text = "使用者要求停止。"));
  });
  task2.Start();
}
範例下載

結論

雖然有三種方法可以監控任務的取消,但這三種方法都有不同的適用情境。第一種的輪詢方式適用於迴圈、第二種的Delegate 適用於簡單的一次性工作、而第三種則適用於多項任務的串連。

2010年6月14日 星期一

TPL 學習日記(2): Cancel a task

繼上一回的 Start new task,這一次當然要Cancel 它了。

範例

為了讓範例能更貼近實際,不得不使用UI 更豐富的 WPF。(Windows Forms 呢?看來微軟愈來愈放棄它了,就漸漸忘了吧)

下面這個例子,按了Start這個鍵會開始由1增加到1000,我們希望按 Stop 後能停止。

image

步驟1

建置基本的 layout 及程式

xaml

<Window x:Class="TPL_CancelAnApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
    <StackPanel >
      <TextBlock x:Name="txtTotal" />
      <Button x:Name="btnStart" Click="btnStart_Click">Start</Button>
      <Button x:Name="btnStop">Stop</Button>
    </StackPanel>
  </Grid>
</Window>
MainWindow.xaml.cs
private void btnStart_Click(object sender, RoutedEventArgs e)
{
  long total = 0;
  while (total < 1000)
  {
    total++;
    Thread.Sleep(1);
    txtTotal.Text = total.ToString();
  }
}

這個步驟執行的結果不如預期。畫面一次就顯示到 1000,而逐次遞增的。試一下澴沒到1000時,程式是沒有任何回應的。這也是常見的UI 回應的問題。步驟2我們就使用 WPF 的 Dispatcter 來修正吧。

步驟2

private void btnStart_Click(object sender, RoutedEventArgs e)
{
  Task.Factory.StartNew(() =>
  {
    long total = 0;
    while (total < 1000)
    {
      total++;
      Thread.Sleep(1);
      Action<string> action = new Action<string>(s => txtTotal.Text = s);
      txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString());
    };
  });
}

這個步驟中增加了 Task.Factory.StartNew  來新增一個 Task。執行過程中,要更新 UI Thread的內容時,不能直接在非 UI Thread 的 task l來更新,故必須透過 WPF 中特殊的 Dispatcher來更新。更新的方法就是呼叫要更新的控制項的 Dispatcher.BeginInvoke,並傳入參數。第一個參數是執行更新的方法,在這裡定義在 action 中。第二個參數是更新的優先度,這裡傳入DispatcherPriority.Normal。第三個則是 action 傳入的參數,即要更新的文字。

經過步驟2,就能以即時顯示的方式來反應UI了。

步驟3

步驟3中,我們要增加 Stop 的機制。TPL 中 Task 的取消方式也很直覺。設計的方法,是在執行的任務中給一個特定的物件 CancellationTokenSource,該物件定義任務中可以用來取消任務的來源。白話點講,「誰可以取消我這個任務呢?有CancellationToken 的來源就可以取消我這個任務」。而誰可這樣的Token呢?CancellationTokenSource就有。於是當CancellationTokenSource執行Cencel()時,我這個任務就收到了Cencel 的 Request 了。

以下是步驟3的程式碼

private void btnStart_Click(object sender, RoutedEventArgs e)
{
  Task.Factory.StartNew(() =>
  {
    long total = 0;
    while (total < 1000)
    {
      total++;
      Thread.Sleep(1);
      Action<string> action = new Action<string>(s => txtTotal.Text = s);
      txtTotal.Dispatcher.BeginInvoke(action, DispatcherPriority.Normal, total.ToString());
      //當收到要求取消任務時,我就 break
      if (tokenSource.IsCancellationRequested) break;
    };
  }, tokenSource.Token); //具有這個 Token 的來源可以取消這個任務
}

private void btnStop_Click(object sender, RoutedEventArgs e)
{
  tokenSource.Cancel();
}

在使用 IsCancellationRequested 時,我用了 break 來離開while迴圈。在長時間的任務中,使用Iteration(迭代)回圈的方式是常見的,但非一定是這樣的處理方式。因此,未必能使用 break。為了能有效且一致方式的終止任務,TPL 使用了 throw exception 方式。

//當收到要求取消任務時,我就 break
if (tokenSource.IsCancellationRequested)
{
  //釋放資源
  throw new OperationCanceledException(tokenSource.Token);
  //break; //被OperationCanceledException取代了
}

上面程式如果沒有要釋放資源,更可以精簡如下

//當收到要求取消任務時,我就終止任務
tokenSource.Token.ThrowIfCancellationRequested();
雖然可以精簡程式,但以 code 來看,誰會將 ThrowIfCancellationRequested() 與終止任務連想在一起呢?這一點我認為設計的並不是很恰當。

結論

這個過程看起來簡單,並且被 Dispatcher 與 TPL 這兩個設計涵蓋住,但裡頭的學問可真不少呢。對於新手來說,可真地說又愛又恨。愛的是寫作變簡單了,恨的是難以了解內部是如何實作的。

範例下載

2010年6月9日 星期三

Visual Studio 2010 Pro Power Tools

Visual Studio 2010 可不是只有4/12 時的版本哦!隨著時間推進,愈來愈多的新功能/工具也跟著發佈。而且,是免費。今天介紹的是2010/06/08 (昨天)發佈的 Pro Power。

安裝

與Visual Studio 2005/2008 相比,安裝在Visual Studio 2010 上顯得極度的簡單。首先執行 Visual Studio 2010, 執行 Tools/Extension Manager

image

在搜尋文字框上輸入Power後,可篩選出符合條件的 Extension。選擇 Visual Studio 2010 Pro Power Tools,點擊「Download」鍵。安裝完畢後會要求重新起動 Visual Studio。

image

功能

新增的功能好多啊!解決了不少的問題。

Add reference diaglog

例如,Add reference(新增參考)在以前的畫面真的是慘不忍睹,想要在 GAC內加入 ReportViewer.WebForms 參考,得在幾百個 dll 內挑選。現在呢,就好像 Windows 7 的搜尋一樣。如下圖。

image

Highlight Current Line

寫程式的時候,常常會發現不知道游標(Cursor)跑到哪裡去了,尤其螢幕愈大愈寬後,這種情況就愈嚴重。Hightlight current line 的功能會突顯出現在游標所在的行,就比較好找了。

image

Fix Mixed Tabs

縮排時,到底應該使用 Tab鍵,還是多個空白鍵(spaces)呢?都可以,開發人員統一即可。但到底這一行的縮排是 tab, 還是 space 呢?該怎麼修正呢?這個功能只需要一個 click 即可。

image

Triple Click

連續按Click三下,可以選一整行。

Move Line Up/Down Commands

按 Alt + Up/Down 可以將一整行上移/下移

結論

我只介紹了部份的功能,完整的功能,請參考Visual Studio 2010 Pro Power Tools。原本的快速鍵最好也背起來。有些重複的功能,參考著用即可。例如 Ctrl + Click ,可以Go To Definition,其實與F12鍵是相等的,看當時你的右手是握著滑鼠還是放在鍵盤上。Pro Power Tools 增加了許多的功能,解決一些開發時的困擾,非常建議安裝。

2010年6月4日 星期五

圖形驗證(4): 使用 Generic Handler 產生圖形

這個系列,請見
(1) 簡介
(2): ASP.NET 簡易實作圖形驗證
(3): ASP.NET 圖形加強版

上一篇加強了圖形後,發現了一個美中不足的部份。圖形是先產了圖檔後,再以 asp:image 元件指到該圖檔,讓使用者可以看圖猜字。但這樣長久下來,會讓圖檔愈來愈多,總有一天會讓硬碟爆掉。雖然可以定下MIS規則讓管理人員定期手動清理,但人是健忘的動物,還是有一天會忘的。

這一次,我們就來加強一下這個部份。

Generic Handler

我們將使用 Generic Handler來解決暫存圖檔的問題。首先新增一個  Captcha.ashx 到網站中。

image

由於在 ashx 中,我們要使用 Session,故必須實作 IRequiresSessionState,如下

public class Captcha : IHttpHandler, IRequiresSessionState 
{

public void ProcessRequest(HttpContext context)
{
  context.Response.ContentType = "image/gif";
  VerifyNow(context);
}

public bool IsReusable
{
  get
  {
    return false;
  }
}

接下來,經過重構之後,之前大部份關於Captcha 圖形驗證的程式都可以搬到 Captcha.ashx.cs 下了。程式如下

Captcha.ashx.cs

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.Web;
using System.Web.SessionState;

namespace Captcha.Utility
{
  /// <summary>
  /// Summary description for Captcha
  /// </summary>
  public class Captcha : IHttpHandler, IRequiresSessionState 
  {

    public void ProcessRequest(HttpContext context)
    {
      context.Response.ContentType = "image/gif";
      VerifyNow(context);
    }

    public bool IsReusable
    {
      get
      {
        return false;
      }
    }

    private void VerifyNow(HttpContext context)
    {
      string randomString = context.Session["ImgText"] as string;
      CreateImage(context, randomString);
    }

    

    private void CreateImage(HttpContext context, string imageText)
    {
      //建立一個 Bitmap。寬高未定,故先給定1, 1
      Bitmap bmpImage = new Bitmap(1, 1);

      //指定字型
      Font font = new Font("Verdana", 24, FontStyle.Bold, GraphicsUnit.Point);

      //進行繪圖。繪圖需透過 Grahpics 物件。
      Graphics graphics = Graphics.FromImage(bmpImage);
      //測量文字的寬高
      int width = (int)graphics.MeasureString(imageText, font).Width;
      int height = (int)graphics.MeasureString(imageText, font).Height;

      //重新指定 Bitmap
      bmpImage = new Bitmap(bmpImage, new Size(width, height));

      //重新繪圖
      Random r = new Random();
      graphics = Graphics.FromImage(bmpImage);
      graphics.Clear(r.NextColor()); //使用亂數底色
      graphics.TextRenderingHint = TextRenderingHint.AntiAlias;  //不要鋸齒

      int avgWidth = width / imageText.Length;
      for (int i = 0; i < imageText.Length; i++)
      {
        font = new Font("Verdana", r.Next(12, 24), FontStyle.Bold, GraphicsUnit.Point);
        graphics.DrawString(imageText.Substring(i, 1), font, new SolidBrush(r.NextColor()), avgWidth * i, 0); //把文字畫上去
      }

      graphics.Flush();

      for (int i = 0; i < 10; i++)
        DrawRandomLine(graphics, height, width, r);

      bmpImage.Save(context.Response.OutputStream, ImageFormat.Gif);

      //記得 release 記憶體
      graphics.Dispose();
      font.Dispose();
      bmpImage.Dispose();
    }

    private static Color NextColor(Random r)
    {
      return Color.FromArgb(r.Next(255), r.Next(255), r.Next(255));
    }

    private static void DrawRandomLine(Graphics graphics, int height, int width, Random random)
    {
      Pen pen = new Pen(random.NextColor());
      pen.Width = random.Next(3);
      Point p1 = new Point(random.Next(width), random.Next(height));
      Point p2 = new Point(random.Next(width), random.Next(height));
      graphics.DrawLine(pen, p1, p2);
    }
  }
}
網頁的部份,就只剩下取得密碼及驗證的部份
Sample1.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Sample1.aspx.cs" Inherits="Captcha.Sample1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <asp:Image runat="server" ID="image" ImageUrl="~/Utility/Captcha.ashx" />
      <asp:TextBox runat="server" ID="txtAnswer" />
      <asp:Button Text="輸入" runat="server" ID="btnSubmit" onclick="btnSubmit_Click" />
    </div>
    </form>
</body>
</html>

Sample1.aspx.cs

using System;
using System.Text;
using System.Web.UI;

namespace Captcha
{
  public partial class Sample1 : System.Web.UI.Page
  {
    protected void Page_Init(object sender, EventArgs e) 
    {
      if (!Page.IsPostBack)
      {
        CreateImageText();
      }
    }

    protected void btnSubmit_Click(object sender, EventArgs e)
    {
      string randomString = Session["ImgText"] as string;
      if (randomString != txtAnswer.Text.Trim())
      {
        Response.Write("驗證錯誤");
        CreateImageText();
      }
      else
        Response.Write("驗證成功");
    }

    private void CreateImageText()
    {
      string randomString = GetRandomString(4);
      Session["ImgText"] = randomString;
    }

    private string GetRandomString(int length)
    {
      char[] chars = @"23456789ABCDEFGHIJKLMNPQRSTUVWXYZ".ToCharArray();
      Random r = new Random((int)DateTime.Now.Ticks);
      StringBuilder sb = new StringBuilder(length);
      for (int i = 0; i < length; i++)
        sb.Append(chars[r.Next(chars.Length)]);
      return sb.ToString();
    }
  }
}

結論

經過這樣一重構,ashx 的可重用性變高了,也不再需要圖片的暫存檔。

範例下載

圖形驗證(3): ASP.NET 圖形加強版

(1) 簡介(2): ASP.NET 簡易實作圖形驗證後,這次針對圖形太簡單的問題再做加強。

加強1

底色換成亂數取色。為了亂數取色,我寫了一個 extension method,延伸了 Random

using System;
using System.Drawing;

namespace Captcha.Utility
{
  public static class RandomExtension
  {
    /// <summary>
    /// 取得下一個顏色
    /// </summary>
    /// <param name="random"></param>
    /// <returns></returns>
    public static Color NextColor(this Random random)
    {
      return Color.FromArgb(random.Next(255), random.Next(255), random.Next(255));
    }
  }
}

因此,可以在換底色時,改成亂數取色。

//重新繪圖
Random r = new Random();
graphics = Graphics.FromImage(bmpImage);
graphics.Clear(r.NextColor()); //使用亂數底色
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;  //不要鋸齒

加強2

讓字型大小也能亂數忽大忽小,並且就亂數取色,我將部份程式修改如下

int avgWidth = width / imageText.Length;
for (int i = 0; i < imageText.Length; i++)
{
font = new Font("Verdana", r.Next(12, 24), FontStyle.Bold, GraphicsUnit.Point);
graphics.DrawString(imageText.Substring(i, 1), font, new SolidBrush(r.NextColor()), avgWidth * i, 0); //把文字畫上去
}

加強3

最後,我再加強甘擾,讓機器人更難以進行 AntiCaptcha,我畫了10條粗細長度顏色不一的線。程式如下

private static void DrawRandomLine(Graphics graphics, int height, int width, Random random)
{
  Pen pen = new Pen(random.NextColor());
  pen.Width = random.Next(3);
  Point p1 = new Point(random.Next(width), random.Next(height));
  Point p2 = new Point(random.Next(width), random.Next(height));
  graphics.DrawLine(pen, p1, p2);
}
...
for (int i = 0; i < 10; i++)
        DrawRandomLine(graphics, height, width, r);

結果

以下是這三個步驟加強後的結果.還不錯呢!

image

結論

看起來不錯,但仍有些美中不足的部份。例如字的顏色如果很接進底色的話,使用者就看不出來了。此時必須讓使用者自行換圖,或者想辦法讓兩者的顏色差異變大。這些小加強就請讀者自行發揮了。

範例程式下載

2010年6月3日 星期四

圖形驗證(2): ASP.NET 簡易實作圖形驗證

接續上一篇的簡介,我們這一次實作一個簡易的 ASP.NET 的圖現驗證。

步驟1: 顯示圖形

一般來說,圖形只能給人工來閱讀,SpamRobot 碰到圖形當然較吃力。這個步驟,我們就先把文字變成最簡單的圖形吧。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.Web.UI;

namespace Captcha
{
  public partial class Sample1 : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      if (!Page.IsPostBack)
      {
        CreateImage("I wanna go home");
      }
      image.ImageUrl = ImageUrl;
    }

    //圖檔放置的位置
    private string ImagePath
    {
      get
      {
        return Server.MapPath(ImageUrl);
      }
    }
    //圖檔的 Url。使用 SessionID 避免重複
    private string ImageUrl
    {
      get
      {
        return "~/imgs/" + Session.SessionID + ".gif";
      }
    }

    private void CreateImage(string imageText)
    {
      //建立一個 Bitmap。寬高未定,故先給定1, 1
      Bitmap bmpImage = new Bitmap(1, 1);

      //指定字型
      Font font = new Font("Verdana", 24, FontStyle.Bold, GraphicsUnit.Point);

      //進行繪圖。繪圖需透過 Grahpics 物件。
      Graphics graphics = Graphics.FromImage(bmpImage);
      //測量文字的寬高
      int width = (int)graphics.MeasureString(imageText, font).Width;
      int height = (int)graphics.MeasureString(imageText, font).Height;

      //重新指定 Bitmap
      bmpImage = new Bitmap(bmpImage, new Size(width, height));

      //重新繪圖
      graphics = Graphics.FromImage(bmpImage);
      graphics.Clear(Color.White); //使用白底
      graphics.TextRenderingHint = TextRenderingHint.AntiAlias;  //不要鋸齒
      graphics.DrawString(imageText, font, new SolidBrush(Color.Red), 0, 0); //把文字畫上去
      graphics.Flush();

      bmpImage.Save(ImagePath, ImageFormat.Gif);

      //記得 release 記憶體
      graphics.Dispose();
      font.Dispose();
      bmpImage.Dispose();
    }
  }
}

而網頁使用了簡單的 asp:image

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Sample1.aspx.cs" Inherits="Captcha.Sample1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <asp:Image runat="server" ID="image" />
    </div>
    </form>
</body>
</html>

在步驟1中,我使用了 Session 作為圖檔的名稱,儲存在網站的目錄下,並以 asp:image 來顯示圖形。

 

步驟2: 亂數

上一個步驟中,驗證的文字是寫固定地。這個步驟我們就來亂一下吧!

 

protected void Page_Load(object sender, EventArgs e)
{
  if (!Page.IsPostBack)
  {
    string randomString = GetRandomString(4);
    CreateImage(randomString);
  }
  image.ImageUrl = ImageUrl;
}

private string GetRandomString(int length)
{
  char[] chars = @"2346789ABCDEFGHIJKLMNPQRTUVWXYZ".ToCharArray();
  Random r = new Random((int)DateTime.Now.Ticks);
  StringBuilder sb = new StringBuilder(length);
  for (int i = 0; i < length; i++)
    sb.Append(chars[r.Next(chars.Length)]);
  return sb.ToString();
}

顯示的文字,不能讓使用者不小心看錯,例如O 與 0,l, i, 與 1,S與5。為了易於使用,必須將這些容易誤解的字元全部移掉。圖形如下

image

步驟3: 驗證

這是最簡單的步驟了。將驗證用的文字儲存起來,放到 Session中,等待使用者的回應並檢查之。

protected void Page_Load(object sender, EventArgs e)
{
  if (!Page.IsPostBack)
  {
    VerifyNow();
  }
}

private void VerifyNow()
{
  string randomString = GetRandomString(4);
  Session["ImgText"] = randomString;
  CreateImage(randomString);
  image.ImageUrl = ImageUrl;
}

protected void btnSubmit_Click(object sender, EventArgs e)
{
  string randomString = Session["ImgText"] as string;
  if (randomString != txtAnswer.Text.Trim())
  {
    Response.Write("驗證錯誤");
    VerifyNow(); //重新產生驗證文字
  }
  else
    Response.Write("驗證成功");
}

結論

這是一個極簡單的CAPTCHA圖形驗證的範例。雖然已經可以運作了,但仍然有不少的缺點,例如可重用性,使用了Session,以及最重要的是「圖形太簡單」的問題。

由於道高一尺魔高一丈的布袋戲真理,一定有 AntiCaptcha 工具的產生,圖形太過於簡單,等同於不設防。另外驗證文字的長度也太短了些,10000次總有一次會猜中,也是個問題。

範例可在這裡下載

圖形驗證(1): 簡介

在網路上無奇不有,什麼都賣,什麼都不奇怪。

為了許多的商業目的,如商品廣告、網站知名度、色情、甚至詐諞、釣魚等網路詐諞等,有時會在各大留言版、部落格、msn 等地方發一些廣告連結。這些文章、訊息通常不能被接受,通稱為惡意文章或訊息。

這些惡意文意如果以人工的方式發文,那當然費日秏時。因此接著發展出機器人,自動發文,甚至自動註冊帳號。免費的 Spam Robots甚至可自行下載。

使用人工來發這些帶有其他目的之訊息,通常不為原網站及網站使用者所喜。為了防止這些不正常目的惡意文章、帳號註冊,常用的防堵方式就是圖形驗證,(英文縮寫為 CAPTCHA),確保的確是由使用者正常的發文,而非機器人。這些圖形(如下圖例),通常希望讓人以目視的方式讀出驗證的通關密碼,防止機器人的攻擊。

然而有攻有防,有圖形驗證,就有破解圖形驗證。PWNtcha 就是一套這樣的工具,並且解釋了各式各樣的 CAPTCHA 及對應的破解率。

個資法通過的今天,圖形驗證已經是必要的加強網站安裝的設計。下一篇會為大家寫一篇 ASP.NET 上CAPTCHA的實作。

2010年6月1日 星期二

資料庫的資料加解密

最近常在搞資安。現在要求一些個資的資料需要在資料表內加密,讓一般的人看不懂。

而在 SQL Server 2005 以上,就內建了加解密的功能。

現在,就建立一個測試的資料庫

CREATE DATABASE [Test] ON  PRIMARY 
( NAME = N'Test', FILENAME = N'C:\db\.mdf' , SIZE = 2048KB , FILEGROWTH = 1024KB )
 LOG ON 
( NAME = N'Test_log', FILENAME = N'C:\db.ldf' , SIZE = 1024KB , FILEGROWTH = 10%)
GO

要加解密之前,需有一些基礎建設,就如同PKI

步驟1

-- 在 database 層級建立 master key
use Test;
GO

CREATE MASTER KEY ENCRYPTION BY
PASSWORD = 'Password1'
GO

步驟2

--建立憑證。會自動使用資料庫層級唯一的 master key
CREATE CERTIFICATE EncryptTestCert
    WITH SUBJECT = 'Test Encrypt'
GO

步驟3

-- 使用憑證建立對稱式金鑰
CREATE SYMMETRIC KEY SymKey
    WITH ALGORITHM = TRIPLE_DES
    ENCRYPTION BY CERTIFICATE EncryptTestCert
GO
最後,就是要加解密資料了。

加解密資料

USE Test
GO
--開啟對稱式金龠
OPEN SYMMETRIC KEY SymKey
DECRYPTION BY CERTIFICATE EncryptTestCert

declare @bin varbinary(256)

-- 對身份證字號加密
set @bin = ENCRYPTBYKEY(KEY_GUID('SymKey'), N'A123456789') 

-- 解密
select @bin
select Convert(nvarchar, DECRYPTBYKEY(@bin))

程式執行結果如下

0x0087FA222A1EE545A4EA753E7B09299801000000147A6F5EEB331D83A9E23024E78DB424319011020A4558E3D5AE13F8758484E3F9628F7464EA098C

(1 row(s) affected)


------------------------------
A123456789

(1 row(s) affected)

加解密的過程,當然可以改成存取資料表的寫法,不過這並不是加解密的重點,讀者可以自行發揮。

結論

資料表內的資料經過上述的操作,可以進行加解密,但是選擇解密欄位的對象,卻要斤斤計較。例如做 foreign key,where 子句的查詢欄位,因為效能的關係,就不適合加密了。

 

TPL 學習日記(1): Start new task

這次要學習 TPL 了. (Task Parallel Library)。當然,還是從最簡單的開始學起。

範例1

static void Main(string[] args)
{
  Task.Factory.StartNew(() => {
    int sum = 0;
    for (int i = 0; i < 100; i++) sum += i;
    Console.WriteLine("總和為{0}",sum);
  });
  Console.WriteLine("完成");
  Console.ReadLine();
}

執行結果

image

這是最簡單的範例了。

Task.Factory.StartNew 起始一個新的 Task 並且開始執行。在大括弧內的為 Task 要執行的程式內容。由於 該 Task 尚未執行完畢,主程式就執行了 Console.WriteLine(“完成”) 故先輸出完成。我們尚未按Enter 時Task 就執行完了,故再輸出 總和為4950。

範例2

這個範例的目的同上一個,只是這次我們想要自行決定 task 何時開始執行。

static void Main(string[] args)
{
  Task task1 = new Task(() =>
  {
    int sum = 0;
    for (int i = 0; i < 100; i++) sum += i;
    Console.WriteLine("總和為{0}", sum);
  });
  task1.Start();
  Console.WriteLine("完成");
  Console.ReadLine();
}

範例3

上述的範例都是在 task 內輸出結果。這次我們想要由  task 內得到結果,但在主程式內輸出。

static void Main(string[] args)
{
  Task<int> task1 = new Task<int>(() =>
  {
    int sum = 0;
    for (int i = 0; i < 100; i++) sum += i;
    return sum;
  });
  task1.Start();
  Console.WriteLine("總和為{0}", task1.Result);
  Console.ReadLine();
}
Task<int> 的<int> 代表回傳值的型別,其結果由 Task.Result 來讀取。

Share with Facebook