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