2016年12月5日 星期一

Autofac memory leak experiment

前言:
現在有很多系統除非太小,不然應該都會使用第三方開發的DI容器,來實現IoC。但是使用DI容器時免不了要對於物件的生命週期有一定的了解,不然到時候出現相關的問題就手忙腳亂了。因此這一篇文章就記錄了我使用Autofac時,遇到的memory leak的問題,並把所做的實驗記錄下來。
實驗的情境主要是run在console環境中,模擬類似windows service的情境,所以web的asp.net環境並不在此實驗的考慮範圍內。
這篇文章使用的原始碼連結如下:
GitHub repo url: https://github.com/RiceBen/AutofacExperiment

內文:
物件的生命週期與最初註冊物件的依賴是怎樣的方式,預設是InstancePerDependency這個方法,它會確保你每次呼叫Resolve方法時可以取得新的物件,並不會與其他呼叫端使用同一份實體。這個特性在大多數的時候都會符合需求。
因此在注入時我們先這樣寫:
ps.快照時間基本上都是10秒30秒各拍一次,執行超過50秒就結束實驗。

實驗一:
public class DAModules : Autofac.Module
{
  protected override void Load(ContainerBuilder builder)
  {
    var assembly = Assembly.Load("NineYi.ERP.DA.ERPDB");
    builder.RegisterType<deamonresourcerepository>()
                    .As<ideamonresourcerepository>()
                    .InstancePerDependency();
  }
}
接下來就是我們這次的實驗目標的寫法:
介面:
/// 
    /// Iinterface of deamon resource repository
    /// 
    public interface IDeamonResourceRepository: IDisposable
    {
        /// 
        /// Eat resource
        /// 
        void ResourceMonster();
    }
實作:
/// 
    /// Deamon resource repository.
    /// 
    /// <seealso cref="NineYi.ERP.DA.ERPDB.Deamon.IDeamonResourceRepository" />
    /// <seealso cref="System.IDisposable" />
    public class DeamonResourceRepository : IDeamonResourceRepository
    {
        /// 
        /// Gets the name.
        /// 
        /// 
        /// The name.
        /// 
        public string Name { get { return typeof(DeamonResourceRepository).FullName; } }

        /// 
        /// 序號 (為了輔助驗證取得的實體)
        /// 
        public int Number { get; set; }

        /// 
        /// 執行與釋放 (Free)、釋放 (Release) 或重設 Unmanaged 資源相關聯之應用程式定義的工作。
        /// 
        public void Dispose()
        {
            Console.WriteLine(string.Format("{0} dispose!", this.Name));
        }

        /// 
        /// Eat resource
        /// 
        public void ResourceMonster()
        {
            this.Number += 1;

            Console.WriteLine(string.Format("DeamonResource Number:{0}", this.Number));
        }
    }
接下來我們在呼叫端這樣寫:
static void Main(string[] args)
  {
    var builder = new ContainerBuilder();
    builder.RegisterModule<NineYi.ERP.DA.ERPDB.Modules.DAModules>();

    using (var container = builder.Build())
    {
      MemoryLeakMethod02(container);
    }
  }

  /// 
  /// 容易不自覺產生memory的語法
  /// 
  /// The container./// 
  /// 重點是使用InstancePerDependency作為物件的生命週期,這會導致每次都會產生一個完全新的實體出來,就算有呼叫dispose,但是autofac會握有一份參考,導致這份實體並不會被GC真正回收。
  /// 
  static void MemoryLeakMethod02(IContainer container)
  {
    using (var lifetimescope = container.BeginLifetimeScope())
    {
      while (1 == 1)
      {
        using (var resource = lifetimescope.Resolve<NineYi.ERP.DA.ERPDB.Deamon.IDeamonResourceRepository>())
        {
          resource.ResourceMonster();
          System.Threading.Thread.Sleep(500);
        }
      }
    }
  }
接下來我們開啟Visual Studio的 Analyze -> Performance Profiler -> 勾選memory usage
就可以開始觀察記憶體的使用了。

Profiler畫面(執行50+秒):

執行畫面:
從這個執行畫面中我們可以看到一個現象,該實體的數字永遠都是回1,表示每次取得的實體都是與上一個不同的實體。

實驗結果一:雖然有呼叫dispose但是還是可以看到記憶體不斷飆升(兩份快照相比後者比前者多了20以上的物件),因此可以預期的是不斷執行後一定會發生OOM的例外訊息


實驗二:
我們改用single instance看看是否真的只會有那麼一個實體被生成出來
protected override void Load(ContainerBuilder builder)
{
  builder.RegisterType<deamonresourcerepository>()
                  .As<ideamonresourcerepository>()
                  .SingleInstance(); 
}

呼叫端則這樣寫:
static void Main(string[] args)
  {
    var builder = new ContainerBuilder();
    builder.RegisterModule<NineYi.ERP.DA.ERPDB.Modules.DAModules>();

    using (var container = builder.Build())
    {
      MemoryLeakMethod02(container);
    }
  }

  /// 
  /// 與實驗一是一樣的寫法,只有物件的生命中期使用方式不同
  /// 
  /// The container./// 
  /// 重點是使用SingleInstance作為物件的依賴方式,這會讓每次叫用的物件只會生成一份,所以看起來就沒有memory leak的問題發生。
  /// 
  static void MemoryLeakMethod02(IContainer container)
  {
    using (var lifetimescope = container.BeginLifetimeScope())
    {
      while (1 == 1)
      {
        using (var resource = lifetimescope.Resolve<NineYi.ERP.DA.ERPDB.Deamon.IDeamonResourceRepository>())
        {
          resource.ResourceMonster();
          System.Threading.Thread.Sleep(500);
        }
      }
    }
  }

Profiler畫面(執行50+秒):
可以看得到兩份快照並無增加的物件,表示並不會隨著時間增加而讓物件在記憶體中不斷產生。

執行畫面:
從這個畫面中我們可以看出每次Number屬性回應的數字都會累加,表明就算呼叫dispose後,依舊可以取得同樣一份實體。

實驗結果二: 有呼叫dispose,不過也因為autofac握有一份參考,所以應該還是沒有真正釋放物件,不過因為依賴方式使用SingleInstance,所以執行再久並沒有memory leak的問題發生。

實驗三:
這次我們改用InstancePerLifetimeScope這個依賴方式
builder.RegisterType<deamonresourcerepository>()
                  .As<ideamonresourcerepository>()
                  .InstancePerLifetimeScope();
依據官方說明文件:這個依賴方式會讓同一個 lifetimescope 中取得相同類別的物件,會取用同一份實體。不同的 lifetimescope 就算呼叫同一個類別也會使用同一份實體,因此我們可以用同樣的情境來驗證是否正確。
呼叫端我們這樣寫:
static void Main(string[] args)
  {
    var builder = new ContainerBuilder();
    builder.RegisterModule<NineYi.ERP.DA.ERPDB.Modules.DAModules>();

    using (var container = builder.Build())
    {
      MemoryLeakMethod02(container);
    }
  }

  /// 
  /// 與實驗一是一樣的寫法,只有物件的依賴方式不同
  /// 
  /// The container./// 
  /// 重點是使用InstancePerLifetimeScope作為物件的依賴方式,同一個scope下取得的實體將會相同。
  /// 
  static void MemoryLeakMethod02(IContainer container)
  {
    using (var lifetimescope = container.BeginLifetimeScope())
    {
      while (1 == 1)
      {
        using (var resource = lifetimescope.Resolve<NineYi.ERP.DA.ERPDB.Deamon.IDeamonResourceRepository>())
        {
          resource.ResourceMonster();
          System.Threading.Thread.Sleep(500);
        }
      }
    }
  }

Profiler畫面(執行50+秒):

執行畫面:
這邊可以看到Number回應的數字不斷增長,表示就算有呼叫dispose方法,但是取得的實體還是同樣一份。

實驗解果三:有呼叫dispose,但是因為autofac擁有一份參考,所以應該沒有真正釋放次件,但是應為依賴方式為InstancePerLifetimeScope所以並不會產生新的實體出來占用記憶體。


實驗四:
我們回到一開始的物件依賴方式
builder.RegisterType<deamonresourcerepository>()
                  .As<ideamonresourcerepository>()
                  .InstancePerDependency();

但是接下來我們的呼叫端就改成這樣:
static void Main(string[] args)
  {
    var builder = new ContainerBuilder();
    builder.RegisterModule<NineYi.ERP.DA.ERPDB.Modules.DAModules>();

    using (var container = builder.Build())
    {
      MemoryLeakMethod02(container);
    }
  }

/// 
/// 使用Child Scope來管理這區域中產生的物件,可以確保物件在這個區域使用完畢後會被釋放
/// 
/// /// 
/// 把using的範圍用在Lifetimescope,而不是產生出來的物件
/// 
private static void NoMemoryLeakMethod02(IContainer container)
{
     using (var lifetimescope = container.BeginLifetimeScope())
     {
          while (1 == 1)
          {
               using (var childScope = lifetimescope.BeginLifetimeScope())
               {
                    using (var resource = childScope.Resolve<NineYi.ERP.DA.ERPDB.Deamon.IDeamonResourceRepository>())
                    {
                         resource.ResourceMonster();
                         System.Threading.Thread.Sleep(500);
                    }
               }
          }
     }
}

Profiler畫面(執行50+秒):
雖然圖示看起來記憶體有偏高,但是兩份快照顯示物件並沒有增加。

執行畫面:
這個實驗我們可以發現Dispose方法被呼叫了兩次!一次是內圈的childScope結束using範圍時呼叫,另一次是外圈的lifetimescope結束using範圍時呼叫!而Number回應的數字也都是1,表明了每次都是取得新的實體。

實驗結果四:我們可以發現dispose有被呼叫,而且記憶體也沒有逐漸上升,代表每個新產生的物件有確實的被回收了!這是因為我們這次的using是包在lifetimescope中,而不是產生出來的實體,在lifetimescope結束的當下,他會負責把在他當中產生的實體也一起回收,而在沒有人握有實體的參照,又呼叫dispose時GC就會把這份實體的資源給回收回去,因此就不會讓記憶體持續累積下去。


以上四組實驗我們可以發現一個基本的autofac在物件的生命週期中,所造成的影響。關於這個主題還有很多可以探討的問題,像是官網中還提到的另外幾種依賴的方式,還有不同的依賴方式適合用在什麼情境中,還有不同的應用系統適合用什麼樣的依賴方式。
對於Autofac有研究的人,歡迎在下方留言或是寄信給我一起討論,謝謝~!


2016年2月13日 星期六

更換MAC的登入畫面

前言:
Windows的用戶可以很輕易的用提供的介面更換登入畫面與桌面的圖片,但是MAC的用戶可就沒這麼方便了,預設會是桌面圖片的模糊化圖片,這篇教學是讓大家可以很輕易的把登入畫面換成自己喜愛的圖片。


內文:
預設上,MAC會用桌面的圖片模糊化後作為登入的畫面,這個行為MAC並沒有提供介面作調整(而且也沒有相關的command啦),但是它卻會把桌面的圖片模糊化後複製一份到資料夾中方便他作存取,所以理論上把這張圖換掉並不會影響桌面的圖片但是可以成功把登入畫面的圖片換成自己的,那我們就來試試看吧~!

在Finder中按下:
Shift+Command+G
輸入:
/Library/Caches
注意!預設會是"~/Library/Caches"這個是你自己的資料夾底下的Caches資料夾,我們要找的是系統的,所以要把前面的相對符號給拿掉!
你應該會在這個資料夾中找到名為:「com.apple.desktop.admin.png」的圖片,而且會與目前你的桌布圖片模糊化後相同。
接下來要作的事情就很直覺了,

STEP1:找一張你喜歡的圖片(要PNG格式的喔!)

STEP2:把他複製到該資料夾中並用「com.apple.desktop.admin.png」名稱儲存

STEP3:當他跳出警示視窗時按下確定,覆蓋掉原本的圖片

SETP4:登出後重新登入就可以看到成果了~!

祝大家換圖開心啦~!

成果>

LogIn:

Desktop:

2015年5月2日 星期六

Run Visual Studio Code on Mac and upgrade to DNX

前言:
距離之前在Mac上面run .NET程式還過沒多久,微軟的腳步越來越快,現在已經推出在Mac平台的編輯器了,雖然目前看來還有不少缺乏的東西,但是依照目前微軟的腳步我想應該過不了多久應該就會補上了。這樣越來越多的時間要花在追技術與新工具上了 XD

內文:
如果是之前完全沒裝過kvm的人會比較單純,畢竟之前的kvm已經用不到了,另外就是之前的kre版本在beta2與beta3經歷過一次變化,讓存放的路徑有所變化,所以對一路追的人來說,這次要把kvm相關的東西移除會比較費事些

1. 移除舊有的kvm
brew uninstall kvm
2.移除舊的執行環境
rm -rf ~/.k ~/.kre ~/.kpm

ps. .k這個目錄是beta3時的新目錄

3.安裝DNVM (其實安裝步驟跟之前安裝kvm是一樣的,不用記特別的安裝步驟是個好消息 XD)
brew tap aspen/dnvm
brew install dnvm

4.更新dnvm
dnvm upgrade
dnvm upgrade -u

ps.預設這個指令會去抓最新的stable版本的dnx,但是目前因為尚未有release的版本所以要帶上“-u”抓取unstable的版本。

2015-05-08 => 這是個已知問題,需要override DNX Feed的位置,在.bash_profile寫下後source這份文件


export DNX_FEED=https://www.nuget.org/api/v2

4.1在bashrc_profile中補上所要求的指令:
export MONO_GAC_PREFIX="/usr/local/"
source dnvm.sh

5. clone 新的demo repository
在console中輸入以下指令(要先切到預計要儲存repo的資料夾)
git clone https://github.com/aspnet/Home.git

ps.這個repo跟之前kvm時的位址是一樣的,但是裡面的檔案名稱確實已經不太一樣了,看看那一系列的dnvm的檔案

上述的作業都處理完之後就算已經把kvm清除並安裝好dnx了(還有vNext這一系列的tool與.net version manager)

6.安裝visual studio code

6.1 下載 vs code : http://go.microsoft.com/fwlink/?LinkID=534106
並安裝

6.2 接下來的指令不做不會怎樣,做了很不一樣

6.2.1 編輯使用者根目錄的.bashrc輸入以下內容
code () {
  if [[ $# = 0 ]]
  then
    open -a "Visual Studio Code"
  else
    [[ $1 = /* ]] && F="$1" || F="$PWD/${1#./}"
    open -a "Visual Studio Code" --args "$F"
  fi
}

6.2.2 你可在下載下來的sample目錄下輸入指令
code .
這樣就可以直接用指令打開visual studio code

6.3 接下來要做的事情是為了讓開發更快速

6.3.1 確保你有安裝Node.js與npm有兩種方法
6.3.1.1
brew install node
=> 我個人偏好用這個來統一管理啦~
6.3.1.2
nvm install stable
=> 2015-05-08 用這個其實也不錯,很好切版本與管理node本身

6.3.2 安裝yeoman, grunt-cli, generator-aspnet, bower
npm install -g yo grunt-cli generator-aspnet bower

安裝這些其實是因為只安裝vs code並沒有包含code template,所以需要另外安裝yeoman與generator-aspnet

6.3.3 在console或是vs code(shift+command + p)中輸入指令:
dnx restore
用這個指令來重新安裝solution所需的package,不然直接用vs code開起來後一定會看到編輯器的錯誤訊息與提示
另外,若是在vs code裡面編譯的話,會需要重新開啟專案才會讓抓不到assembly的提示訊息更新

都做完以上的動作後其實就已經把目前微軟提供在Mac的基本開發環境都裝好了,已經可以在Mac上開發.NET的應用囉~!

接下來是VS Code裡面操作的一點方便的地方

7 VS Code 的command Intellisense
shift + command + p 會跳出指令視窗輸入dnx之後可以看到啟動kestrel的指令,選擇之後就會看到另一個console被啟動起來並輸出started字樣,這就代表web server已經on起來,在server on起來之後你只需要切到browser並輸入http://localhost:5004就可以看到範例站台已經起來了
這個port的設定可以在專案根目錄的project.json裡面設定。



8.有些東西還是不work~~
若你在.cs檔案裡面有更新想要讓他反應在頁面上時只需要先把web server停下來然後再重新啟動就可以看到成果了
但若你是用dnu build嘗試去建置新的assembly時會發生錯誤,這是一個已知的Mono錯誤
Issue:https://github.com/aspnet/Home/issues/498

就等Mono Team的來修掉這個bug吧~ (−_−;)


真心好奇第一個在Mac上面開發出來的.NET應用究竟會是什麼


======2015-05-08補充======

如果使用yo的generator-aspnet來產生程式碼並且執行時會看到另兩個錯誤 XD

Case 1:
dnx的版本是用beta4,也就是目前最新的穩定版
在生完程式碼後執行
dnx restore
dnx . kestrel
可以正確的run起來,但是實際執行網站時會發生error
這個時候
export MONO_MANAGED_WATCHER=disabled

請參考:https://github.com/OmniSharp/generator-aspnet/issues/138



把上述這行加入.bash_profile後就可以正確的執行網站了,不過case2就很麻煩了...

Case 2:

dnx的版本是用beta5,也就是最新的版本(unstable)



個時候只剩下dnx restore有work了 XD

主要的原因是這個grnerator產生的模板dnx版本預設是寫beta4

其實case1的討論串中也有人點出這個問題~

感覺上應該是把dnx版本換成*符號或是直接改成beta5就可以,不過我還沒嘗試就是了

2014年12月16日 星期二

Run Asp.Net vNext on Mac

前言:
.NET已經OpenSource了!但我相信這個消息早就不是新聞了,國外也有很多人分享了如何在很潮的Mac上寫.NET應用。只是英文的教學很多但是中文的卻很少,趁著機會趕緊分享一下自己在Mac上面安裝.NET開發相關環境的心得。

內文:
由於ASP.NET已經正式OpenSource了,原始碼都放在GitHub上而且官方教學也是放那裡,因此我們就從官方的教學來一步步完成吧。
官方連結:https://github.com/aspnet/home#getting-started

1)安裝KVM
首先,要去安裝KVM(K Version Manager)這是一個幫助管理KRE(K Runtime Environment)的工具,我們可以在同一時間安裝不同版本的KRE並且指定目前預設的KRE是哪一套。KVM我們可以用HomeBrew安裝,因此安裝方式超簡單。
下以下command把這個GitHub Repos加入HomeBrew的追蹤裡。

brew tap aspnet/k

接著run以下的command安裝KVM

brew install kvm

接下來依照指示在bash_profile中加入source kvm.sh
vim .bash_profile

2)安裝KRE
裝好KVM後執行upgrade
kvm upgrade
這個指令可以幫助我們下載KRE並且設為預設,現在最基本的環境這樣就架好了!
當然,工欲善其事,必先利其器 接下來我們就是要把開發環境的好用工具都裝上才行

3)下載Sublime Text 3
http://www.sublimetext.com/3

4)安裝Kulture
在Sublime Text中使用package Controller安裝package

5)安裝OmniSharp
在Sublime Text中使用package controller安裝package
這個套件強烈建議安裝,這是for intellisence用的,裝上去之後就會有智慧提醒,不用記長死人不償命的function name那些東西了~ XD

6)下載.NET官方提供的Demo project
切到你的工作目錄下並下指令
git clone https://github.com/shirhatti/Home.git
檔案其實不大,所以很快就會抓完了XD

下載完畢後,它的結構大概是Home資料夾底下有samples資料夾,samples裡面有有三個資料夾,我們先切到ConsoleApp這個資料夾底下並下command
kpm restore
這個指令會幫我們把專案相依的NuGet package給裝起來,由於這個console app所以我們直接下command就可以看到執行結果
k run
這個指令一執行後Terminal就會顯示出那個讓人熟悉又感動的Hello World!

console app是相對簡單的,比較不一樣的是web app,官方提供的samples有兩個web,我們就拿MVC來做例子吧~
一樣我們切到HelloMvc目錄下並把package裝上
這個web專案我們就換用sublime Text示範
用sublime Text開啟專案目錄,並且下 shift+command+p
輸入"k"找到Run k commands,選擇k kestrel這個指令
預設會run在port 5004上(若是用kesterl run的話),設定可以去看project.json的內容裡面都有寫。
選擇k kestrel後會跑一個console去run web server,這個時候去瀏覽器瀏覽http://localhost:5004就可以看到新版的Mvc的預設樣式囉~!

其實.NET寫了幾年了,從沒想過微軟也會走向如此開放的地步,終於可以不用在Linux或Mac上用virtualbox安裝windows來寫.NET應用了XD
還滿感動的 XD

2014年12月2日 星期二

Take a peek at EmberJS - Phase3

前言:
接續上次的文章,我們這次要把要一步一步把功能給實做完成~!
第一階段
第二階段

內文:
延續上次的文章,我們已經完成Todo應用的CUD(新增、修改、刪除),接下來我們要做的便是可以讓使用者filter已經完成的Todo,哪些又是尚未完成的Todo。因此這邊我們需要把我們的Template分成數個子Template並利用框架的Route來做到讓這些事情在同一頁完成。
首先我們先把列表頁的內容抽出來變成一個新的Template
<!-- data-template-name內的值就相當於是一個路由,index是讓這個Template顯示的路由值,裡面的資料來源從哪裡來則從route去設定 -->
<script type="text/x-handlebars" data-template-name="MyTodos/index">
    <ul id="todo-list">
        {{#each itemController="MyTodoList"}}
        <li {{bind-attr class="isCompleted:completed isEditing:editing" }}>
            {{#if isEditing}}
            {{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>
        {{/each}}
    </ul>
</script>
而原本的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=myTitle1 action="createTodo"}}
        </header>

        <section id="main">
            {{outlet}}
            <input type="checkbox" id="toggle-all">
        </section>
</script>
<!--忽略很多-->
Handlebars helper "{{outlet}}" 是用來幫助我們宣告一個區域,當我們改變route時可以render出指定route的畫面,而第一個child route就會是填滿這個區塊的route。
改完View之後我們要新增一個route.js檔案,在裡面撰寫route相關的邏輯。
Todos.Router.map(function () {
    
    // 當Uri為'/'時去render名稱為「MyTodos」的Template
    this.resource('MyTodos', { path: '/' }, function () {
        //額外的child routes
    });
});

Todos.MyTodosRoute = Ember.Route.extend({
    // 表明這個route要顯示的model是那個
    model: function () {
        return this.store.find('MyTodo');
    }
});

// EmberJS的controller預設的route名稱就是Index,因此若直接存取預設的controller那麼根據框架就會去render Index的內容
Todos.MyTodosIndexRoute = Ember.Route.extend({
    // 這邊表示index的model就是MyTodos
    model: function () {
        return this.modelFor('MyTodos');
    }
});

現在重新刷一次頁面應該就可以看到我們的清單顯示於頁面中。不過我們還要這個Todo應用可以切換以做完、未做完的顯示。首先還是來調整一下頁面。
<script type="text/x-handlebars" data-template-name="MyTodos">
<!--忽略很多-->
        <footer id="footer">
            <span id="todo-count">
                <strong>{{remaining}}</strong> {{inflection}} left
            </span>
            <ul id="filters">
                <li>
                    {{#link-to "MyTodos.index" activeClass="selected"}}All{{/link-to}}
                </li>
                <li>
                    {{#link-to "MyTodos.active" activeClass="selected"}}Active{{/link-to}}<--會打到active這個route>
                </li>
                <li>
                    {{#link-to "MyTodos.completed" activeClass="selected"}}Completed{{/link-to}}
                </li>
            </ul>
<!--忽略很多-->
</script>
接著調整一下route.js的內容,讓route知道這兩個route分別要呈現什麼東西。
Todos.Router.map(function () {
    
    // 當Uri為'/'時去render名稱為「MyTodos」的Template
    this.resource('MyTodos', { path: '/' }, function () {
        // 這些route會在MyTodo這個Route之下
        this.route('active');// MyTodos.active
        this.route('completed');// MyTodos.completed
    });
});

<!--忽略...-->

Todos.MyTodosActiveRoute = Ember.Route.extend({
    model: function () {
        return this.store.filter('MyTodo', function (todo) {
            return !todo.get('isCompleted');
        });
    },
    renderTemplate: function (controller) {
        //這裡重複使用了既存的Template來render
        this.render('MyTodos/index', { controller: controller });
    }
});

Todos.MyTodosCompletedRoute = Ember.Route.extend({
    model: function () {
        return this.store.filter('MyTodo', function (todo) {
            return todo.get('isCompleted');
        });
    },
    renderTemplate: function (controller) {
        this.render('MyTodos/index', { controller: controller });
    }
});
現在重新刷一下畫面,應該已經可以切換完成、未完成的項目了。接下來我們新增一個可以清除所有已經完成的項目的功能。還是一樣,首先修改頁面
<footer id="footer">
<!--忽略...-->
            {{#if hasCompleted}}
            <button id="clear-completed">
                {{action "clearCompleted"}}
                Clear completed ({{completed}})
            </button>
            {{/if}}
</footer>
接下來我們要在Controller中新增以下功能
Todos.MyTodosController = Ember.ArrayController.extend({

// 忽略好大一段...

    // 清空已經完成的項目
    clearCompleted: function () {
        var completed = this.filterBy('isCompleted', true);
        // emberjs的array api提供的方法
        completed.invoke('deleteRecord');
        completed.invoke('save');
    },

    hasCompleted: function () {
        return this.get('completed') > 0;
    }.property('completed'),

    completed: function () {
        return this.filterBy('isCompleted', true).get('length');
    }.property('@each.isCompleted')
)};
重刷一下畫面後,功能就實做完成了~!但,既然有清除全部已經做完當然也會有全部已做完的功能才是
因此我們再來改一下畫面
<!--忽略...-->
        <section id="main">
            {{outlet}}
            {{input type="checkbox" id="toggle-all" checked=allAreDone}}
        </section>
<!--忽略...-->
//忽略很多...

allAreDone: function (key, value) {
        if (value === undefined) {
            return !!this.get('length') && this.isEvery('isCompleted', true);
        } else {
            this.setEach('isCompleted', value);
            this.invoke('save');
            return value;
        }
    }.property('@each.isCompleted')
好~改好之後存好檔,重新刷一下頁面這樣我們的全選功能就算完成了~!

以上這些小功能都完成後,一個簡單的Todo應用的基本能力就實做完成了,剩下的就是資料來源的問題,這就流到下次吧 = =汗