2014年11月4日 星期二

Take a peek at EmberJS - Phase2

前言:
接續上個月(拖好久...)的進度我們繼續把這個簡單的應用給完成吧~
第一階段
第一階段我們只是把最簡單的Model型態完成,但是卻還沒賦予Model跟View互動的機制,這階段就是要完成可以對TodoList的項目進行編輯以及把項目標示為已完成。

本文:
首先我們先完成選取在每個項目前面的勾勾就把該項目給畫上一個橫線表示完成的功能。在這個功能中我們會需要對列表的每個項目進行操作,我們的HTML一開始是長這樣的:
{{#each}}
          <li {{bind-attr class="isCompleted:completed"}}>
            <input type="checkbox" class="toggle">
            <label>{{title}}</label><button class="destroy"></button>
          </li>
        {{/each}}
在這裡我們可以新增一個controller來wrapping每個each裡的項目(Ember裡的controller管理狀態與跟畫面的互動,manage state and decorate data),新的controller內容如下:
Todos.MyTodoListController = Ember.ObjectController.extend({
    // 類似Knockout.js的Computed property,這個屬性會依照相依的屬性值的變化而改變
    // 是否完成
    isItemCompleted: function (key, value) {
        var model = this.get('model');

        if (value === undefined) {
            // Getter, 把目前項目的isCompleted回傳
            return model.get('isCompleted');
        } else {
            // Setter
            model.set('isCompleted', value);
            model.save(); // 更改項目的isCompleted值
            return value;
        }
    }.property('model.isCompleted')

    // 或也可以寫成如下方式:
    // 這個寫法看起來比較明確點
    isItemCompleted: Ember.computed('model.isCompleted', function (key, value) {
        var model = this.get('model');

        if (value === undefined) {
            // Getter, 把目前項目的isCompleted回傳
            return model.get('isCompleted');
        } else {
            model.set('isCompleted', value);
            model.save(); // 更改項目的isCompleted值
            return value;
        }
    })

})
這個isItemCompleted屬性後面呼叫了property方法,這相當於宣告這個屬性是一個computed property。
然後在前端的html我們改成這樣:
<ul id="todo-list">
   {{#each itemController="MyTodoList"}}
      <li {{bind-attr class="isCompleted:completed"}}>
         {{input type="checkbox" checked=isCompleted class="toggle"}}
            <label>{{title}}</label><button class="destroy"></button>
      </li>
   {{/each}}
</ul>
這樣在我們點選勾勾時,就可以把該項目劃上一個橫線表示完成了~!
接下來我們還要為這個Todo List增添一個功能,讓已經寫下來的項目可以被修改,同樣的這個功能也是針對each裡面的每個項目的操作,因此我們要把它放在MyTodoListController.js這個檔案裡面。首先我們在Index.schtml改寫成這樣:
<ul id="todo-list">
            {{#each itemController="MyTodoList"}}
            // 這邊分別用isCompleted與isEditing兩個Computed property來切換樣式
            <li {{bind-attr class="isCompleted:completed isEditing:editing" }}>
                // Handlebars有if else可以用,這點比用knockout方便很多
                {{#if isEditing}}
                <input class="edit"/>
                {{else}}
                {{input type="checkbox" checked=isItemCompleted class="toggle"}}
                // 這邊就是綁定事件的地方,讓這個元素(Label)的doubleClick事件與ditTodo方法綁在一起
                <label {{action "editTodo" on="doubleClick" }}>{{title}}</label><button class="destroy"></button>
                {{/if}}
            </li>
            {{/each}}
        </ul>
頁面改成這樣後,我們就要去MyTodoListController.js新增相對應的Code來實現這個功能:
Todos.MyTodoListController = Ember.ObjectController.extend({
    // action屬性裡面的方法(其實也就是屬性)代表與外界互動的方法
    actions: {
        editTodo: function () {
            this.set('isEditing', true);
        }
    },
    // 這個屬性是用來判斷項目是否正處於編輯狀態
    isEditing: false,

    isItemCompleted: function (key, value) {
        var model = this.get('model');

        if (value === undefined) {
            // Getter, 把目前項目的isCompleted回傳
            return model.get('isCompleted');
        } else {
            // Setter
            model.set('isCompleted', value);
            model.save(); // 更改項目的isCompleted值
            return value;
        }
    }.property('model.isCompleted')
})
完成這些Code之後目前Todo的項目就可以點擊兩下後進入編輯模式,但顯然還不完整。
我們需要讓編輯後的Title可以被儲存,並且在清空Title時可以把該筆記錄刪除,為了達到這個目的我們的頁面繼續修改~
<li {{bind-attr class="isCompleted:completed isEditing:editing" }}>
   {{#if isEditing}}
   // 我這邊的寫法跟官方的Toturial不太一樣,官方是用另一個Handlebars來處理這個View,我這邊是直接使用原本的Handlebars來做
   {{input class="edit" value=title focus-out="acceptChanges" insert-newline="acceptChanges"}}
   {{else}}
   {{input type="checkbox" checked=isItemCompleted class="toggle"}}
   <label {{action "editTodo" on="doubleClick" }}>{{title}}</label><button {{action "removeTodo"}} class="destroy"></button>
   {{/if}}
</li>
而在MyTodoListController中新增相對應的方法來處理這個View
Todos.MyTodoListController = Ember.ObjectController.extend({
   actions: {
        editTodo: function () {
            this.set('isEditing', true);
        },
        acceptChanges: function () {
            this.set('isEditing', false);

            if (Ember.isEmpty(this.get('model.title'))) {
                // 呼叫removeTodo方法
                this.send('removeTodo');
            } else {
                this.get('model').save();
            }
        },
        removeTodo: function () {
            // 若Todo的Title為空則把該記錄移除
            var todo = this.get('model');
            // 這個方法是相對於createRecord方法
            todo.destroyRecord();
            // 把更新後的model儲存
            todo.save();
        }
    },
   .................
}
完成以上Code後我們的Todo List就擁有最基本的新增修改刪除功能了~!

而在這個範例的最後一個篇章會介紹如何讓這個Todo List的資料來源變成外部取得~

ps.在看這個部分的教學文件並實做時在controller這個角色上面卡了很久,不是很清楚在Emberjs裡面Controller扮演的角色是什麼,哪裡改新增controller處理邏輯哪裡可以直接用,因此若願意的話歡迎在下方留言給予指教~

2014年10月1日 星期三

Take a peek at EmberJS - Phase1

前言:
時間回到三年前(2011年)...那個時候做Web Application在前端Framework的選擇不像現在多的眼花繚亂,基本上套上jQuery與jQuery UI還有一票jQuery的徒子徒孫就是火力強大的工具了,那個時候也是我第一次使用完整的前端 framework solution。時間沒走多久前端的需求爆炸性的成長,只用jQeury來操作DOM元素已經不敷需求,另外用jQuery來做SPA要手工的地方太多也挺吃力的,所以很多SPA的框架就誕生了。目前最多人擁護的莫過於AngularJS,而後起之秀EmberJS雖然也是有不少擁護者但就台灣的環境來說AngularJS已獲得壓倒性的勝利,一方面是因為AngularJS發展穩定,又有Google這個大公司當靠山,所以網路上的資源比任何SPA Framework都還要多。但我就是比較搞怪一點所以就選EmberJS來看看了XD。目前EmberJS是Open Source的專案host在GitHub上
EmberJS => https://github.com/emberjs/ember.js
EmberJS相對於AngularJS變化比較大所以需要多關注GitHub上面的Issues或是commits,有些時候官網上面的教學文件還落後最新的Stable版本一段差距。

本文:
這篇文章會以官方的Todo教學為實現的功能,功能會到讓這個Todo應用可以塞入新的Todo事項,當中會加上一些我對它的理解。這個EmberJS Application會用到幾個libary,jQuery + Handlebars +Ember + Ember-Data
這其中handlebars至個libary還滿特別的,有用過AngularJS的人一定對他的功能不陌生,它其實就是一個讓我們可以方便建構語意式模版的工具,用法像是以下這樣:
<div class="entry">
  <h1>{{title}}</h1>
  <div class="body">
    {{body}}
  </div>
</div>
這個其實很多框架都有類似的功能(KnockoutJS也有),就看習慣哪一個表達方式的問題了。
另外一個可以抓出來說一下的就是Ember-data這個libary了,其實Emberjs是可以不用這個東西也可以run的很好的,不過這個libary有著Ember遠大的野心,用我的破英文來解讀是這樣的啦~XD
"Ember Data is designed to be agnostic to the underlying persistence mechanism, so it works just as well with JSON APIs over HTTP as it does with streaming WebSockets or local IndexedDB storage.",
"In particular, Ember Data uses Promises/A+-compatible promises from the ground up to manage loading and saving records, so integrating with other JavaScript APIs is easy."這兩段話出自他本身的說明文件,看起來這個libary會是未來Ember收到資料時的預設處理工具吧?

這邊還是用我習慣的ASP.NET MVC來實做,首先依然是開啟一個空白的專案,然後把上述四個會用到的四個Libary包含進去。若有裝ASP.NET Optimization Framework的話可以這樣做:
新增一個ScriptBundleConfig.cs檔於App_Start資料夾中,並於Global.asax.cs中呼叫他的靜態方法
using System.Web.Optimization;

    public class ScriptBundleConfig
    {
        public static void Register(BundleCollection bundleCollection)
        {
            bundleCollection.Add(new ScriptBundle("~/bundle/all").Include(
                "~/Scripts/jquery-2.1.1.js", 
                "~/Scripts/handlebars-v1.3.0.js", 
                "~/Scripts/ember.prod.js",
                "~/Scripts/ember-data.prod.js"));
            // 這個範例的EmberJS版本為1.7
            BundleTable.EnableOptimizations = true;
        }
    }
// addictional lines truncated for brevity

   ScriptBundleConfig.Register(BundleTable.Bundles);

// addictional lines truncated for brevity

而在母版(Views/Shared/_Layout)的.cshtml地方加上這段,這樣就可以把我們所需要的Libary都載入了
@System.Web.Optimization.Scripts.Render("~/bundle/all")

EmberJS屬於前端的MVC框架,而其執行也遵照「naming conventions」這個準則,以下我們會由Todo List這個小功能來驗證這個特性。
我們先在Controller中新增一個Action,Controller的內容相當簡單,這個範例也只有在Client端
public class ToDoController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}
要開始一個EmberJS應用首先我們需要一個起始的js檔案
// new 一個 EmberJS 應用程式的 instance
window.Todos = Ember.Application.create();
上面這個檔案就是宣告一個全域變數叫Todos,把這個檔案include進去我們的Index頁面,這個檔案必須是除了基底libary之外第一個要引用的檔案!
接下來就是Index.cshtml上面的頁面要寫什麼東西呢?
@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
// 模版的名稱會影響到由那個Route渲染這個Template,這個例子則是MyTodosRoute會去渲染這個Template
<script type="text/x-handlebars" data-template-name="MyTodos">

    <section id="todoapp">
        <header id="header">
            <h1>todos</h1>
            {{input type="text" id="new-todo" placeholder="What needs to be done?" 
            value=newTitle action="createTodo"}}
        </header>

        <section id="main">
            <ul id="todo-list">
                {{#each}}
                <li {{bind-attr class="isCompleted:completed"}}>
                    <input type="checkbox" class="toggle" />
                        <label>{{title}}</label><button class="destroy"></button>
                </li>
                {{/each}}
            </ul>

            <input type="checkbox" id="toggle-all">
        </section>
</script>

@section JSSection{
    <script src="~/Scripts/application.js"></script>
}
整塊用script包起來的就是handlebar的作用區域他的type就是「text/x-handlebars」,EmberJS其實本身是可以不需要用這個libary就可以運作的,不過官方推薦搭配這個libary來讓UI更有表達能力,因為Handlebars Template本身就是for UI的Html-like的DSL(特定領域語言),看起來是挺直覺的。
接著就是新增一個route.js檔案來告訴Ember.js要怎麼去render畫面以及這個畫面所需要用到的Model是哪一個,另外根據nameing convention也要有個同名的controller對應到它
Todos.Router.map(function () {
    // Ember會去偵測當Uri為'/'時去render名稱為「todos」的Template
    this.resource('MyTodos', { path: '/' });
});

Todos.MyTodosRoute = Ember.Route.extend({
    //指示這個Route的Model要掛載哪一個Model進來
    model: function () {
        return this.store.find('MyTodo');
    }
});
當建好這隻Route後,我們接著就是要生一個Model出來讓Template render時有東西可以render。follow Ember的naming convention準則,我們的Model也取名叫做MyTodo,不過Model這邊倒是不強制一定要這樣命名
Todos.MyTodo = DS.Model.extend({
    title: DS.attr('string'),
    isCompleted: DS.attr('boolean')
});

// 這個是DS所提供的feature, Fixture data,要啟用這樣的功能我們需要回過頭在application.js裡面做些設定
Todos.MyTodo.FIXTURES = [
 {
     id: 1,
     title: 'Learn Ember.js',
     isCompleted: true
 },
 {
     id: 2,
     title: 'Benjamin Has something to do',
     isCompleted: false
 },
 {
     id: 3,
     title: 'Profit!',
     isCompleted: false
 }
];

//補上以下的程式碼

// 使用 Fixure 資料, 這可以讓DS把資料存取由memory來,這個設定通常是用在開發與測試
// DS Model 是設計用來做為EmberJS的persistence機制
Todos.ApplicationAdapter = DS.FixtureAdapter.extend();
到目前為止,我們已經把頁面上面的靜態資料都實做完成了,若現在run這個web site的話就可以看到browser原先都看不懂的東西現在都已經被我們所輸入的靜態資料填滿,但若想要跟這個Todo互動的話我們還缺少controller的幫忙。
依據Ember的naming convention我們新增一個MyTodosController檔案
Todos.MyTodosController = Ember.ArrayController.extend({
    // 行為(方法)必須要包在actions這個物件中,這也是Ember的一個naming conventioon
    actions: {
        createTodo: function () {
            // 由newTitle取得Title的值
            // 取Template中元素的Value值
            // 這裡的get方法是由Ember提供
            var title = this.get('newTitle');
            if (!title) { return false; }
            if (!title.trim()) { return; }

            // 產生一個新的Todo Model
            var todo = this.store.createRecord('MyTodo', {
                title: title,
                isCompleted: false
            });

            // 把newTitle的值清空
            this.set('newTitle', '');

            // Save the new model
            todo.save();
        }
    }
});
好了~現在再run一次web site,我們現在就可以新增Todo清單了!不過刪除的行為我們還沒有實做,所以目前也只能新增 XD
不過寫到這邊也符合目前第一階段的目標了。
EmberJS雖然不像AngularJS受大眾寵愛,但也是一個備受期待的MVC框架,不過這個框架的教學資源與學習文件確實不像AngularJS這麼豐富,我想這大概也是目前環境還很少人投入這個框架的原因之一吧~
不過學習東西嘛~快樂最重要 XD

之後應該還會有後續的階段,除了按照官方的文件寫之外也會加上實做上的一些心得

Reference:
http://ember.vicramon.com/chapters
http://emberjs.com/guides/getting-started/
http://www.emberjs.cn/guides/getting-started/ => 簡體中文版,這個版本會稍舊一點,不過若真的是英文苦手可以看一下這個版本

2013年10月20日 星期日

DI patterns (DI 模式)

前言:
這篇文章主要講的是DI常見的方式,共四種模式這邊首先介紹最為常見的建構子注入。其實DI這個技巧我們常常都會使用到,甚至就算你不知道也會用它,雖然是什麼相當偉大的技巧但是卻是很實用也很重要的技巧。DI(Dependency Injection, 依賴注入),講的是針對抽象來撰寫程式而非實體。底下會有範例來展示這項技巧。另外,這篇文章主要是依照Dependency Injection in .NET這本書的內容來撰寫的,但有部分觀點屬於個人。

內文:
Constructor Injection (建構子注入):
這個模式應用很廣泛也是很容易實現的一個DI模式,在使用這個模式時會需要類別提供一個公開(public)並且需要一個實體作為參數的的建構子,通常的情況下這個類別只會有這麼一個建構子的存在,另外,請儘量避免overload建構子,保持只有單一一個建構子的情況。
//實現 Constructor Injection 的類別
public class MyFirstDIClass
{
   private readonly AnimalRepository repository;// DI 欄位是readonly可以在類別初始化後避免被修改。
   public MyFirstDIClass(AnimalRepository repository)
   {
      if(repository==null)// 確保注入的實體是存在的
      {
         throw new ArgumentNullException("repository");
      }
      this.repository=repository;// 注入依賴的實體
   }
}
通常所要求輸入的參數會是一個抽象類別或是一個介面(abstract class or interface),之所以使用這兩種Type是因為建構子可以不用去管真正實現的類別究竟為何,只要這個類別繼承或是實作指定的Type即可,因為對於這個注入的實體也只會使用到指定Type所提供的公開方法而已。
在使用這個方法時有個需要注意的地方,要儘量避免在建構子中撰寫其他的邏輯。建構子裡面最好就是只有建構這個類別時所需要的邏輯即可,所有初始化這個類別以外的邏輯都不應該出現在這裡,簡單來講就是維持建構子的單純。
一般來說 Constructor Injection 這個方式的優缺點為
優點:
1)能確保依賴確實的注入到類別中
2)容易實現
缺點:
1)在一些架構下很難去實現(ex. 像是在ASP.NET Web Form 這個 Framework 中就很難去實現這個依賴注入的方法,有些架構就是天生不適合使用DI)

小結:
Constructor Injection這個方法在面對依賴注入是必要的情況下很好使用。而從建構子注入依賴也可以滿足大多數的使用情況,儘量使用這個方法做為首選依賴注入的方法。

Method Injection(方法注入)
---

當遇到Dependency會依據每一次的呼叫而有所不同時,就很適合使用這種依賴注入的方式。

public class UserInfoService
{
public IEnumerable<AdvertisementEntity> ShowAdvertisement(IUserGradeService userGradeService)
{
//// 依據傳入的會員等級不同,顯示不同的優惠
return userGradeService.GetAdvertisements();
}
}
有兩種比較常見的使用時機

* 像是 add-in 一類,主程式提供統一介面注入,實作該介面的實體就會被主程式使用。

* 像是一個領域模型(Domain Entity)(常見於[領域驅動開發](https://en.wikipedia.org/wiki/Domain-driven_design)) 混合資料與商業邏輯。

優點:

1. 允許呼叫者依據情境提供相應的實體
2. 讓依賴得以注入以資料為中心的物件,而非使用組合(composition)的方式

缺點:

1. 使用限制比較多
2. 會讓依賴暴露於 API 層級,向外部的呼叫者透露較多訊息

--全文尚未完結--

2013年4月19日 星期五

using Strong Name Tool to encryption message

前言:

一般來說在.net中實現加密時我們會使用RSACryptoServiceProvider這個類別來實現產生公私鑰進行加解密的行為,而且一個簡單的ToXMLString()方法就能把公鑰(或甚至是私鑰,但實務上很不建議)給吐出來。不過除了使用這個類別來產生鑰匙外我們其實還可以利用強式名稱工具來幫我們產生鑰匙,好處是不但可以作為加解密的鑰匙來源,沒有金鑰容器名稱以及存取層級(User Layer, Machine Layer)的管理issue簡單用,還可以順便用它來簽署組件,一檔多用資源徹底利用。以下就是利用強式名稱工具來實作的方式。

內文:

強式名稱工具(Strong Name Tool)是安裝Visual Studio之後就會附上的開發用工具我們可以開啟"Visual Studio x64 Win64命令提示字元",鍵入"sn.exe /?"來觀看可以使用的參數,這裡我們會用到的就是最簡單的 -k 參數而且就是使用default值。我們就利用這個工具產生一組全新的snk檔案放在D槽吧~




我們已經可以在D槽的跟目錄下面看到產生好的Strong Name Key File了~

要注意的是使用預設的話key size長度就是1024 bit這長度與能拿來加密的文字長度會有直接關係使用預設長度的話一次能夠加密的文字長度為128bits,不過要小心在.net中會有padding的issue造成實際上可以加密的文字長度會更短(註一),這個問題的解決方式我們會在稍後的範例中看到。

準備好之後我們就來開啟一個專案試試看這snk檔要怎麼幫我們加密資料吧~

我們這邊就使用最單純的Console專案來進行測試即可~

ps.這邊記得把D槽根目錄中的snk檔案放在專案的bin資料夾底下。

ps2.為了保持目的單純不小心類別就被搞大了...

using System;
using System.Collections;
using System.IO;
using System.Security.Cryptography;

class Program
{
   private static string _strongNameKeyFile = "Test.snk";
   public static string GetPublicKey
   {
      get;
      private set;
   }
   private static string GetPrivateKey
   {
      get;
      set;
   }

   public static string Data(string plaintext)
   {
      using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
      {
         rsa.ImportCspBlob(File.ReadAllBytes(_strongNameKeyFile));
         GetPublicKey = rsa.ToXmlString(false);
         GetPrivateKey = rsa.ToXMLString(true);

         // base-64 encoded.
         return Convert.ToBase64String(EncryptData(plaintext));
      }
   }

   // 若資料量很大的話(資料量小以預設來說就是資料量小於117 bits,那可以直接呼叫Encrypt()來進行加密及可)
   // 就需要分段加密不然一定會炸掉,吐一個Bad Length的例外訊息。
   private static byte[] EncryptData(string rawdata)
   {
      // 憑證的金鑰預設就是1024 bit
      // buffer扣去11 bits是為了padding使用,請參考註一的連結。
      const int encryptionBufferSize = (1024 / 8) - 11;
      const int DecryptionBufferSize = 1024 / 8;

      RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
      // 這邊利用公鑰來加密,因此解密的一方需要取得密鑰才能解密,這樣可以確保有私鑰的人才有辦法看到資訊。
      rsa.FromXmlString(GetPublicKey);

      // 進行加密時都需要把字串轉成byte陣列來操作,這邊需要注意轉換的問題!
      byte[] dataEncoded = System.Text.Encoding.UTF8.GetBytes(rawdata);
      using (MemoryStream ms = new MemoryStream())
      {
         byte[] buffer;
         int pos = 0;
         int copyLength = encryptionBufferSize;
         while (true)
         {
            if (pos + copyLength > dataEncoded.Length)
               copyLength = dataEncoded.Length - pos;
            buffer = new byte[copyLength];
            Array.Copy(dataEncoded, pos, buffer, 0, copyLength);
            pos += copyLength;

            // 這邊不使用OEAP padding,使用 PKCS#1 v1.5 padding(填補法的差異請參考註一)
            // 另外也要注意雖然加密的資料需要扣除 padding 量,但是寫入的長度依然不變。
            ms.Write(rsa.Encrypt(buffer, false), 0, DecryptionBufferSize);
            Array.Clear(buffer, 0, copyLength);
            if (pos >= dataEncoded.Length)
               break;
         }
         return ms.ToArray();
      }
   }

   public static string GetPlainDataBack(string encryptdata)
   {
      string result = string.Empty;
      using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
      {
         const int decryptionBufferSize = 1024 / 8;

         // 使用公鑰加密就必須用私鑰解密。
         rsa.FromXmlString(GetPrivateKey);
         byte[] encryptdataarray = Convert.FromBase64String(encryptdata);
         using (MemoryStream ms = new MemoryStream(encryptdataarray.Length))
         {
            byte[] buffer = new byte[decryptionBufferSize];
            int pos = 0;
            int copyLength = buffer.Length;
            while (true)
            {
               Array.Copy(encryptdataarray, pos, buffer, 0, copyLength);
               pos += copyLength;
               byte[] resp = rsa.Decrypt(buffer, false);
               ms.Write(resp, 0, resp.Length);
               Array.Clear(resp, 0, resp.Length);
               Array.Clear(buffer, 0, buffer.Length);

               if (pos >= encryptdataarray.Length)
                  break;
            }
            result = System.Text.Encoding.UTF8.GetString((ms.ToArray()));
         }
      }
      return result;
   }

   static void Main(string[] args)
   {
      var plaintext = "This is a plain text!";
      var result = Data(plaintext);
      Console.WriteLine("Orignal Text:{0}", plaintext);
      Console.WriteLine("After Encryption:{0}", result);

      result = GetPlainDataBack(result);
      Console.WriteLine("After Decryption:{0}",result);
      Console.Read();
   }
}


結語:
以上就是使用強式名稱工具來達成資訊加密、解密的需求,我們可以發現若對方要可以解讀這個密文那snk檔是對方勢必得取得的檔案,因此該檔案的存取是一定要控管的,這是不方便之處。另外其實實務上除了加解密之外還會順手做數位簽章....這就留到下次吧...


參考資料:

註一:http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsacryptoserviceprovider.encrypt.aspx

參考1:http://www.dotblogs.com.tw/huanlin/archive/2008/04/23/3309.aspx

參考2:http://msdn.microsoft.com/zh-tw/library/system.security.cryptography.rsacryptoserviceprovider%28v=vs.110%29.aspx

參考3:http://msdn.microsoft.com/zh-tw/library/k5b5tt23%28v=vs.110%29.aspx

參考4:http://en.wikipedia.org/wiki/RSA_%28algorithm%29

2012年11月28日 星期三

ASP.NET Page-Life-Cycle

前言:
開發 ASP.NET Web Form 的工程師對於網頁生命週期(Page Life Cycle)一定要熟記在心中,也要知道每個循環中的會發生的個事件的主要目的,才不會在需求要修改或是出現Bug時不知道該往哪裡找問題。不過,講是這樣講其實ASP.NET的網頁生命週期需要注意的細節是滿多的,而且一個週期內會發生的事件可能比你想像中的還要多。

內文:
一般來說網頁生命週期會經過 "網頁要求"=>"開始"=>"初始化"=>"載入"=>"回傳事件處理"=>"呈現"=>"卸載"

階段 說明
網頁要求 在網頁生命週期開始前會發生網頁要求。當使用者要求網頁時,ASP.NET 會判斷是否需要剖析和編譯網頁 (因此開始網頁的生命週期),或是可以在不執行網頁的情況下,傳送網頁的快取版本做為回應。
開始 在開始階段,會先設定網頁屬性,如 Request 與 Response。在這個階段中,網頁也會判斷要求是否為回傳或是新的要求,然後設定 IsPostBack 屬性。網頁同時也會設定 UICulture 屬性。
初始化 在網頁初始化期間,可以使用網頁上的控制項,並且設定每個控制項的 UniqueID 屬性。如果適用,也會將主版頁面與佈景主題套用到網頁。如果目前的要求是回傳,則尚未載入回傳資料,並且控制項屬性值並未還原至檢視狀態提供的值。
載入 在載入期間,如果目前的要求是回傳,就會使用從檢視狀態和控制項狀態復原的資訊載入控制項屬性。
回傳事件處理 如果是回傳要求,會先呼叫控制項事件處理常式,然後才會呼叫所有驗證程式控制項的 Validate 方法,以設定個別驗證程式控制項與網頁的 IsValid 屬性。
呈現 在呈現前,會儲存網頁和所有控制項的檢視狀態。在呈現階段,網頁會呼叫每個控制項的 Render 方法,藉此提供文字寫入器將其輸出寫入網頁之 Response 屬性的 OutputStream 物件。
卸載 完整呈現網頁之後,會引發 Unload 事件,然後傳送至用戶端予以捨棄。此時將會執行網頁屬性 (如 Response 與 Request) 的卸載及清除工作。
資料來源 表一:MSDN 一般網頁生命週期階段 Page Life Cycle

而在這樣的生命週期階段中會有數十個事件依序發生,而不論是作為網頁容器的 Page 類別或是使用者控制項(User Control)或是伺服器控制項(Server Constrol)幾乎都有相對應的事件。
Page 頁面是每個 Web Form 都會繼承的類別,依據需求上面也可以放置自行撰寫的使用者控制項(User Control),或是裡面也可以直接擺上 Server Control,因此我們可以把這個類別是為一個容器,裡面可以擺放很多顯示用的控制項,而這個控制項裡會有很多事件會在一個 request 進來後依序引發,以下就是網頁生命週期的「生命週期事件」:

OnPreInit:
這個事件會在初始化之前就引發,通常會拿來檢查 PostBack 屬性、建立或是重建動態控制項、動態設定主版頁面、動態設定 Theme 屬性、讀取貨設定設定黨屬性值。

OnInit:
初始化所有控制項並套用任何面板設定後引發。個別控制項的 OnInit 事件會在容器前執行。可以在這個時候讀取或是初始化控制項的屬性。

OnInitComplete:
發生在網頁初始化結束時。

OnPreLoad:
這邊會於載入本身以及所包含的控制項的 ViewState,以及處理 request 執行個體中所附的回傳資料後引發。
若有自定驗證的話通常我會在這邊來進行,就不用載入太多東西。

OnLoad:
這邊容器會呼叫本身的 OnLoad 方法,然後以遞迴的方式對每一個控制項執行相同的動作。(因此控制項的 Load 會比較晚執行)通常我會於此設定控制項中的屬性,以及重建資料庫連線。

OnLoadComplete:
於 OnLoad 事件處理完畢後引發。

OnPreRender:
容器會呼叫自己的 PreRender 事件,然後也是以遞迴的方式對每一個控制項執行相同的動作。這個事件可以在呈現(Render)階段前對控制項的屬性做最後的變更。
通常我會在這邊把需要的 javascript 或 CSS 給引入或是對顯示邏輯進行修改。

OnSaveStateComplete:
這個事件是在儲存網頁所有控制項的 ViewState 後引發,因為是在呼叫Render方法之前,因此所做的任何變更依舊會影響控制項的呈現,但是因為 ViewState 已經儲存了,所以下一次的 PostBack 並不會擷取這些變更!
(因此PreRender才會被視為呈現前最後一個可以變更控制項的事件,因為此時變更後會被儲存下來!)

Render:
這不是個事件...在呈現這個階段每個控制項都會呼叫自己的Render方法來產生各自的HTML Tag。

OnUnload:
會先由控制項引發,控制項都引發完畢後才是容器引發。通常在這邊會對頁面使用到的特定資源進行釋放,像是與資料庫的連線或是關閉開啟的檔案。

以上資料來源 MSDN 生命週期事件 + 我的胡說八道

當然上面這些說明的文件看完之後還是需要手動來驗證一下才比較有感覺!
就讓我們來驗證一下吧!

驗證:

實驗一:
1)首先我們先新增一個空白ASP.NET Web Form專案,並且新增項目選擇Web Form,命名為Index.aspx。

2)打開 Package Manager Console 安裝NLog Install-Package NLog Install-Package NLog.Config
這邊安裝NLog是為了用來記錄每個事件的觸發順序使用。

3)修改NLog.config
<targets>
    <target xsi:type="File" name="test" fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} ${message}"/>
  </targets>

  <rules>
    <logger name="test" minlevel="Trace" writeTo="test" />
  </rules>

4)在Index.aspx.cs中寫下日誌
using System;
using NLog;
public partial class Index : System.Web.UI.Page
{
    private Logger _logger = LogManager.GetLogger("test");

    protected override void OnPreInit(EventArgs e)
    {
        _logger.Trace("OnPreInit event execute!");
        base.OnPreInit(e);
    }

    protected override void OnInit(EventArgs e)
    {
        _logger.Trace("OnInit event execute!");
        base.OnInit(e);
    }

    protected override void OnInitComplete(EventArgs e)
    {
        _logger.Trace("OnInitComplete event execute!");
        base.OnInitComplete(e);
    }

    protected override void OnPreLoad(EventArgs e)
    {
        _logger.Trace("OnPreLoad event execute!");
        base.OnPreLoad(e);
    }

    protected override void OnLoad(EventArgs e)
    {
        _logger.Trace("OnLoad event execute!");
        base.OnLoad(e);
    }

    protected override void OnLoadComplete(EventArgs e)
    {
        _logger.Trace("OnLoadComplete event execute!");
        base.OnLoadComplete(e);
    }

    protected override void OnPreRender(EventArgs e)
    {
        _logger.Trace("OnPreRender event execute!");
        base.OnPreRender(e);
    }

    protected override void OnPreRenderComplete(EventArgs e)
    {
        _logger.Trace("OnPreRenderComplete event execute!");
        base.OnPreRenderComplete(e);
    }

    protected override void OnSaveStateComplete(EventArgs e)
    {
        _logger.Trace("OnSaveStateComplete event execute!");
        base.OnSaveStateComplete(e);
    }

    protected override void OnUnload(EventArgs e)
    {
        _logger.Trace("OnUnload event execute!");
        base.OnUnload(e);
        _logger.Trace("-------------------");
    }
}

5)按下F5成功執行後直接關起來

6)觀察寫下來的Log檔

這邊我們可以發現頁面的事件順序大致上就是先初始化、載入、呈現最後就是卸載。
但若我們在頁面上面多加一個使用者控制項(User Constrol)呢?

實驗二:

1)在原本的專案中新增一個ascx檔,並且附上一個 Server Control。

2)新增如下內容
<asp:TextBox ID="ucTextBox1" runat="server" 
             OnInit="ucTextBox1_Init"
             OnLoad="ucTextBox1_Load"
             OnPreRender="ucTextBox1_PreRender"
             OnUnload="ucTextBox1_Unload"></asp:TextBox>
public partial class UserControl1 : System.Web.UI.UserControl
{
    private Logger _logger = LogManager.GetLogger("test");

    protected override void OnInit(EventArgs e)
    {
        _logger.Trace("User Control OnInit event execute!");
        base.OnInit(e);
    }

    protected override void OnLoad(EventArgs e)
    {
        _logger.Trace("User Control OnLoad event execute!");
        base.OnLoad(e);
    }

    protected override void OnPreRender(EventArgs e)
    {
        _logger.Trace("User Constrol OnPreRender event execute!");
        base.OnPreRender(e);
    }

    protected override void OnUnload(EventArgs e)
    {
        _logger.Trace("User Constrol OnUnload event execute!");
        base.OnUnload(e);
    }

    protected override void OnDataBinding(EventArgs e)
    {
        _logger.Trace("User Control OnDataBinding event execute!");
        base.OnDataBinding(e);
    }

    #region 伺服器控制項的事件
    protected void ucTextBox1_Unload(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Unload event execute!");
    }

    protected void ucTextBox1_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 PreRender event execute!");
    }

    protected void ucTextBox1_Load(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Load event execute!");
    }

    protected void ucTextBox1_Init(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Init event execute!");
    } 
    #endregion
}

3)按下F5成功執行後直接關起來

4)觀察日誌檔

這邊我們可以觀察到初始化時會是控制項的初始化先進行,然後才是 User Control 的初始化,最後是 Page 的初始化(容器的初始化會比較晚)。
而在載入的時候會先由 Page 先行載入,然後是 User Control 的載入,最後才是伺服器控制項的載入(容器會先行載入)。
而在卸載的時候則是控制項先卸載,然後是 User Constrol,最後才是 Page(容器的卸載的比較晚)。

實驗三:
現在我們在 Page 頁面也新增兩個 button 來觀察看看各控制項的事件引發順序。

1)我們分別在使用者控制項(User Control)的前後增加一個伺服器控制項(Server Control)。現在Index.aspx就會變成下面這樣:
<form id="form1" runat="server">
    <div>
        <asp:Button ID="button1" runat="server" Text="Firstbutton" 
                    OnInit="button1_Init"
                    OnLoad="button1_Load"
                    OnPreRender="button1_PreRender"
                    OnUnload="button1_Unload" />
        <uc:UserControl1 ID="uc1" runat="server" Visible="true" />
        <asp:Button ID="button2" runat="server" Text="Secondbutton" 
                    OnInit="button2_Init"
                    OnLoad="button2_Load"
                    OnPreRender="button2_PreRender"
                    OnUnload="button2_Unload" />
    </div>
</form>

2)在兩個 button 的事件中寫下日誌...方法就跟上面的寫法一樣...
#region 伺服器控制項的事件
    protected void button1_Init(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Init event execute!");
    }

    protected void button1_Load(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Load event execute!");
    }

    protected void button1_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton PreRender event execute!");
    }

    protected void button1_Unload(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Unload event execute!");
    }
    protected void button2_Init(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Init event execute!");
    }

    protected void button2_Load(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Load event execute!");
    }

    protected void button2_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton PreRender event execute!");
    }

    protected void button2_Unload(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Unload event execute!");
    } 
#endregion

3)按下F5之後直接關掉觀察日誌檔...

初始化>>
這邊我們可以觀察到初始化的時候會依照控制項出現的順序依序初始化,因此第一個 button 會先被初始化,然後是使用者控制項中的 textbox 初始化,接下來就是使用者控制項的初始化!接下來才是第二個 button 的初始化,最後才是 Page 頁面(基底容器) 的初始化。(個別控制項的初始化會在它的容器前執行)

載入>>
載入時依據MSDN官方說法是容器會先控制項載入,我們來驗證吧~!這邊我們可以看到 Page 頁面的載入確實先觸發了!然後是第一個 button 的載入,接下來是使用者控制項的載入,接著使用者控制項中的 textbox 載入,最後是第二的 button 的載入。載入就一這樣的順序結束了!

呈現(Render)>>
這邊很特別,我們可以看到這個 Render 的順序根本就是頁面從上到下的順序依序呼叫 PreRender,不過也難怪啦~畢竟這邊是要產生 HTML Tag 是最後要呈現在瀏覽器的樣子,會有這樣的行為其實不意外,要是順序不是這樣最後呈現可能也會怪怪的。

卸載>>
卸載這邊則會讓控制項先行卸載,最後才是容器卸載。所以這邊第一個 button 就會先執行卸載,接下來是使用者控制項中的 textbox 卸載,接著是使用者控制項,然後是第二個 button,這些控制項都卸載後最後才是 Page 頁面的卸載。

結語:
在撰寫 ASP.NET Web Form 時了解網頁生命週期是不可或缺的知識,他可以幫助你理解 Web Form 架構時的一些眉角,能夠在正確的時間點做正確的事情。雖然現在在強大的 javascript library 或 framework 的幫助下,已經讓網頁的使用者體驗愈趨近 desktop 導致 Web Form 的式微,但是偶而還是會有需要使用到的時候,到時候就會需要了解這方面的知識了。

參考資料:
MSDN
ASP.NET Page Life Cycle Overview
dotblog
In 91 [ASP.NET]Page Lify Cycle整理
m@rcus學習筆記 [.NET]ASP.NET網頁生命週期Page Life-Cycle