3

Background: New users need to complete a series of training exercises. We have been manually validating that these are completed. I'm now working on a 'validator' visualforce page that will check if these were completed.

Question: One thing that I want is to track the total number of exercises vs the total completed. I've got something that appears to work, but not sure if it will always work, b/c in this SE post, I learned that the order of getters is not predictable.

My controller has a separate validation method for each exercise.

My approach to tracking the totals is to have 2 class variables: completed, and attempted. Within each validation method I set attempted +=1, and if completed successfully, also set completed +=1.

I have getters for getCompletedStatus and getExercisesToDo that I use in my VF validator page to display counts, render output panels, etc, and the result of these methods is dependent on the values of the attempted and completed variables.

This appears to be working properly from a few manual tests, but if the getters could fire in any order, then getCompleted and getAttempted could fire at any point, before the rest of the methods, so the numbers could be off?

If so, what are approaches that would consistently work? Do 'regular' (non-getter/setter) methods all fire before getters/setters? I that case I could use a method to validate each exercise, and a separate getter for each validation method. Is that a good pattern to use?

Controller Code (any and all suggestions on how to improve are also welcome):

public class UserOnboardingValidationExtension {

    public integer Completed = 0;
    public integer Attempted = 0;

    public UserOnboardingValidationExtension(ApexPages.StandardController stdController) {

   }

    public user u = [SELECT Id,FullPhotoUrl,SmallPhotoUrl, AboutMe From USER Where Id =:UserInfo.GetuserId()];

    public User getUser () {
         return u;
         }   

    public boolean getHasCreatedChatterPost() {
        boolean cpPass = FALSE;
        this.Attempted +=1;
        list <feeditem> ChatterPosts = [SELECT Id from FeedItem Where InsertedById =: u.Id];
        IF(ChatterPosts.size() >= 1){
            cpPass = TRUE;
            this.Completed += 1;
            system.debug('Completed = ' + completed);
        }
        return cpPass;
    }

    public boolean getHasCreatedContact(){
        boolean conPass = FALSE;
        this.attempted += 1;
        list <Contact> cList = [SELECT Name FROM Contact Where CreatedById =: u.Id];
        integer conCount = clist.size(); 
        IF (conCount >= 1){
            conPass = TRUE;
            this.Completed += 1;
            system.debug('Completed = ' + completed);
        }
        return conPass;
    }

    public boolean getHasCreatedAccount(){
        boolean accPass = FALSE;
        this.attempted += 1;
        list <Account> alist = [SELECT Name from Account Where CreatedById =: u.id];
       integer AccountCount = alist.size(); 
        IF (AccountCount >= 1){
            accPass = TRUE;
            this.Completed += 1;
            system.debug('Completed = ' + completed);
        }
        return accPass;
    }

    public boolean getHasSharedContact(){
        boolean sharePass = FALSE;
        this.attempted += 1;
        list<ContactShare> cshareList = [SELECT id, UserOrGroupId FROM ContactShare where ContactId IN (SELECT Id FROM Contact Where CreatedById =: u.Id) AND RowCause = 'Manual'];
        integer ShareCount = cshareList.size();
        IF(ShareCount >= 1){
            sharePass = TRUE;
            this.Completed += 1;
            system.debug('Completed = ' + completed);
        }
        return sharePass;
    }

    public boolean getHasAddedPhoto(){
        boolean photoPass = TRUE;
        this.attempted += 1;
        string photoUrl = u.FullPhotoUrl;
        system.debug('photoURL = ' + photoUrl);
        IF(photoUrl.endsWith('/profilephoto/005/F')){
            photoPass = FALSE;
        }

        ELSE {
            this.Completed += 1;
            system.debug('Completed = ' + completed);
        }
        return photoPass;
    }

    public boolean getHasAddedBio(){
        boolean bioPass = FALSE;
        this.attempted += 1;
        string aboutme = u.AboutMe;
        IF(aboutme != NULL){
            bioPass = TRUE;
            this.Completed += 1;
            system.debug('Completed = ' + completed);
        }
        return bioPass;
    }

    public boolean getHasClosedTask(){
        boolean taskPass = FALSE;
        this.attempted += 1;
        list<Task> tasklist = new list <task>([SELECT Id,IsClosed from Task Where OwnerId =: u.Id]);
        IF(tasklist.size() >= 1) {
           taskPass = TRUE;
            this.Completed += 1;
        }
        return taskPass;
    }
    public boolean getCompletedStatus() {
        system.debug('this.completed = ' + this.completed);
        system.debug('this.attempted = ' + this.attempted);
        boolean MadeIt = FALSE;
        IF(this.completed == this.attempted){
            MadeIt = TRUE;}
        system.debug('madeit = ' + madeIt);
        Return MadeIt;
    }

    public integer getExercisesToDo() {
        integer att = this.attempted;
        integer comp = this.completed;
        integer toDo = att - comp;
        system.debug('exercisesRemaining' + toDo);
        RETURN toDo;
    }


 /* not needed until they can auto-request upgrade   
 public PageReference save() {
       update u;
        return null;
        }
*/
}
  • one approach -- create an inner class that has getters for "toDo" and "completed" that in turn invokes the individual getters; then have a getter for the inner class – cropredy Feb 13 '16 at 18:37
  • Thanks! I will try to poke on that. For my understanding, what properties/ behaviors of the inner class resolve the issue? And how would I call the inner class from my visualforce page? Never used an inner class before! – gorav Feb 13 '16 at 18:48
  • Your VF controller constructs an object of 'inner class' type, put a {get; private set;} on it and you can refer to the object in your VF page accessing its properties and methods. There are tons of examples - search on "Visualforce wrapper class" – cropredy Feb 13 '16 at 22:54
  • Thanks been reading abt inner classes, will try wrapper classes. The examples all seem to be a variation on iterating over a list of X, and I'm having a hard time thinking through how it applies, but I've got an inner class that at least will save, and am working on trying to get at it from the vf page. Can't quite get that working, it seems to be looking for getter method in the user standard controller, not my extension. Though the getter methods in the outer class can be called. – gorav Feb 13 '16 at 23:09
1

Not only are getters and setters non-deterministic (i.e. cannot be ordered in any particular way), there's no guarantee that a getter or setter will be called only one time. While there's sometimes a perfectly good reason for using getters to do things like building picklists (which can help reduce view state), there's no good reason for using queries inside a getter, because it causes extra database hits, and, as you can see, since you're adding to "attempted" each pass, you might also be counting too high.

So, what's a developer to do? Simple. Craft a single function that can be called to update the statistics, then set normal variables to the correct values. Here's my implementation of your class:

public class UserOnboardingValidationExtension {

    transient User thisUser = [SELECT FullPhotoUrl, SmallPhotoUrl, AboutMe FROM User WHERE Id = :UserInfo.getUserId()];

    public Boolean hasCreatedChatterPost { get; set; }
    public Boolean hasCreatedContact { get; set; }
    public Boolean hasCreatedAccount { get; set; }
    public Boolean hasCreatedSharedContact { get; set; }
    public Boolean hasAddedPhoto { get; set; }
    public Boolean hasAddedBio { get; set; }
    public Boolean hasClosedTask { get; set; }
    public Boolean completedStatus { get; set; }

    public Integer remainingTasks { get; set; }

    public User getUser() {
        return thisUser;
    }

    public UserOnboardingValidationExtension(ApexPages.StandardController stdController) {
        calculateStatus();
    }

    public void calculateStatus() {
        hasCreatedChatterPost = [SELECT COUNT() FROM FeedItem WHERE InsertedById = :thisUser.Id LIMIT 1] == 1;
        hasCreatedContact = [SELECT COUNT() FROM Contact WHERE CreatedById = :thisUser.Id LIMIT 1] == 1;
        hasCreatedAccount = [SELECT COUNT() FROM Account WHERE CreatedById = :thisUser.Id LIMIT 1] == 1;
        hasCreatedSharedContact = [SELECT COUNT() FROM ContactShare WHERE RowCause='Manual' AND CreatedById = :thisUser.Id LIMIT 1] == 1;
        hasAddedPhoto = thisUser.FullPhotoUrl != null && !thisUser.FullPhotoUrl.endsWith('/profilephoto/005/F');
        hasAddedBio = thisUser.AboutMe != null;
        hasClosedTask = [SELECT COUNT() FROM Task WHERE OwnerId = thisUser.Id AND IsClosed = TRUE LIMIT 1] == 1;
        remainingTasks =
            (hasCreatedChatterPost  ? 0: 1) + (hasCreatedContact        ? 0: 1) +
            (hasCreatedAccount      ? 0: 1) + (hasCreatedSharedContact  ? 0: 1) +
            (hasAddedPhoto          ? 0: 1) + (hasAddedBio              ? 0: 1) +
            (hasClosedTask          ? 0: 1);
        completedStatus = remainingTasks == 0;
    }
}

The tabular formatting I used here is only intended to make the post look less cluttered, but do feel free to use proper formatting. I'd like to call out a few of the optimizations here.

You can simply call calculateStatus at any time (say, as an action function, or on page load, as I've done here, etc...) at any time to get the latest, correct status, using a minimum number of queries. Note that since thisUser is used more than once, but doesn't need to be part of the view state, I just make it a transient variable and query it once. As a bonus, if the user updates their picture, for example, and then clicks an action button that calls calculateStatus, the values will be updated correctly without a refresh.

Since we only care if a user has done something once, we can use LIMIT 1 to reduce the number of rows returned, which speeds up loading time, and is less likely to hit governor limits. Also, using COUNT() means I can get back a simple Integer value, that I then compare to 1 to form a Boolean value. Likewise, I look for a closed task in the query itself, instead of looping through over all results to see if any tasks were closed. Using LIMIT 1 means that we're not likely to hit governor limits, even against large task tables.

You can read the rest of code to figure things out, I'd imagine, but this should give you a good start.

|improve this answer|||||
  • Shouldn't the count values be >=1? – Adrian Larson Feb 14 '16 at 0:06
  • 1
    @AdrianLarson Not necessary, because we "limit" the query. The only two possible output values are 0 or 1. – sfdcfox Feb 14 '16 at 0:08
  • 1
    Ah I see. Gotta read more carefully. Also completedStatus might be easier to just set as remainingTasks == 0? Anyway awesome answer, +1. – Adrian Larson Feb 14 '16 at 0:10
  • @AdrianLarson Good point. I think I'll edit that in. – sfdcfox Feb 14 '16 at 0:11
  • Holy carp, that is beautiful, and it even makes sense to me. One minor clarification - is the "bonus" where the values will be updated without refresh specifically a result of using transient? That seems very interesting, will dig into it. – gorav Feb 14 '16 at 15:30

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.