10

I'm trying to build out a flexible, custom search engine as a component, but am stuck. I want to be able to specify filter fields in a comma delimited input attribute (strFilterFields in my code) and have each of those show up as filterable apex:inputField inputs. As of now, I'm using a generic new sObject so that the input fields take on the characteristics they should (text vs picklist vs lookup, etc) but I'm only able to use this methodology for "first level" fields. Anybody know of a way to allow for this dynamic view while allowing for lookup relationships? For instance, I'd love to be able to search Opportunities by Account.ParentId

Notice querySObject

Here is a dumbed-down version of my visualforce component:

<apex:component controller="ComponentRelatedList" id="theController">
    <apex:attribute type="String" name="componentId" required="true" description="The unique Id of this component" />
    <apex:attribute type="String" name="sObjectName" required="true" assignTo="{!strSObjectName}" description="The name of the object you intend to query" />
    <apex:attribute type="String" name="recordTypeName" required="false" assignTo="{!strRecordTypeName}" description="The name of the RecordType you intend to filter to" />
    <apex:attribute type="String" name="filterFields" required="false" assignTo="{!strFilterFields}" description="The fields you are allowing the user to tweak" />
    <apex:form id="theForm">
            <apex:repeat value="{!filters}" var="filter">
                <apex:inputField required="false" value="{!querySObject[filter]}" />
            </apex:repeat>
            <apex:commandButton value="Go!" />
            <apex:dataTable>
                ... dataTable contents here, not relevant for this discussion...
            </apex:dataTable>
    </apex:form>
</apex:component>

And here is a dumbed-down version of my apex controller:

public class ComponentRelatedList {

    public String strSObjectName { get; set; }
    public String strRecordTypeName { get; set; }
    public String strFilterFields { get; set; }

    public sObject querySObject {
        get {
            if (querySObject == null) {
                querySObject = Schema.getGlobalDescribe().get(strSObjectName).newSObject();
                if (isFilteredRecordType) querySObject.put('RecordTypeId', UtilsVariables.getRecordTypeId(strSObjectName, strRecordTypeName));
            }
            return querySObject;
        }
        set;
    }

}

And here is a snapshot of the output (notice the correct handling of each Field Type [red arrows]):

Search Component

2 Answers 2

5
+50

Yes, you can do that, you'll just need to parse the filter fields so that you get the final sObject and use that for your generic sObject.

As a reference here's some code that I used to implement .get(fieldName) method that dereferences relationships. Hope it helps.

public static Object recursiveGet(sObject record, String field) {
    if(field.contains('.')) {
        Integer firstPeriod = field.indexOf('.');
        String nextObjectName = field.subString(0, firstPeriod);
        String remainingfieldName = field.subString(firstPeriod + 1, field.length());
        sObject nextObject = record.getSObject(nextObjectName);
        if(nextObject == null) {
            return null;
        } else {
            return recursiveGet(nextObject, remainingfieldName);
        }
    } else {
        return record.get(field);   
    }
}
6
  • Intriguing and thank you! I will give this a shot later this evening and let you know how it goes! Dec 1, 2013 at 0:06
  • Hmm, this doesn't seem to work. I created a Map<String, Object> and populated it using this recursiveGet formula and then set my inputField as such (where "querySObject" is the name of the Map<String, Object> and "filter" is the string-based field api name from an apex:repeat): <apex:inputField required="false" value="{!mapFieldObject[filter]}" /> -- I get this error: Could not resolve the entity from <apex:inputField> value binding '{!mapFieldObject[filter]}'. <apex:inputField> can only be used with SObjects, or objects that are Visualforce field component resolvable... Dec 3, 2013 at 17:22
  • 1
    I think the issue is that I'm trying to do this all on a new blank sObject ("querySObject" in my original post) so there are no relationships built out on it (and in order to do so effectively, i would need to fully insert a new account record in order to "get" "Account.ParentId"... I don't want to fully insert since it's a dummy record only!) Dec 3, 2013 at 18:01
  • 1
    Makes sense, you might consider instead using field describe info to walk up the relationships, you're correct that the method I had falls apart if there is no value in the relationship. With the bounty on it, I'm sure you'll get some good responses, when I have moment I'll take a stab at redoing my answer. Dec 3, 2013 at 21:13
  • 1
    I'm going to award you the bounty (in 4 hours, when I'm able) since this ended up being a critical piece I could not have moved forward without (even though it didn't directly answer the original question). Thank you! Dec 4, 2013 at 14:59
0

Ok, I ended up figuring this one out...

I ended up having to abandon the idea of a non-committed shell sObject (querySObject above) since I was having trouble with the relational lookups (non-committed objects don't yet have an Id and therefore cannot represent these relationships correctly). Instead, I wrote a wrapper class, which was intelligent enough to lookup the correct reference sObject (see the "get()" methods below) and also stored enough other information, such as the field label, etc for use on my visualforce page.

I also ended up needing the getRecursive() function (Thanks to @Ralph for pointing me in the right direction! I'm going to award the bounty to him for this purpose even though this will be marked as the accepted answer... Thank you!) on the final Database.query() result to parse the data dynamically to get the relational information:

The wrapper class:

public class ExtendedField {

    public Map<String, Schema.SObjectType> globalDescribe { get; set; }
    public String filterName { get; set; }
    public String sObjectName { get; set; }
    public String fieldName { get; set; }
    public String fieldLabel { get; set; }
    public sObject record { get; set; }

    public ExtendedField(Map<String, Schema.SObjectType> globalDescribe, Map<String, Map<String, String>> mapLabels, String sObjectName, String filterName) {
        setGlobalDescribe(globalDescribe);
        setFilterName(filterName);
        setSObjectName(sObjectName);
        setFieldName();
        setObjectName();
        setLabels(mapLabels);
    }

    public sObject getSObject() {
        return this.record;
    }
    public String getField() {
        return this.fieldName;
    }
    public String getLabel() {
        return this.fieldLabel;
    }
    public String getFilter() {
        return this.filterName;
    }
    public Object getValue() {
        return this.record.get(fieldName);
    }

    public void setGlobalDescribe(Map<String, Schema.SObjectType> globalDescribe) {
        this.globalDescribe = globalDescribe;
    }
    public void setFilterName(String filterName) {
        this.filterName = filterName;
    }
    public void setSObjectName(String sObjectName) {
        this.sObjectName = UtilsGeneral.getLookupSObjectName(sObjectName, this.filterName);
    }
    public void setFieldName() {
        this.fieldName = UtilsGeneral.getLookupFieldName(this.filterName);
    }
    public void setObjectName() {
        this.record = globalDescribe.get(this.sObjectName).newSObject();
        this.record.put(this.fieldName, null);
    }
    public void setLabels(Map<String, Map<String, String>> mapLabels) {
        this.fieldLabel = mapLabels.get(this.sObjectName).get(this.fieldName);
    }

}

The important "get()" methods:

public static String getLookupFieldName(String fieldName) {
    if (fieldName.contains('.')) {
        fieldName = fieldName.subString(fieldName.indexOf('.', fieldName.countMatches('.') - 1) + 1, fieldName.length());
    }
    return fieldName;
}

public static String getLookupSObjectName(String sObjectName, String fieldName) {
    String relationshipName = '';
    system.debug('getLookupSObjectName().start() = ' + sObjectName);
    if (fieldName.contains('.')) {
        Map<String, Schema.SObjectType> globalDescribe = Schema.getGlobalDescribe();
        Map<String, Map<String, Schema.SObjectField>> mapSObjectFields = new Map<String, Map<String, Schema.SObjectField>>();
        mapSObjectFields.put(sObjectName, new Map<String, Schema.SObjectField>());
        mapSObjectFields.get(sObjectName).putAll(globalDescribe.get(sObjectName).getDescribe().fields.getMap());
        for (Integer i = 0; i < fieldName.countMatches('.'); i++) {
            Integer firstCharacter = 0;
            if (i > 0) firstCharacter = fieldName.indexOf('.', i - 1) + 1;
            Integer lastCharacter = fieldName.indexOf('.', i);
            relationshipName = fieldName.subString(firstCharacter, lastCharacter);
            if (relationshipName.right(3) == '__r') {
                relationshipName = relationshipName.left(relationshipName.length() - 3) + '__c';
            } else {
                relationshipName = relationshipName + 'Id';
            }
            relationshipName = relationshipName.toLowerCase();
            sObjectName = mapSObjectFields.get(sObjectName).get(relationshipName).getDescribe().referenceTo[0].getDescribe().getName();
            fieldName = fieldName.subString(lastCharacter + 1, fieldName.length());
            if (!mapSObjectFields.containsKey(sObjectName)) {
                mapSObjectFields.put(sObjectName, new Map<String, Schema.SObjectField>());
                mapSObjectFields.get(sObjectName).putAll(globalDescribe.get(sObjectName).getDescribe().fields.getMap());
            }
        }
        system.debug('getLookupSObjectName().end() = ' + sObjectName);
    }
    return sObjectName;
}

The important "getRecursive()" method:

public static Object getRecursive(sObject record, String fieldName) {
    if(fieldName.contains('.')) {
        Integer firstPeriod = fieldName.indexOf('.');
        String nextSObjectName = fieldName.subString(0, firstPeriod);
        String nextfieldName = fieldName.subString(firstPeriod + 1, fieldName.length());
        sObject nextRecord = record.getSObject(nextSObjectName);
        if(nextRecord == null) {
            return null;
        } else {
            return getRecursive(nextRecord, nextfieldName);
        }
    } else {
        return record.get(fieldName);   
    }
}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .