18

I'm writing some new code, and I came across a curious situation. We are using a dynamic piece of code that determines how a record should be handled based on its ID value (e.g. different objects have different fields). The code look briefly like this:

public class Mapping {
    public SObjectField name, unitprice, quantity, ...;

    public static Mapping getMapFor(SObjectType entity) {
        // populate a new map based on given values
        if(entity == Opportunity.SObjectType) {
            name = Opportunity.Name;
            unitprice = null;
            ...
        }
        if(entity == OpportunityLineItem.SObjectType) {
            ...
        }
    }
}

NOTE: The actual code has over a thousand lines of code, caching, etc. Example is illustrative only.

This code works beautifully, since it lets us abstract discrete logic across multiple data types.

As example, we can do:

record.put(m.totalprice, (decimal)record.get(m.unitprice) * (decimal)record.get(m.quantity));

Instead of writing ten versions of the code that do the exact same function with different records.

Next, there's a component that accepts a standard controller as an attribute. This gives us access to cancel(), save(), etc that would be available on the actual Visualforce page with very little overhead, and, of course, allows direct access to the record.

Now, here's where the twist comes into play:

public class componentController {
    public ApexPages.StandardController controller { get; set; }
    // Other data goes here

    public void init() { // called via action function on component load,
                         // since we can't access controller in constructor
                         // and can't call an action on a component directly.

        // The following line fails to compile
        Mappng m = Mapping.getMapFor(controller.getId().getSObjectType());
    }
}

One would wholly expect this code to function verbatim, because ApexPages.StandardController getId logically returns an Id, right? Right? Wrong!. Instead, it returns a String value.

There's no documentation on why it returns a String, since clearly it will always be an ID value (or will it...?). I have at least two easy workarounds (I'm going to use controller.getRecord().Id.getSobjectType()), but I would like to know why the Powers That Be™ decided that they wanted to return a simple String instead of correctly typing the value as an Id.

Is there something I need to know? Are they planning on changing how this function works? Should I not rely on getId now or in the future? Could someone fix this so that it works correctly?

  • 1
    Interesting find; I can only speculate it'd be to make it more generic/versatile in case of bad input, but if it isn't of type id then I'd expect the controller to just throw an uncatchable exception. – Mike Chale Jan 17 '14 at 20:03
  • have you tried casting controller.getId() as an id? – Scott Pelak Jan 17 '14 at 20:04
  • @amatorVitae That's a workaround. I'm aware I can do that, but that misses the point. That's like String.valueOf(Object) returning an Object instead of String. – sfdcfox Jan 17 '14 at 20:05
  • 1
    but it is still a solution? :) I know id s are considered BOTH a string and an id. When used as an id, SFDC runs a validity check. That's the only difference between the two – Scott Pelak Jan 17 '14 at 20:06
  • @amatorVitae Not this time; we need a permanent fix. I'm going to also outline this elsewhere and hope that someone issues a Known Issue for this. A fundamental class shouldn't be broken like this. – sfdcfox Jan 17 '14 at 20:09
2

It's not uncommon in e.g. MVC frameworks to work like that.

On the Controller, you have some routing information like URL parameters. In the case of SF, we know there's always an Id, so it's safe to add the getID() function on the Controller. It simply returns a String; it's a URL parameter, nothing more.

Then, once SF parses the URL information, having fetched the corresponding object data and determined the object's type, it can then create an Object (e.g. Account) that holds object data and also holds information about its type. That type information is then available via the ID of the Object, which is really handy when passing IDs (but not the full Objects) around.

I would argue however that that type information makes only sense as a 'property' of an Object, not so much as a 'property' of a URL parameter. So it might make sense to leave the URL parameter as-is but add type information to the Object nonetheless.

  • I'm tempted to believe that, except for one little detail: if you provide an invalid ID for the standard controller, you get an "ID is invalid for SObject type X" error, and your constructor is never called. Contrariwise, you can provide a normal "id" parameter to a Visualforce page without a standard controller, which should be a string. I can guarantee with 100% certainty that, today, the ID parameter for the standard controller must always be a ID or null, because Visualforce demands it. However, there's no contractrual obligation for it to be an ID, hence my concern. – sfdcfox Feb 14 '14 at 23:15
  • Think of it this way: controller.getId() should return the same value as controller.getRecord().Id. However, one returns a string, the other an Id. Instead, we have to think about (Id)controller.getId() or Id.valueOf(controller.getId()), which are both insane, because we already know it's an ID. Unless, of course, there is a way to determine that the ID were invalid and thus left to the developer to catch the glitch, then it would make perfect sense for it to be a String. Since there isn't, we shouldn't be required to cast from an ID to an ID. – sfdcfox Feb 14 '14 at 23:19
  • I agree that being able to construct a controller with an invalid id would be good 'proof' of my assumptions, and I agree that that proof is not there. Salesforce is indeed being smart: it knows that a controller without valid id does not make sense, so it doesn't allow constructing one when the id is invalid. Moreover, SF already names the function getid() (instead of geturlparam1()). Yet, the only reason I can think of why it returns a string is that it treats it like a URL param. As for the constructor not being able to handle a String, that's weird. Let's hope an SF employee knows why :) – Willem Mulder Feb 18 '14 at 11:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.