五、扩展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有两个外键。这当然是接近关系数据库的完成方式。

posted @ 2012-03-22 17:05  commanderss  阅读(944)  评论(1编辑  收藏  举报