ASP.Net in Debian/Ubuntu

Ubuntu 下架設 mono xsp 其實還蠻愉快的。
Debian/Ubuntu的mono-xsp2套件提供了 mono-xsp2-admin 指令,用這個很快就能新增/刪除你的應用程式。
用法很簡單:

sudo mono-xsp2-admin add --path=your_app_path --application=/your_app_name

接著重新啟動 mono-xsp2 服務即可:

sudo /etc/init.d/mono-xsp2 restart

這個方法不需要安裝 mod_mono,因為這個時候 xsp2 是以一個獨立的 Daemon 存在。如果你希望 xsp2 應用程式是作為 Apache Web server 下的一個子目錄的話,你需要安裝 mod_proxy, mod_proxy_http, mod_proxy_html:

sudo apt-get install libapache2-proxy-html
sudo a2enmod proxy proxy_html

然後在你的 VirtualHost 設定裡面加上:

    <Proxy *>
Order deny,allow
Allow from all
</Proxy>
ProxyPass /your_app_name http://localhost:your_port/your_app_name
ProxyPassReverse /your_app_name http://localhost:your_port/your_app_name
ProxyHTMLURLMap http://localhost:your_port /your_app_name/
<Location /your_app_name>
ProxyPassReverse /
SetOutputFilter proxy-html
ProxyHTMLURLMap / /your_app_name/
Order allow,deny
Allow from all
</Location>

接著重新啟動 Apache,這時候你在瀏覽器的網址列輸入 http://localhost/your_app_name/ 應該就可以存取到你的 asp.net 應用程式了。

備註:如果你不想使用 mono-xsp2,而只想把 Apache 當作前端,IIS當後端的話,同樣也可以用 mod_proxy, mod_proxy_html, mod_proxy_http 來解。

參考資料:

在 Monorail 裡啟動 Logging 的方法

參考這篇:Enabling logging就行了。

整理如下:

  1. 在 public 下新增一個檔案 log4net.config:
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
    <log4net>
    <!-- Define some output appenders -->
    <appender name="rollingFile" type="log4net.Appender.RollingFileAppender,log4net" >
    <param name="File" value="log.txt" />
    <param name="AppendToFile" value="true" />
    <param name="RollingStyle" value="Date" />
    <param name="DatePattern" value="yyyy.MM.dd" />
    <param name="StaticLogFileName" value="true" />
    <layout type="log4net.Layout.PatternLayout,log4net">
    <param name="ConversionPattern" value="%d [%t] %-5p %c [%x] (%X{auth}) - %m%n" />
    </layout>
    </appender>
    <root>
    <!-- priority value can be set to ALL|INFO|WARN|ERROR -->
    <priority value="INFO" />
    <appender-ref ref="rollingFile" />
    </root>
    </log4net>
    </configuration>
    
  2. 在 public/web.config 裡面,註冊 service:
    <monorail>
    <services>
    <service
    id="Custom"
    interface="Castle.Core.Logging.ILoggerFactory, Castle.Core"
    type="Castle.Services.Logging.Log4netIntegration.Log4netFactory, Castle.Services.Logging.Log4netIntegration" />
    </services>
    <!--略...-->
    

Monorail ActiveRecord Scaffold

今天看官方文件時,看到這個:ActiveRecord Scaffold
只要有 model,monorail 可以自動幫你把 controller configure 成有 scaffold 功能 (CRUD都具備) 的 controller:

[Scaffolding( typeof(Blog) )]
public class BlogsController : Controller
{
}

這真是令人驚訝…不過 rc2 預設的 Layout 有問題,所以你最好加上 [Layout] attribute,自訂 Layout 比較好…
缺點是,要作 localization 的話,依照現在 rc2 的代碼,根本不可能…

結論是,有空可以來看看這邊的代碼(ARFormHelper.cs/PresentationHelper.cs),因為 Generator 裡面的 ScaffoldHelper 有些情況沒考慮的很仔細。

How to monorail by Generator tool(4)

在研究過之後,我決定在 ScaffoldHelper.cs 裡面添加 GetText 這個函數,用來取得字串。

#region Localization helpers
public string GetText( IResource resource, string str )
{
if( resource==null )
return str;
object resStr = resource[ str ];
if( resStr==null )
return str;
return resStr.ToString();
}
#endregion

這樣在 .vm (template)檔案裡面,就可以這麼使用,以取得本地化後的字串:

$ScaffoldHelper.GetText($textres, "Name" )

當然你還是可以用原先的方法:

$textres.Name

所以 PageBrowser() 也可以跟著修正為:

#region Pagination helpers
public string PageBrowser( Page page )
{
return PageBrowser( page, null );
}
public string PageBrowser( Page page, IResource resource )
{
StringWriter output = new StringWriter();
PaginationHelper helper = new PaginationHelper();
helper.SetController(this.Controller);
string firstText = GetText( resource, "First" );
string prevText = GetText( resource, "Previous" );
string nextText = GetText( resource, "Next" );
string lastText = GetText( resource, "Last" );
if (page.HasFirst)
output.Write(helper.CreatePageLink(1, firstText ));
else
output.Write( firstText );
output.Write(" | ");
if (page.HasPrevious)
output.Write(helper.CreatePageLink(page.PreviousIndex, prevText ));
else
output.Write(prevText);
output.Write(" | ");
if (page.HasNext)
output.Write(helper.CreatePageLink(page.NextIndex, nextText ));
else
output.Write( nextText );
output.Write(" | ");
if (page.HasLast)
output.Write(helper.CreatePageLink(page.LastIndex, lastText ));
else
output.Write( lastText );
return output.ToString();
}
#endregion

最後在資源檔裡面加上必要的字串並且修改 .vm (template)以後,就可以得到本土化後的結果。

How to monorail by Generator tool(3)

這一次要講的是 Localization,不幸的是 Generator 沒有考慮到 Localization 問題,所以由 Generator 產生出來的代碼,並不包含這部份,但這卻是一個很好的機會讓我去了解 Generator 與 monorail 底下的機制。

Castle官方網站上已經有介紹該怎麼 Localization:Resources and Localization,但不幸的是,他使用 Visual Studio.Net 作為範例,Visual Studio.Net 已經掩蓋了所有細節。
在研究官方文件與相關文件之後,以下是我的步驟。

首先,你要先在 app/controllers 加上:

[Resource("textres", "ToDo.categories")]

這表示在 view(template) 那兒使用 $textres 就可以存取資源檔的內容。

所以在 app/views/categories/list.vm 裡,就可以把必要的字串改用$textres.字串名稱替代掉,這裡我就不列出整個 list.vm 了,只列出幾行:

<!--略-->
$HtmlHelper.LinkTo( $textres.AddNewCategory, "categories", "new")
<!--略-->
$HtmlHelper.LinkTo( $textres.View, "categories", "view", $category.Id)
<!--略-->
$HtmlHelper.LinkTo( $textres.Edit, "categories", "edit", $category.Id)
<!--略-->
$AjaxHelper.LinkToRemote( $textres.Delete, "delete.aspx", $DictHelper.CreateDict("with='id=$category.Id'", "condition=confirm('Delete?')", "onsuccess=new Effect.Fade('category$category.Id')"))
<!--略-->

然後我選擇在 app/ 下建立一個 resources 目錄,用來放置資源檔:

;Filename: categories.txt
;Language: en-US
View=View
Edit=Edit
Delete=Delete
AddNewCategory=Add a new Category
;Filename: categories.zh-tw.txt
;Language: zh-tw
View=檢視
Edit=編輯
Delete=刪除
AddNewCategory=加新類別

這裡要特別注意,把文字檔的編碼存為 unicode 或是 utf-8,否則會有亂碼出現。

最後修改 default.build,以便把資源檔編譯進去,這裡我也只列出關鍵的幾行:

<!--略-->
<target name="build" description="Compile all source files">
<mkdir dir="${dir.bin}" />
<copy flatten="true" todir="${dir.bin}">
<fileset>
<include name="${dir.lib.castle}/**" />
<include name="${dir.lib.npgsql}/**" />
<include name="${dir.lib.mysql}/**" />
<include name="${dir.lib.migrator}/**" />
<include name="${dir.lib.nunit}/nunit.framework.dll" />
</fileset>
</copy>
<resgen todir="${dir.src.app}/resources/">
<resources>
<include name="${dir.src.app}/resources/*.txt" />
</resources>
</resgen>
<csc target="library" output="${dir.bin}/${project::get-name()}.dll">
<sources>
<include name="${dir.config}/Boot.cs" />
<include name="${dir.src.app}/**/*.cs" />
<include name="${dir.src.test}/**/*.cs" />
<include name="${dir.migrations}/**/*.cs" />
</sources>
<resources basedir="." prefix="ToDo">
<include name="${dir.src.app}/resources/*.resources"/>
</resources>
<references basedir="${dir.bin}">
<include name="System.Web.dll" />
<!--略-->

最重要的就是 resgen 這個區塊與 csc 裡面的 resources 區塊,resgen 是把文字檔轉換為 .resources 資源檔,而 csc 裡面的 resources 則是表示要把產生出來的 .resources 編譯進去。

好了,該修改的都修改了,接下來就是重新建立,輸入 nant 就會自動重新編譯。
有點美中不足的是,下面顯示頁次的部份仍然是英文,這是因為這些字串被寫死在 ScaffoldHelper.cs 裡面,我想下次再來修改這部份。

後記:

  • csc resources 所指定的 prefix 與 [Resource(“textres”, “ToDo.categories”)] 的第二個參數息息相關,不妨修改來試試看。
  • 網站文件有提到 LocalizationFilter 的部份,但是用了卻不行,有待繼續研究。

How to monorail by Generator tool(2)

接下來,就參考這篇有名的 Four Days on Rails 來試試看吧~
首先呢,由於隔了這麼久才寫這篇,所以 Generator 改位置了,你得從這裡:http://svn.castleproject.org:8080/svn/castle/trunk/Experiments/Generator/ Checkout,其他大致都跟上篇一樣。

  1. 先利用 monorail ToDo,建立專案,接著依照上篇文章作必要的設定,同時我們使用 rc2,不使用最新的版本(也就是你不用更動 default.build),設定的部份就不再贅述。
  2. 建立資料庫,這邊我是用MySQL,建立的資料庫名稱是 ToDoDev,不過這只有跟修改連線字串有關係。
  3. 修改資料庫的連線字串,連線字串是放在 config/databases/ 下的 ActiveRecord 設定檔裡面,你可以看到這邊有兩個設定檔,他很貼心的把開發用與測試用的資料庫分開:development.xml、test.xml,事實上,你還可以增加一個 Production (或是其他名字)的 ActiveRecord 設定檔,表示這是正式上線用的資料庫。切換的時候,可以更動 config/boot.cs 以決定使用哪個 ActiveRecord 設定檔,預設是 development。
  4. 建立 Model,首先先產生”Category”,Category 的欄位如下:

    id int autoincrement
    name varchar(255)
    created_on datetime
    updated_on datetime

    ,在專案的目錄下輸入:script\generate model Categories name created_on updated_on,這邊要注意的是,不需要特別寫 id,因為慣例至上,Generator會自動幫你增加 Id 這個欄位。Generator 會幫你作三件事情:產生 Category 這個類別、產生測試 Category 的 Unit test 類別以及建立資料表格的程式,你可以從指令的輸出結果很清楚的看到。

    exists app\models
    create app\models\Categories.cs
    exists test\models
    create test\models\CategoriesTest.cs
    create db\migrations
    create db\migrations01_AddCategoriesTable.cs

  5. 接下來,要去修改 Categories 類別的屬性型態,Generator 預設都是把型態設為字串,db\migrations 那邊則都是建立 varchar(50)。所以除了 app\models\Categories.cs 要修改型態之外,db\migrations01_AddCategoriesTable.cs 下也要改!這邊把 Name 改為 255 個字元,created_on與updated_on 則改為 DateTime。
  6. 都修改完畢之後,就可以執行 nant migrate,這個步驟會幫你去實體資料庫作必要的事情。為甚麼要這樣子作?我想是因為一般資料庫的 script 是沒有作管理的,因此,才刻意把這些要去資料庫建立表格、修改欄位等的動作都放到 db\migrations 下,以便把這些程式也都納入版本控制。
  7. 執行 script\generate scaffold Category,這個步驟會一口氣幫你把 CRUD 的頁面都弄出來。
  8. 那麼,就編譯吧,直接執行 nant,只是卻出現了…

    [csc] app\controllers\CategoriesController.cs(36,72): error CS0246: 找不到型別或命名空間名稱 ‘Category’ (您是否遺漏 using 指示詞或組件參考?)
    [csc] app\controllers\CategoriesController.cs(56,45): error CS0246: 找不到型別或命名空間名稱 ‘Category’ (您是否遺漏 using 指示詞或組件參考?)

    這個,得去修改 CategoriesController.cs,把 Category 都改為 Categories,這看來是 Generator 的問題。接著又再出現

    error CS1501: 方法 ‘CreatePagination’ 沒有任何多載使用 ‘2’ 個引數

    ,唉~這個錯誤訊息實在是太黯然又太銷魂…你必須再次修改 CategoriesController.cs,添加這兩行:

    using System.Collections;
    using System.Collections.Generic;
    

    然後把

    PropertyBag["categories"] = PaginationHelper.CreatePagination( Categories.FindAll(), 10);

    改為

    PropertyBag["categories"] = PaginationHelper.CreatePagination( (IList)Categories.FindAll(), 10);

    ,接下來應該就沒問題了。

  9. 先試試看到目前為止的成果吧,輸入script\server,然後打開你的瀏覽器,在位址列輸入:http://localhost:8080/categories/list.aspx,就能看到結果了。

執行結果:
categories_list_aspx

Gtk# UI設計隨便聊

前一陣子用MonoDevelop來寫 Gtk# 應用程式玩,發現真的是很簡單。
幾乎可以跟 SharpDevelop 或 Visual Studio.Net 設計 Windows form 應用程式一樣了,同樣也是拖拉放,就可以完成。
對於初入門的人來說,最痛苦的可能還是對於 Gtk# 的物件模型不熟悉,因為不熟悉,所以也就不知道該去處理哪些屬性與事件。
好在Mono官方網站有提供入門文件:

用力唸一唸這些文件跟自己去嘗試,大致上應該是夠了。但這些文件大部分都是自己去寫 Layout code。
如果你正在使用目前版本的 MonoDevelop 在 Layout 的話,你會發現世界全然不同,因為MonoDevelop引進了 Stetic
現在使用 MonoDevelop 拉出來的 Form,實際上都會被存放在專案目錄下的 gtk-gui 目錄下的某個 xml 檔案,在每次 build 之前,MonoDevelop 會先把 xml 轉換為一個存放於 gtk-gui 目錄下的 partial 類別(.cs),然後 build 的時候,再 build 到一起。而在專案目錄下所看到的代碼,只會有一行 Build()。
這真的是方便很多,開發者只需要專注於如何處理程式邏輯就可以,而不需要去管 UI 代碼的生成。
MonoDevelop 加入 Stetic 之前,大部分的 Gtk# 應用程式還是用手動加代碼或是用 Glade# 來設計介面(詳細的 Glade# 用法,可以參考 Your First Glade# Application 的說明)。
Glade# 其實也是把整個 UI 存成一個 xml 檔案,所以你得先使用 Glade 應用程式設計出 UI,得到 .glade 檔案,然後在程式裡面:

  1. 載入 .glade 檔案
  2. 為控制項(Widget)加上 Attribute
  3. 撰寫事件處理並繫結


這當然是比手動寫代碼方便許多,但比起 Stetic,還是要多作事,往往用 Glade 修改 UI 後,可能會忘記加必要的 Widget 宣告,或是對應的事件繫結。

Gtk.TreeView(3)

在 Linux 裡面,所謂的”事件”,多半是用 Signal 來表示,所以你看到 Signal 時,可以概略地當作”事件”來看。
而這些事件跟 Windows Form 的事件命名法差異相當大。
以下這些是TreeView比較常用到的:

    protected virtual void OnTreeview1RowActivated (object o, Gtk.RowActivatedArgs args)
{
// double click
Console.WriteLine( String.Format("[{0}]: {1}", o.GetType().ToString(), "row activated") );
}
protected virtual void OnTreeview1RowCollapsed (object o, Gtk.RowCollapsedArgs args)
{
// collapse
Console.WriteLine( String.Format("[{0}]: {1}", o.GetType().ToString(), "row collapsed") );
}
protected virtual void OnTreeview1RowExpanded (object o, Gtk.RowExpandedArgs args)
{
// expand
Console.WriteLine( String.Format("[{0}]: {1}", o.GetType().ToString(), "row expanded") );
}
protected virtual void OnTreeview1CursorChanged (object sender, System.EventArgs e)
{
// click
Console.WriteLine( String.Format("[{0}]: {1}", sender.GetType().ToString(), "Cursor changed") );
}

我是怎麼查到的呢?
坦白說,我是用笨方法,看到那些 Console.WriteLine 了沒?
我先試著在可能的事件裡面放置這些 Console.WriteLine,接著執行程式,試著去 Click、Expand,然後看 Console 輸出就知道了…