Showing posts with label Maps. Show all posts
Showing posts with label Maps. Show all posts

Monday, May 27, 2013

Bug with apex:selectCheckboxes

The Project:
Make a search with multi-select checkboxes for better filtering. 

The Solution:
Simple right? Just make a page and controller with some <apex:selectCheckboxes> and a <apex:commandButton> to make it all go. 

<apex:page showHeader="true" sidebar="true" controller="sfg_testBugWithActionButton">
<apex:form>
    <apex:outputpanel id="mainWrap">
        <apex:repeat value="{!filterMap}" var="key">
          <div class="filterItem">
            <h2>{!key}</h2>
            <apex:selectCheckboxes value="{!filterKeys[key]}" layout="pageDirection">
              <apex:selectOptions value="{!filterMap[key]}" />
            </apex:selectCheckboxes>
          </div>
        </apex:repeat>
        <apex:commandButton action="{!preformAction}" rerender="renderWrap" value="Submit Action" />
      <apex:outputpanel id="renderWrap">
        {!resultString}
      </apex:outputpanel>
    </apex:outputpanel>
  </apex:form>
</apex:page>



public class sfg_testBugWithActionButton {

  public List<String> fGender {get; set;}

  public List<String> fGrade {get; set;}

  public List<String> fRole {get; set;}

  public String resultString {get; set;}



  public Map<String, List<SelectOption>> filterMap {get; set;}
  public Map<String, String> filterKeys {get; set;}

  public sfg_testBugWithActionButton() {
    filterKeys = new Map<String, String> {
      'Gender' => 'fGender',
      'Grade' => 'fGrade',
      'Role' => 'fRole'
    };
    createfilterMap();
    resultString = 'on Load of page';
  }

  public PageReference preformAction() {
    resultString = 'button action preformed';
    return null;
  }

  private void createfilterMap() {
    filterMap = new Map<String, List<SelectOption>>();
    List<SelectOption> options = new List<SelectOption>();

    for (String s : filterKeys.keySet()) {
      if (s == 'Gender') {
        options = new List<SelectOption>();
        options.add(new SelectOption('Male', 'Male'));
        options.add(new SelectOption('Female', 'Female'));
        filterMap.put('Gender', options);
      }
      if (s == 'Grade') {
        options = new List<SelectOption>();
        options.add(new SelectOption('A', 'A'));
        options.add(new SelectOption('B', 'B'));
        options.add(new SelectOption('C', 'C'));
        filterMap.put('Grade', options);
      }
      if (s == 'Role') {
        options = new List<SelectOption>();
        options.add(new SelectOption('Support', 'Support'));
        options.add(new SelectOption('Sales', 'Sales'));
        options.add(new SelectOption('Marketing', 'Marketing'));
        filterMap.put('Role', options);
      } 
    }
  }
}

(no style so nothing pretty, just to get the idea)

But WAIT! Why isn't anything happening when I click the button? The text next to the button should be changing. hummm Lets put a debug at the top of the method being called by the button and check the logs to see if its hit...

public PageReference preformAction() {
  system.debug('Gender: ' + fGender);
  system.debug('Grade: ' + fGrade);
  system.debug('Role: ' + fRole);
  resultString = 'button action preformed';
  return null;
}

hummm nothing.. not even a mention of the button getting clicked in the logs, let alone my debug statements. Ok I know from experience I can force the button by using the <apex:actionRegion> attribute..

<apex:actionRegion>
  <apex:commandButton action="{!preformAction}" rerender="renderWrap" value="Submit Action" />
</apex:actionRegion>
      
Click and well now the debug statement is being hit, but its not picking up the values in the  <apex:selectCheckboxes> hummmm.. I know I shouldnt need the <apex:actionRegion> so lets remove that and see what happens when i just click the button, without selecting any of the checkboxes first.. Oh shit it works... the values are null, but they should be since nothing is selected.. Ok lets try changing the <apex:selectCheckboxes> to <apex:selectList> and see if they work.. damn, it seems to work, the resultsString is getting updated, but the values are still showing as null in the logs so I guess its not working, but atleast its working better than the <apex:selectCheckboxes>.. 

It shouldn't be anything to do with the maps or repeat but why dont we do this the long way and see if that helps..

<apex:page showHeader="true" sidebar="true" controller="sfg_testBugWithActionButton">
<apex:form>
    <apex:outputpanel id="mainWrap">
      <div class="filterItem">
        <h2>Grade</h2>
        <apex:selectCheckboxes value="{!fGrade}" layout="pageDirection">
          <apex:selectOptions value="{!soGrade}" />
        </apex:selectCheckboxes>
      </div>
      <div class="filterItem">
        <h2>Gender</h2>
        <apex:selectCheckboxes value="{!fGender}" layout="pageDirection">
          <apex:selectOptions value="{!soGender}" />
        </apex:selectCheckboxes>
      </div>
      <div class="filterItem">
        <h2>Role</h2>
        <apex:selectCheckboxes value="{!fRole}" layout="pageDirection">
          <apex:selectOptions value="{!soRole}" />
        </apex:selectCheckboxes>
      </div>
      <apex:commandButton action="{!preformAction}" rerender="renderWrap" value="Submit Action" />
      <apex:outputpanel id="renderWrap">
        {!resultString}
      </apex:outputpanel>
    </apex:outputpanel>
  </apex:form>
</apex:page>


public class sfg_testBugWithActionButton {
  public String fGender {get; set;}
  public String fGrade {get; set;}
  public String fRole {get; set;}
  public List<SelectOption> soGender {get; set;}
  public List<SelectOption> soGrade {get; set;}
  public List<SelectOption> soRole {get; set;}
  public String resultString {get; set;}

  public sfg_testBugWithActionButton() {
    createfilterMap();
    resultString = 'on Load of page';
  }

  public PageReference preformAction() {
    system.debug('Gender: ' + fGender);
    system.debug('Grade: ' + fGrade);
    system.debug('Role: ' + fRole);
    resultString = 'button action preformed';
    return null;
  }

  private void createfilterMap() {
    soGender = new List<SelectOption>();
    soGender.add(new SelectOption('Male', 'Male'));
    soGender.add(new SelectOption('Female', 'Female'));
      
    soGrade = new List<SelectOption>();
    soGrade.add(new SelectOption('A', 'A'));
    soGrade.add(new SelectOption('B', 'B'));
    soGrade.add(new SelectOption('C', 'C'));
    
    soRole = new List<SelectOption>();
    soRole.add(new SelectOption('Support', 'Support'));
    soRole.add(new SelectOption('Sales', 'Sales'));
    soRole.add(new SelectOption('Marketing', 'Marketing'));
  }
}

No joy, same problem with or without the repeat when using <apex:selectCheckboxes>. Now lets try the same thing but with <apex:selectList> instead.. And it works! But why wouldn't it work when in the repeat or with the <apex:selectCheckboxes>? I also made sure it works when I added the attribute multiselect="true" to them, since that is what I would need anyhow if I had to use Lists over checkboxes.

So lets review: 
Right now it seems that anything with the <apex:selectCheckboxes> wont work (regardless of version since I did try bumping it back on both the page and controller). When trying to be dynamic and using a repeat to populate the Lists it fails as well for both <apex:selectCheckboxes> and <apex:selectList>. But if you do it the long way, you can get <apex:selectList> to work at the very least, although that means more fighting with style to get it to look like checkboxes if that is the desired effect. 

*sad panda*

I posted a simpler test of this not working here: Stackoverflow

**Update**
ANSWER FOUND and so not a bug lol bellow is the update made to the controller, and no update was needed to the page(see page at top of post for code):


public class sfg_testBugWithActionButton {

  public List<String> fGender {get; set;}
  public List<String> fGrade {get; set;}
  public List<String> fRole {get; set;}

  public String resultString {get; set;}

  public Map<String, List<SelectOption>> filterMap {get; set;}

  //this had to changed from Map<String, String> to what you see below
  public Map<String, List<String>> filterKeys {get; set;}

  public sfg_testBugWithActionButton() {
    //this was also added, and helped make it all work
    fGender = new List<String>();
    fGrade = new List<String>();
    fRole = new List<String>();

    // and so this got updated as well to use the param and not a string
    filterKeys = new Map<String, List<String>> {
      'Gender' => fGender,
      'Grade' => fGrade,
      'Role' => fRole
    };
    createfilterMap();
    resultString = 'on Load of page';
  }

  public PageReference preformAction() {
    resultString = 'button action preformed';
    return null;
  }

  private void createfilterMap() {
    filterMap = new Map<String, List<SelectOption>>();
    List<SelectOption> options = new List<SelectOption>();

    for (String s : filterKeys.keySet()) {
      if (s == 'Gender') {
        options = new List<SelectOption>();
        options.add(new SelectOption('Male', 'Male'));
        options.add(new SelectOption('Female', 'Female'));
        filterMap.put('Gender', options);
      }
      if (s == 'Grade') {
        options = new List<SelectOption>();
        options.add(new SelectOption('A', 'A'));
        options.add(new SelectOption('B', 'B'));
        options.add(new SelectOption('C', 'C'));
        filterMap.put('Grade', options);
      }
      if (s == 'Role') {
        options = new List<SelectOption>();
        options.add(new SelectOption('Support', 'Support'));
        options.add(new SelectOption('Sales', 'Sales'));
        options.add(new SelectOption('Marketing', 'Marketing'));
        filterMap.put('Role', options);
      } 
    }
  }
}


HAZA! :-)

Tuesday, March 26, 2013

Benchmark Testing: maps vs for-loop

Speed and efficiency should always be in the forefront of a developers mind when writing code. This, most agree on. What is not agreed upon is how that should be accomplished. Each developer has their our own preferences on how it should be written for various reasons. Usually it's due to how they were taught or trained, maybe its because it is more aesthetically pleasing or reads cleaner. Regardless of the cause, the effect is that the code could have performed more efficiently or at a faster rate. There is always more than one way to accomplish a task, benchmark testing is used to prove what methodology is the faster one, although might not be the prettiest. 

The Argument:

Jim thinks that it is faster to collect data via a for-loop:

Set<Id> accountIds = new Set<Id>();

for (Account a : [select Id from Account]) {
  accountIds.add(a.Id);
}
someMethod(accountIds);

Jane however insists that it is faster to do it via SOQL to Map:

Map<Id, Account> accounts = new Map<Id, Account>(
  [select Id from Account]
);
someMethod(accounts.keySet());

After much debate, a few harsh words and a broken pen, a bet was struck.

The Test:

A test class can be written, alternatively this could be done in 'execute anonymous' but with a test class the real data wont be effected as it would the other way. The tests will run different amounts of data through each pattern and record the time it took for the comparison. First the data needs to be created that they will iterate over. 


private static void createAccounts(String pName) {
  Account a;
  List<Account> accounts = new List<Account>();
  for (Integer i = 0; i < 200; i++) {
    accounts.add(a = new Account(Name = pName + i));
  }
  insert accounts;
}

The name needs to be unique for each Account created, to help accomplish this a string will be passed in to add to it when the account is being created. Since Salesforce has govonerner limits in place, only 200 records can be created at a time and so requiring a second method to call createAccounts() and allow more than 200 to be made at a time. 

private static void setupTests(Integer pLimit) {
  for (Integer i = 0; i < pLimit; i++) {
    createAccounts(String.valueOf(i));
  }
}

To test the for-loop and map, a timestamp is logged at the start and end of the method in milliseconds (this makes the comparison easier). Since each is using the data created in the test, the amount being iterated over is known and no limit is needed in the SOQL statement.


private static Long runViaForLoop() {
  Long lStart = DateTime.now().getTime();
  Set<Id> accountIds = new Set<Id>();
  for (Account a : [select Id from Account]) {
    accountIds.add(a.Id);
  }
  return DateTime.now().getTime() - lStart;
}

private static Long runViaMap() {
  Long lStart = DateTime.now().getTime();
  Map<Id, Account> accounts = new Map<Id, Account>(
    [select Id from Account]
  );
  return DateTime.now().getTime() - lStart;
}

Its time to actually do some testing. The first test should be over a low number of records like 1 or 5 and slowly working up to larger numbers like 10k. Debug statements are necessary to see the output of the numbers in the logs after the tests complete. This will show which pattern took less time to iterate over the various record sizes, and thus show which one is better to use, and who wins the bet!

private static testmethod void compaireTime() {
  setupTests(1);
  system.debug('for-loop: ' + runViaForLoop()  + 'ms');
  system.debug('map: ' + runViaMap() + 'ms');
}

After the tests run, the output will show how long each took to complete the same task. 

DEBUG| for-loop: 36ms
DEBUG| map: 10ms

Looks like the Map is the winner. Unfortunately though, Jim is a sore loser and since there are other factors that can effect the output, its best to run the tests a few times along with expanding it to larger sets of records such as 4k and 10k:

private static testmethod void compaireTime() {
  setupTests(20);
  system.debug('4k for-loop: ' + runViaForLoop() + 'ms');
  system.debug('4k map: ' + runViaMap() + 'ms');
  setupTests(30);
  system.debug('10k for-loop: ' + runViaForLoop() + 'ms');
  system.debug('10k map: ' + runViaMap() + 'ms');
}


DEBUG| 4k for-loop: 876ms
DEBUG| 4k map: 138ms

DEBUG| 10k for-loop: 1777ms
DEBUG| 10k map: 246ms

DEBUG| 4k for-loop: 787ms
DEBUG| 4k map: 107ms

DEBUG| 10k for-loop: 2234ms
DEBUG| 10k map: 340ms

DEBUG| 4k for-loop: 1580ms
DEBUG| 4k map: 113ms

DEBUG| 10k for-loop: 1740
DEBUG| 10k map: 220


DEBUG| 4k for-loop: 796
DEBUG| 4k map: 115

DEBUG| 10k for-loop: 1898
DEBUG| 10k map: 279



The graph above shows the average of 20 tests, and it shows that as the record size increased, the time saved using maps speaks for itself. Jane is clearly the victor here, and Jim has to pay up. Numbers always win arguments.