Custom Module M:N class for self-referencing class

Christian Nickel asked on July 17, 2014 17:47

I was able to follow the documentation to create many-to-many relationships between two different classes using this page. However the same process does not work when using a many-to-many binding class to reference the same table.

For example, I have a Project class with PK ProjectID, I have a RelatedProject M:N class with fields PK ProjectID, and PK RelatedProjectID, each bound to the Project class. How can I properly create this relationship in Kentico?

There are two issues that arose when I tried this approach:

1 - In the generated code for the RelatedProjectInfoProvider class, many of the methods used projectId more than once in the method definition. This was easy enough to fix for compilation purposes, but I believe when I changed the 2nd parameter to relatedProjectId it may have caused issues further up the stack.

2 - Once the compiled classes are in place, the new page with binding template is created, and the properties are correctly configured, any time I attempt to add a related project, I get the error: "Cannot insert the value NULL into column 'ProjectID', table '

Recent Answers


Christian Nickel answered on July 17, 2014 18:01

Edit - Somehow my submitted answer is missing much of the information I added after part two, here is the rest:

I ran in debug and traced the issue back to CMSModules_AdminControls_Controls_UIControls_BindingEditItem.GetBindingColumnNames() in this block of code:

foreach (ObjectDependency od in objProvider.TypeInfo.DependsOn) { if (od.DependencyObjectType.EqualsCSafe(obj.ObjectType, true)) { objCol = od.DependencyColumn; }

if (od.DependencyObjectType.EqualsCSafe(objTarget.ObjectType, true))
{
    targetCol = od.DependencyColumn;
}

}

The objCol string is properly set in part 1 of GetBindingColumnNames() to "ProjectID", but in part 3 pasted above, it is set back to "RelatedProjectID" because both conditions are true, the obj and objTarget are both of the same type.

Is there a work-around for this issue?

Here is the relevant portion of the stack.

CMS.DataEngine.AbstractDataConnection.HandleError(String queryText, Exception ex) +181 CMS.DataEngine.AbstractDataConnection.ExecuteScalar(String queryText, QueryDataParameters queryParams, QueryTypeEnum queryType, Boolean requiresTransaction) +283 CMS.DataEngine.GeneralConnection.ExecuteScalar(QueryParameters query) +447 CMS.DataEngine.SimpleDataClass.Insert(Boolean getId) +144 CMS.DataEngine.AbstractInfo1.InsertData() +288 CMS.DataEngine.AbstractInfoProvider2.SetInfo(TInfo info) +445 MINE.RelatedProjectInfoProvider.SetRelatedProjectInfoInternal(RelatedProjectInfo infoObj) in c:\Projects\KenticoCMS8\CMS\Old_App_Code\CMSModules\CSRC\RelatedProjectInfoProvider.cs:101 MINE.RelatedProjectInfoProvider.SetRelatedProjectInfo(RelatedProjectInfo infoObj) in c:\Projects\KenticoCMS8\CMS\Old_App_Code\CMSModules\CSRC\RelatedProjectInfoProvider.cs:41 MINE.RelatedProjectInfo.SetObject() in c:\Projects\KenticoCMS8\CMS\Old_App_Code\CMSModules\CSRC\RelatedProjectInfo.cs:101 CMSModules_AdminControls_Controls_UIControls_BindingEditItem.SaveData() in c:\Projects\KenticoCMS8\CMS\CMSModules\AdminControls\Controls\UIControls\BindingEditItem.ascx.cs:406 CMSModules_AdminControls_Controls_UIControls_BindingEditItem.editElem_OnSelectionChanged(Object sender, EventArgs ea) in c:\Projects\KenticoCMS8\CMS\CMSModules\AdminControls\Controls\UIControls\BindingEditItem.ascx.cs:256 CMSAdminControls_UI_UniSelector_UniSelector.RaisePostBackEvent(String eventArgument) in c:\Projects\KenticoCMS8\CMS\CMSAdminControls\UI\UniSelector\UniSelector.ascx.cs:2711 System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +3804

0 votesVote for this answer Mark as a Correct answer

Christian Nickel answered on July 17, 2014 18:06

Didn't realize there was a better way to post code:

    foreach (ObjectDependency od in objProvider.TypeInfo.DependsOn)
{
    if (od.DependencyObjectType.EqualsCSafe(obj.ObjectType, true))
    {
        objCol = od.DependencyColumn;
    }

    if (od.DependencyObjectType.EqualsCSafe(objTarget.ObjectType, true))
    {
        targetCol = od.DependencyColumn;
    }
}
0 votesVote for this answer Mark as a Correct answer

Yehuda Lando answered on July 22, 2014 02:07

About #2, "Cannot insert the value NULL into column 'ProjectID'" is happening because that column is set as the ID column in the Info class. Make sure it's set as null instead.

About #1, it shouldn't happen. Can you post the code and the class definition?

0 votesVote for this answer Mark as a Correct answer

Christian Nickel answered on July 22, 2014 15:38

Thanks for your help!

2 I see "ProjectID" set as the parentIdColumn parameter for the new ObjectTypeInfo instance, is that what I should set to null?

1 Sure, here is the generated class, I had changed the name of the table after I initially posted the question:

using System;
using System.Data;
using System.Runtime.Serialization;
using System.Collections.Generic;

using CMS;
using CMS.DataEngine;
using CMS.Helpers;
using MINE;

[assembly: RegisterObjectType(typeof(ProjectRelatedProjectInfo), ProjectRelatedProjectInfo.OBJECT_TYPE)]

namespace MINE
{
    /// <summary>
    /// ProjectRelatedProjectInfo data container class.
    /// </summary>
    [Serializable]
    public class ProjectRelatedProjectInfo : AbstractInfo<ProjectRelatedProjectInfo>
    {
        #region "Type information"

        /// <summary>
        /// Object type
        /// </summary>
        public const string OBJECT_TYPE = "MINE.projectrelatedproject";


        /// <summary>
        /// Type information.
        /// </summary>
#warning "You will need to configure the type info."
        public static ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(ProjectRelatedProjectInfoProvider), OBJECT_TYPE, "MINE.ProjectRelatedProject", "ProjectID", null, null, null, null, null, null, "ProjectID", "MINE.project")
        {
            ModuleName = "MINE",
            TouchCacheDependencies = true,
            DependsOn = new List<ObjectDependency>() 
            {
                new ObjectDependency("RelatedProjectID", "MINE.project", ObjectDependencyEnum.Binding), 
            },
        };

        #endregion


        #region "Properties"

        /// <summary>
        /// Project ID
        /// </summary>
        [DatabaseField]
        public virtual int ProjectID
        {
            get
            {
                return ValidationHelper.GetInteger(GetValue("ProjectID"), 0);
            }
            set
            {
                SetValue("ProjectID", value);
            }
        }


        /// <summary>
        /// Related project ID
        /// </summary>
        [DatabaseField]
        public virtual int RelatedProjectID
        {
            get
            {
                return ValidationHelper.GetInteger(GetValue("RelatedProjectID"), 0);
            }
            set
            {
                SetValue("RelatedProjectID", value);
            }
        }

        #endregion


        #region "Type based properties and methods"

        /// <summary>
        /// Deletes the object using appropriate provider.
        /// </summary>
        protected override void DeleteObject()
        {
            ProjectRelatedProjectInfoProvider.DeleteProjectRelatedProjectInfo(this);
        }


        /// <summary>
        /// Updates the object using appropriate provider.
        /// </summary>
        protected override void SetObject()
        {
            ProjectRelatedProjectInfoProvider.SetProjectRelatedProjectInfo(this);
        }

        #endregion


        #region "Constructors"

        /// <summary>
        /// Constructor for de-serialization.
        /// </summary>
        /// <param name="info">Serialization info</param>
        /// <param name="context">Streaming context</param>
        public ProjectRelatedProjectInfo(SerializationInfo info, StreamingContext context)
            : base(info, context, TYPEINFO)
        {
        }


        /// <summary>
        /// Constructor - Creates an empty ProjectRelatedProjectInfo object.
        /// </summary>
        public ProjectRelatedProjectInfo()
            : base(TYPEINFO)
        {
        }


        /// <summary>
        /// Constructor - Creates a new ProjectRelatedProjectInfo object from the given DataRow.
        /// </summary>
        /// <param name="dr">DataRow with the object data</param>
        public ProjectRelatedProjectInfo(DataRow dr)
            : base(TYPEINFO, dr)
        {
        }

        #endregion
    }
}

And here is the generated Provider class, note the use of multiple "projectId" parameters in most methods:

using System;
using System.Linq;

using CMS.DataEngine;

namespace MINE
{    
    /// <summary>
    /// Class providing ProjectRelatedProjectInfo management.
    /// </summary>
    public class ProjectRelatedProjectInfoProvider : AbstractInfoProvider<ProjectRelatedProjectInfo, ProjectRelatedProjectInfoProvider>
    {
        #region "Public methods"

        /// <summary>
        /// Returns all ProjectRelatedProjectInfo bindings.
        /// </summary>
        public static ObjectQuery<ProjectRelatedProjectInfo> GetProjectRelatedProjects()
        {
            return ProviderObject.GetObjectQuery();
        }


        /// <summary>
        /// Returns ProjectRelatedProjectInfo binding structure.
        /// </summary>
        /// <param name="projectId">MINE Project ID</param>
        /// <param name="projectId">MINE Project ID</param>  
        public static ProjectRelatedProjectInfo GetProjectRelatedProjectInfo(int projectId, int projectId)
        {
            return ProviderObject.GetProjectRelatedProjectInfoInternal(projectId, projectId);
        }


        /// <summary>
        /// Sets specified ProjectRelatedProjectInfo.
        /// </summary>
        /// <param name="infoObj">ProjectRelatedProjectInfo to set</param>
        public static void SetProjectRelatedProjectInfo(ProjectRelatedProjectInfo infoObj)
        {
            ProviderObject.SetProjectRelatedProjectInfoInternal(infoObj);
        }


        /// <summary>
        /// Deletes specified ProjectRelatedProjectInfo binding.
        /// </summary>
        /// <param name="infoObj">ProjectRelatedProjectInfo object</param>
        public static void DeleteProjectRelatedProjectInfo(ProjectRelatedProjectInfo infoObj)
        {
            ProviderObject.DeleteProjectRelatedProjectInfoInternal(infoObj);
        }


        /// <summary>
        /// Deletes ProjectRelatedProjectInfo binding.
        /// </summary>
        /// <param name="projectId">MINE Project ID</param>
        /// <param name="projectId">MINE Project ID</param>  
        public static void RemoveProjectFromProject(int projectId, int projectId)
        {
            ProviderObject.RemoveProjectFromProjectInternal(projectId, projectId);
        }


        /// <summary>
        /// Creates ProjectRelatedProjectInfo binding. 
        /// </summary>
        /// <param name="projectId">MINE Project ID</param>
        /// <param name="projectId">MINE Project ID</param>   
        public static void AddProjectToProject(int projectId, int projectId)
        {
            ProviderObject.AddProjectToProjectInternal(projectId, projectId);
        }

        #endregion


        #region "Internal methods"

        /// <summary>
        /// Returns the ProjectRelatedProjectInfo structure.
        /// Null if binding doesn't exist.
        /// </summary>
        /// <param name="projectId">MINE Project ID</param>
        /// <param name="projectId">MINE Project ID</param>  
        protected virtual ProjectRelatedProjectInfo GetProjectRelatedProjectInfoInternal(int projectId, int projectId)
        {
            return GetSingleObject()
                .Where("ProjectID", QueryOperator.Equals, projectId)
                .Where("RelatedProjectID", QueryOperator.Equals, projectId);
        }


        /// <summary>
        /// Sets specified ProjectRelatedProjectInfo binding.
        /// </summary>
        /// <param name="infoObj">ProjectRelatedProjectInfo object</param>
        protected virtual void SetProjectRelatedProjectInfoInternal(ProjectRelatedProjectInfo infoObj)
        {
            SetInfo(infoObj);
        }


        /// <summary>
        /// Deletes specified ProjectRelatedProjectInfo.
        /// </summary>
        /// <param name="infoObj">ProjectRelatedProjectInfo object</param>
        protected virtual void DeleteProjectRelatedProjectInfoInternal(ProjectRelatedProjectInfo infoObj)
        {
            DeleteInfo(infoObj);
        }


        /// <summary>
        /// Deletes ProjectRelatedProjectInfo binding.
        /// </summary>
        /// <param name="projectId">MINE Project ID</param>
        /// <param name="projectId">MINE Project ID</param>  
        protected virtual void RemoveProjectFromProjectInternal(int projectId, int projectId)
        {
            var infoObj = GetProjectRelatedProjectInfo(projectId, projectId);
            if (infoObj != null) 
            {
                DeleteProjectRelatedProjectInfo(infoObj);
            }
        }


        /// <summary>
        /// Creates ProjectRelatedProjectInfo binding. 
        /// </summary>
        /// <param name="projectId">MINE Project ID</param>
        /// <param name="projectId">MINE Project ID</param>   
        protected virtual void AddProjectToProjectInternal(int projectId, int projectId)
        {
            // Create new binding
            var infoObj = new ProjectRelatedProjectInfo();
            infoObj.ProjectID = projectId;
            infoObj.RelatedProjectID = projectId;

            // Save to the database
            SetProjectRelatedProjectInfo(infoObj);
        }

        #endregion      
    }
}
0 votesVote for this answer Mark as a Correct answer

Christian Nickel answered on July 22, 2014 15:39

I wish I could edit these questions, I didn't realize the pound symbol would turn text into headers.

0 votesVote for this answer Mark as a Correct answer

Brenden Kehren answered on July 23, 2014 18:31

@Christian, have you read the docs on module creating? Specifically creating the binding classs you're working with? I've read through both module and binding classes documentation many times and pick up new things each time. I have successfully created the interface without issue now several times. Its a matter of following the directions to a "T"!

To get to the point, you have to do some customization to the "Info" file, specifically, set the idColumn parameter to null and set the IsBinding value = true in the TypeInfo definition. You can also set some FK values within SQL Server Mgmt Studio if you'd like but not required.

0 votesVote for this answer Mark as a Correct answer

Christian Nickel answered on July 23, 2014 18:46

@Brenden, I have and was able to create many to many binding relationships between different tables, but not for same table.

So, I can create the relationship like:

project 1-* projectTopic *-1 topic

But I have to modify some Kentico code to get it to work like this:

project 1-* projectRelatedProject *-1 project

Make sense? The ProjectRelatedProjectInfo class I pasted was what Kentico generated, but the class I'm using has set idColumn to null and IsBinding = true.

I was able to get this relationship to work if I altered some of the GetBindingColumnNames, but I will not keep a solution like that in place, simply for proof of concept. Anyway, I have decided to not use a custom Module for my purposes going forward.

0 votesVote for this answer Mark as a Correct answer

   Please, sign in to be able to submit a new answer.