When you have more complex custom metadata needs or multiple custom metadata types, the UI requirements for users to manage records may also need to be more sophisticated. For example if you wish to build a setup wizard or type of visual editor. Much like you see with the Flow or Process Builder tools. So if you find you want to build your own UI for custom metadata records what is involved?
The native Apex Metadata API allows for Layout metadata as well as Custom Metadata records to be manipulated directly in Apex, without the traditional need for callouts or elevated permissions. Due to its generic API design, the data types for custom metadata records are represented as generic name/value pairs. Because of this it does require a bit more coding than you might expect to update records. Certainly compared to the SObject and DML oriented experience you get when working with Custom Object records in Apex. There is also the fact that it is async only, requiring careful transfer of results received via callbacks back to the user.
Custom Metadata Services (CMS) is a small library created to wrap the native Apex Metadata API to leverage the native SObject types for custom metadata in a more DML orientated way. It also aims to simplify the handling of the async results when developing clients in Lightning, inspired in this case by Lightning Data Services.
Updating records in Apex
When you create a Custom Metadata Type under the Setup menu, the platform automatically generates a corresponding SObject type you can use within Apex. This so that you can query those records using regular SOQL.
List<WidgetPreset__mdt> records = [select DeveloperName, Label, DefaultNotification__c, Alias__c from WidgetPreset__mdt];
Using the native SObjectType and the related field references (SObjectField tokens) is preferable to using String literals from a referential integrity perspective and also namespace management for packaged code. So CMS uses these whenever possible. The following code will upsert a Custom Metadata record.
String deployId = CustomMetadata.Operations .callback( // Simple callback that outputs to debug new DebugCallback()) .enqueueUpsertRecords( // Custom Metadata object type WidgetPreset__mdt.getSObjectType(), new List<Map<SObjectField, Object>> { // Custom Metadata record new Map<SObjectField, Object> { WidgetPreset__mdt.DeveloperName => 'BluetoohToothbrush', WidgetPreset__mdt.Label => 'Bluetooh Toothbrush', WidgetPreset__mdt.DefaultNotification__c => 'Good day!', WidgetPreset__mdt.Alias__c => 'wdbtt' } } ) .deployId;
- The only operation currently supported is an upsert. The use of this terminology reflects the fact that the native Metadata API deploys a list of records that will either insert or update records.
- Because its not possible to assign values directly to __mdt SObject fields, the above code uses an Apex map that references the fields via their SObjectField tokens.
The native Apex Metadata API uses a background job to update the records so the results are not know immediately to the calling code. CMS tailors the DeployCallback interface with its own base class which is more orientated around custom metadata record results. CMS also uses its own callback ID rather than the native one, allowing the client to more easily identify the corresponding callback.
public class DebugCallback extends CustomMetadata.SaveResultCallback { public override void handleResult(String deployId, List<SaveRecordResult> results) { System.debug('Deploy Id is ' + deployId); for(SaveRecordResult result : results) { System.debug('Fullname of custom metadata reocord is ' + result.fullName); System.debug('Success is ' + result.success); if(!result.success) { System.debug('Message is' + result.message); } } } }
The callback method also supports the ability to send a Platform Event instead. This is preferable in cases where you are building UI’s, since the UI can listen for the callback directly. The Metadata Deployment platform event object is included in the library.
String deployId = CustomMetadata.Operations .callback( // Platform event for deploy status MetadataDeployment__e.getSObjectType(), MetadataDeployment__e.DeploymentId__c, MetadataDeployment__e.Result__c) .enqueueUpsertRecords( // Custom Metadata object type WidgetPreset__mdt.getSObjectType(), new List<Map<SObjectField, Object>> { // Custom Metadata record new Map<SObjectField, Object> { WidgetPreset__mdt.DeveloperName => 'BluetoohToothbrush', WidgetPreset__mdt.Label => 'Bluetooh Toothbrush', WidgetPreset__mdt.DefaultNotification__c => 'Good day!', WidgetPreset__mdt.Alias__c => 'wdbtt' } } ) .deployId;
Building a Lightning UI
Here is a simple UI built with CMS, to demonstrate loading, saving and error handling.
When building UIs you can utilise the Apex code above from your server side controller logic. Though as an alternative, CMS includes a Lightning component for use within client side controllers. The design of this component follows that of the force:recordData component used for managing Custom Object records. It is currently aimed at single record retrieval and updates. Here is the markup for the above component.
The metadataRecordData component has its own Apex controller leveraging the CMS Apex API above and then listens for the MetadataDeployment__e event (included). This is transparent to the consumers of the component. The controller code interacts with the saveRecord method and metadataRecordDataResult event.
Building a Visualforce UI
It is possible to include Lightning Components in Visualforce pages using Lightning Out and thus reuse the above code in both Classic and Lightning Experience.
However if you still prefer to develop using Visualforce or simply have a number of pages you’d rather continue to develop for a while. You can still utilise the Platform Event mechanism using the client side library called cometd. The sample code for the page below can be found in the GitHub repo.
Summary
I plan on utilising CMS to update the rollup tools UI in the future and avoid the need for callouts (and related permissions) when using its UI. In full disclosure I would also classify the library as alpha at this stage, its still needs a little more refinement and robustness adding. It is open source, so free to join me in fleshing it out further!
