二十四画生的Blog


        ——开始学习Orchard框架
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

在Orchard中创建一个自定义字段类型

Posted on 2011-07-28 18:01  二十四画生  阅读(4017)  评论(4编辑  收藏  举报
    在《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文件,代码如下: 
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); 
            }
        }
    }

 

一个字段类型需要被定为一个类,并且需要继承于ContentField,这样我们就可以利用字段类型的一些现有功能,比如数据存储。字段类型的数据都是以字符串的形式存储到数据库中的,我们通过定义这个类就可以将字段数据转换成我们想要的类型比如,日期类型,数字类型等。字段数据我们不用创建相应的表去存储,Orchard框架会替我们完成这项工作。这些字段数据,将以xml的形式存储到Orchard_Framework_ContentItemRecord表的Data字段中,如:


建立视图模型

由于一个地点字段是由城市和地址组成,所以本例我们还需要建立一个视图模型来作为视图的Model,而不是直接用LocaleField作为视图的Model。虽然这步工作不一定是必须的,但是建立一个视图模型却是一个好的习惯,这样可以使我们的代码看起来更清晰更易懂。在此项目中添加一个ViewModels目录,并在该目录下添加一个LocaleFieldViewModel.cs文件,输入以下代码: 
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 { getset; }
        
public string City { getset; }
        
public string Address { getset; }


        
/// <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文件,输入以下代码: 
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), nullnull)) 
            {
                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文件作为该自定义字段的显示画面,代码如下: 
Fields\MyCompany.Locale.cshtml
<!--这个画面的Model是一个动态类型,这个动态类型是在相应的驱动器中定义的-->
<!--这个动态类型的其中一个属性Model是一个LocaleFieldViewModel类型的-->
<class="text-field">
  
<span class="name">@Model.Model.Name:</span> 
  @Model.Model.City 
  @Model.Model.Address
</p

 

接着,我们还需要在Views下面创建一个EditorTemplates目录,并在其下面创建一个Fields目录,然后再在里面在创建一个MyCompany.Locale.cshtml文件作为该自定义字段的编辑画面,代码如下: 

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

 

最后我们别忘了添加一个Placement.info文件,没有这个文件我们刚刚定义的模版就无法在页面中显示,关于Placement.info文件的介绍可以查看《Understanding the placement.info File》,这里我们输入以下代码: 

Placement.info
<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文件 )