August 11, 2017

Metadata Layouts in Apex & Visualforce

One of the newest features Apex has gained is the ability to access metadata natively, to say this has been a long time coming is an understatement, but at least we're finally starting to see it happen. I say starting because as of the time of writing the only data available is custom metadata and page layouts, but in all fairness, from a developer's perspective, page layouts is one of the big ticket items that's always been missing.

Show Me The Layout!

As you'd likely hope, grabbing the layouts is actually pretty simple, you can simply ask for them by name as per the following code.

List<String> componentNameList = new List<String>{'Opportunity-Opportunity Layout', 'Account-Business Layout'};
List<Metadata.Metadata> components = Metadata.Operations.retrieve(Metadata.MetadataType.Layout, componentNameList);

Metadata.Layout opptyLayout = (Metadata.Layout) components.get(0);

The keen-eyed reader will notice the Metadata.Layout type, in fact there's a whole hierarchy of these types which you can use to work your way through the page structure. The documentation is the best bet for the details, but essentially it boils down to a hierarchy where a Metadata.Layout contains Metadata.LayoutSection objects (as well as other top-level data), which in turn contain Metadata.LayoutColumn objects which contain Metadata.LayoutItem objects.

The Layout Items

The items are the individual fields and other objects (such as Visualforce frames) that can be contained within a layout. Interestingly, in what is probably an attempt to avoid duplication of effort/complexity, the only information you get on a field is the field's reference, relative to the layout (that's the documentation's phrasing, not mine). What this means is that you need to use this reference along with describe information to actually get details on the field (unless you're just using <apex:inputField> or <lightning:input>.

A Visualforce Gotcha

Something that tripped me up was trying to use the LayoutItem's behavior property in a Visualforce page to control whether or not the field was displayed. The property is an enumerated type:

behavior

Determines the field behavior.

Signature

public Metadata.UiBehavior behavior {get; set;}

Property Value

Type: Metadata.UiBehavior

This type has the following values:

Edit The layout field can be edited but is not required.

Readonly The layout field is read-only.

Required The layout field can be edited and is required.

I'm sure some people have seen this elsewhere in the platform, but I wanted to iterated over my layout and sections etc. to display fields that weren't read only. The innermost repeat and conditional looked like this:

<apex:repeat value="{!col.layoutItems}" var="item">
    <apex:outputPanel rendered="{!NOT(item.behavior = 'Readonly')}">

The problem is that the output panel was always being rendered, clearly item.behaviour was not equal to the String 'Readonly'. This makes sense given it's an enumeration, but I did expect the Visualforce expression parser to coerce the type. Trying TEXT() didn't work, and trying to concatenate the value with '' didn't with an error to the effect that item.behavior is an object. In the end the solution I came up with was to store this type in a property so I could compare against that:

Apex Controller:
public Object readOnly {get; private set;}
  ** snip **
readOnly = Metadata.UiBehavior.Readonly;
Visualforce Page:
<apex:outputPanel rendered="{!item.behavior = readOnly}">

This works but it doesn't feel right. Using Metadata.UiBehavior.Readonly directly in the page didn't work, so I'm curious to know if there are any other options. Either way, if you're hitting the same issue when trying to use an enumerated type in Visualforce maybe this will help you out.