Many-toMany difficulties ()

Topics: General, Writing modules
Nov 2, 2012 at 12:53 AM

Hi Orchard community -

I am getting this error when adding values to a join / lookup table:

IdentifierGenerationException: attempted to assign id from null one-to-one property: ContentItemRecord

I have a many-to-many relationship:

EventLocationPartRecord <-- LocationLeagueRecord --> LeaguePartRecord

So, a Location can be mapped to many leagues, a league can have many locations assigned. Simple. I am basically following the docs and the Pluralsight Orchard Advanced course for guidance. I've built many Orchard sites but this is the first time I've needed many-to-many.

My Record classes:

namespace ShakingHand.FourKicks.Models
{
  public class EventLocationPartRecord : ContentPartRecord
  {
    public virtual bool IsOutdoors { get; set; }
    public virtual string Address { get; set; }
    public virtual string Address2 { get; set; }
    public virtual double Latitude { get; set; }
    public virtual double Longitude { get; set; }
    public virtual string MapUrl { get; set; }

    // a location can be available to all or only selected leagues
    public virtual bool IsIncludedInAllLeagues { get; set; }
    public virtual IList<LocationLeagueRecord> Leagues { get; set; }

    public EventLocationPartRecord()
    {
      Leagues = new List<LocationLeagueRecord>();
    }

  }
}

namespace ShakingHand.FourKicks.Models
{
  // join between Locations <--> Leagues
  public class LocationLeagueRecord //: ContentPartRecord
  {
    public virtual int Id { get; set; }
    public virtual EventLocationPartRecord EventLocationPartRecord { get; set; }
    public virtual LeaguePartRecord LeaguePartRecord { get; set; }
  }
}

namespace ShakingHand.FourKicks.Models
{
  public class LeaguePartRecord : ContentPartRecord
  {
    public virtual IList<LocationLeagueRecord> Locations { get; set; }

    public LeaguePartRecord()
    {
      Locations = new List<LocationLeagueRecord>();
    }

  }
}

I've tried having the join Record class ("LocationLeagueRecord") implement ContentPartRecord and not. When I do, I include a Handler:

namespace ShakingHand.FourKicks.Handlers
{
  public class LocationLeagueHandler : ContentHandler
  {
    public LocationLeagueHandler(IRepository repository)
    {
      Filters.Add(StorageFilter.For(repository));
    }
  }
}

All is well until I attempt the insert:
public void UpdateLocationPart(EventLocationViewModel model, EventLocationPart part)
    {
      part.Address = model.Address;
      part.Address2 = model.Address2;
      part.IsOutdoors = model.IsOutdoors;
      part.IsIncludedInAllLeagues = model.IsIncludedInAllLeagues;
      part.Latitude = model.Latitude;
      part.Longitude = model.Longitude;
      part.MapUrl = model.MapUrl;

      // map new locations:
      if (part.IsIncludedInAllLeagues)
      {
        // remove all league lookups
        foreach (var league in part.Leagues)
        {
          var toRemove = _locLeagueRepository.Get(l => l.LeaguePartRecord.Id.Equals(league.Id));
          if(toRemove != null)
            _locLeagueRepository.Delete(toRemove);
        }

      }
      else
      {
        // add / remove leagues as required
        // find the league, fetch all of the locations
        var oldLeagues = _locLeagueRepository
          .Fetch(l => l.EventLocationPartRecord.Id.Equals(part.Id)) // this location
          .Select(g => g.LeaguePartRecord.Id).ToList(); // leagues associated with this location

        // remove the ones that have been x'ed
        foreach (var oldLeagueId in oldLeagues.Except(model.IncludedLeagues))
        {
          var toDelete = _locLeagueRepository.Get(l => l.LeaguePartRecord.Id.Equals(oldLeagueId));
          if(toDelete != null)
            _locLeagueRepository.Delete(toDelete);
        }

        // add the new ones
        foreach (var newLeagueId in model.IncludedLeagues.Except(oldLeagues))
        {
          var toAdd = _leagueRepository.Get(newLeagueId);
          _locLeagueRepository.Create(new LocationLeagueRecord { LeaguePartRecord = toAdd, EventLocationPartRecord = part.Record });
        }
      }

    }

Where _locLeagueRepository is IRepository<LocationLeagueRecord>. When the call 

_locLeagueRepository.Create(new LocationLeagueRecord { LeaguePartRecord = toAdd, EventLocationPartRecord = part.Record });

.. is made, I get the error shown at the top of the message.

Now, my original migration created the join table fine, but even though I specified:

 

private void CreateLeagueLocationJoin()
    {
      string recordClass = typeof(LocationLeagueRecord).Name;

      SchemaBuilder.CreateTable(recordClass, builder => builder
        .ContentPartRecord()
        .Column<int>("LeaguePartRecord_Id")
        .Column<int>("EventLocationPartRecord_Id"));

      SchemaBuilder.CreateForeignKey("FK_League", recordClass, new string[] { "LeaguePartRecord_Id" },
        typeof(LeaguePartRecord).Name, new string[] { "Id" });

      SchemaBuilder.CreateForeignKey("FK_Location", recordClass, new string[] { "EventLocationPartRecord_Id" },
        typeof(EventLocationPartRecord).Name, new string[] { "Id" });
    }

.. the table created had an Id int PK column but it was not set as an identity. I fixed this manually, but still no love.

Any ideas? Any obvious thing I'm doing wrong? Do I even need to manage the join table (Kuebler does it in the Pluralsight course, so I guess I do)?

Thanks everybody.

Kurt Mang

Nov 2, 2012 at 12:59 AM

Guys - just wonderin' ... since I've specified these are Foreign Key references, I'll bet the content items have to be created first and then assigned to the join table, right?!?

I am going to try that now, will post how it goes. I'll bet that's it though ..

Kurt

Nov 2, 2012 at 1:33 AM

Well ... I notice that in the Pluralsight code no effort is made to create the Record first before the join. When I step into my code, the EventLocationPartRecord - which I am creating - has a Record.Id associated with it. By the time the offending code is called, it has a fully formed / populated EventLocationRecord (with Id) and LeaguePartRecord (with Id).

Back to the drawing board - any help is appreciated.

Kurt

Developer
Nov 2, 2012 at 1:44 AM

Just out of curiosity.. does it work when you remove the foreign key constraint?

Nov 2, 2012 at 1:49 AM

So - added a corresponding ContentPart for the lookup record:

 

public class LocationLeagueRecordPart : ContentPart<LocationLeagueRecord>
  {
    public EventLocationPartRecord EventLocation
    {
      get { return Record.EventLocationPartRecord; }
      set { Record.EventLocationPartRecord = value; }
    }

    public LeaguePartRecord League
    {
      get { return Record.LeaguePartRecord; }
      set { Record.LeaguePartRecord = value; }
    }

  }

 

And of course, the Record is:

public class LocationLeagueRecord : ContentPartRecord
  {
    //public virtual int Id { get; set; }
    public virtual EventLocationPartRecord EventLocationPartRecord { get; set; }
    public virtual LeaguePartRecord LeaguePartRecord { get; set; }
  }

No good still. Full error is:

2012-11-01 17:46:55,104 [25] Orchard.ContentManagement.Drivers.Coordinators.ContentPartDriverCoordinator - IdentifierGenerationException thrown from IContentPartDriver by ShakingHand.FourKicks.Drivers.EventLocationPartDriverNHibernate.Id.IdentifierGenerationException: attempted to assign id from null one-to-one property: ContentItemRecord   at NHibernate.Id.ForeignGenerator.Generate(ISessionImplementor sessionImplementor, Object obj)   at NHibernate.Event.Default.AbstractSaveEventListener.SaveWithGeneratedId(Object entity, String entityName, Object anything, IEventSource source, Boolean requiresImmediateIdAccess)   at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.SaveWithGeneratedOrRequestedId(SaveOrUpdateEvent event)   at NHibernate.Event.Default.DefaultSaveEventListener.SaveWithGeneratedOrRequestedId(SaveOrUpdateEvent event)   at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.EntityIsTransient(SaveOrUpdateEvent event)   at NHibernate.Event.Default.DefaultSaveEventListener.PerformSaveOrUpdate(SaveOrUpdateEvent event)   at NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(SaveOrUpdateEvent event)   at NHibernate.Impl.SessionImpl.FireSave(SaveOrUpdateEvent event)   at NHibernate.Impl.SessionImpl.Save(Object obj)   at Orchard.Data.Repository`1.Create(T entity) in C:\Shared\Projects\Personal\OrchardSites\4Kicks\src\Orchard\Data\Repository.cs:line 96   at Orchard.Data.Repository`1.Orchard.Data.IRepository<T>.Create(T entity) in C:\Shared\Projects\Personal\OrchardSites\4Kicks\src\Orchard\Data\Repository.cs:line 36   at ShakingHand.FourKicks.Services.LeagueService.UpdateLocationPart(EventLocationViewModel model, EventLocationPart part)   at ShakingHand.FourKicks.Drivers.EventLocationPartDriver.Editor(EventLocationPart part, IUpdateModel updater, Object shapeHelper)   at System.Dynamic.UpdateDelegates.UpdateAndExecute4[T0,T1,T2,T3,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2, T3 arg3)   at Orchard.ContentManagement.Drivers.ContentPartDriver`1.Orchard.ContentManagement.Drivers.IContentPartDriver.UpdateEditor(UpdateEditorContext context) in C:\Shared\Projects\Personal\OrchardSites\4Kicks\src\Orchard\ContentManagement\Drivers\ContentPartDriver.cs:line 80   at Orchard.ContentManagement.Drivers.Coordinators.ContentPartDriverCoordinator.<>c__DisplayClass10.<UpdateEditor>b__f(IContentPartDriver driver) in C:\Shared\Projects\Personal\OrchardSites\4Kicks\src\Orchard\ContentManagement\Drivers\Coordinators\ContentPartDriverCoordinator.cs:line 63   at Orchard.InvokeExtensions.Invoke[TEvents](IEnumerable`1 events, Action`1 dispatch, ILogger logger) in C:\Shared\Projects\Personal\OrchardSites\4Kicks\src\Orchard\InvokeExtensions.cs:line 17

This is crazy - I don't understand why I can't add a record to a join table. I must be over-complicating it or something.

Thanks

Kurt

Nov 2, 2012 at 1:56 AM

@sfmskywalker - good thinking, but unfortunately no good. Still throws "attempted to assign id from null one-to-one property: ContentItemRecord"

I'm pretty much out of ideas. I know Orchard supports this - I've seen it - but my god it isn't easy to implement. I can't see what I'm doing wrong here.

Thanks

Kurt

Developer
Nov 2, 2012 at 2:20 AM

Perhaps take a step back and replace the navigation properties (EventLocation and League) with integers (EventLocationId and LeagueId). Since you're working with content items you probably need more than just the contentpart records, and you would wind up loading the entire content items anyway.

So if you just link the content ids together, and maybe even implement LazyFields on your content part, your problem may be solved and you would have in my opinion a better implementation.

Nov 2, 2012 at 2:40 AM

OK, I'll take a look at that. I'll post my code when I get a chance to try it.

I guess my problem - and I'm sure it's not unique - is that I want to create a many-to-many relationship between ContentParts to be managed through Orchard. All the examples - Pluralsight, the docs -  have relations between ContentParts and types that are managed outside the system. I find it weird that my case is virtually ignored (as far as I can tell), though it seems like it would be the most common.

There MUST be people out there who have implemented this type of relationship ... once I find out how, I will be sure to post it for the community to benefit from. It can't be that hard - can it??

Kurt

Nov 2, 2012 at 2:44 AM

By the way - you are referring to my LocationLeagueRecordPart, right?

public class LocationLeagueRecordPart : ContentPart<LocationLeagueRecord>
  {
    public EventLocationPartRecord EventLocation
    {
      get { return Record.EventLocationPartRecord; }
      set { Record.EventLocationPartRecord = value; }
    }

    public LeaguePartRecord League
    {
      get { return Record.LeaguePartRecord; }
      set { Record.LeaguePartRecord = value; }
    }

  }

So try changing these props into integers, like so:

public int EventLocationPartRecord_Id
    {
      get { return Record.EventLocationPartRecord.Id; }
      set { Record.EventLocationPartRecord.Id = value; }
    }

    public int LeaguePartRecord_Id
    {
      get { return Record.LeaguePartRecord.Id; }
      set { Record.LeaguePartRecord.Id = value; }
    }

Nov 2, 2012 at 9:13 AM

OK, so I'm following the N-N doc and the Pluralsight examples as closely as my requirements allow. The code runs, the repository is called and executes, but no changes are made. Nothing is logged and the error log is empty. I do not get a success message, but rather an empty red error message across the top of the screen. There are no exceptions when debugging.

The only difference between what I am doing and what's in the docs / Pluralsight is that both sides of my relationship - the foreign key tables - are ContentPartRecords. Otherwise identical.

For the record:

public class LeaguePartRecord : ContentPartRecord
  {
    [CascadeAllDeleteOrphan]
    public virtual IList<LocationLeagueRecord> Locations { get; set; }

    public LeaguePartRecord()
    {
      Locations = new List<LocationLeagueRecord>();
    }

  }

public class EventLocationPartRecord : ContentPartRecord
  {
    public virtual bool IsOutdoors { get; set; }
    public virtual string Address { get; set; }
    public virtual string Address2 { get; set; }
    public virtual double Latitude { get; set; }
    public virtual double Longitude { get; set; }
    public virtual string MapUrl { get; set; }

    // a location can be available to all or only selected leagues
    public virtual bool IsIncludedInAllLeagues { get; set; }
    public virtual IList<LocationLeagueRecord> Leagues { get; set; }

    public EventLocationPartRecord()
    {
      Leagues = new List<LocationLeagueRecord>();
    }

  }

// join between Locations <--> Leagues
  // PS: not a ContentPartRecord. This exactly as PS has it
  public class LocationLeagueRecord //: ContentPartRecord
  {
    public virtual int Id { get; set; }
    public virtual EventLocationPartRecord EventLocationPartRecord { get; set; }
    public virtual LeaguePartRecord LeaguePartRecord { get; set; }
  }


// DRIVER

protected override DriverResult Editor(EventLocationPart part, IUpdateModel updater, dynamic shapeHelper)
    {
      EventLocationViewModel model = new EventLocationViewModel();
      updater.TryUpdateModel(model, Prefix, null, null);

      _leagueSvc.UpdateLocationPart(model, part);
      if (part.ContentItem.Id != 0)
      {
        _leagueSvc.UpdateLeaguesForLocation(part.ContentItem, model.IncludedLeagues);
      }

      return Editor(part, shapeHelper);
    }


// LeagueService:


public void UpdateLeaguesForLocation(ContentItem locationItem, IEnumerable<int> leaguesToAdd)
    {
      var part = locationItem.As<EventLocationPart>();
      var record = part.Record;

      if (part.IsIncludedInAllLeagues)
      {
        // remove all league lookups
        foreach (var league in part.Leagues)
        {
          var toRemove = _locLeagueRepository.Get(l => l.LeaguePartRecord.Id.Equals(league.Id));
          if (toRemove != null)
            _locLeagueRepository.Delete(toRemove);
        }
      }
      else
      {
        // add / remove leagues as required
        // find the league, fetch all of the locations
        var oldLeagues = _locLeagueRepository
          .Fetch(l => l.EventLocationPartRecord.Id.Equals(part.Id)) // this location
          .Select(g => g.LeaguePartRecord.Id).ToList(); // leagues associated with this location

        // remove the ones that have been x'ed
        foreach (var oldLeagueId in oldLeagues.Except(leaguesToAdd))
        {
          var toDelete = _locLeagueRepository.Get(l => l.LeaguePartRecord.Id.Equals(oldLeagueId));
          if (toDelete != null)
            _locLeagueRepository.Delete(toDelete);
        }

        foreach (var newLeagueId in leaguesToAdd.Except(oldLeagues))
        {
          var toAdd = _leagueRepository.Get(newLeagueId);
          _locLeagueRepository.Create(new LocationLeagueRecord { LeaguePartRecord = toAdd, EventLocationPartRecord = part.Record });
        }
      }

    }

UpdateLeaguesForLocation runs, the model is populated as expected. The line:

_locLeagueRepository.Create(new LocationLeagueRecord { LeaguePartRecord = toAdd, EventLocationPartRecord = part.Record });

.. runs without error, and the objects passed in are fully populated and have the expected Ids etc. No problems whatsoever.

I can also go to the database itself, create a join record manually, and it is populated by NHibernate. But for the life of me I cannot perform Create / Delete operations on the join table.

As far as I can see I am doing everything right. I've tried dozens of permutations and no luck. I have a bunch of these relations I have to model my site, but I just can't get this first one off the ground. And dudes, it's 1:15 AM - I'm trying. Any help very, very welcome.

Thanks - Kurt

Developer
Nov 2, 2012 at 8:37 PM

Although I can't tell for sure why you get this exception without diving into it myself, I do think you really should not use navigation properties of content part records in this way. For one thing, the primary key values of content part records or not the same as the Id of the content part itself (which actually returns the Id of the content item).

If you want to use navigation properties for content parts, implement them using LazyField<T>.

 

 

Nov 2, 2012 at 10:36 PM

OK, I hear you - so rejig my join record class to be:

public class LocationLeagueRecord
  {
    public virtual int Id { get; set; }
    public virtual int EventLocationPartRecord_Id { get; set; }
    public virtual int LeaguePartRecord_Id { get; set; }
  }

Everything else the same. I'll try it. Not sure why this is such a difficult requirement - again, I can GET the data from the navigation props if I manually insert it first, so NHibernate has that part figured out. I just can't insert / delete using the join record / table repository.

I appreciate your feedback, I'll give it a whirl over the weekend.

Kurt

Nov 4, 2012 at 1:12 AM

Nope, no good. I redefined my join table / Record class to be just ints (as per sfmskywalker), I dropped and recreated my database just in case there was any problem there, and otherwise have followed all of the guidance available for multiple joins. NO ERROR is throw, the error log and debug log is empty, but an empty validation result is shown on screen and - of course - the changes are not persisted to the database. The record can be saved if no join relations are attempted.

For the record, my Records / migration code:

public class LeaguePartRecord : ContentPartRecord
  {
    [CascadeAllDeleteOrphan]
    public virtual IList<LocationLeagueRecord> Locations { get; set; }

    public LeaguePartRecord()
    {
      Locations = new List<LocationLeagueRecord>();
    }

  }

public class EventLocationPartRecord : ContentPartRecord
  {
    public virtual bool IsOutdoors { get; set; }
    public virtual string Address { get; set; }
    public virtual string Address2 { get; set; }
    public virtual double Latitude { get; set; }
    public virtual double Longitude { get; set; }
    public virtual string MapUrl { get; set; }

    // a location can be available to all or only selected leagues
    public virtual bool IsIncludedInAllLeagues { get; set; }
    public virtual IList<LocationLeagueRecord> Leagues { get; set; }

    public EventLocationPartRecord()
    {
      Leagues = new List<LocationLeagueRecord>();
    }

  }

public class LocationLeagueRecord
  {
    public virtual int Id { get; set; }
    public virtual int EventLocationPartRecord_Id { get; set; }
    public virtual int LeaguePartRecord_Id { get; set; }
  }

// migrations:

private void CreateLeague()
    {
      SchemaBuilder.CreateTable(typeof(LeaguePartRecord).Name, table => table
        .ContentPartRecord());

      ContentDefinitionManager.AlterTypeDefinition("League", builder => builder
        .WithPart("CommonPart")
        .WithPart("TitlePart")
        .WithPart("AutoroutePart", part => part
          .WithSetting("AutorouteSettings.AllowCustomPattern", "true")
          .WithSetting("AutorouteSettings.AutomaticAdjustmentOnEdit", "false")
          .WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'League', Pattern: 'league/{Content.Slug}', Description: 'league/league-title'}]")
          .WithSetting("AutorouteSettings.DefaultPatternIndex", "0"))
        .WithPart(typeof(LeaguePart).Name)
        .Creatable());

      ContentDefinitionManager.AlterPartDefinition(typeof(LeaguePartRecord).Name, part => part.Attachable());
    }

    // physical location where events are held (field, rink etc.)
    private void CreateEventLocation()
    {
      SchemaBuilder.CreateTable(typeof(EventLocationPartRecord).Name, tbl => tbl
        .ContentPartRecord()
        .Column<bool>("IsOutdoors")
        .Column<string>("Address", col => col.WithLength(100))
        .Column<string>("Address2", col => col.WithLength(100))
        .Column<double>("Latitude")
        .Column<double>("Longitude")
        .Column<string>("MapUrl", col => col.WithLength(100))
        .Column<bool>("IsIncludedInAllLeagues"));

      ContentDefinitionManager.AlterTypeDefinition("EventLocation", builder => builder
        .WithPart("CommonPart")
        .WithPart("TitlePart")
        .WithPart("AutoroutePart", part => part
          .WithSetting("AutorouteSettings.AllowCustomPattern", "true")
          .WithSetting("AutorouteSettings.AutomaticAdjustmentOnEdit", "false")
          .WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'Location', Pattern: 'location/{Content.Slug}', Description: 'location/location-title'}]")
          .WithSetting("AutorouteSettings.DefaultPatternIndex", "0"))
        .WithPart(typeof(EventLocationPart).Name)
        .Creatable());

      ContentDefinitionManager.AlterPartDefinition(typeof(EventLocationPart).Name, part => part.Attachable());
    }

    // join table between league <--> locations
    private void CreateLeagueLocationJoin()
    {
      string recordClass = typeof(LocationLeagueRecord).Name;

      SchemaBuilder.CreateTable(recordClass, builder => builder
        .Column<int>("Id", col => col.PrimaryKey().Identity())
        .Column<int>("LeaguePartRecord_Id")
        .Column<int>("EventLocationPartRecord_Id"));

    }


At this point,since I haven't heard any other replies and haven't seen any source doing what I am attempting, I have to conclude that this is a bug in Orchard. I'll have to rejig my data model to not have one side of the relationship be a ContentPartRecord - which SUCKS - or I'll just have to build my own MVC / Entity Framework solution. I really thought Orchard was the way to go on this, but the many-to-many join issue is a major showstopper and really limits the usefulness of the CMS. I hate to throw my hands up but I have tried everything I can think of now.

If anyone has a working sample of two ContentPartRecords with a many to many relationship, PLEASE share. But I have a nagging concern that either Orchard or NHibernate don't support this scenario. I really hope I'm wrong about that.

Kurt

Nov 4, 2012 at 1:39 AM
Edited Nov 4, 2012 at 6:54 AM

I filed a bug for this complete with a screen capture:

http://orchard.codeplex.com/workitem/19218

Nov 4, 2012 at 7:59 AM
Edited Nov 4, 2012 at 8:00 AM

A little more info - because I am desperate, I thought I'd try redefining the relationships. I severed the formal ties between the two foreign key tables (EventLocationPartRecord and LeaguePartRecord), leaving the join table (record) LocationLeagueRecord intact but with no navigation properties on the other records - they don't know they are joined, see? Totally sucky, but like I said I am desperate. I figured maybe I could just write service-level code to manage the relationships 'informally'.

Doesn't work though! I can access my manually created records like so:

var listing = _locLeagueRepository.Table.ToList();

I see the record I inserted through SQL Manager.

Then I do this:

var newJoin = new LocationLeagueRecord { EventLocationPartRecord_Id = part.Record.Id, LeaguePartRecord_Id = newLeagueId };         

_locLeagueRepository.Create(newJoin);

.. where part.Record.Id and newLeagueId are actual, for real int values that have corresponding values in their own tables (not that that should matter of course - as far as this table is concerned any two ints should do just fine). This code runs, but nothing happens in the DB and the same old non-error gets presented on screen. No changes are made to the entity either - everything gets rolled back.

Is this some kind of transaction weirdness? The table has an Identity type PK that auto-increments. I can happily insert values into it. I can create any other content part records in my system - as long as I don't try to do it through IRepository<T>.Create().

WTF?

[bangs head against desk for another 10 minutes ...]

Nov 4, 2012 at 8:06 AM

Oh, and I can arbitrarily do this, in a completely unrelated method:

_locLeagueRepository.Create(new LocationLeagueRecord { EventLocationPartRecord_Id = 17, LeaguePartRecord_Id = 99 });

.. and it works! I just can't create / update my EventLocationPart if it has this code associated with it (although I can if it doesn't have this code associated with it).

Absolutely baffling.

Nov 4, 2012 at 4:44 PM

OK, I solved this problem - it had little or nothing to do with the data persistence in NHibernate / Orchard, but was in fact a driver-related problem.

My admin ViewModel was posting to a standard ContentDriver like so:

protected override DriverResult Editor(EventLocationPart part, IUpdateModel updater, dynamic shapeHelper)
    {
      EventLocationViewModel model = new EventLocationViewModel();
      updater.TryUpdateModel(model, Prefix, null, null);
      _leagueSvc.UpdateLocationPart(model, part);

      return Editor(part, shapeHelper);
    }

As I chronicled above, this would rn but fail with an empty message.

The correct way to do this was / is:

protected override DriverResult Editor(EventLocationPart part, IUpdateModel updater, dynamic shapeHelper)
    {
      EventLocationViewModel model = new EventLocationViewModel();
      updater.TryUpdateModel(model, Prefix, null, new[] { "LeaguesList" });
      _leagueSvc.UpdateLocationPart(model, part);

      return Editor(part, shapeHelper);
    }

Notice the last argument in TryUpdateModel - Orchard has to be told to ignore any ViewModel properties that are not part of the ContentPart and are involved in the join record persistence. Once I added this exclusion, everything "just worked".

Hugerelief, and huge gotcha. Hope this saves someone the frustration I suffered. Maybe there is a way of providing an error message or logging something when this happens? Would have saved me a couple days ...

Kurt

Developer
Nov 5, 2012 at 2:51 AM

Actually, you don't have to tell Orchard, but the model binder. The TryUpdateModel method is implemented in Controller (or one of its bases). Orchard doesn't care which properties you do or don't include :)

Anyway it is great to see you figured it out!

Dec 12, 2012 at 6:28 AM

Also have a look at http://scienceproject.codeplex.com/wikipage?title=Mechanics&referringTitle=Documentation

for many-to-many modeling with another content part. This is great.

Coordinator
Dec 14, 2012 at 2:27 AM

Orchard now ships with content picker field, which allows for n-n out of the box.