五、扩展Orchard(六) Creating a custom field type
Objectives
本文在Orchard中添加一个新field type,目标是有一个日期和时间编辑框,能加入到任何内容类型中,并且要很容易的选择一个日期或时间。
Creating a 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的好主意。
Creating a View Model
好的做法是创建一个或几个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; } } }
Creating Settings for the Field
在呈现中的灵活性,我们仅介绍了在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; } } }
Writing the Driver
不完全像part,field也有driver,用于在添加到内容类型时处理field的显示和编辑行为。
创建Drivers文件夹并在此下创建DateTimeFieldDriver.cs文件:
using System; using JetBrains.Annotations; using Orchard; using Orchard.ContentManagement; using Orchard.ContentManagement.Drivers; using Contrib.DateTimeField.Settings; using Contrib.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方法在屏幕上呈现编辑框。
Writing the Templates
我们需要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>
Managing the Field Settings
在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>
Updating the Project File
如果使用的是VS,这步可以跳过。
在CustomFields.csproj文件中加入下面信息:
<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" />
Adding the Style Sheet
创建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; }
Using the Field
要使用新的field,必须先启用Orchard.ContentTypes功能,同时要启用新建的DateTimeField功能。
下面我们管理 Manage content types管理面板,新建一个content type命名为“Event”,然后添加新的DateTime field。
field的settings能决定在前端的什么地方显示这个field,我们路过这步。下面我们添加 Route part因此我们的Event 有一个titlle,然后保存。
下面我们能点击Create Event新建event了,
在前端能查看到发布的event