Content Part Editors - Beyond the Basics

  1. Create a record class 
  2. Create a content part
  3. Create a content part driver
  4. Create a content part handler
  5. Create a content part editor template
  6. Create a content part display template
  7. Update Placement.info to place the content part editor shape and content part display shape

In this post, we'll walk through this process and handle some advanced scenario's where we'll use a crafty combination of LazyField, ShapeDisplayingEvents and MVC Editor Templates.

A Scenario

Imagine that you have a Customer which in turn has two Addresses: ShippingAddress and BillingAddress. These addresses or content items, just like the customer is.
Next, you want to create a content part editor where the user can edit the Customer fields as well as the fields of the two addresses.
Furthermore, the Address has a Country navigation property, where Country is a simple entity class, not a content item.
We want to enable the user to pick one of the available countries using a dropdown list.

Basically, we want to give the user the following edit screen:

Now, I'll agree that this edit screen isn't the most exciting screen in the world, and could surely use some CSS love, but other than that there are a couple of interesting things to note about it:

  • There are 2 custom content types: Customer and Address
  • The Customer edit screen renders 3 editors: one for the Customer and two for the Addresses
  • The 2 addresses use a dropdown list for country selection


In this demo, we will implement this editor and do the following:

  • We'll create a Country entity, AddressPart and CustomerPart.
  • We'll use LazyFields to bind 2 address content items to 1 customer
  • We'll create a classic Country MVC editor template that renders a dropdownlist. We will use ShapeDisplayingEvents to provide the list of countries.

Creating the module

First of all, we need to create a module. Please see the docs on how to do that or check out my webshop tutorial for a step-by-step guide on creating a module.

Creating the Model

Once we have an empty module, we can start creating the domain model.
We'll add 3 models: Customer, Address and Country. Customer and Address will be part of content items, so we will create both a CustomerPart and CustomerPartRecord class. The same goes for Address: we'll create an AddressPart and AddressPartRecord.
Since Country will not be used to define a new content type, all we need is a Country class.

Models/Country.cs:

namespace LazyFieldDemo.Models {
    public class Country {
        public virtual int Id { get; set; }
        public virtual string Name { get; 

set; }
        public virtual string Code { get; 

set; }
    }
}

Notice that we don't have to suffix the type with "Record" in order to make it persistable. All that is required for a class to be persistable is that it needs to be part of a Models namespace and that its members are virtual.
Lately I tend to use the following naming conventions:

- If an entity is not a content part, just use the entity name (e.g. Country).
- If an entity represents the record of a content part, suffix the type with "PartRecord" (e.g. CustomerPartRecord)
- If and entity represents a content part, suffix the type with "Part" (e.g. CustomerPart)

 The last two items are well established standards within the Orchard community, however I'm not sure about the first one. Some use "CountryRecord" while others use "Country".

Models/AddressPart.cs:

using System.Linq;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Aspects;
using Orchard.ContentManagement.Records;

namespace LazyFieldDemo.Models {
    public class AddressPart : ContentPart<AddressPartRecord>, ITitleAspect {

        public string AddressLine1 

{
            get { return Record.AddressLine1; }
            set { Record.AddressLine1 = value; }
        }

        public string AddressLine2 

{
            get { return Record.AddressLine2; }
            set { Record.AddressLine2 = value; }
        }

        public string Zipcode {
            get { return Record.Zipcode; }
            set { Record.Zipcode = 

value; }
        }

        public string City {
            get { return Record.City; }
            set { Record.City = 

value; }
        }

        public Country Country {
            get { return Record.Country; }
            set { Record.Country = 

value; }
        }

        public string Title {
            get { return string.Join(", ", new[]{AddressLine1, AddressLine2, Zipcode, City, Country != 

null ? Country.Name : ""}.Where(x => !string.IsNullOrWhiteSpace(x)) ); }
        }
    }

    public class AddressPartRecord 

: ContentPartRecord {
        public virtual string AddressLine1 { get; set; }
        public virtual string AddressLine2 { get; set; }
        public virtual string Zipcode { get; 

set; }
        public virtual string City { get; 

set; }
        public virtual Country 

Country { get; set; }
    }
}

Notice that AddressPart implements the ITitleAspect interface. This allows us to customize the displayed text by building it up based on the non-empty members of the address.
This also means that we will not (and should not) attach the TitlePart to our Address content type., since we're taking care of the title ourselves. 

Also notice that we implemented the Country property as a navigation property. This enables us to access the Country without having to manually query the database (which we would have to do if we implemented a CountryId).
Orchard will take care of the mapping for us (provided that we setup the table correctly, which we'll get to in a moment). 

Models/CustomerPart.cs:

using Orchard.ContentManagement;
using Orchard.ContentManagement.Aspects;
using Orchard.ContentManagement.Records;
using Orchard.ContentManagement.Utilities;

namespace LazyFieldDemo.Models {
    public class CustomerPart : ContentPart<CustomerPartRecord>, ITitleAspect {

        internal LazyField<AddressPart> ShippingAddressField = new LazyField<AddressPart>();
        internal LazyField<AddressPart> BillingAddressField = new LazyField<AddressPart>();

        public string FirstName 

{
            get { return Record.FirstName; }
            set { Record.FirstName = 

value; }
        }

        public string LastName {
            get { return Record.LastName; }
            set { Record.LastName = 

value; }
        }

        public AddressPart ShippingAddress {
            get { return ShippingAddressField.Value; }
            set { ShippingAddressField.Value = value; }
        }

        public AddressPart BillingAddress {
            get { return BillingAddressField.Value; }
            set { BillingAddressField.Value = value; }
        }

        public string Title {
            get { return string.Format("{0} {1}", 

FirstName, LastName); }
        }
    }

    public class CustomerPartRecord 

: ContentPartRecord {
        public virtual string FirstName { get; 

set; }
        public virtual string LastName { get; 

set; }
        public virtual int? ShippingAddressId { get; set; }
        public virtual int? BillingAddressId { get; set; }
    }
}

What's interesting about CustomerPart is how we implemented the ShippingAddress and BillingAddress properties. Since Address is a content type, we cannot simply use Orchard's lazy loading mechanism that it inherits from NHibernate.
Instead, we will implement it ourselves using the LazyField<T> class. To learn more about LazyField<T>, please see: http://skywalkersoftwaredevelopment.net/orchard- development/lazyfield-t

Later on we'll see how to setup the lazy fields - we need to do some migrations first.

Creating the Migrations

Now that we have our models in place, we are ready to setup the database so we can store instances of our models.
To do so we'll create a Migration class:

Migrations/CrmMigrations.cs:

using System.Data;
using LazyFieldDemo.Models;
using Orchard.ContentManagement.MetaData;
using Orchard.Core.Contents.Extensions;
using Orchard.Data;
using Orchard.Data.Migration;

namespace LazyFieldDemo.Migrations {
    public class CrmMigrations : DataMigrationImpl {
        private readonly 

IRepository<Country> _countryRepository;
        public CrmMigrations(IRepository<Country> countryRepository) {
            _countryRepository = countryRepository;
        }

        public int Create() {
             SchemaBuilder.CreateTable("Country", table 

=> table
                 .Column<int>("Id", c => c.Identity ().PrimaryKey())
                 .Column<string>("Code", c => c.WithLength (3).WithType(DbType.StringFixedLength))
                 .Column<string>("Name", c => c.WithLength (50)));

             SchemaBuilder.CreateTable("AddressPartRecord", 

table => table
                 .ContentPartRecord()
                 .Column<string>("AddressLine1", c => c.WithLength (256))
                 .Column<string>("AddressLine2", c => c.WithLength (256))
                 .Column<string>("Zipcode", c => c.WithLength (20))
                 .Column<string>("City", c => c.WithLength (50))
                 .Column<int>("Country_Id", c => c.Nullable ()));

             SchemaBuilder.CreateTable("CustomerPartRecord", table => table
                 .ContentPartRecord()
                 .Column<string>("FirstName", c => c.WithLength (50))
                 .Column<string>("LastName", c => c.WithLength (50))
                 .Column<int>("ShippingAddressId", c => c.Nullable ())
                 .Column<int>("BillingAddressId", c => c.Nullable ()));

             ContentDefinitionManager.AlterPartDefinition("AddressPart", part => part.Attachable(false));
             ContentDefinitionManager.AlterPartDefinition("CustomerPart", part => part.Attachable(false));

             ContentDefinitionManager.AlterTypeDefinition("Address", type => type
                 .WithPart("CommonPart")
                 .WithPart("AddressPart")
                 .Draftable(false)
                 .Creatable());

             ContentDefinitionManager.AlterTypeDefinition("Customer", type => type
                 .WithPart("CommonPart")
                 .WithPart("CustomerPart")
                 .Draftable(false)
                 .Creatable());

             _countryRepository.Create(new Country { Name = "United States", 

Code = "USA"});
             _countryRepository.Create(new Country { Name = "France", Code = "FRA" });
             _countryRepository.Create(new Country { Name = "Belgium", Code 

= "BEL" });
             _countryRepository.Create(new Country { Name = "Germany", Code 

= "DEU" });
             _countryRepository.Create(new Country { Name = "Netherlands", Code 

= "NLD" });
             _countryRepository.Create(new Country { Name = "Poland", Code = "POL" });
             _countryRepository.Create(new Country { Name = "Spain", Code = "ESP" });
             _countryRepository.Create(new Country { Name = "Hungary", Code 

= "HUN" });
             _countryRepository.Create(new Country { Name = "Canada", Code = "CAN" });
             _countryRepository.Create(new Country { Name = "Croatia", Code 

= "HRV" });
             _countryRepository.Create(new Country { Name = "Russian Federation", Code = "RUS" });
             _countryRepository.Create(new Country { Name = "Mozambique", Code 

= "MOZ" });

             return 1;
         }
    }
}

There are a couple of interesting things to note about this migration:

  • The AddressPartRecord table has a column called Country_Id. Remember that the AddressPartRecord class doesn't have a property with that name - instead it has a property called Country of type Country. In order to map navigation properties to a table column, the convention dictates that the column name in question needs to have the same name as the navigation property, followed by "_Id".
  • At the end of the Create step we're seeding the CountryRecord table using the generic IRepository<T> interface (where we substituted Country for T). This is a simple way to seed the database with sample data. Alternatively, you could create a Command like "country create Netherlands, USA, Belgium" to create multiple countries in one batch. This command could be executed as part of a recipe. We'll see how to do this in another post.

Creating the Drivers

The next step is creating the drivers for the AddressPart and the CustomerPart:

Drivers/AddressPartDriver.cs:

using LazyFieldDemo.Helpers;
using LazyFieldDemo.Models;
using LazyFieldDemo.ViewModels;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.Handlers;
using Orchard.Data;

namespace LazyFieldDemo.Drivers {
    public class AddressPartDriver 

: ContentPartDriver<AddressPart> {
        private readonly 

IRepository<Country> _countryRepository;
        public AddressPartDriver(IRepository<Country> countryRepository) {
            _countryRepository = countryRepository;
        }

        protected override string Prefix {
            get { return "Address"; }
        }

        protected override 

DriverResult Display(AddressPart part, string displayType, dynamic shapeHelper) {
            return ContentShape("Parts_Address", 

() => shapeHelper.Parts_Address());
        }

        protected override 

DriverResult Editor(AddressPart part, dynamic shapeHelper) {
            var viewModel = AddressViewModel.Map(part);
            return ContentShape("Parts_Address_Edit", () => shapeHelper.EditorTemplate (TemplateName: "Parts/Address", Model: viewModel, Prefix: Prefix));
        }

        protected override 

DriverResult Editor(AddressPart part, IUpdateModel updater, dynamic shapeHelper) {
            var viewModel = new AddressViewModel();
            if(updater.TryUpdateModel(viewModel, Prefix, null, null)) {
                AddressViewModel.Map(viewModel, part, _countryRepository);
            }
            return Editor(part, shapeHelper);
        }

        protected override void Exporting(AddressPart part, ExportContentContext context) {
            context.Element(part.PartDefinition.Name).SetAttributeValue("AddressLine1", part.AddressLine1);
            context.Element(part.PartDefinition.Name).SetAttributeValue("AddressLine2", part.AddressLine2);
            context.Element(part.PartDefinition.Name).SetAttributeValue("Zipcode", part.Zipcode);
            context.Element(part.PartDefinition.Name).SetAttributeValue("City", part.City);
            context.Element(part.PartDefinition.Name).SetAttributeValue("CountryCode", part.Country != null ? part.Country.Code : "");
        }

        protected override void Importing(AddressPart part, ImportContentContext context) {
            context.ImportAttribute(part.PartDefinition.Name, "AddressLine1", x 

=> part.AddressLine1 = x);
            context.ImportAttribute(part.PartDefinition.Name, "AddressLine2", x 

=> part.AddressLine2 = x);
            context.ImportAttribute(part.PartDefinition.Name, "Zipcode", x => part.Zipcode = x);
            context.ImportAttribute(part.PartDefinition.Name, "City", x => part.City = x);
            context.ImportAttribute(part.PartDefinition.Name, "CountryCode", x 

=> part.Country = _countryRepository.Get(c => c.Code == x));
        }
    }
}

 

Notice that we're using a Map method of a class called AddressViewModel. We'll define that class in a minute, so please ignore any warnings / errors for now.
Notice also that we are implementing the import/export support for our AddressPart. Doing so is good practice and very easy to do.

Exporting a part basically works as follows: For a given element (typically named after the Part definition name, e.g. "AddressPart"), set an attribute value to a certain value (e.g. set attribute "AddressLine1" to the value of the AddressLine1 property of the part being exported).

Importing is just the other way around: given an element, read in a specific attribute value and assign it to the corresponding property of the part being imported. Notice that we're usng the ImportAttribute method to make it easy: using this method our import routine won't throw an exception if a specified element and or attribute does not exist or if its value is null:

Orchard.Framework/ContentManagement/Handlers/ImportContentContext.cs (snippet):

public void ImportAttribute(string 

elementName, string attributeName, Action<string> value, Action empty)  {
            var importedText = Attribute(elementName, attributeName);
            if (importedText != null) {
                try {
                    value(importedText);
                }
                catch {
                    empty();
                }
            }
            else {
                empty();
            }
        }

The CustomerPartDriver looks like this:


Drivers/CustomerPartDriver.cs:

using LazyFieldDemo.Helpers;
using LazyFieldDemo.Models;
using LazyFieldDemo.ViewModels;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Data;

namespace LazyFieldDemo.Drivers {
    public class CustomerPartDriver 

: ContentPartDriver<CustomerPart> {
        private readonly 

IContentManager _contentManager;
        private readonly 

IRepository<Country> _countryRepository;

        public CustomerPartDriver(IContentManager 

contentManager, IRepository<Country> countryRepository) {
            _contentManager = contentManager;
            _countryRepository = countryRepository;
        }

        protected override string Prefix {
            get { return "Customer"; }
        }

        protected override 

DriverResult Display(CustomerPart part, string displayType, dynamic shapeHelper) {
            return Combined(
                ContentShape("Parts_Customer",  () => shapeHelper.Parts_Customer(
                    ShippingAddress: _contentManager.BuildDisplay(part.ShippingAddress, displayType),
                    BillingAddress: _contentManager.BuildDisplay(part.BillingAddress, displayType))),
                ContentShape("Parts_Customer_SummaryAdmin", () => 

shapeHelper.Parts_Customer_SummaryAdmin(
                    ShippingAddress: _contentManager.BuildDisplay(part.ShippingAddress, displayType),
                    BillingAddress: _contentManager.BuildDisplay(part.BillingAddress, displayType)))
            );
        }

        protected override 

DriverResult Editor(CustomerPart part, dynamic shapeHelper) {
            var viewModel = new CustomerViewModel {
                FirstName = part.FirstName,
                LastName = part.LastName,
                ShippingAddress = AddressViewModel.Map(part.ShippingAddress),
                BillingAddress = AddressViewModel.Map(part.BillingAddress),
            };
            return ContentShape("Parts_Customer_Edit", () => shapeHelper.EditorTemplate (TemplateName: "Parts/Customer", Model: viewModel, Prefix: Prefix));
        }

        protected override 

DriverResult Editor(CustomerPart part, IUpdateModel updater, dynamic shapeHelper) {
            var viewModel = new CustomerViewModel();
            if(updater.TryUpdateModel(viewModel, Prefix, null, null)) {
                part.FirstName = viewModel.FirstName.TrimSafe() ;
                part.LastName = viewModel.LastName.TrimSafe() ;
                AddressViewModel.Map(viewModel.ShippingAddress, part.ShippingAddress, _countryRepository);
                AddressViewModel.Map(viewModel.BillingAddress, part.BillingAddress, _countryRepository);
            }
            
            return Editor(part, shapeHelper);
        }
    }
}

Now this is a more interesting driver: notice that the Display method returns a shape that in turn contains 2 other shapes: one for the ShippingAddress and another for the BillingAddress. We are creating these shapes using the ContentManager.BuildDisplay method. ContentManager.BuildDisplay essentially invokes the drivers for the specified content item (to be more precise, you specify an instance of IContent, which in turn has a property called ContentItem).

We'll see in a moment how we to render this complex shape.
Also notice again the use of the AddressViewModel.Map method. It basically maps properties from AddressPart to a yet to be defined AddressViewModel.
We're also using a helper method called TrimSafe. This is a small extension method that looks like this:

Helpers/StringHelpers.cs:

namespace LazyFieldDemo.Helpers {
    public static class StringHelpers {
         public static string TrimSafe(this string s) {
             return s != null ? s.Trim() : "";
         }
    }
}

Creating the Content Handlers

In order for the content manager to be able to load and persist our AddressPart and CustomerPart, we need to register a so-called StorageProvider. We'll do that using a content handler:

Handlers/AddressPartHandler.cs:

using LazyFieldDemo.Models;
using Orchard.ContentManagement.Handlers;
using Orchard.Data;

namespace LazyFieldDemo.Handlers {
    public class AddressPartHandler 

: ContentHandler {
        public AddressPartHandler(IRepository<AddressPartRecord> repository) {
            Filters.Add(StorageFilter.For(repository)) ;
        }
    }
}

Handlers/CustomerPartHandler.cs:

using LazyFieldDemo.Models;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.Data;

namespace LazyFieldDemo.Handlers {
    public class CustomerPartHandler 

: ContentHandler {
        private readonly 

IContentManager _contentManager;

        public CustomerPartHandler(IRepository<CustomerPartRecord> repository, 

IContentManager contentManager) {
            _contentManager = contentManager;
            Filters.Add(StorageFilter.For(repository)) ;
            OnActivated<CustomerPart> (LazyLoadHandlers);
        }

        private void 

LazyLoadHandlers(ActivatedContentContext context, CustomerPart part) {
            part.ShippingAddressField.Loader(address => {
                var addressContentItem = part.Record.ShippingAddressId != null ? _contentManager.Get<AddressPart>(part.Record.ShippingAddressId.Value) : default(AddressPart);
                if(addressContentItem == null) {
                    addressContentItem = _contentManager.Create<AddressPart>("Address", 

VersionOptions.Published);
                    part.Record.ShippingAddressId = addressContentItem.Id;
                }
                return addressContentItem;
            });

            part.ShippingAddressField.Setter(address => {
                part.Record.ShippingAddressId = address != null ? address.Id : default(int?);
                return address;
            });

            part.BillingAddressField.Loader (address => {
                var addressContentItem = part.Record.BillingAddressId != null ? _contentManager.Get<AddressPart>(part.Record.BillingAddressId.Value) : default(AddressPart);
                if (addressContentItem == null) {
                    addressContentItem = _contentManager.Create<AddressPart>("Address", 

VersionOptions.Published);
                    part.Record.BillingAddressId = addressContentItem.Id;
                }
                return addressContentItem;
            });

            part.BillingAddressField.Setter (address => {
                part.Record.BillingAddressId = address != null ? address.Id : default(int?);
                return address;
            });
        }
    }
}

What we're doing in the CustomerPartHandler are the following things:

  1. We're adding a StorageFilter for the CustomerPartRecord class.
  2. We are implementing an event handler called LazyLoadHandlers for the OnActivated "event", which gets raised the moment a content item is being loaded that has the CustomerPart attached.
  3. Inside the LazyLoadHandlers method, we are setting up the LazyFields we defined earlier in the CustomerPart. LazyField<T> contains a method to setup the loader of the field and a method to setup the setter of the field. What we do in the loader and setter is entirely up to us. In our case, what we want to do is that whenever the ShippingAddress is requested, we'll try and see if we can find one based on the ShippingAddressId of the CustomerPartRecord instamce. If we find it, that's the item we'll return (the LazyField will keep a referende to this instance, so the next time we invoke ShippingAddress, it will return that instance immediately). If we can't find it, we'll just create a new Address content item on the fly and update the ShippingAddressId of the CustomerPartRecord instance.
    The setter is even simpler: all we do is set the ShippingAddressId to the Id of the specified address item. If the specified address item is null, we'll just set the ShippingAddressId to null as well. 
  4. The same goes for BillingAddress. 

Setting up lazy fields like this is very powerful and eases the use of our CustomerPart: whenever we access the ShippingAddress property, we can be sure that we get a valid content item. How we get this content item is totally up to us. Even though we cannot inject dependencies into our content parts, implementing LazyFields makes it easy to do it in another way by means of the content handler.

Creating the ViewModels

As you've seen we are using view model classes in our drivers instead of using the content parts directly as our models for our views. The reason for that is simple: in our view templates we need more information than just the content part data.
The view models look like this:

ViewModels/AddressViewModel.cs:

using System.ComponentModel.DataAnnotations;
using LazyFieldDemo.Helpers;
using LazyFieldDemo.Models;
using Orchard.Data;

namespace LazyFieldDemo.ViewModels {
    public class AddressViewModel 

{
        public string AddressLine1 

{ get; set; }
        public string AddressLine2 

{ get; set; }
        public string Zipcode { get; set; }
        public string City { get; set; }

        [DataType("Country")] 
        public int? CountryId { get; set; }

        public static 

AddressViewModel Map(AddressPart source)  {
            return Map(source, new AddressViewModel());
        }

        public static 

AddressViewModel Map(AddressPart source, AddressViewModel target) {
            target.AddressLine1 = source.AddressLine1;
            target.AddressLine2 = source.AddressLine2;
            target.Zipcode = source.Zipcode;
            target.City = source.City;
            target.CountryId = source.Country != null ? source.Country.Id : default(int?);
            return target;
        }

        public static void Map(AddressViewModel source, AddressPart target, IRepository<Country> countryRepository) {
            target.AddressLine1 = source.AddressLine1.TrimSafe() ;
            target.AddressLine2 = source.AddressLine2.TrimSafe() ;
            target.Zipcode = source.Zipcode.TrimSafe() ;
            target.City = source.City.TrimSafe() ;
            target.Country = source.CountryId != null ? countryRepository.Get(source.CountryId.Value) : 

null;
        }
    }
}

The Map methods are here to help mapping from an AddressviewModel to an AddressPart and vice versa from various places in our code. some purists may argue that these methods need to be part of another class dedicated for mapping between models. Although I tend to agree with that, I'm fine with the current solution for now.

Another interesting to note is the use of the DataTypeAttribute that decorates the CountryId property. This will cause MVC to use the "Country" editor template when we invoke the EditorFor HTML helper on this property. We'll see how this works in a moment.

ViewModels/CustomerViewModel.cs:

namespace LazyFieldDemo.ViewModels {
    public class CustomerViewModel 

{
        public string FirstName 

{ get; set; }
        public string LastName { get; set; }
        public AddressViewModel ShippingAddress { 

get; set; }
        public AddressViewModel BillingAddress { 

get; set; }
    }
}

Notice that the CustomerViewModel defines 2 properties that are of type AddressViewModel. We'll define an editor template for these models in a moment.

Implementing the views

Since our drivers emit shapes, we need to create templates for these shapes:

  • Views/Parts.Address.cshtml (shape)
  • Views/Parts.Customer.cshtml (shape)
  • Views/Parts.Customer.SummaryAdmin.cshtml (shape)
  • Views/EditorTemplates/Parts/Address.cshtml (shape)
  • Views/EditorTemplates/Parts/Customer.cshtml (shape)
These templates in turn will make use of the following templates:
  • Views/EditorTemplates/AddressViewModel.cshtml (MVC editor template)
  • Views/EditorTemplates/Country.cshtml (MVC editor template)
  • Views/CountryDropDownList.cshtml (ad hoc shape)

The views look like this:

Views/Parts.Address.cshtml:

using LazyFieldDemo.Models
@{
    var part = (AddressPart)Model.ContentPart;
}
<div class="address">
    @part.AddressLine1<br />
    @part.AddressLine2<br />
    @string.Format("{0} {1}", 

part.Zipcode, part.City)<br />
    @string.Format("{0}", part.Country != null ? part.Country.Name : "")
</div>

Views/Parts.Customer.cshtml:

@using LazyFieldDemo.Models
@{
    var part = (CustomerPart)Model.ContentPart;
}
<div class="customer">
    @string.Format("{0} {1}", 

part.FirstName, part.LastName)<br />
    
    <strong>@T("Shipping Address")</strong>
    @Display(Model.ShippingAddress)
    
    <strong>@T("Billing Address")</strong>
    @Display(Model.BillingAddress)
</div>

Notice here how we are rendering the ShippingAddress and BillingAddress shapes, which are created in the driver.

Views/Parts.Customer.SummaryAdmin.cshtml:

@using LazyFieldDemo.Models
@{
    var part = (CustomerPart)Model.ContentPart;
}
<div class="customer">
    @string.Format("{0} {1}", 

part.FirstName, part.LastName)<br />
</div>

The reason for the Parts_Customer_SummaryAdmin shape is so that we can have a clean list of content items in the admin. If we didn't create this shape, the content list in the admin would render the two address shapes for each customer item:

Thanks to the CustomSummary version of the shape, we can have a listing that looks like this:

Views/EditorTemplates/Parts/Address.cshtml:

@model LazyFieldDemo.ViewModels.AddressViewModel
@Html.EditorForModel()

The HTML helper EditorForModel will use the AddressViewModel.cshtml template to render the editor. We are doing it like this because we want to render the AddressviewModel from within our Customer shape template as well, which we'll see next:

Views/EditorTemplates/Parts/Customer.cshtml:

@model LazyFieldDemo.ViewModels.CustomerViewModel
<fieldset>
    <div>
        @Html.LabelFor(m => m.FirstName, T("First Name"))
        @Html.TextBoxFor(m => m.FirstName, new { @class = "textMedium" 

})
        @Html.ValidationMessageFor(m => m.FirstName, "*")
    </div>
    <div>
        @Html.LabelFor(m => m.LastName, T("Last Name"))
        @Html.TextBoxFor(m => m.LastName, new { @class = "textMedium" 

})
        @Html.ValidationMessageFor(m => m.LastName, "*")
    </div>
</fieldset>

<strong>@T

("Shipping Address")</strong>
@Html.EditorFor(m => m.ShippingAddress)

<strong>@T

("Billing Address")</strong>
@Html.EditorFor(m => m.BillingAddress)

Remember that the CustomerViewModel has two properties of type AddressViewModel, so that's why we delegated the editor template for the AddressViewModel to its own template. This way the template is reusable.

The AddressViewModel editor template looks like this:

Views/EditorTemplates/AddressViewModel.cshtml:

@model LazyFieldDemo.ViewModels.AddressViewModel
<fieldset>
    <div>
        @Html.LabelFor(m => m.AddressLine1, T("Address Line 1"))
        @Html.TextBoxFor(m => m.AddressLine1, new { @class = 

"textMedium" })
        @Html.ValidationMessageFor(m => m.AddressLine1, "*")
    </div>
    <div>
        @Html.LabelFor(m => m.AddressLine2, T("Address Line 2"))
        @Html.TextBoxFor(m => m.AddressLine2, new { @class = 

"textMedium" })
        @Html.ValidationMessageFor(m => m.AddressLine2, "*")
    </div>
    <div>
        @Html.LabelFor(m => m.Zipcode, T("Zipcode"))
        @Html.TextBoxFor(m => m.Zipcode, new { @class = "textMedium" })
        @Html.ValidationMessageFor(m => m.Zipcode, "*")
    </div>
    <div>
        @Html.LabelFor(m => m.City, T("City"))
        @Html.TextBoxFor(m => m.City, new { @class = "textMedium" })
        @Html.ValidationMessageFor(m => m.City, "*")
    </div>
    <div>
        @Html.LabelFor(m => m.CountryId, T("Country"))
        @Html.EditorFor(m => m.CountryId)
        @Html.ValidationMessageFor(m => m.CountryId, "*")
    </div>
</fieldset>

Nothing special going on here: just a plain vanilla MVC editor template.
What's interesting, however, is that we are using the EditorFor HTML helper on the CountryId property. If you remember, that property is decorated with the DataTypeAttribute:

[DataType("Country")]
public int? CountryId { get; set; }

That will cause the following editor template to be rendered:

Views/EditorTemplates/Country.cshtml:

@Display.CountryDropDownList()

That's right: just a single line. What we are doing here is rendering a so called ad hoc shape. Ad hoc shapes are just shapes, created on the fly.
The reason we're doing that is because it allows us to provide data to the shape in a clean, seperated manner by means of a ShapeDisplayEvents implementation.
To see how that works, we'll first implement the shape template:

Views/CountryDropDownList.cshtml:

@using System.Linq
@using LazyFieldDemo.Models
@{
    var countries = (IEnumerable<Country>)Model.Countries;
    var currentValue = ViewData.TemplateInfo.FormattedModelValue as int?;
    var options = countries.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString(), Selected = x.Id == currentValue });
}
@Html.DropDownList("", options, "Select Country")

Nothing fancy here, except for line 4 where we are accessing Model.Countries: where does that come from?

As mentioned before, we're using a class that implements ShapeDisplayEvents.
How this works is as follows: whenever you call Display, it will invoke all of the ShapeDisplayEvents' Displaying method.
Implementing that method enables us to modify the shape being rendered. This is perfect for our case, as it allows us to set additional data on our ad hoc shape.
Our implementation looks like this:

Shapes/EditorShapes.cs:

using System.Linq;
using LazyFieldDemo.Models;
using Orchard.Data;
using Orchard.DisplayManagement.Implementation;

namespace LazyFieldDemo.Shapes {
    public class EditorShapes : ShapeDisplayEvents {
        private readonly IRepository<Country> _countryRepository;
        public EditorShapes(IRepository<Country> countryRepository) {
            _countryRepository = countryRepository;
        }

        public override void Displaying(ShapeDisplayingContext context) {

            switch (context.ShapeMetadata.Type) {
                case "CountryDropDownList":
                    context.Shape.Countries = _countryRepository.Table.OrderBy(x => x.Name).ToList();
                    break;
            }
        }
    }
}

We're using a switch statement here to test against the shape type being displayed. Currently we only support shapes of type "CountryDropDownList", but we may extend this class with other shapes in the future.

We're injecting an IRepository<Country> so that we can select all countries, which we set to the Countries property of the shape being displayed.

This technique is quite powerful. Imagine how you would do this using regular MVC: you would either have to setup the view bag from the controller, or use an action filter.
Which are perfectly fine option of course, but I just think this Orchard technique is quite nifty. 

Placement.info

Finally, we need to create a Placement,info file with the following content:

<Placement>
  <Place Parts_Address="Content:0"
         Parts_Address_Edit="Content:0" />

  <Place Parts_Customer="Content:0"
         Parts_Customer_Edit="Content:0" />

  <Match DisplayType="SummaryAdmin">
    <Place Parts_Customer="-"
           Parts_Customer_SummaryAdmin="Content:0"/>
  </Match>
</Placement>

Conclusion

Creating content part editors in Orchard can be a trivial task, but sometimes you need more advanced editors.
Combining LazyField<T>, ad hoc shapes, ShapeDisplayingEvents and MVC's editor templates can be very powerful to help you achieve that. 

Source code

The source code can be found here: https://lazyfielddemo.codeplex.com/releases/view/95579

9 comments

  • ... Posted by SciencApp Posted 10/02/2012 09:28 PM

    Thanks! Very well written. Could you please write a blog on QueryHints and the use of Eager Loading in Orchard. Especially to calculate a property using several properties from several entities (domain models). For example, property (Xentity)total = ((Yentity)Price * (Zentity)*VAT) + (Ventity)Number. And then save this (total) outcome. So we have related objects, 4 entities (Xentity, Yentity, Zentity and Ventity). According to Bernard you have to use QueryHints, but HOW?

  • ... Posted by moh_kin Posted 10/03/2012 10:15 AM
    <p>Wow, one of the best articles ever on building complex Orchard part.<br>Thanks man and thumbs up!</p>

    So could you please crack the QueryHints code for us? :)

  • ... Posted by GadgetGeek Posted 10/12/2012 07:05 AM

    Can the country selector text be localised? If so, how?

  • ... Posted by Sipke Schoorstra Posted 10/12/2012 08:52 PM (Author)
    <p>Yes, you could do that in at least 3 ways:<br>1. Create another table that stores the localized versions (e.g. CountryLocalized). It would have a CountryId column and a CultureCode column.<br>2. Turn the Country entity into a ContentItem and use the LocalizedPart to allow for the Country type to be localized<br>3. Use .PO files to maintain translations of the list of countries. That way you can use the T delegate to translate a country name.</p>
  • ... Posted by Sipke Schoorstra Posted 10/12/2012 08:53 PM (Author)

    Thanks! Yes, great idea. I will write something as soon as I find some time to do it.

  • ... Posted by Sipke Schoorstra Posted 10/12/2012 08:53 PM (Author)

    Thanks! Definitely, I will write something as soon as I find some spare time.

  • ... Posted by Michael Posted 11/16/2012 10:00 PM
    <p>Spike,<br>Out of curiosity, I see that you MAP address information in the AddressViewModel, but not in the CustomerViewModel - any reason why? I am of the thought that you are "mapping" the customer information straight in the CustomerPartDriver. Is that correct? I ask only because I am new to this and it helps to do things "right" and consistently. You did comment about purists arguing the mapping should be separate from the view model. Could I, for example, to keep my OCD in check :) just do customer mapping in the CustomerViewModel? Or should I do all the "mapping" in the driver. Thanks!</p>
  • ... Posted by Kegan Maher Posted 11/07/2014 07:55 AM

    I'm a little late to this party, but having a great time none-the-less; thank you Sipke Schoorstra!

    I do have a question about setting up the LazyField in the CustomerPartHandler. You've setup the Loader for each LazyField to use the ContentManager to create the AddressPart if it doesn't yet exist. My understanding is, this is so the LazyField will always have a reference to a valid AddressPart (with an Id). This makes sense.

    The side-effect of course is that an entry is made in the AddressPartRecord table. This is true even if the corresponding Customer content item is not published. For example, you start creating a new Customer and your browser crashes (or something...the point is, you don't complete creating the Customer either by "Saving" or "Publishing").

    Is there a way to "clean up" the AddressPartRecord table for cases when an AddressPartRecord was only created by the LazyField Loader, but never actually used/populated? (e.g. just the Id column has a value, no others). Is OnActivated the best/only event to setup the LazyField Loader?

    Thanks again, this is great!

  • ... Posted by Mel Shellam Posted 09/19/2017 06:00 AM

    In the Customer Part Handler (using Orchard 1.10.2) I get a warning:

    'LazyField<AddressPart>.Loader(Func<AddressPart, AddressPart>)' is Obsolete

Leave a comment