创建用户自定义内容字段类型

目标

本文在Orchard中添加一个新field type,目标是有一个日期和时间编辑框,能加入到任何内容类型中,并且要很容易的选择一个日期或时间。

日期和时间编辑框

创建一个模块(Module)

通过命令行输入:

codegen module CustomFields /IncludeInSolution:true

编辑module.txt文件:

Name: CustomFields
AntiForgery: enabled
Author: Me
Website: http://orcharddatetimefield.codeplex.com
Version: 0.6.1
OrchardVersion: 0.8.0
Description: A bunch of custom fields for use in your custom content types.
Features:
    CustomFields:
        Description: Custom fields for Orchard.
        Category: Fields
    DateTimeField:
        Description: A date and time field with a friendly UI.
        Category: Fields
        Dependencies: CustomFields, Orchard.jQuery, Common, Settings

我们定义了两个功能,这个模块最终会包含更多的fields。

Modeling the Field

新建一个CustomeFields文件夹,并在此文件夹下新建DateTimeField.cs文件:

using System;
using System.Globalization;
using Orchard.ContentManagement;
using Orchard.ContentManagement.FieldStorage;
using Orchard.Environment.Extensions;

namespace CustomFields.DateTimeField.Fields {
    [OrchardFeature("DateTimeField")]
    public class DateTimeField : ContentField {

        public DateTime? DateTime {
            get {
                var value = Storage.Get<string>();
                DateTime parsedDateTime;

                if (System.DateTime.TryParse(value, CultureInfo.InvariantCulture,
                    DateTimeStyles.AdjustToUniversal, out parsedDateTime)) {

                    return parsedDateTime;
                }

                return null;
            }

            set {
                Storage.Set(value == null ?
                    String.Empty :
                    value.Value.ToString(CultureInfo.InvariantCulture));
            }
        }
    }
}

field被定义为从ContentField继承的类,field会作为字符串进行存储,从字符串到日期能自动转换,但我们要做的,明确地给你如何做更复杂field的好方法。

创建一个视图模型

好的做法是创建一个或几个view models作为admin template中的model使用,用来呈现我们的field,在ViewModels文件夹下创建DateTimeFieldViewModel.cs文件:

namespace CustomFields.DateTimeField.ViewModels {

    public class DateTimeFieldViewModel {

        public string Name { get; set; }

        public string Date { get; set; }
        public string Time { get; set; }

        public bool ShowDate { get; set; }
        public bool ShowTime { get; set; }
    }
}

创建设置字段

在呈现中的灵活性,我们仅介绍了在view model中能暴露field的settings,这样,管理员就能在创建内容类型时配置field以适应实际的需要。

创建Settings文件夹,并在此下创建DataTimeFieldSettings.cs文件:

namespace CustomFields.DateTimeField.Settings {

    public enum DateTimeFieldDisplays {
        DateAndTime,
        DateOnly,
        TimeOnly
    }

    public class DateTimeFieldSettings {
        public DateTimeFieldDisplays Display { get; set; }
    }
}

写驱动器

不完全像part,field也有driver,用于在添加到内容类型时处理field的显示和编辑行为。

创建Drivers文件夹并在此下创建DateTimeFieldDriver.cs文件:

using System;
using JetBrains.Annotations;
using Orchard;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using CustomFields.DateTimeField.Settings;
using CustomFields.DateTimeField.ViewModels;
using Orchard.ContentManagement.Handlers;
using Orchard.Localization;

namespace CustomFields.DateTimeField.Drivers {
    [UsedImplicitly]
    public class DateTimeFieldDriver : ContentFieldDriver<Fields.DateTimeField> {
        public IOrchardServices Services { get; set; }

        // EditorTemplates/Fields/Custom.DateTime.cshtml
        private const string TemplateName = "Fields/Custom.DateTime";

        public DateTimeFieldDriver(IOrchardServices services) {
            Services = services;
            T = NullLocalizer.Instance;
        }

        public Localizer T { get; set; }

        private static string GetPrefix(ContentField field, ContentPart part) {
            // handles spaces in field names
            return (part.PartDefinition.Name + "." + field.Name)
                   .Replace(" ", "_");
        }

        protected override DriverResult Display(
            ContentPart part, Fields.DateTimeField field,
            string displayType, dynamic shapeHelper) {

            var settings = field.PartFieldDefinition.Settings
                                .GetModel<DateTimeFieldSettings>();
            var value = field.DateTime;

            return ContentShape("Fields_Custom_DateTime", // key in Shape Table
                    field.Name, // used to differentiate shapes in placement.info overrides, e.g. Fields_Common_Text-DIFFERENTIATOR
                    // this is the actual Shape which will be resolved
                    // (Fields/Custom.DateTime.cshtml)
                    s =>
                    s.Name(field.Name)
                     .Date(value.HasValue ?
                         value.Value.ToLocalTime().ToShortDateString() :
                         String.Empty)
                     .Time(value.HasValue ?
                         value.Value.ToLocalTime().ToShortTimeString() :
                         String.Empty)
                     .ShowDate(
                         settings.Display == DateTimeFieldDisplays.DateAndTime ||
                         settings.Display == DateTimeFieldDisplays.DateOnly)
                     .ShowTime(
                         settings.Display == DateTimeFieldDisplays.DateAndTime ||
                         settings.Display == DateTimeFieldDisplays.TimeOnly)
                );
        }

        protected override DriverResult Editor(ContentPart part,
                                               Fields.DateTimeField field,
                                               dynamic shapeHelper) {

            var settings = field.PartFieldDefinition.Settings
                                .GetModel<DateTimeFieldSettings>();
            var value = field.DateTime;

            if (value.HasValue) {
                value = value.Value.ToLocalTime();
            }

            var viewModel = new DateTimeFieldViewModel {
                Name = field.Name,
                Date = value.HasValue ?
                       value.Value.ToLocalTime().ToShortDateString() : "",
                Time = value.HasValue ?
                       value.Value.ToLocalTime().ToShortTimeString() : "",
                ShowDate =
                    settings.Display == DateTimeFieldDisplays.DateAndTime ||
                    settings.Display == DateTimeFieldDisplays.DateOnly,
                ShowTime =
                    settings.Display == DateTimeFieldDisplays.DateAndTime ||
                    settings.Display == DateTimeFieldDisplays.TimeOnly

            };

            return ContentShape("Fields_Custom_DateTime_Edit",
                () => shapeHelper.EditorTemplate(
                          TemplateName: TemplateName,
                          Model: viewModel,
                          Prefix: GetPrefix(field, part)));
        }

        protected override DriverResult Editor(ContentPart part,
                                               Fields.DateTimeField field,
                                               IUpdateModel updater,
                                               dynamic shapeHelper) {

            var viewModel = new DateTimeFieldViewModel();

            if (updater.TryUpdateModel(viewModel,
                                       GetPrefix(field, part), null, null)) {
                DateTime value;

                var settings = field.PartFieldDefinition.Settings
                                    .GetModel<DateTimeFieldSettings>();
                if (settings.Display == DateTimeFieldDisplays.DateOnly) {
                    viewModel.Time = DateTime.Now.ToShortTimeString();
                }

                if (settings.Display == DateTimeFieldDisplays.TimeOnly) {
                    viewModel.Date = DateTime.Now.ToShortDateString();
                }

                if (DateTime.TryParse(
                        viewModel.Date + " " + viewModel.Time, out value)) {
                    field.DateTime = value.ToUniversalTime();
                }
                else {
                    updater.AddModelError(GetPrefix(field, part),
                                          T("{0} is an invalid date and time",
                                          field.Name));
                    field.DateTime = null;
                }
            }

            return Editor(part, field, shapeHelper);
        }

        protected override void Importing(ContentPart part, Fields.DateTimeField field, 
            ImportContentContext context) {

            var importedText = context.Attribute(GetPrefix(field, part), "DateTime");
            if (importedText != null) {
                field.Storage.Set(null, importedText);
            }
        }

        protected override void Exporting(ContentPart part, Fields.DateTimeField field, 
            ExportContentContext context) {
            context.Element(GetPrefix(field, part))
                .SetAttributeValue("DateTime", field.Storage.Get<string>(null));
        }
    }
}

我们列举一下代码都做些什么事,看看它是如何工作的。

这个driver从ContentFieldDriver<DateTimeField>继承,为了被Orchard识别,从driver的代码中通过强类型访问field的值。

我们开始注入本地化的依赖(T属性),因此,我们可以通过代码创建本地化的字符串。

静态GetPrefix方法是传统定义的方法,用于在数据库中field类型实例创建唯一的列名。

有两个行为:Display and Editor,取得field的settings和value并创建shapes。

shapeHelper 提供了一些创建shapes的辅助方法,在这里能看到两个。第二个Editor方法就是一个,当管理表单被提交时调用。它的工作是映射提交给field的数据然后调用第一个Editor方法在屏幕上呈现编辑框。


编写模板

我们需要views以决定在管理面板和前端如何显示fields。

在Views文件夹下创建Fields和EditorTemplates目录,然后在EditorTemplates目录下创建Fields目录,在Views/Fields下创建CustomDateTime.cshtml:

<p class="text-field"><span class="name">@Model.Name:</span> 
    @if(Model.ShowDate) { <text>@Model.Date</text> } 
    @if(Model.ShowTime) { <text>@Model.Time</text> }
</p>

代码显示field名称:然后根据field的配置显示Date和Time.

在Views/EditorTemplates/Fields创建同样名字的文件CustomDateTime.cshtml:

@model CustomFields.DateTimeField.ViewModels.DateTimeFieldViewModel

@{
Style.Include("datetime.css");
Style.Require("jQueryUI_DatePicker");
Style.Require("jQueryUtils_TimePicker");
Style.Require("jQueryUI_Orchard");

Script.Require("jQuery");
Script.Require("jQueryUtils");
Script.Require("jQueryUI_Core");
Script.Require("jQueryUI_Widget");
Script.Require("jQueryUI_DatePicker");
Script.Require("jQueryUtils_TimePicker");
}

<fieldset>
  <label for="@Html.FieldIdFor(m => Model.Date)">@Model.Name</label>

  @if ( Model.ShowDate ) {
  <label class="forpicker"
    for="@Html.FieldIdFor(m => Model.Date)">@T("Date")</label>
  <span class="date">@Html.EditorFor(m => m.Date)</span>
  }

  @if ( Model.ShowTime ) {
  <label class="forpicker"
    for="@Html.FieldIdFor(m => Model.Time)">@T("Time")</label>
  <span class="time">@Html.EditorFor(m => m.Time)</span>
  }
  @if(Model.ShowDate) { <text>@Html.ValidationMessageFor(m=>m.Date)</text> }
  @if(Model.ShowTime) { <text>@Html.ValidationMessageFor(m=>m.Time)</text> }
</fieldset>

@using(Script.Foot()) {
<script type="text/javascript">
$(function () {
  $("#@Html.FieldIdFor(m => Model.Date)").datepicker();
  $("#@Html.FieldIdFor(m => Model.Time)").timepickr();
});
</script>
}

模板中有一些样式和脚本,还有根据field的配置定义了date和time的编辑框拾取器。

我们需要添加placement.info文件到模块的根目录中:

<Placement>
    <Place Fields_Custom_DateTime_Edit="Content:2.5"/>
    <Place Fields_Custom_DateTime="Content:2.5"/>
</Placement>

设置字段

在Settings文件夹下创建DateTimeFieldEditorEvents.cs文件

using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Builders;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentManagement.ViewModels;

namespace CustomFields.DateTimeField.Settings {
    public class DateTimeFieldEditorEvents : ContentDefinitionEditorEventsBase {

        public override IEnumerable<TemplateViewModel>
          PartFieldEditor(ContentPartFieldDefinition definition) {
            if (definition.FieldDefinition.Name == "DateTimeField") {
                var model = definition.Settings.GetModel<DateTimeFieldSettings>();
                yield return DefinitionTemplate(model);
            }
        }

        public override IEnumerable<TemplateViewModel> PartFieldEditorUpdate(
          ContentPartFieldDefinitionBuilder builder, IUpdateModel updateModel) {
            var model = new DateTimeFieldSettings();
            if (builder.FieldType != "DateTimeField") {
              yield break;
            }

            if (updateModel.TryUpdateModel(
              model, "DateTimeFieldSettings", null, null)) {
                builder.WithSetting("DateTimeFieldSettings.Display",
                                    model.Display.ToString());
            }

            yield return DefinitionTemplate(model);
        }
    }
}

这等同于driver,但是关于field settings的。第一个方法获取settings决定呈现的模板,第二个方法用提交表单的值更新model然后调用第一个方法。

field编辑框的模板由下面的DateTimeFieldSettings.cshtml定义,需要先在Views文件夹下创建DefinitionTemplates文件夹:

@model CustomFields.DateTimeField.Settings.DateTimeFieldSettings
@using CustomFields.DateTimeField.Settings;

<fieldset>
    <label for="@Html.FieldIdFor(m => m.Display)"
      class="forcheckbox">@T("Display options")</label>  
    <select id="@Html.FieldIdFor(m => m.Display)"
      name="@Html.FieldNameFor(m => m.Display)">
        @Html.SelectOption(DateTimeFieldDisplays.DateAndTime, 
          Model.Display == DateTimeFieldDisplays.DateAndTime,
          T("Date and time").ToString())
        @Html.SelectOption(DateTimeFieldDisplays.DateOnly,
          Model.Display == DateTimeFieldDisplays.DateOnly,
          T("Date only").ToString())
        @Html.SelectOption(DateTimeFieldDisplays.TimeOnly,
          Model.Display == DateTimeFieldDisplays.TimeOnly,
          T("Time only").ToString())
    </select> 

    @Html.ValidationMessageFor(m => m.Display)

</fieldset>

更新项目文件

如果使用的是VS,这步可以跳过。

在CustomFields.csproj文件中加入下面信息:

<ItemGroup>
  <Compile Include="Drivers\DateTimeFieldDriver.cs" />
  <Compile Include="Fields\DateTimeField.cs" />
  <Compile Include="Settings\DateTimeFieldEditorEvents.cs" />
  <Compile Include="Settings\DateTimeFieldSettings.cs" />
  <Compile Include="ViewModels\DateTimeFieldViewModel.cs" />
</ItemGroup>

添加样式文件

创建Styles文件夹并创建datetime.css文件:

html.dyn label.forpicker {
    display:none;
}

html.dyn input.hinted {
    color:#ccc;
    font-style:italic;
}
.date input{
    width:10em;
}
.time input {
    width:6em;
}

使用字段

要使用新的field,必须先启用Orchard.ContentTypes功能,同时要启用新建的DateTimeField功能。

下面我们管理 Manage content types管理面板,新建一个content type命名为“Event”,然后添加新的DateTime field。

22.PNG

field的settings能决定在前端的什么地方显示这个field,我们路过这步。下面我们添加 Route part因此我们的Event 有一个titlle,然后保存。

下面我们能点击Create Event新建event了

33.PNG

在前端能查看到发布的event

4.PNG

本实例源码下载:CustomFields.zip


Add a Comment