五、扩展Orchard(九) Creating 1-n and n-n relations
存在于Lists的一部分表或Lists中选择一部分是很常见的内容。例如:地址能通过预定义列表来选择国家或地区,这是一个1-n的关系。 一个n-n关系可能 例如是客户获得收益的商业清单。Orchard当然支持这些方案,本主题贯穿这些内容。
Building a 1-N Relationship
在这里我们要建立的模型存在于Address part,能附加到 例如一个Customer 内容类型。Address part有street address,a zip code,a city name and a state. state 就是我们将建立的1-n关系模型的states表。
Modeling the Address Part
Address part的代码:
using Orchard.ContentManagement;
namespace RelationSample.Models {
public class AddressPart : ContentPart<AddressPartRecord> {
public string Address {
get { return Record.Address; }
set { Record.Address = value; }
}
public string City {
get { return Record.City; }
set { Record.City = value; }
}
public StateRecord State {
get { return Record.StateRecord; }
set { Record.StateRecord = value; }
}
public string Zip {
get { return Record.Zip; }
set { Record.Zip = value; }
}
}
}
代理的所有属性
using Orchard.ContentManagement.Records;
namespace RelationSample.Models {
public class AddressPartRecord : ContentPartRecord {
public virtual string Address { get; set; }
public virtual string City { get; set; }
public virtual StateRecord StateRecord { get; set; }
public virtual string Zip { get; set; }
}
}
staterecord类:
namespace RelationSample.Models {
public class StateRecord {
public virtual int Id { get; set; }
public virtual string Code { get; set; }
public virtual string Name { get; set; }
}
}
这是我们刚才代码的展示
Creating the Database Tables and Part
能从一个migration创建我们刚才建立的model的数据库结构:
public int Create() {
SchemaBuilder.CreateTable("AddressPartRecord",
table => table
.ContentPartRecord()
.Column<string>("Address")
.Column<string>("City")
.Column<int>("StateRecord_Id")
.Column<string>("Zip")
);
SchemaBuilder.CreateTable("StateRecord",
table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<string>("Code", column => column.WithLength(2))
.Column<string>("Name")
);
ContentDefinitionManager.AlterPartDefinition("AddressPart",
builder => builder.Attachable());
return 1;
}
这个migration创建一个AddressPartRecord 表,是content part record(给我们content part需要的默认field),添加address, city, zip列,会自动映射到我们的record’s properties。
这里有意思的列是StateRecord_Id,你我们看到的,它的类型是StateRecord类的Id列类型,因为系统会根据关系自动识别这个foreign key并映射这个inteter值到StateRecord属性。重要的是,这里将代表关系的列名是“1”关系结束的列名,后跟一个下划线和名称列的“N”的关系结束。
StateRecord表中没什么特别的,它就是映射Id到主键和约束code列2个字符长度。
Populating the State Table
因为states列表相对来说是稳定的,我不创建他的content items(尽管这是个小工作),替代的,我在migration代码中用states list填充表,migration类有个state存储的引用:
private readonly IRepository<StateRecord> _stateRepository;
[...]
public RelationSampleDataMigration(IRepository<StateRecord> stateRepository) {
_stateRepository = stateRepository;
}
也有要加入数据库的state列表:
private readonly IEnumerable<StateRecord> _states =
new List<StateRecord> {
new StateRecord {Code = "AL", Name = "Alabama"},
new StateRecord {Code = "AK", Name = "Alaska"},
[...]
new StateRecord {Code = "WS", Name = "Western Australia"},
};
数据表的填充用下面代码完成:
public int UpdateFrom1() {
if (_stateRepository == null)
throw new InvalidOperationException("Couldn't find state repository.");
foreach (var state in _states) {
_stateRepository.Create(state);
}
return 2;
}
The Address Part Handler
Address part的handler相当无趣,仅是到存储的连接:
using Orchard.Data;
using Orchard.ContentManagement.Handlers;
using RelationSample.Models;
namespace RelationSample.Handlers {
public class AddressPartHandler : ContentHandler {
public AddressPartHandler(IRepository<AddressPartRecord> repository) {
Filters.Add(StorageFilter.For(repository));
}
}
}
The Address Part Driver
driver挺有意思,它为渲染准备shapes并处理管理表单的回传:
using JetBrains.Annotations;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using RelationSample.Models;
using RelationSample.Services;
using RelationSample.ViewModels;
namespace RelationSample.Drivers {
[UsedImplicitly]
public class AddressPartDriver : ContentPartDriver<AddressPart> {
private readonly IAddressService _addressService;
private const string TemplateName = "Parts/Address";
public AddressPartDriver(IAddressService addressService) {
_addressService = addressService;
}
protected override string Prefix {
get { return "Address"; }
}
protected override DriverResult Display(
AddressPart part,
string displayType,
dynamic shapeHelper) {
return ContentShape("Parts_Address",
() => shapeHelper.Parts_Address(
ContentPart: part,
Address: part.Address,
City: part.City,
Zip: part.Zip,
StateCode: part.State.Code,
StateName: part.State.Name));
}
protected override DriverResult Editor(
AddressPart part,
dynamic shapeHelper) {
return ContentShape("Parts_Address_Edit",
() => shapeHelper.EditorTemplate(
TemplateName: TemplateName,
Model: BuildEditorViewModel(part),
Prefix: Prefix));
}
protected override DriverResult Editor(
AddressPart part,
IUpdateModel updater,
dynamic shapeHelper) {
var model = new EditAddressViewModel();
updater.TryUpdateModel(model, Prefix, null, null);
if (part.ContentItem.Id != 0) {
_addressService.UpdateAddressForContentItem(
part.ContentItem, model);
}
return Editor(part, shapeHelper);
}
private EditAddressViewModel BuildEditorViewModel(AddressPart part) {
var avm = new EditAddressViewModel {
Address = part.Address,
City = part.City,
Zip = part.Zip,
States = _addressService.GetStates()
};
if (part.State != null) {
avm.StateCode = part.State.Code;
avm.StateName = part.State.Name;
}
return avm;
}
}
}
当在前端显示,我们准备了Parts_Address,有到原来part的引用(尽管这不是必须的),所有part的属性清空并state的代码和名称都可用。
当在管理面板中,我们使用静态类型的view model创建shapes,因为当使用form fields和mvc model binding时这些仍旧很容易使用。这个view model,就像这个在前端使用的shape,有一个我们需要显示的空view,但它也有一个可用states全部的列表,view呈现state drop-down list:
using System.Collections.Generic;
using RelationSample.Models;
namespace RelationSample.ViewModels {
public class EditAddressViewModel {
public string Address { get; set; }
public string City { get; set; }
public string StateCode { get; set; }
public string StateName { get; set; }
public string Zip { get; set; }
public IEnumerable<StateRecord> States { get; set; }
}
}
最后要注意的是driver中editor重写了处理回传的handles,仅使用updater.TryUpdateModel(),足够使提交表单值加到view model上。
The Address Service Class
address service class依赖state存储以使能查询states的完整列表。它的其它方法,UpdateAddressForContentItem,复制EditAddressViewModel加入到一个content item的address content part。它也从model使用state code在state 存储查询state record。
using System.Collections.Generic;
using System.Linq;
using Orchard;
using Orchard.ContentManagement;
using Orchard.Data;
using RelationSample.Models;
using RelationSample.ViewModels;
namespace RelationSample.Services {
public interface IAddressService : IDependency {
void UpdateAddressForContentItem(
ContentItem item, EditAddressViewModel model);
IEnumerable<StateRecord> GetStates();
}
public class AddressService : IAddressService {
private readonly IRepository<StateRecord> _stateRepository;
public AddressService(IRepository<StateRecord> stateRepository) {
_stateRepository = stateRepository;
}
public void UpdateAddressForContentItem(
ContentItem item,
EditAddressViewModel model) {
var addressPart = item.As<AddressPart>();
addressPart.Address = model.Address;
addressPart.City = model.City;
addressPart.Zip = model.Zip;
addressPart.State = _stateRepository.Get(
s => s.Code == model.StateCode);
}
public IEnumerable<StateRecord> GetStates() {
return _stateRepository.Table.ToList();
}
}
}
Building the Views
The Front-End View
part的前端view易理解,就象它仅显示shape的属性
<p class="adr">
<div class="street-address">@Model.Address</div>
<span class="locality">@Model.City</span>,
<span class="region">@Model.StateCode</span>
<span class="postal-code">@Model.Zip</span>
</p>
The Editor View
@model RelationSample.ViewModels.EditAddressViewModel
<fieldset>
<legend>Address</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Address, T("Street Address"))
</div>
<div class="editor-field">
@Html.TextAreaFor(model => model.Address)
@Html.ValidationMessageFor(model => model.Address)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.City, T("City"))
</div>
<div class="editor-field">
@Html.TextBoxFor(model => model.City)
@Html.ValidationMessageFor(model => model.City)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.StateCode, T("State"))
</div>
<div class="editor-field">
@Html.DropDownListFor(model => model.StateCode,
Model.States.Select(s => new SelectListItem {
Selected = s.Code == Model.StateCode,
Text = s.Code + " " + s.Name,
Value = s.Code
}),
"Choose a state...")
@Html.ValidationMessageFor(model => model.StateCode)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Zip, T("Zip"))
</div>
<div class="editor-field">
@Html.TextBoxFor(model => model.Zip)
@Html.ValidationMessageFor(model => model.Zip)
</div>
</fieldset>
DropDownListFor 方法为展示属性用一个表达式,SelectListItems 的列表建立在完整的states列表之上并且我们知道当前地些的state(注意selected的表达式)
The Placement File
最终,我们需要placement 文件决定我们part的位置
<Placement>
<Place Parts_Address_Edit="Content:10"/>
<Place Parts_Address="Content:10"/>
</Placement>
Using the Address Part
现在我们能在features管理面板启用 RelationSample功能,然后到content types创建一个新Customer内容类型,添加Common和Address parts,
现在,New菜单下会有Customer项了,让我们新建一个customer
在前端可能显示成这样
Building an N-N Relationship
建立n-n关系与建立1-n关系的原理是一样的,主要的不同是在part record上有一个外键,我们有一个关于关系的中间对象到records有两个外键。这当然是接近关系数据库的完成方式。

浙公网安备 33010602011771号