Building a parent-child relationship with the forms

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.


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. 


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.    


Following is the markup for the control that would allow us to reach my goal:
  1. <asp:UpdatePanel ID="up" runat="server">
  2. <ContentTemplate>
  3. <asp:Repeater ID="rptRecords" runat="server" OnItemDataBound="rptRecords_ItemDataBound">
  4. <ItemTemplate>
  5. <cms:BizForm ID="bfItem" runat="server" IsLiveSite="true"
  6. FormName='<%# Eval("Form") %>' SiteName='<%# Eval("Site") %>' AlternativeFormFullName='<%# Eval("AlternativeForm") %>'
  7. ItemID='<%# Eval("ID") %>' />
  8. </ItemTemplate>
  9. </asp:Repeater>
  10. <cms:BizForm ID="viewBiz" runat="server" IsLiveSite="true" FormClearAfterSave="true"/>
  11. </ContentTemplate>
  12. </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:
  1. private BizForm ParentForm
  2. {
  3. get { return (BizForm)(this.NamingContainer); }
  4. }

Now let's add data binding for a list of added attendees: 
  1. private void BindData()
  2. {
  3. var classInfo = CacheHelper.Cache(cs => GetFormClassInfo(cs), new CacheSettings(30, string.Format("{0}|{1}", "formclassinfo", BizFormName)));
  4. var items = BizFormItemProvider.GetItems(classInfo.ClassName)
  5. .Where(i => i.GetIntegerValue(ReferenceColumnName, 0) == ParentForm.ItemID)
  6. .Select(i => new
  7. {
  8. Form = BizFormName,
  9. Site = SiteName,
  10. AlternativeForm = AlternativeFormName,
  11. ID = i.ItemID
  12. });
  13. rptRecords.DataSource = items;
  14. rptRecords.DataBind();
  15. }
  16. private DataClassInfo GetFormClassInfo(CacheSettings cs)
  17. {
  18. BizFormInfo formObject = BizFormInfoProvider.GetBizFormInfo(BizFormName, SiteContext.CurrentSiteID);
  19. DataClassInfo formClass = DataClassInfoProvider.GetDataClassInfo(formObject.FormClassID);
  20. return formClass;
  21. }

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:
  1. protected void rptRecords_ItemDataBound(object sender, RepeaterItemEventArgs e)
  2. {
  3. (e.Item.FindControl("bfItem") as BizForm).ReloadData();
  4. }

Once we have code, that will load data we can start adding code, that will setup the control:
  1. protected override void OnLoad(EventArgs e)
  2. {
  3. base.OnLoad(e);
  4. if (ParentForm.ItemID > 0 && rptRecords.Items.Count == 0)
  5. {
  6. BindData();
  7. }
  8. viewBiz.FormName = BizFormName;
  9. viewBiz.SiteName = SiteName;
  10. viewBiz.AlternativeFormFullName = AlternativeFormName;
  11. viewBiz.ControlContext.ContextName = CMS.ExtendedControls.ControlContext.LIVE_SITE;
  12. viewBiz.OnBeforeSave += viewBiz_OnBeforeSave;
  13. viewBiz.OnAfterSave += viewBiz_OnAfterSave;
  14. if (!IsPostBack)
  15. {
  16. viewBiz.ReloadData();
  17. }
  18. }

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:
  1. void viewBiz_OnAfterSave(object sender, EventArgs e)
  2. {
  3. ParentForm.ReloadData();
  4. }
  5. void viewBiz_OnBeforeSave(object sender, EventArgs e)
  6. {
  7. if (ParentForm.ItemID <= 0)
  8. {
  9. ParentForm.SaveData("");
  10. }
  11. if (string.IsNullOrEmpty(ReferenceColumnName) || ParentForm.ItemID <= 0)
  12. {
  13. viewBiz.StopProcessing = true;
  14. }
  15. viewBiz.Data.SetValue(ReferenceColumnName, ParentForm.ItemID);
  16. }

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.


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.


As a result of this 'lab' you should get something like this. If you don't  want to mess around with the pieces of code provided in this blog, you can download export packages for version 8.2 and 9 directly from our site, import them to your site and enjoy!