Building a parent-child relationship with the forms

#Custom software development #.NET

SHARE

 
In this post I'd like to walk you through a process of building parent-child relation with the forms. Those, who know Kentico and available range of features could ask absolutely obvious question: why would I build parent-child relation with forms while there are modules, or, at least, custom tables? I can see two good reasons for doing this:
  • parent object is already created and there is cost related with conversion it to a module class
  • it allows non-technical users, such as content editors, creation of the parent-child data structure

 
Recently I've got a request to add 7 exactly the same sections to an existing form. The form was job application and already contained huge list of fields. It was presented on front end with great Multi-steps Online Forms web part from Kentico markeplace and that was quite beneficial for me (I'll explain why later). Those 7 sections I needed to add were previous employments. Each section contained around 20 fields, so I could end up adding 140 fields to an existing form. But this approach did not seem to me enough flexible: what if user will populate just 2 sections? Or what if user would like to enter more than 7 employments? Also adding 140 fields is not the most exciting thing to do, so I've starter to design some better solution, which I want to share with all of you.
 

Goal

So I thought about creation of a form control that would list all the child record with possibility to edit them as well as adding new one. 
 

Prerequisites

Before beginning actual development we need to create 2 forms to run tests against. Let's pretend we need to build form for event registration, which allows user to register his friends or colleagues as well. So the first form is EventRegistration and second one Attendee that will store any additional attendee that user registers along. EventRegistration form will contain one extra field for attendees, which could be with or without database representation, and it use SubForm (control we are building) as a form control. We will get back to this field later. There is requirement for Attendee form: it should contain a fields-reference to its parent object - EventRegirstration record.    
 

Control

Following is the markup for the control that would allow us to reach my goal:
 
<asp:UpdatePanel ID="up" runat="server">
	<ContentTemplate> 
		<asp:Repeater ID="rptRecords" runat="server" OnItemDataBound="rptRecords_ItemDataBound">
			<ItemTemplate>
				<cms:BizForm ID="bfItem" runat="server" IsLiveSite="true"
					FormName='<%# Eval("Form") %>' SiteName='<%# Eval("Site") %>' AlternativeFormFullName='<%# Eval("AlternativeForm") %>'
					ItemID='<%# Eval("ID") %>' />
				</ItemTemplate>
			</asp:Repeater>
		<cms:BizForm ID="viewBiz" runat="server" IsLiveSite="true" FormClearAfterSave="true"/>
	</ContentTemplate>
</asp:UpdatePanel>

As you can see it contains a repeater listing existing records. Each item of the repeater is a BizForm. Also control contains a separate BizForm, that would be responsible for creation of a new record. Real advantages of using BizForm here is the fact that it handles creation and/or update of the records as well as it knows how to present an item either by building default layout, or using alternative form. The entire control is wrapped with update panel, so it will not cause page reload, which is always good from user experience prospective.

As we are developing custom form control, control should inherit FormEngineUserControl and implement its methods and
 property: IsValid(), GetOtherValue(string name)
and Value. We need to add extra properties to the control:
  • BizFormName - code name of the child form
  • AlternativeFormName - layout of the child form (optional)
  • SiteName
  • ReferenceColumnName - this will tell our custom control which form fields should be used to store reference to parent record (EventRegistration in our case)
I've also added private property in order to get easy access to parent form object:
 
private BizForm ParentForm
    {
        get { return (BizForm)(this.NamingContainer); }
    }

Now let's add data binding for a list of added attendees: 
 
private void BindData()
    {
        var classInfo = CacheHelper.Cache(cs => GetFormClassInfo(cs), new CacheSettings(30, string.Format("{0}|{1}", "formclassinfo", BizFormName)));

        var items = BizFormItemProvider.GetItems(classInfo.ClassName)
            .Where(i => i.GetIntegerValue(ReferenceColumnName, 0) == ParentForm.ItemID)
            .Select(i => new
            {
                Form = BizFormName,
                Site = SiteName,
                AlternativeForm = AlternativeFormName,
                ID = i.ItemID
            });
        rptRecords.DataSource = items;
        rptRecords.DataBind();
    }

    private DataClassInfo GetFormClassInfo(CacheSettings cs)
    {
        BizFormInfo formObject = BizFormInfoProvider.GetBizFormInfo(BizFormName, SiteContext.CurrentSiteID);
        DataClassInfo formClass = DataClassInfoProvider.GetDataClassInfo(formObject.FormClassID);

        return formClass;
    }

As you can see each item of the repeater is a BizForm control and we are passing just basic setting to it: form name, layout, site name and record ID, so now we need to make sure BizForm will load data of an actual record. This could be achieved by adding one line of code to the item data bound event handler:
 
protected void rptRecords_ItemDataBound(object sender, RepeaterItemEventArgs e)
    {
        (e.Item.FindControl("bfItem") as BizForm).ReloadData();
    }

Once we have code, that will load data we can start adding code, that will setup the control:
 
protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        if (ParentForm.ItemID > 0 && rptRecords.Items.Count == 0)
        {
            BindData();
        }

        viewBiz.FormName = BizFormName;
        viewBiz.SiteName = SiteName;
        viewBiz.AlternativeFormFullName = AlternativeFormName;
        viewBiz.ControlContext.ContextName = CMS.ExtendedControls.ControlContext.LIVE_SITE;
        viewBiz.OnBeforeSave += viewBiz_OnBeforeSave;
        viewBiz.OnAfterSave += viewBiz_OnAfterSave;

        if (!IsPostBack)
        {
            viewBiz.ReloadData();
        }
    }

Here we simply setup BizForm control responsible for adding new records (Attendees) and calling data load for already created items. Also we assign for before and after save evens. Here are appropriate handlers:
 
void viewBiz_OnAfterSave(object sender, EventArgs e)
    {
        ParentForm.ReloadData();
    }

    void viewBiz_OnBeforeSave(object sender, EventArgs e)
    {
        if (ParentForm.ItemID <= 0)
        {
            ParentForm.SaveData("");
        }
        if (string.IsNullOrEmpty(ReferenceColumnName) || ParentForm.ItemID <= 0)
        {
            viewBiz.StopProcessing = true;
        }
        viewBiz.Data.SetValue(ReferenceColumnName, ParentForm.ItemID);
    }

Before save handler sets reference to the parent record (EventRegistration) as well as saves parent record in case it has not been saved yet. Save after handler ensures parent form reload.
 

Register Form Control

Now it is time to register our new control in Kentico. Go to Form Controls application and create a new form control. Set following values:
  • Display name: SubForm
  • Type: Multifield
  • File name: select path to your control
  • Use control for: Text
  • Show control in: Forms
Save your changes and navigate properties tab. Add following properties:
  • BizFormName: Text, Required, BizForm selector
  • AlternativeFormName:Text, Alternative form selector
  • SiteName: Text, Site selector
  • ReferenceColumnName: Text, Text box
Now SubForm control should be available as editing control for text fields in your form.
 

SubForm Usage

Let's try to set up SubForm as an editing control for EventRegistration form mentioned earlier.
First of all we need to make sure that field for storing reference to EventRegistration has been added to Attendee form. I've called it ParentId.
Now navigate fields on EventRegistration form and new one called Attendees. Set Text as type and select SubForm as form control, expand Editing control setting section and configure child object: select Attendee form for Biz Form field and ParentId for Reference Column Name. You may also specify alternative form for child object. Save your changes. That should be it.
 

Variations

I've mentioned Multi-Step Online form web part earlier, saying it gave me some advantages. So the main advantage was that it saves forms on each step, which mean there is no necessary to check if parent record has been saved, so no need to save it. Saving of parent record introduces complication and extra processing to this control that I'd recommend to avoid if possible. So if you're good with the scenario where user has to save parent object before adding children, you could optimize this control a bit.
However, example in this post gives you generic control that could be reused with multiple scenarios.
 

Result

The form was job application and already contained huge list of fields. It was presented on front end with great Multi-steps Online Forms web part from Kentico markeplace and that was quite beneficial for me (I'll explain why later). Those 7 sections I needed to add were previous employments. Each section contained around 20 fields, so I could end up adding 140 fields to an existing form. But this approach did not seem to me enough flexible: what if user will populate just 2 sections? Or what if user would like to enter more than 7 employments? Also adding 140 fields is not the most exciting thing to do, so I've starter to design some better solution, which I want to share with all of you.
 

Goal

So I thought about creation of a form control that would list all the child record with possibility to edit them as well as adding new one. 
 

Prerequisites

Before beginning actual development we need to create 2 forms to run tests against. Let's pretend we need to build form for event registration, which allows user to register his friends or colleagues as well. So the first form is EventRegistration and second one Attendee that will store any additional attendee that user registers along. EventRegistration form will contain one extra field for attendees, which could be with or without database representation, and it use SubForm (control we are building) as a form control. We will get back to this field later. There is requirement for Attendee form: it should contain a fields-reference to its parent object - EventRegirstration record.    
 

Control

Following is the markup for the control that would allow us to reach my goal:
 
<asp:UpdatePanel ID="up" runat="server">
	<ContentTemplate> 
		<asp:Repeater ID="rptRecords" runat="server" OnItemDataBound="rptRecords_ItemDataBound">
			<ItemTemplate>
				<cms:BizForm ID="bfItem" runat="server" IsLiveSite="true"
					FormName='<%# Eval("Form") %>' SiteName='<%# Eval("Site") %>' AlternativeFormFullName='<%# Eval("AlternativeForm") %>'
					ItemID='<%# Eval("ID") %>' />
				</ItemTemplate>
			</asp:Repeater>
		<cms:BizForm ID="viewBiz" runat="server" IsLiveSite="true" FormClearAfterSave="true"/>
	</ContentTemplate>
</asp:UpdatePanel>

As you can see it contains a repeater listing existing records. Each item of the repeater is a BizForm. Also control contains a separate BizForm, that would be responsible for creation of a new record. Real advantages of using BizForm here is the fact that it handles creation and/or update of the records as well as it knows how to present an item either by building default layout, or using alternative form. The entire control is wrapped with update panel, so it will not cause page reload, which is always good from user experience prospective.

As we are developing custom form control, control should inherit FormEngineUserControl and implement its methods and
 property: IsValid(), GetOtherValue(string name)
and Value. We need to add extra properties to the control:
  • BizFormName - code name of the child form
  • AlternativeFormName - layout of the child form (optional)
  • SiteName
  • ReferenceColumnName - this will tell our custom control which form fields should be used to store reference to parent record (EventRegistration in our case)
I've also added private property in order to get easy access to parent form object:
 
private BizForm ParentForm
    {
        get { return (BizForm)(this.NamingContainer); }
    }

Now let's add data binding for a list of added attendees: 
 
private void BindData()
    {
        var classInfo = CacheHelper.Cache(cs => GetFormClassInfo(cs), new CacheSettings(30, string.Format("{0}|{1}", "formclassinfo", BizFormName)));

        var items = BizFormItemProvider.GetItems(classInfo.ClassName)
            .Where(i => i.GetIntegerValue(ReferenceColumnName, 0) == ParentForm.ItemID)
            .Select(i => new
            {
                Form = BizFormName,
                Site = SiteName,
                AlternativeForm = AlternativeFormName,
                ID = i.ItemID
            });
        rptRecords.DataSource = items;
        rptRecords.DataBind();
    }

    private DataClassInfo GetFormClassInfo(CacheSettings cs)
    {
        BizFormInfo formObject = BizFormInfoProvider.GetBizFormInfo(BizFormName, SiteContext.CurrentSiteID);
        DataClassInfo formClass = DataClassInfoProvider.GetDataClassInfo(formObject.FormClassID);

        return formClass;
    }

As you can see each item of the repeater is a BizForm control and we are passing just basic setting to it: form name, layout, site name and record ID, so now we need to make sure BizForm will load data of an actual record. This could be achieved by adding one line of code to the item data bound event handler:
 
protected void rptRecords_ItemDataBound(object sender, RepeaterItemEventArgs e)
    {
        (e.Item.FindControl("bfItem") as BizForm).ReloadData();
    }

Once we have code, that will load data we can start adding code, that will setup the control:
 
protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        if (ParentForm.ItemID > 0 && rptRecords.Items.Count == 0)
        {
            BindData();
        }

        viewBiz.FormName = BizFormName;
        viewBiz.SiteName = SiteName;
        viewBiz.AlternativeFormFullName = AlternativeFormName;
        viewBiz.ControlContext.ContextName = CMS.ExtendedControls.ControlContext.LIVE_SITE;
        viewBiz.OnBeforeSave += viewBiz_OnBeforeSave;
        viewBiz.OnAfterSave += viewBiz_OnAfterSave;

        if (!IsPostBack)
        {
            viewBiz.ReloadData();
        }
    }

Here we simply setup BizForm control responsible for adding new records (Attendees) and calling data load for already created items. Also we assign for before and after save evens. Here are appropriate handlers:
 
void viewBiz_OnAfterSave(object sender, EventArgs e)
    {
        ParentForm.ReloadData();
    }

    void viewBiz_OnBeforeSave(object sender, EventArgs e)
    {
        if (ParentForm.ItemID <= 0)
        {
            ParentForm.SaveData("");
        }
        if (string.IsNullOrEmpty(ReferenceColumnName) || ParentForm.ItemID <= 0)
        {
            viewBiz.StopProcessing = true;
        }
        viewBiz.Data.SetValue(ReferenceColumnName, ParentForm.ItemID);
    }

Before save handler sets reference to the parent record (EventRegistration) as well as saves parent record in case it has not been saved yet. Save after handler ensures parent form reload.
 

Register Form Control

Now it is time to register our new control in Kentico. Go to Form Controls application and create a new form control. Set following values:
  • Display name: SubForm
  • Type: Multifield
  • File name: select path to your control
  • Use control for: Text
  • Show control in: Forms
Save your changes and navigate properties tab. Add following properties:
  • BizFormName: Text, Required, BizForm selector
  • AlternativeFormName:Text, Alternative form selector
  • SiteName: Text, Site selector
  • ReferenceColumnName: Text, Text box
Now SubForm control should be available as editing control for text fields in your form.
 

SubForm Usage

Let's try to set up SubForm as an editing control for EventRegistration form mentioned earlier.
First of all we need to make sure that field for storing reference to EventRegistration has been added to Attendee form. I've called it ParentId.
Now navigate fields on EventRegistration form and new one called Attendees. Set Text as type and select SubForm as form control, expand Editing control setting section and configure child object: select Attendee form for Biz Form field and ParentId for Reference Column Name. You may also specify alternative form for child object. Save your changes. That should be it.
 

Variations

I've mentioned Multi-Step Online form web part earlier, saying it gave me some advantages. So the main advantage was that it saves forms on each step, which mean there is no necessary to check if parent record has been saved, so no need to save it. Saving of parent record introduces complication and extra processing to this control that I'd recommend to avoid if possible. So if you're good with the scenario where user has to save parent object before adding children, you could optimize this control a bit.
However, example in this post gives you generic control that could be reused with multiple scenarios.
 

Author

Check other articles

Bitsorchestra
5 5

What our clients say

Bits Orchestra team are outstanding developers​. They listen carefully to our business needs and easily turns our business objectives into a well thought out and executed development effort. Roman is very bright and definitely the most capable developer that has worked on our site. He is not only a Kentico expert but has successfully tackled other complicated development assignments demonstrating expertise in both front and backend development. Roman takes initiative to suggest enhancements that make site maintenance easier while improving the customer experience. The team is very responsive to our work requests and has great follow up. They have also worked very business partners and this has reflected positively on our company. Roman is a true partner for us and a tremendous asset to our organization. We will continue to work with them and would highly recommend Roman and his team for your development needs. He and his team will exceed your expectations!
 Alan Lehmann
Alan Lehmann
President at In energy sector

What our clients say

The Bits Orchestra team does excellent work. They are always available and I appreciate our frequent calls and screen-shares together. Their dedication to the projects and knowledge of Kentico is outstanding. They truly care about the quality of their work, and became a part of our team easily!
Shena Lowe
Shena Lowe
Managing Partner at Consensus Interactive

What our clients say

We hired Roman for a Kentico analysis project and have been very satisfied. He is very skilled and professional. We are looking to hire him and his team again on future projects.
Sylvain Audet
Sylvain Audet
CEO at MyDevPartner.com

What our clients say

Roman and team have taken over an existing Kentico EMS site for a large US Oil Company. So far, they have handled every single request that we have thrown at them and these were diverse, challenging, often bespoke, usually urgent and almost daily, over the last 11 months. Their work is of an extremely high quality, they are capable, quick and we have great confidence in the support that we are getting.
Jon Hollis
Jon Hollis
Head of Web Development at confidential

What our clients say

Bits Orchestra team was very helpful, they had a good understanding of the brief and deep knowledge of the system. They were always keen to provide advice and recommendations that benefit the project substantially.
Ramon Lapenta
Ramon Lapenta
Senior Front End Developer at Cyber-Duck Ltd