Monorail tutorial
1 Reusing UI portions (ViewComponents)
一些ui部分在一些页面经常被复用。如果这些内容决大多数是静态的内容,我们可以使用ViewComponentViewComponent类和Controller类的功能类似。都可以使用views,可以传送数据去view。同样还支持inner sections和paraments。
Creating a ViewComponent
ViewComponent类
继承自ViewComponent抽象类。有3个方法可以重载 Initialize:用来初始化view component的状态,通常用来核查提供的参数。
Render:(selects the view or uses another approach to render the component content)选择一个view或者使用 SupportsSection:invoked by the view engine to check if the component supports the section supplied on the view
public class HeaderComponent : ViewComponent
{
}
上面的ViewComponent因为客户什么都没定义所以返回1个default行为。这个default行为呈现出和这个Component相关的view,这个view是 views/components/headercomponent/default.vm.
像Controller一样你可以选择不同的view
public class HeaderComponent : ViewComponent
{
public override void Render()
{
RenderView("otherview");
}
}
上面的代码会选择views/components/headercomponent/otherview.vm.
Using a ViewComponent
ViewComponent和Controller没有关系,只和被Controller选定的views有关
#blockcomponent(NewsComponent)
<ul>
#foreach($new in $news)
<li>$news.Date $news.Title</li>
#end
</ul>
#end
Using parameters
你可以提供一些参数给ViewComponent.使用ComponentParams可以在ViewComponent使用参数}
public class TableComponent : ViewComponent
{
private ICollection elements;
private object border;
private string style;
private object cellpadding;
private object cellspacing;
public override void Initialize()
{
elements = (ICollection) ComponentParams["elements"];
border = ComponentParams["border"];
style = (String) ComponentParams["style"];
cellpadding = ComponentParams["cellpadding"];
cellspacing = ComponentParams["cellspacing"];
base.Initialize();
}
#end
Block and nested sections
This is the inner content
$counter
#end
public class RepeatComponent : ViewComponent
{
public override void Render()
{
for(int i=0; i < 5; i++)
{
PropertyBag["counter"] = i;
Context.RenderBody();
}
}
}
#colheaders
<tr>
<th> </th>
<th>Element</th>
</tr>
#end
#item
<tr>
<td>$index</td>
<td>$item</td>
</tr>
#end
#altitem
<tr>
<td align="center">$index</td>
<td>$item</td>
</tr>
#end
#end
public class TableComponent : ViewComponent
{
private ICollection elements;
private object border;
private string style;
private object cellpadding;
private object cellspacing;
public override void Initialize()
{
elements = (ICollection) ComponentParams["elements"];
border = ComponentParams["border"];
style = (String) ComponentParams["style"];
cellpadding = ComponentParams["cellpadding"];
cellspacing = ComponentParams["cellspacing"];
base.Initialize();
}
public override void Render()
{
RenderText(
String.Format("<table border=\"{0}\" style=\"{1}\" cellpadding=\"{2}\" cellspacing=\"{3}\">",
border, style, cellpadding, cellspacing));
if (Context.HasSection("colheaders"))
{
Context.RenderSection("colheaders");
}
if (elements != null)
{
int index = 0;
foreach(object item in elements)
{
PropertyBag["index"] = ++index;
PropertyBag["item"] = item;
if (Context.HasSection("altitem") && index % 2 != 0)
{
Context.RenderSection("altitem");
}
else
{
Context.RenderSection("item");
}
}
}
RenderText("</table>");
}
public override bool SupportsSection(string name)
{
return name == "colheaders" || name == "item" || name == "altitem";
}
}
更多关于 SmartDispatcherController
Source
默认的binder使用Params收集参数,你也可以自定义这些:
public class ProductController : SmartDispatcherController
{
public void Create([DataBind("product", From=ParamStore.Form)] Product product)
{
}
}
Defining accessible properties
DataBindAttribute经常作用于Domain Model Class,你可能不想所有的属性都可绑定。你可以使用Allow 和Exclude
用逗号分隔属性
{
public void CreateAccount([DataBind("account", Allow="Name,Email,Password")] Account account)
{
}
}
上面的例子表示你只想 Name,Email,Password属性绑定值,其它的都被忽视
Exclude属性和Allow属性刚好相反。
Binding Errors
Binding errors可能出现,如验证日期。使用简单的binding ,异常会被抛出,当使用了DataBindAttribute,异常不会被抛出
可以使用GetDataBindErrors方法进入错误信息
{
public void CreateAccount([DataBind("account")] Account account)
{
ErrorList errors = GetDataBindErrors(account);
}
}
ErrorList继承自ICollection,所以你可以枚举错误信息。你也可以检查一个属性的转换。
{
public void CreateAccount([DataBind("account")] Account account)
{
ErrorList errors = GetDataBindErrors(account);
if (errors.Contains("DateOfBirth"))
{
Flash["error"] = errors["DateOfBirth"].ToString(); // Or Exception
RedirectToAction("New", Params);
}
}
}
BindObject and BindObjectInstance
You do not need to always use parameters to have an object bound. The methods BindObject and BindObjectInstance, exposed by the SmartDispatcherController, allow you to have the same functionality. The benefit is that not under every case you want to perform the bindings. For example:
{
public void CreateAccount(bool acceptedConditions)
{
if (acceptedConditions)
{
Account account = (Account) BindObject(ParamStore.Form, typeof(Account), "account");
}
}
}
SmartDispatcherController
SmartDispatcherController继承自Controller,支持参数绑定。他允许你绑定来自form的参数到action的arguments。另外还支持重载。
monorail可以绑定简单的值还可以绑定复杂的对象。
Simple binding
考虑下面的html表单
Name: <input type="text" name="name" />
Email: <input type="text" name="email" />
Country:
<select name="country">
<option value="44">England</option>
<option value="55">Brazil</option>
</select>
<input type="submit" value="Search" />
</form>
标准的在Cntroller获得窗体元素值的,方法如下
Params:has query string, form and environment entries
Form:Has only form entries (method post)
Query:Has only query string entries
eg:
public class UserController : Controller
{
public void Search()
{
String name = Form["name"];
String email = Form["email"];
String country = Form["country"];
// Perform search
}
}
现在如果你使用SmartDispatcherController你可以用更简洁的代码代替上面的
public class UserController : SmartDispatcherController
{
public void Search(string name, string email, string country)
{
// Perform search
}
}
SmartdispathcherController履行一些约定(更多在下面)。如果值没被提供,参数会假设是个默认值。另外,如果提供了值,但是无法转换成相应的类型,那么就会throw exception,这个action就不会执行了
Array support
Controller支持Array。你可以使用2种方法使得窗体元素工作。
第1种方法是重复这个元素的name
eg:
<input type="text" name="name" value="1" />
<input type="text" name="name" value="2" />
<input type="text" name="name" value="3" />
<input type="text" name="name" value="4" />
<input type="text" name="name" value="5" />
</form>
第2种方法是使用indexed value notation。index value 对monorail来说是没什么意义的,但是必须唯一。
<input type="text" name="name[0]" value="1" />
<input type="text" name="name[1]" value="2" />
<input type="text" name="name[2]" value="3" />
<input type="text" name="name[3]" value="4" />
<input type="text" name="name[4]" value="5" />
</form>
eg:
public class UserController : SmartDispatcherController
{
public void SaveValues(string[] name)
{
...
}
}
Using the DataBindAttribute
比起一般的值来,我相信你更想绑定对象,使用DataBindAttribute这一切将变得可能
First of all you must use a prefix which is required to avoid name clashing. It is as giving the form elements a name space. The form below uses product as a prefix:
简单的值,聚合对象,数组都被支持。
值得注意的是,只用form元素使用了正确的name约定,binder才会工作。
首先你需要你一个前缀,这样就能避免name发生冲突。这相当于给了窗体元素一个命名空间。
下面的窗体使用 product作为前缀。
eg:
<input type="text" name="product.id" />
<input type="text" name="product.name" />
<input type="checkbox" name="product.inStock" id=" value="true" />
</form>
On the controller action you must specify the prefix as the argument to the DataBindAttribute:
在Controller Action 这1端 你必须把上面使用的product前缀最为DataBind的参数
using Castle.MonoRail.Framework;
public class ProductController : SmartDispatcherController
{
public void Create([DataBind("product")] Product prod)
{
}
}
public class Product
{
private int id;
private String name;
private bool inStock;
public int Id
{
get { return id; }
set { id = value; }
}
public string Name
{
get { return name; }
set { name = value; }
}
public bool InStock
{
get { return inStock; }
set { inStock = value; }
}
}
Arrays
1.窗体元素必须使用indexed notation(像之前所说的)
<input type="text" name="product[0].id" />
<input type="text" name="product[0].name" />
<input type="checkbox" name="product[0].inStock" id=" value="true" />
<input type="text" name="product[1].id" />
<input type="text" name="product[1].name" />
<input type="checkbox" name="product[1].inStock" id=" value="true" />
</form>
2.Controller必须声明参数是Products数组类型
public class ProductController : SmartDispatcherController
{
public void Create([DataBind("product")] Product[] prods)
{
}
}
public class Product
{
private Category[] categories;
// others fields omitted
public Category[] Categories
{
get { return categories; }
set { categories = value; }
}
// others properties omitted
}
public class Category
{
private String name;
public string Name
{
get { return name; }
set { name = value; }
}
}
<input type="text" name="product.id" />
<input type="text" name="product.name" />
<input type="checkbox" name="product.inStock" id=" value="true" />
<input type="checkbox" name="product.categories[0].name" value="Kitchen" />
<input type="checkbox" name="product.categories[1].name" value="Bedroom" />
<input type="checkbox" name="product.categories[2].name" value="Living-room" />
</form>
Layouts
Layout 允许你使用template ,这样你可以在每个View使用一些公共的html
Layout是标准的View,大那时他们需要放在名字为layouts的活页夹下,layouts活页夹必须放在views活页夹下。
你可以使用Layout Attribute把Controller和Layout关联起来。
eg:
[Layout("application")]
public class CustomerController : Controller
{
public void Index()
{
}
}
In some scenarios you might want to turn off the layout processing. To do so use CancelLayout method. There are other cases where you want to render a specific view and turn off layout at the same time. The RenderView and RenderSharedView have overloads to allow you to do that.
某些情况下你不想呈现layout。你可以使用CancelLayout取消呈现layout。
使用下面的方法也可以取消呈现layout
RenderView(String name, bool skipLayout)
RenderView(String controller, String name, bool skipLayout)
RenderSharedView(String name, bool skipLayout)
[Layout("application")]
public class CustomerController : Controller
{
public void Index()
{
RenderView("welcome", true);
}
}
<html>
Welcome
$childContent
Footer
</html>
Rescues
rescue是一个特殊的view,只有在发生异常的时候才呈现出来。rescue的视图文件必须在views活页夹下的rescues活页夹下。
rescue可以和一个controller或者1个action联系起来工作。你可以为一个特定的exception写1个rescue.
如果1个action发生了exception,MonoRail会匹配和这个exception最接近的定义了的rescue,
[Rescue("dberror", typeof(System.Data.SqlException))]
public class ProductController : Controller
{
[Rescue("commonerror")]
public void Index()
{
throw new System.Data.SqlException("fake error");
}
[Rescue("dumbprogrammer", typeof(DivideByZeroException))]
public void List()
{
int val = 0;
int x = 10 / val;
}
public void Search()
{
}
上面的rescue定义了下面的规则
1.这个Controller的任何action throws SqlException,view/rescues/dberror 将被呈现
2.如果Index Action发生任意exception(包括SqlException),view/rescues/commonerror将呈现,他会覆盖Controller级别的Rescue
3.如果List Action throws DivideByZeroException view/rescues/dumbprogrammer 将呈现
Flash
Flash是一种在Requests之间保留暂时值的方法。当你执行一些操作然后redirect的时候十分有用。
在redirect目标页面你可以check flash得到一些状态玛,或者错误信息等
public class AdminController : Controller
{
public void PasswordManagement()
{
}
public void ChangePassword()
{
String passwd = Params["password"];
if (passwd.Length < 6)
{
Flash["error"] = "Password too weak, operation aborted";
}
else
{
// Change password
}
RedirectToAction("PasswordManagement");
}
}
上面的代码可能不太清楚。下面让我们看看到底发生了什么
1。 用户进入PasswordManagement Action
2 一个包含修改密码的 post去 ChangePassword action 的form的页面出现
3。执行ChangePassword action的时候 添加一些实体到Flash里面
4。用户又被send去PasswordManagement action
5。PasswordManagement View检查 Flash,显示一些有意义的信息
Working with Views
活页夹结构约定
必须有一个活页夹。如果Controller和area联系,那么这个必须反映在View活页夹结构上选择一个View呈现
当action被调用的时候mr会预选择和这个action同名的view呈现。
eg:
using Castle.MonoRail.Framework;
public class CustomerController : Controller
{
public void Index()
{
}
}
当index action被调用,views\customer\index View会被预选择
如果我们不想views\customer\index view被呈现,我们可以使用RenderView方法,选择不同的view
public class CustomerController : Controller
{
public void Index()
{
RenderView("welcome");
}
}
上面的代码选择了一个 views\customer\welcome view
Note:
当RenderView被调用的时候View并不执行,只是选择了。只有在action方法返回的时候view才执行
传参数给View
You would probably want to supply data to the view so it can generate dynamic content. This should be done using the PropertyBag. For example:
可能你想提供一些数据给view这样你就能生成动态内容。这应该使用PropertyBag。
eg:
public class TestController : Controller
{
public void ShowTime()
{
PropertyBag["now"] = DateTime.Now;
}
}
PropertyBag 是一个词典。每种View引擎使用一个信道使得数据在view里面可得。下面的例子使用引擎
NVelocity
eg:
Hello, the time now is $now
</html>
共享Views
一些Views可能被某些Controllers共享。这种情况我们使用RenderShareView
public class CustomerController : Controller
{
public void Index()
{
RenderSharedView("common/welcome");
}
}
上面的代码选择views\common\welcome view
取消View
尽管听起来很奇怪,在某些情况下我们不想view呈现
我们可以使用CancelView 方法
public class CustomerController : Controller
{
public void Index()
{
CancelView();
}
}
Filters
Filters在action的前后执行。对于安全来说这个很有用(提交数据之前进行验证),还可以减少重复代码
创建一个Filter
Filter必须实现IFilter接口,这样filter才能和你的Controller结合起来
public class AuthenticationFilter : IFilter
{
public bool Perform(ExecuteEnum exec, IRailsEngineContext context, Controller controller)
{
if (context.Session.Contains("user"))
{
return true;
}
else
{
context.Response.Redirect("account", "login");
}
return false;
}
}
ExecuteEnum属性可以设置Filter在什么时候被执行。
ExecuteEnum fields Description
BeforeAction 在action之前执行.
AfterAction 在action之后执行.
AfterRendering 在rendering后执行.
Always 每一步都执行.
只用FilterAttribute可以把filter和Controller联系起来
[FilterAttribute(ExecuteEnum.BeforeAction, typeof(AuthenticationFilter))]
public class AdminController : Controller
{
public void Index()
{
}
}
Ordering
一个Controller可以使用多个Filter,而且可以使用ExecutionOrder属性对Filter执行的先后进行排序。
值越低,优先级越高。
eg:
[FilterAttribute(ExecuteEnum.BeforeAction, typeof(AuthenticationFilter), ExecutionOrder=0)]
[FilterAttribute(ExecuteEnum.BeforeAction, typeof(LocalizationFilter), ExecutionOrder=1)]
public class AdminController : Controller
{
public void Index()
{
}
}
上面的代码,AuthenticationFilter 在LocalizationFilter之前执行
Skipping filters
有些情况下我们不想1个action或多个action执行一个或多个filter。
那么我们可以使用SkipFilter在这些case上
eg:
[FilterAttribute(ExecuteEnum.BeforeAction, typeof(AuthenticationFilter), ExecutionOrder=0)]
[FilterAttribute(ExecuteEnum.BeforeAction, typeof(LocalizationFilter), ExecutionOrder=1)]
public class AdminController : Controller
{
[SkipFilter]
public void Index()
{
}
[SkipFilter(typeof(LocalizationFilter))]
public void Create()
{
}
public void Update()
{
}
}
在上面的代码中
Index action不会执行任何Filter
Create action不会执行LocalizationFilter
Update action 执行所有的Filter
给Filter附参数
参数类
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=true), Serializable]
public class MyCoolFilterAttribute : FilterAttribute
{
private readonly string fileName;
public MyCoolFilterAttribute(String fileName) : base(ExecuteEnum.BeforeAction, typeof(CoolFilterImpl))
{
this.fileName = fileName;
}
public string FileName
{
get { return fileName; }
}
}
Filter类
public class CoolFilterImpl : IFilter, IFilterAttributeAware
{
private MyCoolFilterAttribute attribute;
// Implementation of IFilterAttributeAware
public FilterAttribute Filter
{
set { attribute = (MyCoolFilterAttribute) value; }
}
// Implementation of IFilter
public bool Perform(ExecuteEnum exec, IRailsEngineContext context, Controller controller)
{
// Now you can access the parameters:
String fileName = attribute.FileName;
// Work
// Allow the process to go on
return true;
}
}
Controller类
[MyCoolFilterAttribute("customer_messages.txt")]
public class CustomerController : Controller
{
public void Index()
{
}
Controller 名字的公约
官方建议使用Name+Controller后缀最为Controller类的名字。这样一个Controller被注册的时候,MonoRail会去掉后缀Comtroller使用Name。Controller名字在url进入的时候被使用。
例如ProductController的名字是Product.
我们还可以自己定义名字而不是用之前的约定,这样我们就要使用到了ControllerDetails属性
eg:
[ControllerDetails("cust")]
public class Customer : Controller
{
}
在这我们使用cust作为进入名字
Areas
monorail支持使用概念areas,这是个逻辑Controller群组。这样可以定义一些Controller属于一个群组。
默认的areas为空
为了定义Areas我们还要使用ControllerDetails属性
eg:
[ControllerDetails(Area="admin")]
public class UsersController : Controller
{
public void Index()
{
}
}
这个Controller可以通过下面的url进入
/admin/users/index.rails
上面的Areas是一级的 我们还可以定义多级的
eg:
[ControllerDetails(Area="admin/users")]
public class PasswordMngController : Controller
{
public void Index()
{
}
}
我们使用下面的url进入这个Controller
/admin/users/passwordmng/index.rails
Default Action
当程序找不到匹配的方法的时候,会去调用用DefaultAction属性定义的方法。
这样网叶设计人员就可以添加一个View而不需要程序员去添加一个新的方法。
使用DefaultAction属性给Controller提供一个默认action。
DefaultAction只可以定义在class级别
有下面2种情况
1. 没有制定DefaultAction的名字,那么默认的方法就是public void DefaultAction()
public class HomeController : Controller
{
public void Index()
{
}
public void DefaultAction()
{
string template = "notfound";
if (HasTemplate("home/" + Action))
{
template = Action;
}
RenderView(template);
}
}
2 . 指定了DefaultAction的名字 那么调用的方法就是你指定的方法
public class HomeController : Controller
{
public void Index()
{
}
public void foo()
{
string template = "notfound";
if (HasTemplate("home/" + Action))
{
template = Action;
}
RenderView(template);
}
}
Redirecting
Controller提供了大量的Redirect
Name |
Description |
RedirectToAction(string action) |
定向到同一个Controller的别的action |
RedirectToAction(String action, params String[] queryStringParameters) |
Redirects to another action in the same controller specifying query string entries. |
RedirectToAction(String action, IDictionary parameters) |
Redirects to another action in the same controller specifying query string entries. |
Redirect(String url) |
Redirects to the specified URL. |
Redirect(String url, IDictionary parameters) |
Redirects to the specified URL specifying query string entries. |
Redirect(String controller, String action) |
Redirects to another controller and action. |
Redirect(String area, String controller, String action) |
Redirects to another controller and action (within an area). |