代码改变世界

如何扩展Orchard

2013-04-12 08:37  JustRun  阅读(...)  评论(...编辑  收藏

翻译自:
 
 
动态类型系统
Content item是Orchard中的原子, 比如blog post, pages, products, widgets
 
探索Content item原子
作为开发者,我们首先会想到Content item是一个类的实例(比如blog post类), 类中包含了property, method等. 实际的Content item不是由简单类型的属性等组成的, 而是由content part组成,这是Orchard中的重要概念.
 
一个blog post典型的由URL, title, date, rich text body, tags和comments这些parts组成. 但是这些parts不只是为blog post所用. 比如tag, rich text body也会被用在page上.
 
另外, CMS中的content type并不是一成不变的. Blog post以前是simple text, 但是很快就发展的更加复杂. 可以包含videos, podcasts或者image galleries. 甚至, 如果你在旅行, 你会在blog post中提供位置服务.
 
Content parts是解决变化的key, 需要位置服务, 那么只需要给blog post content item添加上mapping part. 这些不能有developer来做,而是admin来. 所以, 扩展不能是依赖于.net的一些类型的, 而是metadata-driven, 在运行时创建, 能够通过后台页面管理. 下图就是Orchard的content type编辑页.
 
 
 
Orchard扩展方法:
使用后台的Orchard Content type editor
界面能做的事情,也可以通过代码实现, 如下:
item.Weld(part);
 
上面的代码是在content item中动态的添加了part. 但这只是给instance添加了part, 如果我们想给该Content type都添加上part, 可以这样:
    ContentDefinitionManager.AlterTypeDefinition(
      "BlogPost", ctb => ctb.WithPart("MapPart")
    );
 
上面的方式也是实际上blog post content type的构建方式:
 
  ContentDefinitionManager.AlterTypeDefinition("BlogPost",
      cfg => cfg
        .WithPart("BlogPostPart")
        .WithPart("CommonPart", p => p
          .WithSetting("CommonTypePartSettings.ShowCreatedUtcEditor", "true"))
          .WithPart("PublishLaterPart")
          .WithPart("RoutePart")
          .WithPart("BodyPart")
      );
 
你可能注意到blog post content type中没有包含tags和comments. 的确是这样, orchard系统是通过其它方式来实现添加tags和comments的.
 
 
点菜
site有个初始化的xml文件, 在site setup的时候, 会根据这个配置来初始化content type, 下面是blog post的配置.
 
<BlogPost ContentTypeSettings.Draftable="True" TypeIndexing.Included="true">
  <CommentsPart />
  <TagsPart />
  <LocalizationPart />
</BlogPost>
 
 
创建Part
这是一个为添加keyword和description的part, 可以用来提高SEO友好. 具体使用的样子:
 
 
在上面填写的内容会生成到页面上:
    <meta content="Orchard is an open source Web CMS built on ASP.NET MVC."  name="description" />
    <meta content="Orchard, CMS, Open source" name="keywords" />
 
The Record
第一步是要解决如何存储这些信息到数据库. 严格的说, 并不是所有的part都需要record, 因为不是所有的part的数据都存在数据库中.
下面是我们用来存储keywords和description的MetaRecord:
public class MetaRecord : ContentPartRecord {
      public virtual string Keywords { get; set; }
      public virtual string Description { get; set; }
}
 
继承自ContentPartRecord不是必须的, 但是继承的话比较方便. 这个类有2个属性, 都是virtual的, 这是为了让orchard在运行的时候能够方便的生成proxy.
添加MetaHandler来实现数据库存储.
 
  public class MetaHandler : ContentHandler {
      public MetaHandler(IRepository<MetaRecord> repository) {
        Filters.Add(StorageFilter.For(repository));
      }
    }
 
对于数据库的修改migration
 
  public class MetaMigrations : DataMigrationImpl {
      public int Create() {
        SchemaBuilder.CreateTable("MetaRecord",
          table => table
            .ContentPartRecord()
            .Column("Keywords", DbType.String)
            .Column("Description", DbType.String)
        );
        ContentDefinitionManager.AlterPartDefinition(
          "MetaPart", cfg => cfg.Attachable());
        return 1;
      }
    }
 
上面的代码, 第一部分是创建表, 并且和record匹配.
第二部分是说明任何content type都可以从Admin UI上attach MetaPart
 
The Part Class
实际的Part是另外一个类, 继承自ContentPart
    public class MetaPart : ContentPart<MetaRecord> {
      public string Keywords {
        get { return Record.Keywords; }
        set { Record.Keywords = value; }
      }
      public string Description {
        get { return Record.Description; }
        set { Record.Description = value; }
      }
    }
 
这里的这个Part做成了MetaRecord的代理类. 如果没有代理MetaRecord的属性, 也可以通过父类ContentPart的Record属性访问到.
 
任何使用了MetePart的Content item, 都能够方便的访问到Keywords和Description属性
var metaKeywords = item.As<MetaPart>().Keywords;
 
Parts也应该是你具体实现一些behavior的地方, 比如, 一个组装机器的part, 可以添加方法或者property来列举它的部件或者计算总价.
 
至于Part添加的这些behavior如何和用户交互,这就是drivers要做的事情.
 
The Driver
Content item中的part都有机会参与到request的生命周期中和在asp.net MVC controller起作用. 但是只能在request的部分生命周期中起作用, 而不是全部.
Content part driver扮演这个角色, 它不是一个完整意义上的controller, 没有route匹配到它的方法.
它是为一些事先定义好的events写响应的方法, 比如Display, Editor.
Driver继承自ContentPartDriver.
 
protected override DriverResult Display(MetaPart part, string displayType, dynamic shapeHelper)
{
      var resourceManager = _wca.GetContext().Resolve<IResourceManager>();
      if (!String.IsNullOrWhiteSpace(part.Description)) {
        resourceManager.SetMeta(new MetaEntry {
          Name = "description",
          Content = part.Description
        });
      }
      if (!String.IsNullOrWhiteSpace(part.Keywords)) {
        resourceManager.SetMeta(new MetaEntry {
          Name = "keywords",
          Content = part.Keywords
        });
      }
      return null;
}

 
这个driver实际上不是典型的, 因为大部分的drivers result是rendering, 这里要把meta part呈现到head的meta标签中.
HTML的head section是shared resource, 所以有一些特殊处理.
Orchard提供了API访问这些shared resource. 这里我使用了resource manager来设置metatags. resource manager会呈现实际的tags.
 
之所以这个方法返回null是因为它不需要呈现任何东西到页面上. 一般driver的方法需要返回一个称做shape的dynamic type, 类似于asp.net mvc中的 view model.
它是一个非常flexible的对象, 你能够接上任何东西, 匹配对应的template来呈现它, 并不需要创建一个特定的view model类
 
protected override DriverResult Editor(MetaPart part, dynamic shapeHelper) {
      return ContentShape("Parts_Meta_Edit",
        () => shapeHelper.EditorTemplate(
          TemplateName: "Parts/Meta",
          Model: part,
          Prefix: Prefix));
}
 
Editor方法是用来呈现改part editor UI的. 比较典型的是返回一个特殊的shape对象用来, 组合成edition UI.
 
driver中的最后一个方法是处理从editor发送回来的post请求:
 
protected override DriverResult Editor(MetaPart part, IUpdateModel updater, dynamic shapeHelper) {
    updater.TryUpdateModel(part, Prefix, null, null);
    return Editor(part, shapeHelper);
}
 
这个方法中的代码调用了TryUpdateModel自动地更新part数据. 当更新完成, 再调用Editor方法来返回同样的editor shape.
 
Rendering Shapes
 
通过调用所有的parts的drivers, Orchard能够创建一个shapes tree, 在整个request中创建一个庞大的动态的view model.
下一个任务是搞清楚如何通过templates来呈现这些shapes. 它是通过每个shape的名字(如果是editor方法, 就是Parts_Meta_Edit)在整个系统定义范围内寻找模板文件, 比如当前的theme和module的views文件夹.
这是非常重要的扩展点, 因为它允许你能够重写系统的呈现, 只需要你在你的theme中放入正确name的template 文件即可.
 
在Views\EditorTemplates\Parts下的我的module文件夹下, 我写了一个Meta.cshtml文件
 
   
@using Vandelay.Industries.Models
    @model MetaPart
    <fieldset>
      <legend>SEO Meta Data</legend>
      <div class="editor-label">
        @Html.LabelFor(model => model.Keywords)
      </div>
      <div class="editor-field">
        @Html.TextBoxFor(model => model.Keywords, new { @class = "large text" })
        @Html.ValidationMessageFor(model => model.Keywords)
      </div>
      <div class="editor-label">
        @Html.LabelFor(model => model.Description)
      </div>
      <div class="editor-field">
        @Html.TextAreaFor(model => model.Description)
        @Html.ValidationMessageFor(model => model.Description)
      </div>
    </fieldset>
 
所有的都是content
在我讲到其它扩展的时候, 我想提醒一旦你懂了content item type系统, 你就懂得了Orchard中最重要的部分. 很多系统中重要的部分都是定义成了content item.
比如, a user是content item, 这样我们能够方便的添加properties到profile modules中. 我们也有widget content item, 能够呈现到theme定义的zones中. 这是orchard中如何显示search form, blog archives, tag clouds和其它的sidebar UI的方式.
但最使人意外应该是site本身也使用了content items. orchard中的 site settings也是content items, 这也是orchard的优势所在. 当你想添加自己的site settings的时候, 你所要做的就只是再添加一个新的part到site content type中, 然后添加一个admin edition UI. 一个统一扩展的content type系统是一个非常好的概念.
 
 
打包Extensions
Extensions都是以modules的方法分发的, 和实现相关的是以themes分发.
典型的Theme是一堆图片, stylessheets和templates, 打包放到Themes文件夹下. 一般也有一个theme.txt文件来定义一些metadata信息.
类似的, module是在Modules文件夹下. 它也是一个asp.net mvc area, 包含了一些配置. 比如, 它需要一个module.txt来说明module的metadata。
作为一个大网站的area, module需要能够处理好和一些share resources的关系. 比如, routes必须定义在一个继承IRouteProvider的类中. Orchard会取所有modules的route来构建整个网站的route. 类似地, modules能够实现INavigationProvider来创建新的admin menu.
 
有意思的是, module的代码通常不是以编译好的二进制发布(虽然这是可行的). 这是一个有意的安排, 我们鼓励对于module的修改, 当你从gallery下载一个module的时候,你可以动手修改来满足你的需求.
可以自己动手修改代码是一些PHP CMS的强大之处, 比如Drupal, wordpress, 所以我们也想在Orchard中提供这个. 当你下载一个module的时候,你下载了它的源代码, 然后动态编译. 当你改动了代码, 改动立马生效,然后module会重新编译.
 
 
依赖注入
目前,我们都在关注type system扩展, 因为这是orchard modules的主要功能, 但是orchard有很多其它的扩展点. 我想说一些使用这个框架的重要的原则.
一个重要的是在这个松散的module系统中做正确的事情. 在Orchard中, 机会所有低级别至上的都是module. 甚至modules都是由某个module来管理的. 如果你想这些modules之间不互相耦合, 那么你实现的时候,就要注意解耦合.
实现这个的方法是使用依赖注入. 当你需要使用一个class的时候, 你不要直接实例化它, 那样会导致你直接依赖于那个class. 你需要创建一个接口来抽象那个class, 然后用这个接口对象作为构造参数.
 
 
   
private readonly IRepository<ContentTagRecord> _contentTagRepository;
    private readonly IContentManager _contentManager;
    private readonly ICacheManager _cacheManager;
    private readonly ISignals _signals;
    public TagCloudService(
      IRepository<ContentTagRecord> contentTagRepository,
      IContentManager contentManager,
      ICacheManager cacheManager,
      ISignals signals)
      _contentTagRepository = contentTagRepository;
      _contentManager = contentManager;
      _cacheManager = cacheManager;
      _signals = signals;
    }
 
通过上面的例子, 你依赖于接口,而不是class, 不管这个接口的实现如何, 你都不需要修改你的代码.
具体使用这个接口的代码中不需要具体指明如何实例化. 这样就实现了控制反转, 具体提供什么实例则是有framework来决定的.
 
当然, 这不限于orchard定义的接口. 任何module都能够提供它们自己的扩展点, 只需要指明继承自IDependency, 非常简单.
 
一些其它的扩展点
这里, 我只是揭示一下扩展点. Orchard中有很多的接口可以用于扩展. 更加夸张的说, orchard什么都不是, 只是一个可以随意扩展的engine. 系统中所有的东西都包装成了扩展.
 
在结束本文之前, 我想说一下系统中一些非常有用的接口. 由于没有更大的篇幅来说的更深, 但是可以指明一个方向, 你可以深入代码中去看看这些接口的用法.
 
  • IWorkContextAccessor 可以让你的代码来访问当前request的work context. work context提供了HttpContext, current layout, site configuration, user, theme和culture信息. 它也提供了方法取得某个接口的具体实现.
  • IContentManager提供了你要查询和管理content items的所有东西
  • IRepository<T> 提供了方法访问更低级别的数据, 如果IContentManager不够用, 可以用这个.
  • IShapeTableProvider 是用来提供shape的. 你可以再shapes中拦截事件, 可以修改shapes, 添加members, 在layout上移动shapes.
  • IBackgroundTask, IScheduledTask and IScheduled­TaskHandler 是一些接口来实现诸如在后台执行一些时间长,或者定期执行的任务的
  • IPermissionsProvider 运行你自己的module来定义你module中的权限.