Custom Data Type
Ucommerce comes with the ability to add custom data types. A custom data type gives you the ability to hook up custom editors to save special data for custom properties on products, categories, campaign items, data types, etc.
This guide will walk you through and show you how easily we can extend the list of available editors. If you want to know how data types are tied together with definitions, you can read more about definitions here.
Overview of Data Types
As you can see in the DataType
table, it has a reference to a Definition
which describes the data available on a DataType
. A DataType
will have at least two DefinitionField
on its Definition
and those two are named definition and editor. The definition property is used to store which Definition
is selected for the DataType
and the editor property stores which editor should be used when the data is presented. We'll use the latter in the custom data type to differ what to do with it.
The editor property are defined on the "Data Type" definition which is the base definition for data types, which all other data type definitions must inherit. You can read more about inherited definitions here.
The editor picked for the DataType
is stored as an EntityProperty
and to get the value, we will use the IEntityPropertyValueService
which will be elaborated in the next section.
When creating a custom data type there are two interfaces that we must implement:
- IControlFactory: has the responsibility to create a UI for the data type.
- IControlAdapter: as the responsibility to extract the entered/selected value from the control.
Implementing IControlFactory
The interface is found under the namespace UCommerce.Presentation.Web.Controls
and you'll need a reference to the assembly UCommerce.Presentation.dll
.
In this example, we'll create a control that allows us to pick a price group for a product.
We'll start of by creating the class and since we want to support price groups, we'll call it PriceGroupPickerControlFactory:
using System; using System.Web.UI; using UCommerce.EntitiesV2; using UCommerce.EntitiesV2.Definitions; using UCommerce.Presentation.Web.Controls; namespace MyUCommerceApp.DataTypes { public class PriceGroupPickerControlFactory : IControlFactory { public bool Supports(DataType dataType) { throw new NotImplementedException(); } public Control GetControl(IProperty property) { throw new NotImplementedException(); } } }
The interface has two methods that we'll need to implement, which are:
- Supports
- GetControl
The use of IControlFactory
is provider based, which means that the underlying framework will ask every registration of IControlFactory
whether they support a specific data type. You can create multiple control factories, which support the same data types, but the first IControlFactory
that returns true from the Supports
method will be used. Therefore, the order of the registration in configuration is very important.
Implementing Supports
The first method we'll need to implement is Supports
which is what tells the framework if we have a provider registered for the specific DataType
.
private readonly IRepository<PriceGroup> _priceGroupRepository; private readonly IEntityPropertyValueService _entityPropertyValueService; public PriceGroupPickerControlFactory(IRepository<PriceGroup> priceGroupRepository, IEntityPropertyValueService entityPropertyValueService) { _priceGroupRepository = priceGroupRepository; _entityPropertyValueService = entityPropertyValueService; } public bool Supports(DataType dataType) { var editor = _entityPropertyValueService.GetPropertyValue(dataType.Definition, dataType.Guid, "Editor"); return editor == "PriceGroupPicker"; }
We will use the IEntityPropertyValueService
to get the value of editor property for the DataType
and check if the selected editor is equal to "PriceGroupPicker" for which we want to create the control. Remember it is possible to create multiple data types with the same definition and we do want to support them all.
Implementing GetControl
Now that we have told the framework we support properties with a data type that has the "PriceGroupPicker" as its editor, we'll be asked to deliver a control for every property with a data type that has the "PriceGroupPicker" as its editor.
The GetControl
method needs to be able to handle scenarios where a value has previously been selected and should be shown as the selected value. It also needs to handle scenarios where no value has been selected.
In this case, we'll provide a drop down list where we will set the id of the price group as the value that may be selected. Ultimately this results in the value of the selected element being saved in the database as the value of the property as a EntityProperty
.
public Control GetControl(IProperty property) { //This is the drop down list we'll return to use in the UI. var dropDownList = new SafeDropDownList(); var priceGroups = _priceGroupRepository.Select().ToList(); foreach (var priceGroup in priceGroups) { dropDownList.Items.Add(new ListItem { Value = priceGroup.PriceGroupId.ToString(), Text = priceGroup.Name, //We'll mark this item as selected if the value of the property is equal to the price group id. Selected = property.GetValue().ToString() == priceGroup.PriceGroupId.ToString() }); } return dropDownList; }
Below we have the full implementation of the "PriceGroupPickerControlFactory".
using System; using System.Web.UI; using UCommerce.EntitiesV2; using UCommerce.EntitiesV2.Definitions; using UCommerce.Presentation.Web.Controls; namespace MyUCommerceApp.DataTypes { public class PriceGroupPickerControlFactory : IControlFactory { private readonly IRepository<PriceGroup> _priceGroupRepository; private readonly IEntityPropertyValueService _entityPropertyValueService; public PriceGroupPickerControlFactory(IRepository<PriceGroup> priceGroupRepository, IEntityPropertyValueService entityPropertyValueService) { _priceGroupRepository = priceGroupRepository; _entityPropertyValueService = entityPropertyValueService; } public bool Supports(DataType dataType) { var editor = _entityPropertyValueService.GetPropertyValue(dataType.Definition, dataType.Guid, "Editor"); return editor == "PriceGroupPicker"; } public Control GetControl(IProperty property) { //This is the drop down list we'll return to use in the UI. var dropDownList = new SafeDropDownList(); var priceGroups = _priceGroupRepository.Select().ToList(); foreach (var priceGroup in priceGroups) { dropDownList.Items.Add(new ListItem { Value = priceGroup.PriceGroupId.ToString(), Text = priceGroup.Name, //We'll mark this item as selected if the value of the property is equal to the price group id. Selected = property.GetValue().ToString() == priceGroup.PriceGroupId.ToString() }); } return dropDownList; } } }
So where do the editor options come from?
That's a good question. It comes from the id of the registration of the IControlFactory. So the id of the component we'll register now is very important.
In this case, we'll register the new factory with the id "PriceGroupPickerControlFactory" as shown below.
<component id="PriceGroupPickerControlFactory" service="UCommerce.Presentation.Web.Controls.IControlFactory, UCommerce.Presentation" type="MyUCommerceApp.DataTypes.PriceGroupPickerControlFactory, MyUCommerceApp" />
If you want to learn more about how to register a component check out How to register a component in Ucommerce.
Now that we have registered it with the id "PriceGroupPickerControlFactory", we can select "Price Group Picker" in the list. The framework will trim away ControlFactory and add spaces where there's camel casing, so if we registered another factory with: "MultiNodeTreePickerControlFactory" it would result in "Multi Node Tree Picker" being in the list.
Implementing IControlAdapter
The control adapter is responsible for extracting the value of the control, which is produced by the control factory. The reason for which this is necessary is that .NET doesn't provide a unified method for reading values from controls, e.g. a TextBox
has Text
property while a CheckBox
has a Checked
property and so forth.
The ControlAdapter
smoothes out these differences for us.
You don't always have to build a new control adapter to support your data type because they are reusable across controls, e.g. you may have control factories to display drop downs for price groups and shipping methods, but they both use the underlying control DropDownList
and thus you only need a single control adapter for both of them.
Ucommerce ships with a number of control adapters out of the box, which means you might not have to create your own adapter at all. You can see these in "DataTypes.config" under the "Control Adapter" section. Please note that "Shell.config" will potentially hold factories and adapters specific to the CMS you're using as well.
using System.Web.UI; using UCommerce.Presentation.Web.Controls; namespace MyUCommerceApp.DataTypes { public class PriceGroupPickerControlAdapter : IControlAdapter { public bool Adapts(Control control) { throw new NotImplementedException(); } public object GetValue(Control control) { throw new NotImplementedException(); } } }
Now that we've created a provider for selecting Price Groups we want to let the framework know how to get at the actual value of the control.
using System.Web.UI; using UCommerce.Presentation.Web.Controls; namespace MyUCommerceApp.DataTypes { public class PriceGroupPickerControlAdapter : IControlAdapter { public bool Adapts(Control control) { //We can easy tell that we know to Adapt and get a value from a SafeDropDownList //If we can cast the control to one of that type, we knows how to get the value. return (control as SafeDropDownList != null); } public object GetValue(Control control) { //Since the framework was told that we adapt the control, it will ask for the value. //We know we can cast it, so we do that and call SelectedValue. return (control as SafeDropDownList).SelectedValue; } } }
All there's left to do is register it like you registered the other component. The id of the IControlAdapter
component is less important this time, but using the same convention is always a good idea and best practice. Below you can see the registration of the new control adapter.
<component id="PriceGroupPickerControlAdapter" service="UCommerce.Presentation.Web.Controls.IControlAdapter, UCommerce.Presentation" type="MyUCommerceApp.DataTypes.PriceGroupPickerControlAdapter, MyUCommerceApp" />
Add a definition field with the new picker.
First you must create a new data type and set the editor to "Price Group Picker". Right-click the data types node in the tree and click "Create". You can use whatever name you prefer.
You now have a new data type that you can use to create a definition fields. You can read more about definitions here.
Summary
In this article you've learned how to create a custom data type:
- You can use a custom data type to select special values for custom properties on products, categories, campaign items, data types, etc.
-
IControlFactory is used to deliver a control to edit a custom property for a
DataType
.- The IControlFactory should support a specific editor and not the
DataType
itself. - The id you use for the IControlFactory when registering it, is important since it will be used to display and as the actual name of the editor.
- The IControlFactory should support a specific editor and not the
- IControlAdapter is used to deliver the value of the control since the Framework Itself does not know how to extract the value.
-
Both interfaces are used in a provider pattern so the framework will ask all implementations of both interfaces until one returns true in Supports / Adapts.
- Therefore, the order in which the registered components are picked up and what they return true for are important.