How to make a complex content type (with many to many relationships) draftable

Topics: Core, Writing modules
Jun 13, 2012 at 11:32 AM

Hi everyone,

I have a custom content type which participates in many to many relationship. What I'm trying to do is to make this content type draftable. I inherited it from ContentPartVersionRecord and altered the Migrations file to call ContentPartVersionRecord() method instead of ContentPartRecord. 

I'm trying to save a draft version through the following piece of code:

var contentItem = _services.ContentManager.Get(id, VersionOptions.DraftRequired);

        var shape = _services.ContentManager.UpdateEditor(contentItem, this);

        return new ShapeResult(this, shape);

But I'm getting the following error while calling _services.ContentManager.Get method:  

NHibernate.Impl.AbstractSessionImpl - DTC transaction prepre phase failedNHibernate.HibernateException: Found shared references to a collection

It sounds like a bug because aforesaid approach works for simple content types (without many to many relationships).

I will be grateful for any help you can provide.

Jul 16, 2012 at 2:11 PM

Hi, did you manage to fix this issue?

I'm getting the same error as you after publishing an item (which causes another version to be created on the next save). My model is slightly different to the example in the documentation (I didn't need the linking entity, so just referenced the parent part from the child), but I can see that when the POST arrives in the Editor method in my driver that part.Record has a new Id but the old items in the collection still contain references to the old part.Record.Id. I assume this is why we get the error as it appears to be maintaining the relationship between the collection and both versions of the items.

Jul 16, 2012 at 9:39 PM

I'm suffering from the same problem.

Jul 17, 2012 at 1:06 PM

Just been reading the discussions on the Science Project codeplex and found that versioned relationships are supposed to be supported in that so I may have a look into using it, although I think the version containing the code to do it isn't released in the gallery yet.

http://scienceproject.codeplex.com/discussions/346468

Jul 17, 2012 at 5:55 PM

Unfortunately the science project has it's own problems...

Jul 17, 2012 at 6:24 PM

Have created an issue as I couldn't see one already - http://orchard.codeplex.com/workitem/18826

Jul 18, 2012 at 12:43 PM
Edited Jul 18, 2012 at 12:46 PM

I have a workaround which appears to work with my model (a container part that has a collection of related records) but I'm not sure if it'll work with the examples in the Orchard 1-n and n-n documentation. The problem was as I suspected - the Orchard versioning code uses NHibernate to copy each part when creating a new draft, including related records.

My model:

 

public class MyPartRecordWithCollection : ContentPartVersionRecord
    {
        public MyPartRecordWithCollection()
        {
            this.Collection = new List<MyCollectionItem>();    
        }

        public virtual IList<MyCollectionItem> Collection { get; set; }
    }
 

 

public class MyCollectionItem 
    {
        public virtual int Id { get; set; }

        // some other properties

        public virtual MyPartRecordWithCollection MyPartRecordWithCollection { get; set; }
    }

 

Migration:

 

this.SchemaBuilder.CreateTable("MyCollectionItem", table => table
                .Column<int>("Id", column => column.PrimaryKey().Identity())
                .Column<int>("MyPartRecordWithCollection_id"));

this.SchemaBuilder.CreateTable("MyPartRecordWithCollection", table => table.ContentPartVersionRecord());

 

 

I've found I can modify ContentHandler to use a custom storage filter rather than calling StorageFilter.For, and change the way the new version is created, as the storage filter that's assigned by default is what's causing the problem...

Filters.Add(new MyStorageVersionFilter(repository))

Here is the code for the filter:

public class MyStorageVersionFilter : StorageVersionFilter<MyPartRecordWithCollection>
    {
        public MyStorageVersionFilter(
            IRepository<MyPartRecordWithCollection> repository)
            : base(repository)
        {
            Logger = NullLogger.Instance;
        }

        public ILogger Logger { get; set; }

        protected override void Versioning(VersionContentContext context, ContentPart<MyPartRecordWithCollection> existing, ContentPart<MyPartRecordWithCollection> building)
        {
            // overriding the base copy logic to create new references rather than copying the existing ones
            CopyRecord(existing, building);

            // only the up-reference to the particular version differs at this point
            building.Record.ContentItemVersionRecord = context.BuildingItemVersionRecord;

            // push the new instance into the transaction and session
            _repository.Create(building.Record);
        }

        private void CopyRecord(ContentPart<MyPartRecordWithCollection> existing, ContentPart<MyPartRecordWithCollection> building)
        {
            Logger.Debug("Copy {0} {1}", existing, building);

            foreach (var existingCollectionItem in existing.Record.Collection)
            {
                var newCollectionItem = new MyCollectionItem();

                // Use existingCollectionItem to populate the new item - do it manually, use reflection, or however you prefer
                // I'm using ValueInjecter and I also set the Id to zero because it maps the old Id

                newCollectionItem.MyPartRecordWithCollection = building; // create relationship between the collection item and the new part record

                building.Record.ContentItemRecord = existing.Record.ContentItemRecord;
                building.Record.ContentItemVersionRecord = existing.Record.ContentItemVersionRecord;

                building.Record.Collection.Add(newCollectionItem);
            }
        }
    }

It is just a work around though. You probably wouldn't want to create a custom storage filter with copy logic for every part that has a relationship.

Jul 18, 2012 at 4:17 PM

This is a better solution than the one I used which was to not reference the collection in the nhibernate models and copy the records from the OnVersioned handler event using a repository for my collection model. Thanks for sharing it!

Sep 27, 2012 at 11:42 AM
Edited Sep 27, 2012 at 12:08 PM

Thanks for sharing your solution! I'm trying to get it to work on my machine, but am utterly failing :(

The error I'm now getting is: object references an unsaved transient instance - save the transient instance before flushing. Type: My.Models.AccommodationTypeRecord, Entity: My.Models.AccommodationTypeRecord

The weird thing is, if I ommit the entire "copyRecord" code, it still gets copied and saved, only without the related AccommodationTypeRecords :S Is there a second mechanism at work that saves this stuff?

Here's my code, hope you can give me some pointers!:

  public class MyHomepagePartRecord : ContentPartVersionRecord
  {
    public MyHomepagePartRecord()
    {
      Accommodations = new List<AccommodationTypeRecord>();
    }

    //public virtual string Teaser1 { get; set; }
    public virtual string RichTextBlock1 { get; set; }
    public virtual string RichTextBlock2 { get; set; }
    public virtual IList<AccommodationTypeRecord> Accommodations { get; set; }

  }
  public class AccommodationTypeRecord
  {
    public virtual string Name { get; set; }
    public virtual string Image { get; set; }
    public virtual string Url { get; set; }
    public virtual int Id { get; set; }
    public virtual MyHomepagePartRecord MyHomepagePartRecord { get; set; }
  }
 public class MyHomepageHandler : ContentHandler
  {

    public MyHomepageHandler(IRepository<MyHomepagePartRecord> repository)
    {
      //Filters.Add(StorageFilter.For(repository));
      Filters.Add(new MyStorageVersionFilter(repository));
    }

  }

  public class MyStorageVersionFilter : StorageVersionFilter<MyHomepagePartRecord>
  {
    public MyStorageVersionFilter(IRepository<MyHomepagePartRecord> repository)
      : base(repository)
    {
    }

    protected override void Versioning(VersionContentContext context, ContentPart<MyHomepagePartRecord> existing, ContentPart<MyHomepagePartRecord> building)
    {
      // overriding the base copy logic to create new references rather than copying the existing ones
      CopyRecord(existing, building);

      // only the up-reference to the particular version differs at this point
      building.Record.ContentItemVersionRecord = context.BuildingItemVersionRecord;

      // push the new instance into the transaction and session
      _repository.Create(building.Record);
    }

    private void CopyRecord(ContentPart<MyHomepagePartRecord> existing, ContentPart<MyHomepagePartRecord> building)
    {
      building.Record.ContentItemRecord = existing.Record.ContentItemRecord;
      building.Record.ContentItemVersionRecord = existing.Record.ContentItemVersionRecord;

      building.Record.RichTextBlock1 = existing.Record.RichTextBlock1;
      building.Record.RichTextBlock2 = existing.Record.RichTextBlock2;

      foreach (var existingCollectionItem in existing.Record.Accommodations)
      {
        var newCollectionItem = new AccommodationTypeRecord();

        // Use existingCollectionItem to populate the new item - do it manually, use reflection, or however you prefer
        // I'm using ValueInjecter and I also set the Id to zero because it maps the old Id
        newCollectionItem.Image = existingCollectionItem.Image;
        newCollectionItem.Name = existingCollectionItem.Name;
        newCollectionItem.Url = existingCollectionItem.Url;

        newCollectionItem.MyHomepagePartRecord = building.Record; // create relationship between the collection item and the new part record

        building.Record.Accommodations.Add(newCollectionItem);
      }
    }
  }
Developer
Sep 28 at 8:24 PM
I just resolved the issue, give it a shot and let me know if it works for you.

Resolved in changeset a40bbbab51623f63a624661a4943ee8daafe120a