在Orchard中创建一个自定义字段类型
在《Orchard中的一些名词》一文中介绍过字段(
Field)的概念,它是一种轻量级的内容部件,通常只有一个字段,可以是一个文本输入框,或是一个日期选择框。我们可以把它理解为一种输入控件,或者称为字段类型。再来回顾一下《Orchard中的内容管理》中的介绍的一些概念,在Orchard中的任何数据都可以把它理解为内容(Content)。页面是内容、博客是内容、文章也是内容。整个网站就是由这些不同类型的内容项(Content Item)构成的。对这些不同内容的类型定义就是内容类型(Content Type)。而内容类型则是通过若干个不同的内容部件(Content Part)组成。其实对于定义一些简单的内容类型,或者说后续无复杂处理要求的内容类型时,我们也不一定非得去创建内容部件,而完全可以用一些现有的内容部件在加上若干字段(Field)就可以了。
字段(Field)和部件(Part)的区别在于,字段是不用单独去创建表来存储数据的,而是通过Orchard框架的功能将其以xml的形式存储到Orchard_Framework_ContentItemRecord表的Data字段中,而Part则需要创建相应的表去存储这些记录。这种存储上的差异就决定了,字段形式适用于简单场景,如只是单纯显示一个单一实体;而部件方式适用于后续有较复杂的逻辑处理的场景,如存在一对多、多的多关系等。更进一步的来说,一个部件可以理解为一个完整功能的业务实体,而一个字段只是业务实体上的一个属性。下面我们就通过一个示例来介绍在Orchard中如何定义字段类型。
目标
我需要定义一个活动类型(Event),包括标题,时间,地点等。如果用Part的做法,我们当然可以把时间和地点加在一起做成活动部件。如果要用字段的形式来做,我们就可以添加一个Route部件,加上两个文本字段(TextField)即可。文本字段是Orchard自带的字段类型,功能就是一个简单文本输入框。其实字段类型还可以做的更加友好些,比如:输入时间的时候可以弹出一个日期选择控件,输入地点的时候可以选择所在城市。关于日期时间的字段类型Orchard应用商店已经有了,我们可以去安装一个,在Gallery中搜索DateTimeField即可。现在我们需要创建一个地点字段类型(LocaleField),包括一个城市选择和一个地点输入。
创建模块
创建一个新的字段类型其实和创建一个模块一样,我们也可以用命令行工具首先创建一个代码模板。输入:codegen module MyCompany.LocaleField,运行成功后在Orchard解决方案的Modules目录下就会多出一个MyCompany.LocaleField的项目了。
建立字段模型
我们可以在此项目中添加一个Fields文件夹,并在该目录中添加一个LocaleField.cs文件,代码如下:

using System;
using Orchard.ContentManagement;
using Orchard.ContentManagement.FieldStorage;
namespace MyCompany.LocaleField.Fields
{
/// <summary>
/// 自定义的字段需要继承ContentField类
/// Storage相当于是在ContentField中获取或设置值
/// </summary>
public class LocaleField : ContentField
{
public string Locale
{
get
{
var value = Storage.Get<string>();
return value;
}
set
{
Storage.Set(value == null ? String.Empty : value);
}
}
}
using Orchard.ContentManagement;
using Orchard.ContentManagement.FieldStorage;
namespace MyCompany.LocaleField.Fields
{
/// <summary>
/// 自定义的字段需要继承ContentField类
/// Storage相当于是在ContentField中获取或设置值
/// </summary>
public class LocaleField : ContentField
{
public string Locale
{
get
{
var value = Storage.Get<string>();
return value;
}
set
{
Storage.Set(value == null ? String.Empty : value);
}
}
}
一个字段类型需要被定为一个类,并且需要继承于ContentField,这样我们就可以利用字段类型的一些现有功能,比如数据存储。字段类型的数据都是以字符串的形式存储到数据库中的,我们通过定义这个类就可以将字段数据转换成我们想要的类型比如,日期类型,数字类型等。字段数据我们不用创建相应的表去存储,Orchard框架会替我们完成这项工作。这些字段数据,将以xml的形式存储到Orchard_Framework_ContentItemRecord表的Data字段中,如:
建立视图模型
由于一个地点字段是由城市和地址组成,所以本例我们还需要建立一个视图模型来作为视图的Model,而不是直接用LocaleField作为视图的Model。虽然这步工作不一定是必须的,但是建立一个视图模型却是一个好的习惯,这样可以使我们的代码看起来更清晰更易懂。在此项目中添加一个ViewModels目录,并在该目录下添加一个LocaleFieldViewModel.cs文件,输入以下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MyCompany.LocaleField.ViewModels
{
public class LocaleFieldViewModel
{
public string Name { get; set; }
public string City { get; set; }
public string Address { get; set; }
/// <summary>
/// 作为示例这里就只简单构造一些城市的数据
/// 如果要做成产品,当然这部分需要处理的更好些
/// </summary>
public SelectList CityList
{
get
{
var lst = new List<SelectListItem>();
lst.Add(new SelectListItem { Text = "北京", Value = "北京" });
lst.Add(new SelectListItem { Text = "上海", Value = "上海" });
lst.Add(new SelectListItem { Text = "天津", Value = "天津" });
lst.Add(new SelectListItem { Text = "重庆", Value = "重庆" });
lst.Add(new SelectListItem { Text = "武汉", Value = "武汉" });
return new SelectList(lst, "Value", "Text", this.City);
}
}
}
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MyCompany.LocaleField.ViewModels
{
public class LocaleFieldViewModel
{
public string Name { get; set; }
public string City { get; set; }
public string Address { get; set; }
/// <summary>
/// 作为示例这里就只简单构造一些城市的数据
/// 如果要做成产品,当然这部分需要处理的更好些
/// </summary>
public SelectList CityList
{
get
{
var lst = new List<SelectListItem>();
lst.Add(new SelectListItem { Text = "北京", Value = "北京" });
lst.Add(new SelectListItem { Text = "上海", Value = "上海" });
lst.Add(new SelectListItem { Text = "天津", Value = "天津" });
lst.Add(new SelectListItem { Text = "重庆", Value = "重庆" });
lst.Add(new SelectListItem { Text = "武汉", Value = "武汉" });
return new SelectList(lst, "Value", "Text", this.City);
}
}
}
写驱动器
正如前文所说,字段类型其实是一种轻量级的部件。和部件一样它也需要一个驱动器(Driver)来处理它的呈现逻辑,只是它不需要处理器(Handler)来处理它的数据存储。因为Orchard框架已经帮我们实现了这一点。我们在此项目中添加一个Drivers目录,并在该目录下创建一个LocaleFieldDriver.cs文件,输入以下代码:

using System;
using JetBrains.Annotations;
using Orchard;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using MyCompany.LocaleField.ViewModels;
using Orchard.Localization;
namespace MyCompany.LocaleField.Drivers
{
public class LocaleFieldDriver : ContentFieldDriver<Fields.LocaleField>
{
/// <summary>
/// 定义模板查找规则,在Views目录下按照Fields/MyCompany.Locale规则查找模板
/// 比如显示画面就直接查找Fields目录下MyCompany.Locale.cshtml文件。(注:aspx模板也是支持的)
/// 编辑画面会查找EditorTemplates目录下的Fields\MyCompany.Locale.cshtml文件
/// </summary>
private const string TemplateName = "Fields/MyCompany.Locale"; // EditorTemplates/Fields/MyCompany.Locale.cshtml
/// <summary>
/// 获取字段前缀
/// 可以使编辑画面中各字段的表单控件都有唯一的名称,不至于出现同名冲突。
/// </summary>
/// <param name="field"></param>
/// <param name="part"></param>
/// <returns></returns>
private static string GetPrefix(ContentField field, ContentPart part)
{
return part.PartDefinition.Name + "." + field.Name;
}
/// <summary>
/// 显示画面
/// </summary>
/// <param name="part"></param>
/// <param name="field"></param>
/// <param name="displayType"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Display(ContentPart part, Fields.LocaleField field, string displayType, dynamic shapeHelper)
{
var value = field.Locale;
var viewModel = new LocaleFieldViewModel
{
Name = field.Name,
//地点数据由城市和地址构成,城市和地址用逗号分隔,如:上海,浦东新区张杨路XXX号
City = string.IsNullOrEmpty(value) ? "" : value.Substring(0, value.IndexOf(',')),
Address = string.IsNullOrEmpty(value) ? "" : value.Substring(value.IndexOf(',') + 1)
};
return ContentShape("Fields_MyCompany_Locale", // this is just a key in the Shape Table
() =>
shapeHelper.Fields_MyCompany_Locale( // Fields_MyCompany_Locale是一个动态类型,是视图Fields/MyCompany.Locale.cshtml的module,其中的Model属性,是LocaleFieldViewModel
Model: viewModel)
);
}
/// <summary>
/// 编辑画面显示
/// </summary>
/// <param name="part"></param>
/// <param name="field"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Editor(ContentPart part, Fields.LocaleField field, dynamic shapeHelper)
{
var value = field.Locale;
var viewModel = new LocaleFieldViewModel
{
Name = field.Name,
City = string.IsNullOrEmpty(value) ? "" : value.Substring(0, value.IndexOf(',')),
Address = string.IsNullOrEmpty(value) ? "" : value.Substring(value.IndexOf(',') + 1)
};
return ContentShape("Fields_MyCompany_Locale_Edit", // this is just a key in the Shape Table
() => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: viewModel, Prefix: GetPrefix(field, part)));
}
/// <summary>
/// 编辑画面Post
/// </summary>
/// <param name="part"></param>
/// <param name="field"></param>
/// <param name="updater"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Editor(ContentPart part, Fields.LocaleField field, IUpdateModel updater, dynamic shapeHelper)
{
var viewModel = new LocaleFieldViewModel();
if(updater.TryUpdateModel(viewModel, GetPrefix(field, part), null, null))
{
field.Locale = viewModel.City + "," + viewModel.Address;
}
return Editor(part, field, shapeHelper);
}
}
using JetBrains.Annotations;
using Orchard;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using MyCompany.LocaleField.ViewModels;
using Orchard.Localization;
namespace MyCompany.LocaleField.Drivers
{
public class LocaleFieldDriver : ContentFieldDriver<Fields.LocaleField>
{
/// <summary>
/// 定义模板查找规则,在Views目录下按照Fields/MyCompany.Locale规则查找模板
/// 比如显示画面就直接查找Fields目录下MyCompany.Locale.cshtml文件。(注:aspx模板也是支持的)
/// 编辑画面会查找EditorTemplates目录下的Fields\MyCompany.Locale.cshtml文件
/// </summary>
private const string TemplateName = "Fields/MyCompany.Locale"; // EditorTemplates/Fields/MyCompany.Locale.cshtml
/// <summary>
/// 获取字段前缀
/// 可以使编辑画面中各字段的表单控件都有唯一的名称,不至于出现同名冲突。
/// </summary>
/// <param name="field"></param>
/// <param name="part"></param>
/// <returns></returns>
private static string GetPrefix(ContentField field, ContentPart part)
{
return part.PartDefinition.Name + "." + field.Name;
}
/// <summary>
/// 显示画面
/// </summary>
/// <param name="part"></param>
/// <param name="field"></param>
/// <param name="displayType"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Display(ContentPart part, Fields.LocaleField field, string displayType, dynamic shapeHelper)
{
var value = field.Locale;
var viewModel = new LocaleFieldViewModel
{
Name = field.Name,
//地点数据由城市和地址构成,城市和地址用逗号分隔,如:上海,浦东新区张杨路XXX号
City = string.IsNullOrEmpty(value) ? "" : value.Substring(0, value.IndexOf(',')),
Address = string.IsNullOrEmpty(value) ? "" : value.Substring(value.IndexOf(',') + 1)
};
return ContentShape("Fields_MyCompany_Locale", // this is just a key in the Shape Table
() =>
shapeHelper.Fields_MyCompany_Locale( // Fields_MyCompany_Locale是一个动态类型,是视图Fields/MyCompany.Locale.cshtml的module,其中的Model属性,是LocaleFieldViewModel
Model: viewModel)
);
}
/// <summary>
/// 编辑画面显示
/// </summary>
/// <param name="part"></param>
/// <param name="field"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Editor(ContentPart part, Fields.LocaleField field, dynamic shapeHelper)
{
var value = field.Locale;
var viewModel = new LocaleFieldViewModel
{
Name = field.Name,
City = string.IsNullOrEmpty(value) ? "" : value.Substring(0, value.IndexOf(',')),
Address = string.IsNullOrEmpty(value) ? "" : value.Substring(value.IndexOf(',') + 1)
};
return ContentShape("Fields_MyCompany_Locale_Edit", // this is just a key in the Shape Table
() => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: viewModel, Prefix: GetPrefix(field, part)));
}
/// <summary>
/// 编辑画面Post
/// </summary>
/// <param name="part"></param>
/// <param name="field"></param>
/// <param name="updater"></param>
/// <param name="shapeHelper"></param>
/// <returns></returns>
protected override DriverResult Editor(ContentPart part, Fields.LocaleField field, IUpdateModel updater, dynamic shapeHelper)
{
var viewModel = new LocaleFieldViewModel();
if(updater.TryUpdateModel(viewModel, GetPrefix(field, part), null, null))
{
field.Locale = viewModel.City + "," + viewModel.Address;
}
return Editor(part, field, shapeHelper);
}
}
首先,和部件驱动器不一样是:字段驱动器需要继承于ContentFieldDriver,并指定泛型参数T为LocaleField类型。其次,还需要定义一个GetPrefix的静态方法,这样就可以使编辑画面中各字段的表单控件都有唯一的名称,不至于出现同名冲突。最后,我们还要定义几个方法,类似于Controller中的action,用于构建这个字段类型的显示画面,编辑画面,以及编辑画面Post处理。
写模板
有了驱动器和视图模型,就相当于有了MVC中的
Controller
和Model,现在我需要创建的是这个字段类型的View,也就是Orchard概念中的模板(Templates)。根据在驱动器中所定义的模板查找规则,我们需要在Views目录下创建一个Fields目录,并在该目录下创建一个MyCompany.Locale.cshtml文件作为该自定义字段的显示画面,代码如下:

<!--这个画面的Model是一个动态类型,这个动态类型是在相应的驱动器中定义的-->
<!--这个动态类型的其中一个属性Model是一个LocaleFieldViewModel类型的-->
<p class="text-field">
<span class="name">@Model.Model.Name:</span>
@Model.Model.City
@Model.Model.Address
</p
<!--这个动态类型的其中一个属性Model是一个LocaleFieldViewModel类型的-->
<p class="text-field">
<span class="name">@Model.Model.Name:</span>
@Model.Model.City
@Model.Model.Address
</p
接着,我们还需要在Views下面创建一个EditorTemplates目录,并在其下面创建一个Fields目录,然后再在里面在创建一个MyCompany.Locale.cshtml文件作为该自定义字段的编辑画面,代码如下:

@model MyCompany.LocaleField.ViewModels.LocaleFieldViewModel
<fieldset>
<label for="@Html.FieldIdFor(m => Model.City)">@Model.Name</label>
City:
@Html.DropDownListFor(m => m.City, this.Model.CityList)
Address:
@Html.TextBoxFor(m => m.Address)
</fieldset
<fieldset>
<label for="@Html.FieldIdFor(m => Model.City)">@Model.Name</label>
City:
@Html.DropDownListFor(m => m.City, this.Model.CityList)
Address:
@Html.TextBoxFor(m => m.Address)
</fieldset
最后我们别忘了添加一个Placement.info文件,没有这个文件我们刚刚定义的模版就无法在页面中显示,关于Placement.info文件的介绍可以查看《Understanding the placement.info File》,这里我们输入以下代码:

<Placement>
<Place Fields_MyCompany_Locale_Edit="Content:2.5"/>
<Match DisplayType="SummaryAdmin">
<Place Fields_MyCompany_Locale="Content"/>
</Match>
<Place Fields_MyCompany_Locale="Content:after"/>
</Placement
<Place Fields_MyCompany_Locale_Edit="Content:2.5"/>
<Match DisplayType="SummaryAdmin">
<Place Fields_MyCompany_Locale="Content"/>
</Match>
<Place Fields_MyCompany_Locale="Content:after"/>
</Placement
使用自定义字段
经过以上一些简单的工作后,一个新的字段类型就定义成功。下面我们定义一个新的活动类型,来使用我们刚创建好的地点字段类型。
首先,用管理员帐号登录并进入管理后台(Dashboard)点击Content菜单,选择Content Types选项卡,点击Create new type按钮。在出现的画面中输入以下内容:

然后,点击Create按钮,选择活动类型需要包含的Part,在这里我们就选择几个基本的部件:Route路由部件,用于显示活动标题和为此活动指定一个路由(Url);Common通用部件,用于显示活动发起人和发起时间;Containable集合部件,能让活动以列表形式在页面上呈现。
接下来,我们就在这个活动类型中添加两个字段:一个表示活动时间(When)用Orchard应用商店中提供的DateTimeField字段类型;一个表示活动地点(Where)用我们刚创建的LocaleField字段类型。

最后我们就可以在Content Types列表中找到刚创建的Event类型,点击Create New Evnet来创建新的活动了。
编辑画面
管理活动列表画面

活动详情画面
功能扩展
以上介绍的地点字段类型其实是一个非常简单的示例。但是自定义字段类型,却还是有更多可扩展的空间。比如,我们还可以在地点字段中增加一个国家的概念、省市的概念并实现其联动选择。而且国家、省市的数据还可以以xml的形式存储在文件中,这样比写在代码里要方便维护。我们也可以为这个自定义字段增加若干设置,如:是否显示国家选择,或默认显示那个国家,或是否允许获取当前用户的位置作为默认值,等等。关于如何添加自定义字段设置功能的介绍可参考《Creating a Custom Field Type》中的内容。
总结
通过本文,我们讲解了Orchard中字段(Field)的概念,并介绍了如何自定义一个新的字段类型,也介绍了字段的用法,同时还指明了字段和部件的区别。其实自定义字段看似简单其实功能却十分实用,通过组合自定义字段,我们可以方便的组织一张自定义表单。自定义字段的类型也是丰富多样的,本文就介绍了三种:纯文本字段、日期时间字段,地点字段,更多样的字段类型就要看大家的想象力了,套用一句话:没有做不到,只有想不到。
参考文档
示例代码
点击下载 (在Orchard后台Modules菜单的Installed选项卡中安装压缩包里的nupkg文件 )
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架