Question

Working on creating my first Orchard Module and I am running into issues getting the form data saved back to the database. I have everything registered correctly as far as I can tell from looking at a lot of samples so I must be missing something minor.

I am able to get the Apartment form to show under the new menu, validation is working but when I fill the form completly and hit save I get:

Your Apartment has been created.

Checking the database the record is not in the table and checking the logs shows:

2013-12-19 09:15:23,416 [19] NHibernate.Transaction.ITransactionFactory - DTC transaction prepre phase failed NHibernate.Exceptions.GenericADOException: could not execute batch command.[SQL: SQL not available] ---> System.Data.SqlClient.SqlException: Cannot insert the value NULL into column 'FloorPlanName', table 'Orchard.dbo.CommunityWebsiteSolutions_ApartmentPartRecord'; column does not allow nulls. INSERT fails.

Running SQL Profiler shows an insert with all columns being set to NULL.

Migrations.cs

    SchemaBuilder.CreateTable(typeof(ApartmentPartRecord).Name, table => table
                .ContentPartRecord()
                .Column<string>("FloorPlanName", c => c.WithLength(25).NotNull())
                .Column<string>("FullAddress", c => c.WithLength(256).NotNull()))
                .Column<string>("ShortDescription", c => c.WithLength(150).NotNull())
                .Column("NumberOfBedrooms", DbType.Int32, c => c.NotNull())
                .Column("NumberOfBathrooms", DbType.Int32, c => c.NotNull())
                .Column("SquareFootage", DbType.Int32, c => c.NotNull())
                .Column("WhenAvailable", DbType.DateTime)
                .Column("RentAmount", DbType.Decimal)
                );

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

ApartmentPart

public class ApartmentPartRecord : ContentPartRecord {
    public virtual string FloorPlanName { get; set; }
    public virtual string ShortDescription { get; set; }
    public virtual string FullAddress { get; set; }
    public virtual int? NumberOfBedrooms { get; set; }
    public virtual int? NumberOfBathrooms { get; set; }
    public virtual int? SquareFootage { get; set; }
    public virtual DateTime? WhenAvailable { get; set; }
    public virtual decimal? RentAmount { get; set; }
}

public class ApartmentPart : ContentPart<ApartmentPartRecord> {
    [Required, StringLength(256)]
    [Display(Name = "Address / Unit Number")]
    public string FullAddress {
        get { return Record.FullAddress; }
        set { Record.FullAddress = value; }
    }

    [Required, StringLength(25)]
    [Display(Name = "Floor Plan")]
    public string FloorPlanName {
        get { return Record.FloorPlanName; }
        set { Record.FloorPlanName = value; }
    }

    [Required, StringLength(150)]
    [Display(Name = "Sales Description")]
    public string ShortDescription {
        get { return Record.ShortDescription; }
        set { Record.ShortDescription = value; }
    }

    [Required]
    [Display(Name = "Bedroom Count")]
    public int? NumberOfBedrooms {
        get { return Record.NumberOfBedrooms; }
        set { Record.NumberOfBedrooms = value; }
    }

    [Required]
    [Display(Name = "Bathroom Count")]
    public int? NumberOfBathrooms {
        get { return Record.NumberOfBathrooms; }
        set { Record.NumberOfBathrooms = value; }
    }

    [Required]
    [Display(Name = "Square Footage")]
    public int? SquareFootage {
        get { return Record.SquareFootage; }
        set { Record.SquareFootage = value; }
    }

    [Display(Name = "First Availability")]
    public DateTime? WhenAvailable {
        get { return Record.WhenAvailable; }
        set { Record.WhenAvailable = value; }
    }

    [Display(Name = "Rent Amount")]
    public decimal? RentAmount {
        get { return Record.RentAmount; }
        set { Record.RentAmount = value; }
    }
}

Driver

public class ApartmentPartDriver : ContentPartDriver<ApartmentPart>
    {
        protected override string Prefix
        {
            get { return "Apartment"; }
        }

        //GET
        protected override DriverResult Editor(ApartmentPart part, dynamic shapeHelper)
        {
            return ContentShape("Parts_Apartment_Edit", 
                () => shapeHelper.EditorTemplate(
                    TemplateName: "Parts/Apartment", 
                    Model: part, 
                    Prefix: Prefix));
        }

        //POST
        protected override DriverResult Editor(ApartmentPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            updater.TryUpdateModel(part, Prefix, null, null);
            return Editor(part, shapeHelper);
        }
    }

Handler

public class ApartmentPartHandler : ContentHandler {
        public ApartmentPartHandler(IRepository<ApartmentPartRecord> repository)
        {
            Filters.Add(StorageFilter.For(repository));
        }
    }
Was it helpful?

Solution

Your error message explains this pretty clearly:

System.Data.SqlClient.SqlException: Cannot insert the value NULL into column 'FloorPlanName', table 'Orchard.dbo.CommunityWebsiteSolutions_ApartmentPartRecord'; column does not allow nulls. INSERT fails.

Your problem occurs because:

  1. You are using nullable types such as string and int? types in your Record class, which means you want to allow nulls.
  2. Yet, you are specifying in your DB migration that you want to disallow nulls.
  3. And when C# instantiates your Record class, it initializes the fields using the default value, which is null for nullable types.

You can do one of the following:

  1. Make your DB columns nullable (remove NotNull)
  2. Make your Record class use non-nullable types (for example, int instead of int?). Note that this is not an option for reference types such as string.
  3. Give non-null default values to the fields of your Record class by giving the class a constructor. This is arguably bad practice since you will be calling virtual properties in a base class, but seems to be ok in NHibernate.
  4. Give non-null default values to the fields of your Record class by giving your part an OnInitializing handler, which would be placed in your Handler class.

UPDATE

You commented that you are expecting the fields to be filled in by the TryUpdateModel in the Editor function of your driver class. This does eventually happen, but the actual sequence of events that occurs is this (you can see this in the CreatePOST method of Orchard.Core.Contents.Controllers.AdminController):

  1. ContentManager.New() with the content type ID to create content item in memory. This step calls OnInitializing for the appropriate content parts for the content type, which are defined in handlers.
  2. ContentManager.Create() with the content item in Draft Mode. This step actually tries to persist the item to the DB once.
  3. ContentManager.UpdateEditor(). This is the call that actually calls Editor of the appropriate driver for the content type.
  4. Check the ModelState and roll back the transaction if anything has failed.

Step 2 will fail if you have NULL values in columns marked NotNull, because the fields have default values at that point. For these columns, you have to fill them in before step 2 by using OnInitializing or by using a constructor on your Record part.

In other words, TryUpdateModel in your driver is actually applying changes directly to the entity that has already been Created and is now attached to the NHibernate session.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top