Skip to main content
Version: 2021.1

Sharepoint custom control

SDK custom control is another method of enhancing WEBCON BPS in a way that makes the user interface better suited for the users’ needs. This plugin will enable the creation of a control, which can be placed on the BPS form as if it were a normal form field. However, in contrast to form field extensions, data shown in a custom control does not have to be stored on the workflow instance. This data can be stored pretty much anywhere. While this grants more freedom, the drawback is that data saving and loading isn’t automatic, and must be implemented manually by the person designing the control. The example given below portrays the creation of a control which represents a time period confined by two dates ‘from’ and ‘to’. There is a default “Date and Time” form field available in the system that is used to enter one date on the form. The custom control will instead allow two dates and times to be entered, therefore defining a time period. One important thing to note is the fact that custom control described in this article is one of the UI extensions for classic form based on SharePoint interface.

Adding SDK custom control to the project

To create a custom control, first create two projects – Class Library which will contain the logic of your control and SharePoint which will contain the UI. We recommend using templates provided by us that make this process rapid and simple. They are available in the Visual Studio templates section in resources and also in the WEBCON BPS installer inside Documentation\Quick start BPS SDK\Templates.zip file. By using them you will be guided step by step through the process and much of the required configuration along with plugins packages creation automation is generated for you. Although you will be prompted about it, we would like to remind that in order to create SharePoint project using our templates first you will have to create the logic one.

create_new_project

The logic project will be compiled into the extensions library (DLL) whereas UI project will create the WSP package which has to be deployed to your SharePoint. Additionally, during deployment the ASCX file must be placed into the correct SharePoint catalog (Layouts\WEBCON). When using the extension’s project template for SharePoint e.g. BPS 2019 Extensions – SharePoint 2016 UI Controls”, it is enough to add the custom control to the Layouts directory in the project. During deployment, the entire contents of this directory will be placed in a SharePoint Layouts\WEBCON directory. The structure of this example can look like this:

project_structure

The project will automatically map all files in Layouts sub-catalogs to corresponding “LAYOUTS\WEBCON” sub-catalogs. In order to confirm where files will be placed after deployment, find and expand the “Package” catalog, open the Package.package file, and go to the “Manifest” tab. Once there, under the “TemplateFiles” you will find files that will be loaded and where they will be allocated. Preview of package manifest:

<Solution xmlns="http://schemas.microsoft.com/sharepoint/" SolutionId="d618d716-69b5-4af2-af79-338824a4ae12" SharePointProductVersion="15.0">
<Assemblies>
<Assembly Location="CustomControlUI.dll" DeploymentTarget="GlobalAssemblyCache" />
</Assemblies>
<TemplateFiles>
<TemplateFile Location="LAYOUTS\WEBCON\CustomControls\DateRange\DateRangeControl.ascx" />
</TemplateFiles>
</Solution>

If the custom control is created in different way than by using the provided template, make sure that the ASCX file is placed in the correct “LAYOUTS” sub-catalog during deployment. When your projects are set up you can start creating the classes used by your custom control.

Custom control logic

Create the classes

Let’s start with creating the classes of the written custom control. As with projects, we also recommend using one of our templates that prepare all the configuration and necessary classes for you.

create_classes

control_template_creator

Plugin configuration class

The second step is to implement the plugin’s configuration class which has been just created for you if you have made use of our template. Custom control has the same configuration mechanism as the one available to the SDK action. In this example, the configuration properties will be used to define form fields into which data from the control will be saved. As mentioned in the introduction, saving data from custom controls in the system must be implemented manually – for this example, data will be saved into technical form fields defined in the configuration. The easiest way to achieve this is to create text fields in the configuration. Then simply enter the database column ids in the BPS Designer Studio that correspond to the desired form fields into the configuration text fields.

using WebCon.WorkFlow.SDK.Common;
using WebCon.WorkFlow.SDK.ConfigAttributes;
namespace WebCon.BPS.SampleEN.DateRangeControl.CustomControls
{
public class DateRangeConfig : PluginConfiguration
{
[ConfigEditableText(DisplayName = "Date from", Description = "Database field where \"date from\" will be stored")]
public int DateFromDbID { get; set; }
[ConfigEditableText(DisplayName = "Date to", Description = "Database field where \"date to\" will be stored")]
public int DateToDbID { get; set; }
}
}

studio_configuration

Model class

The third step is to implement the class that will store values from the control. The BPS is responsible for taking care of communication between the logic of the control and the UI. The model class can be defined in order to be able to interact between these two parts. In this case we will create two nullable DateTime properties that will hold the dates.

using System;
namespace CustomControlLogic.CustomControls.DateRange
{
public class DateRangeValue
{
public DateTime? FromDate { get; set; }
public DateTime? ToDate { get; set; }
}
}

Logic class

The fourth step is to implement the class that is responsible for all the logic operations on the control.

using WebCon.WorkFlow.SDK.Common.Model;
using WebCon.WorkFlow.SDK.FormFieldPlugins;
using WebCon.WorkFlow.SDK.FormFieldPlugins.Model;
namespace CustomControlLogic.CustomControls.DateRange
{
public class DateRange : CustomFormField<DateRangeConfig, DateRangeValue>
{
public override DateRangeValue LoadValue(LoadValueParams args)
{
return new DateRangeValue()
{
FromDate = args.Context.CurentDocument.DateTimeFields.GetByID(Configuration.DateFromDbID).Value,
ToDate = args.Context.CurentDocument.DateTimeFields.GetByID(Configuration.DateToDbID).Value
};
}

public override void OnBeforeElementSave (BeforeSaveParams<CustomFormFieldValueContextInfo> args)
{ args.Context.CurentDocument.DateTimeFields.GetByID(Configuration.DateFromDbID).SetValue(args.Context.Value?.FromDate);
args.Context.CurentDocument.DateTimeFields.GetByID(Configuration.DateToDbID).SetValue(args.Context.Value?.ToDate);
}

public override void Validate (ControlValidationParams<CustomFormFieldValueContextInfo> args)
{
if (!args.Context.CurrentField.IsRequired)
return;
if (args.Context.Value == null ||
args.Context.Value.FromDate == null ||
args.Context.Value.ToDate == null)
{
args.IsValid = false;
args.ErrorMessage = "Date from and date to must contain values!";
return;
}
if (args.Context.Value.FromDate > args.Context.Value.ToDate)
{
args.IsValid = false;
args.ErrorMessage = "Date to cannot be earlier than date from!";
}
}
}
}

After that the logic for your custom control is ready. The methods used are discussed in the next sections of this article.

Custom control UI

At the beginning, same as in the “Custom control logic” section, let’s start with creating the necessary classes.

new_control

control_ui_template

ASCX file

Our aim is to create a control, which will be used to enter a time period spanning from one date to the other. Therefore, in the ASCX file we create two date controls with relevant labels. ASCX file markup is as follows:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="DateRangeControl.ascx.cs" Inherits="CustomControlUI.Layouts.CustomControls.DateRange.DateRangeControl, $SharePoint.Project.AssemblyFullName$" %>
<%@ Assembly Name="WebCon.WorkFlow.SDK, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c30f1f18c194ceba" %>
<%@ Assembly Name="WebCon.WorkFlow.SDK.SP, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c30f1f18c194ceba" %>
<%@ Register Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral,PublicKeyToken=71e9bce111e9429c" Namespace="Microsoft.SharePoint.WebControls" Tagprefix="cc1" %>
<table>
<tr>
<td>
<asp:Label runat="server" ID="lblControlName"></asp:Label>
</td>
<td>
<table border ="1">
<tr>
<td>FROM</td>
<td>
<cc1:DateTimeControl ID="DateTimeControl1" runat="server" />
</td>
</tr>
<tr>
<td>TO</td>
<td>
<cc1:DateTimeControl ID="DateTimeControl2" runat="server" />
</td>
</tr>
</table>
</td>
</tr>
</table>

User interface class

using WebCon.WorkFlow.SDK.Documents.Model.Base;
using WebCon.WorkFlow.SDK.SP.FormFieldPlugins;
using WebCon.WorkFlow.SDK.SP.FormFieldPlugins.Model;
using CustomControlLogic.CustomControls.DateRange;
namespace CustomControlUI.Layouts.CustomControls.DateRange
{
public partial class DateRangeControl : CustomFormFieldSPControl<DateRangeConfig,DateRangeValue>
{
public override DisplayNamePlace FormFieldDisplayNamePlace => DisplayNamePlace.Beside;
public override DateRangeValue GetControlValue(GetControlValueParams args)
{
lblControlName.Text = args.Context.CurrentField.DisplayName + (args.Context.CurrentField.IsRequired ? " *" : string.Empty);
if (!DateTimeControl1.IsDateEmpty && !DateTimeControl2.IsDateEmpty)
return new DateRangeValue()
{
FromDate = DateTimeControl1.SelectedDate,
ToDate = DateTimeControl2.SelectedDate
};
else
return null;
}
public override void SetControlValue(SetControlValueParams<CustomFormFieldContext,DateRangeValue> args)
{
ValidateReadOnly(args.Context.CurrentField);
if (args.ValueToSet != null)
{
if (args.ValueToSet.FromDate != null)
DateTimeControl1.SelectedDate = args.ValueToSet.FromDate.Value;
if (args.ValueToSet.ToDate != null)
DateTimeControl2.SelectedDate = args.ValueToSet.ToDate.Value;
}
}
private void ValidateReadOnly(FormElement currentField)
{
DateTimeControl1.Enabled = currentField.IsEditable;
DateTimeControl2.Enabled = currentField.IsEditable;
}
}
}

The implementation of the methods used there is discussed in the next sections of this article. Due to the fact that CustomFormFieldSPControl class derives from PluginBaseControlSP which in turn derives from UserControl we also have access to other methods of the control class like OnLoad, OnInit or OnPreRender which means that it is possible to freely adjust what happens on each stage of an ASP.NET Page Life Cycle.

Deployment and plugin configuration

In order to make your custom control working you have to deploy the project with the UI (right click the project name in Visual Studio and select Deploy) so the project gets located in SharePoint and register plugins package inside the WEBCON Business Process Suite – Designer Studio. This process is the same as with the SDK action. You can read about it here. In order to use the newly configured plugin, first add a new form field to a process and then select Custom control (SDK) as its type. The “Customization of form field controls (SDK)” in the bottom right Settings section should become available – select the recently created plugin from the drop-down menu.

register_control

If the plugin was deployed correctly, the configuration properties that were discussed earlier can be found in the “Advanced configuration” window. After mapping the configuration properties to appropriate storage form fields, the custom control form field can be added onto a form and its visibility can be defined like that of any other regular form field.

control_visibility

In order to verify whether the implemented solution actually works set the control form field as required on the field matrix in Designer Studio and attempt to use the transition path without filling out the two dates in the control. You should receive a message about the error and access to the next step should be denied.

Form field label and requiredness settings

Because the custom control plugin can be used to create controls that have any design and layout, it does not support setting form fields as required out of the box. The option to toggle form fields as required or not must be implemented separately.

Form field label

By default, the fields matrix in Designer Studio defines the behavior of each form field on each step – the available behavior options are: Visible, Read-only and Required. Toggling the last of these options causes the form field to check if a value is entered into it – if it is empty, the workflow instance will not be able to progress to the next step. Form fields that are ‘Required’ are denoted in the BPS form by a red asterisk (*) next to their respective labels. For custom controls, we must manually ensure that a label and asterisk is displayed. The IsRequired flag which you can find inside args.Context.CurrentField will be helpful in achieving this. It defines if the form field is marked as required on the field matrix. Whether or not a red asterisk is displayed by the label is decided based on the value of this flag. Correctly displaying the label, while also including the requiredness setting are handled within a GetControlValue method

public override DateRangeValue GetControlValue(GetControlValueParams args)
{
lblControlName.Text = args.Context.CurrentField.DisplayName + (args.Context.CurrentField.IsRequired ? " *" : string.Empty);
if (!DateTimeControl1.IsDateEmpty && !DateTimeControl2.IsDateEmpty)
return new DateRangeValue()
{
FromDate = DateTimeControl1.SelectedDate,
ToDate = DateTimeControl2.SelectedDate
};
else
return null;
}

This code will cause two things to happen:

  1. The field name in Designer Studio will appear beside the custom control on the BPS form
  2. Setting the custom control form field as required through the field matrix will also cause a red asterisk to appear next to its label

Validating requiredness

With the visual aspects sorted, we will discuss a mechanism for validating whether the form field has a value entered into it. The way in which this validation is carried out depends on the structure and expected behavior of the control. In our example, we expect both dates (‘from’ and ‘to’) to be filled out, and it would also make sense to ensure that ‘Date to’ doesn’t occur before ‘Date from’. For validation, we can use one of two available methods of the base class: Validate() or CheckSaveRestrictions(). These two vary in purpose and also the timing in which they are activated.

  1. CheckSaveRestrictions is supposed to verify whether data entered is in the correct format, so that it may be saved into the database. It is activated both when using a transition path, and when progress on a workflow instance form is saved.
  2. Validate method is supposed to verify the logical correctness of the data and also if data has been entered when it is required. The Validate method is only run when using a transition path for which the ‘Validate’ option has been toggled on. For this example, we will carry out our validation using the second option (the Validate method) because we want to prevent users from using transition paths when the data is invalid, but at the same time they should be able to save the workflow instance at will, even if the required data isn’t filled out. The ControlValidationParams generic class includes fields which can deny access to transition paths (IsValid) and also display messages to the user (ErrorMessage).
public override void Validate (ControlValidationParams<CustomFormFieldValueContextInfo> args)
{
if (!args.Context.CurrentField.IsRequired)
return;
if (args.Context.Value == null ||
args.Context.Value.FromDate == null ||
args.Context.Value.ToDate == null)
{
args.IsValid = false;
args.ErrorMessage = "Date from and date to must contain values!";
return;
}
if (args.Context.Value.FromDate > args.Context.Value.ToDate)
{
args.IsValid = false;
args.ErrorMessage = "Date to cannot be earlier than date from!";
}
}

Saving and loading data

The next methods to discuss are mechanisms for saving data that has been entered into it and also for loading any existing data. As mentioned in the introduction, custom control can handle pretty much any way and location for storing data. There are two main schools of thought – saving data from the control into one or multiple form fields, or saving the data outside the workflow instance altogether (e.g. in a separate dedicated table of the database). In order to make both of these scenarios easier to implement, the base generic class of the logic part CustomFormField provides two methods that can be overwritten: OnBeforeElementSave and OnAfterElementSave. If we want to save data to another form field of the same workflow instance, it is best to utilize OnBeforeElementSave. The chosen form fields can be freely modified within it, and the changes will be saved to the database along with the entire workflow instance. If we want to save data in an external table (for example), it is better to utilize the OnAfterElementSave method. This method provides access to the ID’s of new instances (newly created instances don’t have allocated ID’s until they are saved, therefore in the OnBeforeElementSave method the ID is empty), once we have obtained this instance ID, we can associate it with the relevant table entry.

Saving data

For our example, we want to take the two dates entered into the custom control and save them into two separate form fields. Therefore, we need to set the values in the OnBeforeElementSave method to the fields entered in the custom control configuration. The method’s BeforeSaveParams argument has the Context property which in turn has CurrentDocument property that contains all the form fields of the workflow instance.

public override void OnBeforeElementSave (BeforeSaveParams<CustomFormFieldValueContextInfo> args)
{ args.Context.CurentDocument.DateTimeFields.GetByID(Configuration.DateFromDbID).SetValue(args.Context.Value?.FromDate);
args.Context.CurentDocument.DateTimeFields.GetByID(Configuration.DateToDbID).SetValue(args.Context.Value?.ToDate);
}

CurrentDocument represents as the name states current document – after being saved, any changes are carried over onto the database.

Loading data

Now let’s discuss handling displaying any existing data on the BPS form – currently the saved data is not displayed in the custom control. Filling controls with existing data is handled by LoadValue method from the logic part and SetControlValue in the UI part.

public override void SetControlValue (SetControlValueParams<CustomFormFieldContext, DateRangeValue> args)
{
SetDateTimeControlsAccesibility(args.Context.CurrentField);
if (args.ValueToSet != null)
{
if (args.ValueToSet.FromDate != null)
DateTimeControl1.SelectedDate = args.ValueToSet.FromDate.Value;
if (args.ValueToSet.ToDate != null)
DateTimeControl2.SelectedDate = args.ValueToSet.ToDate.Value;
}
}

Read-only and administrator modes

The ‘Editable’ state of our sample custom control is now fully implemented. However, in the field matrix, a form field can also be set as visible but in a read-only state. In such a scenario, the custom control should display its contents on the form, but the option to edit the values should be disabled. To handle this setting, we have created ValidateReadOnly method that is then used in SetControlValue method.

private void SetDateTimeControlsAccesibility(FormElement currentField)
{
DateTimeControl1.Enabled = currentField.IsEditable;
DateTimeControl2.Enabled = currentField.IsEditable;
}