A slight departure this week - I've received a number of questions through different channels regard the Advanced Developer Certification exam. Rather than answering these point to point I thought I'd create living blog entry that gets updated as new questions come through and spread the knowledge out a bit. Feel free to post further questions in the comments section - I'll then uplift them into the main post with an answer (if I have one!), so don't be surprised if a comment gets deleted!
I can't go into specifics of the actual exam questions or assignment subject, as this is contravenes the test taker agreement that we all sign up to at the beginning of the exam. Outside of that I'll do my best to answer.
Q. What not to study for multiple choice exam. There are topics like salesforce mobile, apex chart, visual workflow these are not specified in study guide so can I ignore these topics?
A. I don't remember any questions that were not based on GA features. I did make sure that I was familiar with the information on the beta/pilot features that were available, but mainly from reading the release notes and developers guide.
Q. From start to end, ie appearing for 1st exam to receiving the result whats the total duration it took for you?
A. Just over 6 months, but some of that was waiting. I took the first exam around the middle of May, but then had to wait until September for a chance to sit the assignment. There's around 1 month to complete the assignment, and then a 6-7 week wait for the results.
Q. If somebody fails in programming assignment than he/she has appear again for multiple choice again?
A. I'm not 100% sure, but I believe that you can retake the programming assignment without having to retake the multiple-choice exam. This is based on comments from someone on LinkedIn though, so it would be better to check this with the certification team from Salesforce
Q. The 20 hrs of timeline is actually restricted to 20 hrs by monitoring login time or you can take more time and login to sandbox/production anytime for your assignment?
A. No, it was made clear that 20 hours was the suggested minimum amount of time it would take you to complete the assignment, there was no mention of any upper limit. I recorded around 28 hours, although some of that was researching and building the QA scripts.
Q. Any tips apart from the blog post?
A. The multiple choice exam is a little different from others I've taken - the questions tended to be a lot longer (including code snippets in the main), so there was more to understand and take in. More knowledge of reference material was required (e.g. triggers and order of execution, standard component attributes). Aside from that, the usual tips of ruling out any obviously wrong answers to narrow down the opportunities, making sure that you read the questions and answers properly.
Q. When is the next registration window for the programming assignment?
A. According to Nina Marinova on Linked In, the assignment window opens Dec 21. Full details at: bit.ly/sXmySO
Update - the registration for the assignment was full in a few hours this time. All the more reason to ensure you are following salesforce certification on linked in, facebook and twitter.
Pages
▼
Sunday, 11 December 2011
Saturday, 3 December 2011
Certified Force.com Advanced Developer
As of 3rd December 2011 I'm a Certified Force.com Advanced Developer! This post gives an overview of the exam and a few tips to hopefully assist others. Please don't ask for or post any exam questions/answers, as this is contravenes the test taker agreement that we all sign up to at the beginning of the exam and devalues the certification.
The first step is to join the Salesforce.com Certified Professionals group on Linked In. This is where a lot of information around the exams is disseminated (the assignment registration windows, for example) and the Salesforce certification team often comment in the discussions around test taking.
You'll need to know the following documents intimately:
There are three parts to the exam - multiple choice, programming assignment and essay.
Multiple Choice
This is an online proctored exam lasting up to 2 hours. There are 69 Questions and the pass rate is 73%, which means that you need to get just over 50 of the answers right. This is different to most of the other exams that I've taken, in that you needed to know reference material in detail (e.g. triggers and orders of execution), plus there was a lot more to take in for the scenarios for most questions.
One thing I found particularly useful was the amount of time that I spend on the Developerforce Discussion boards - being able to quickly comprehend unfamiliar code and spot errors is a definite advantage.
Programming Assignment
After passing the multiple choice, you the have the opportunity to register for the programming assignment. Assignments are available several times a year, but spaces are limited, so my advice would be keep your eyes peeled for the announcement that the window is open and register immediately. The last window was open for around 6 days and fully subscribed in 48 hours.
Assuming you are successful in registering, you'll receive the assignment requirements. They are very detailed and give you clear instructions as to what you need to focus on and what you shouldn't waste time on as you'll gain no extra marks. The assignments have a deadline date, but you don't have to wait until that date to submit it if you are finished in advance.
The recommendation is to spend 20 hours on the assignment. I spent more than this, initially on research to check the approach that I was using was the correct one (in my opinion at least!) and the rest on revisiting my unit tests as other scenarios occurred to me. I find that once I start coding unit tests it's difficult to stop!
I approached this as I would any customer project:
Once you are happy with the assignment, you notify Salesforce that it is ready for assessment and proceed to the final component of the exam.
Essay
The essay portion of the exam is also online proctored and has an allotted time of 1 hour. Its important to stay focused for this exam - I found myself writing long paragraphs on the actual code I'd written, which I ended up deleting as it wasn't really relevant to the question.
While an essay exam sounds intimidating, you can expect questions that ask you to explain your approach, how you handled particular requirements and what compromises were made, so as long as you've taken the assignment seriously it shouldn't be a problem. It is possible that the essay questions will make you think of something that you've missed, in which case you have the opportunity to try to come up with a plausible explanation as part of the answer.
The cynical side of me says that the essay questions are to ensure that whoever has submitted the project has actually written it, and isn't simply riding off other peoples's efforts.
Once this is done, there's a wait of around 6 weeks before the results get sent out.
The first step is to join the Salesforce.com Certified Professionals group on Linked In. This is where a lot of information around the exams is disseminated (the assignment registration windows, for example) and the Salesforce certification team often comment in the discussions around test taking.
You'll need to know the following documents intimately:
- Force.com Apex Code Developer's Guide
- Visualforce Developer's Guide
- Web Service API
- Metadata API
- Force.com Migration Too
There are three parts to the exam - multiple choice, programming assignment and essay.
Multiple Choice
This is an online proctored exam lasting up to 2 hours. There are 69 Questions and the pass rate is 73%, which means that you need to get just over 50 of the answers right. This is different to most of the other exams that I've taken, in that you needed to know reference material in detail (e.g. triggers and orders of execution), plus there was a lot more to take in for the scenarios for most questions.
One thing I found particularly useful was the amount of time that I spend on the Developerforce Discussion boards - being able to quickly comprehend unfamiliar code and spot errors is a definite advantage.
Programming Assignment
After passing the multiple choice, you the have the opportunity to register for the programming assignment. Assignments are available several times a year, but spaces are limited, so my advice would be keep your eyes peeled for the announcement that the window is open and register immediately. The last window was open for around 6 days and fully subscribed in 48 hours.
Assuming you are successful in registering, you'll receive the assignment requirements. They are very detailed and give you clear instructions as to what you need to focus on and what you shouldn't waste time on as you'll gain no extra marks. The assignments have a deadline date, but you don't have to wait until that date to submit it if you are finished in advance.
The recommendation is to spend 20 hours on the assignment. I spent more than this, initially on research to check the approach that I was using was the correct one (in my opinion at least!) and the rest on revisiting my unit tests as other scenarios occurred to me. I find that once I start coding unit tests it's difficult to stop!
I approached this as I would any customer project:
- Read the specs carefully and understand the requirements
- Produce a design
- Implement the solution and unit tests
- Write a formal QA plan to cover all of the scenarios mentioned in the specs, plus as many others as I can think of, paying attention to security and scalability.
- Use the system for some real-world scenarios
Once you are happy with the assignment, you notify Salesforce that it is ready for assessment and proceed to the final component of the exam.
The essay portion of the exam is also online proctored and has an allotted time of 1 hour. Its important to stay focused for this exam - I found myself writing long paragraphs on the actual code I'd written, which I ended up deleting as it wasn't really relevant to the question.
While an essay exam sounds intimidating, you can expect questions that ask you to explain your approach, how you handled particular requirements and what compromises were made, so as long as you've taken the assignment seriously it shouldn't be a problem. It is possible that the essay questions will make you think of something that you've missed, in which case you have the opportunity to try to come up with a plausible explanation as part of the answer.
The cynical side of me says that the essay questions are to ensure that whoever has submitted the project has actually written it, and isn't simply riding off other peoples's efforts.
Once this is done, there's a wait of around 6 weeks before the results get sent out.
Saturday, 26 November 2011
Retrieve Related Object Fields
A short post this week, again based on a question from the
DeveloperForce Visualforce Discussion Board
regarding the retrieval of a related objects fields. In this particular
case, an sobject is being created/edited via Visualforce and once a lookup field
is populated, the developer wanted to display some fields related to the
lookup.
Unsurprisingly, simply adding some outputfields for the related object as shown below doesn't do the trick:
Once the lookup is selected, the outputfield values remain empty, as all that has been specified is the id of the account via the inputfield - the related object details aren't populated in the sobject graph, so attempting to traverse the account relationship doesn't bring back any data:
One way to get the information back would be to save the contact once the lookup is populated and carry out a client side redirect to the current page with the id of the newly created contact. When the standard controller retrieves the new sobject, it will populate the related Account information automatically. This isn't a great user experience though, as the save is likely to be well before the user is ready, and doesn't give them a chance to change their mind part way through. Likely to lead to a lot of unwanted and half-populated contacts.
Therefore it looks like an extension controller is the route to go. The page has been updated with an actionsupport component that is attached to the account lookup. This invokes an action method on the extension controller which executes a SOQL query to populate the related account information.
Revised Page:
Extension controller:
Also, as I've been able to write the information into the object relationship, its the same markup that renders the account information regardless of whether it was retrieved via my extension controller or via the standard controller "reflection" when the page is opened with a specified contact id.
Finally, a word of advice - if you are writing an extension controller for this purpose, make sure that the related object has some data present in the fields, otherwise you'll be convinced your code is failing when in fact its working perfectly, just rendering empty fields!
Unsurprisingly, simply adding some outputfields for the related object as shown below doesn't do the trick:
<apex:page standardcontroller="Contact"> <apex:form > <apex:pageBlock title="Contact Create/Edit"> <apex:pageBlockSection title="Contact Information"> <apex:inputField value="{!contact.FirstName}"/> <apex:inputField value="{!contact.LastName}"/> </apex:pageBlockSection> <apex:pageBlockSection title="Account Information"> <apex:inputField value="{!contact.AccountId}"/> <apex:outputField value="{!contact.Account.AccountNumber}"/> <apex:outputField value="{!contact.Account.Site}"/> </apex:pageBlockSection> </apex:pageBlock> </apex:form> </apex:page>
Once the lookup is selected, the outputfield values remain empty, as all that has been specified is the id of the account via the inputfield - the related object details aren't populated in the sobject graph, so attempting to traverse the account relationship doesn't bring back any data:
One way to get the information back would be to save the contact once the lookup is populated and carry out a client side redirect to the current page with the id of the newly created contact. When the standard controller retrieves the new sobject, it will populate the related Account information automatically. This isn't a great user experience though, as the save is likely to be well before the user is ready, and doesn't give them a chance to change their mind part way through. Likely to lead to a lot of unwanted and half-populated contacts.
Therefore it looks like an extension controller is the route to go. The page has been updated with an actionsupport component that is attached to the account lookup. This invokes an action method on the extension controller which executes a SOQL query to populate the related account information.
Revised Page:
<apex:page standardcontroller="Contact" extensions="RelatedController"> <apex:form > <apex:pageMessages id="msgs"/> <apex:pageBlock title="Contact Create/Edit"> <apex:pageBlockSection title="Contact Information"> <apex:inputField value="{!contact.FirstName}"/> <apex:inputField value="{!contact.LastName}"/> </apex:pageBlockSection> <apex:actionRegion > <apex:pageBlockSection id="accinfo" title="Account Information"> <apex:inputField value="{!contact.AccountId}"> <apex:actionSupport event="onchange" action="{!AccountPopulated}" rerender="accinfo, msgs"/> </apex:inputField> <apex:outputField value="{!contact.Account.AccountNumber}"/> <apex:outputField value="{!contact.Account.Site}"/> </apex:pageBlockSection> </apex:actionRegion> <apex:pageBlockButtons > <apex:commandButton value="Cancel" action="{!cancel}"/> <apex:commandButton value="Save" action="{!save}"/> </apex:pageBlockButtons> </apex:pageBlock> </apex:form> </apex:page>
Extension controller:
public with sharing class RelatedController { private ApexPages.StandardController stdCtrl; public RelatedController(ApexPages.StandardController std) { stdCtrl=std; } public void AccountPopulated() { Contact cont=(Contact) stdCtrl.getRecord(); cont.Account=[select AccountNumber, Site from Account where id=:cont.AccountId]; } }Choosing the account now carries out an Ajax request and rerenders the account information with the fields populated.
Also, as I've been able to write the information into the object relationship, its the same markup that renders the account information regardless of whether it was retrieved via my extension controller or via the standard controller "reflection" when the page is opened with a specified contact id.
Finally, a word of advice - if you are writing an extension controller for this purpose, make sure that the related object has some data present in the fields, otherwise you'll be convinced your code is failing when in fact its working perfectly, just rendering empty fields!
Saturday, 19 November 2011
Bypass Validation Rules from Apex
Today's post concerns a topic that crops up pretty frequently on the
Developerforce Discussion Boards - how to allow apex code to bypass validation rules.
In my example scenario, I've created a validation rule against the Contact standard object that checks either the contact's email address or phone number has been provided:
Thus when I attempt to create a Contact via the UI, if I don't provide one of these pieces of information I receive an error message:
All well and good. However, what about the situation where I am creating a Contact programmatically? In this case the validation rule is still applied:
Obviously creating via the system log is a contrived example, but in practice I may be receiving the details from an external system as part of an integration (via an apex web service) or data migration (via the Apex Data Loader), and the information may simply not be available in that system. If its a data migration, I could disable the validation rule for the duration, but if its a real-time integration that isn't an option.
The solution I came up with was to provide a mechanism for the record itself to indicate how it was being created/edited and to take that into account for in the validation rule.
The first step in the solution is to create a new custom field on the Contact object - a checkbox that if set to true indicates the record is being processed in an apex context. This checkbox is not made available on any page layouts, as we don't want regular UI users to be able to check it.
Next, change the validation rule to allow both the email and phone fields to be empty if the apex context field is set to true:
Its tempting at this time to think that the problem is now solved - the Apex Context field can simply be set to true and the validation rule will allow the record through. However, the record will then be saved to the database with the Apex Content field set to true, and the next time that a user edits the record in the UI, the validation rule will be bypassed - not the behaviour I am looking for. Therefore I've created a workflow rule and associated field update to unset the checkbox whenever it is found to be set.
Workflow rule:
Field update:
and that's all there is to it. Now I can create a contact via the system log without providing the email or phone field:
But if I then navigate to that contact via the UI and attempt to edit the record without providing these fields, the validation rule blocks this:
In my example scenario, I've created a validation rule against the Contact standard object that checks either the contact's email address or phone number has been provided:
Thus when I attempt to create a Contact via the UI, if I don't provide one of these pieces of information I receive an error message:
All well and good. However, what about the situation where I am creating a Contact programmatically? In this case the validation rule is still applied:
Obviously creating via the system log is a contrived example, but in practice I may be receiving the details from an external system as part of an integration (via an apex web service) or data migration (via the Apex Data Loader), and the information may simply not be available in that system. If its a data migration, I could disable the validation rule for the duration, but if its a real-time integration that isn't an option.
The solution I came up with was to provide a mechanism for the record itself to indicate how it was being created/edited and to take that into account for in the validation rule.
The first step in the solution is to create a new custom field on the Contact object - a checkbox that if set to true indicates the record is being processed in an apex context. This checkbox is not made available on any page layouts, as we don't want regular UI users to be able to check it.
Next, change the validation rule to allow both the email and phone fields to be empty if the apex context field is set to true:
Its tempting at this time to think that the problem is now solved - the Apex Context field can simply be set to true and the validation rule will allow the record through. However, the record will then be saved to the database with the Apex Content field set to true, and the next time that a user edits the record in the UI, the validation rule will be bypassed - not the behaviour I am looking for. Therefore I've created a workflow rule and associated field update to unset the checkbox whenever it is found to be set.
Workflow rule:
Field update:
and that's all there is to it. Now I can create a contact via the system log without providing the email or phone field:
But if I then navigate to that contact via the UI and attempt to edit the record without providing these fields, the validation rule blocks this:
Saturday, 12 November 2011
Automatically Add User to Chatter Group
Following on from an earlier blog post that dealt with user autofollowing, I was recently asked about
automatically adding users to a chatter group. Here at BrightGen we have a chatter group that everyone gets added to when a user is
created for them on our Salesforce instance. Its not too arduous to add
these users to the group, but every now and then one gets overlooked.
Chatter groups are stored as CollaborationGroup records, with the members of the group stored in CollaborationGroupMember records related via the CollaborationGroupId field.
The first step is to create a trigger that is executed wheh a user record is added. I've reused the trigger I created for my autofollow user post:
The second line, containing the call to the AddToGroups is the new part - this is the method that does the heavy lifting.
The group name is hardcoded for the purposes of this blog - not a good idea in practice and for a production system I'd store this information in a custom setting. Once the groups have been retrieved, the inserted users are iterated and added to each of the groups. As an added flourish I've added a post to the group to notify members the new user has joined, and a post to the new user's feed to let them know this has happened. As I'm keen to grab all the credit that is going, I've also mentioned in each post that this was done via my trigger!
Now I can add a user via the UI:
Once the user is created, they are automatically added to the All Users group and a post announces this:
And navigating to the new users profile, shows a post on their chatter feed informing them this has taken place:
And finally, a shout out to my BrightGen colleage Zak Crammond who asked the question that led to this post.
Chatter groups are stored as CollaborationGroup records, with the members of the group stored in CollaborationGroupMember records related via the CollaborationGroupId field.
The first step is to create a trigger that is executed wheh a user record is added. I've reused the trigger I created for my autofollow user post:
trigger ai_user on User (after insert) { ChatterAutofollow.AutofollowUsers(trigger.newMap.keySet()); ChatterAutoFollow.AddToGroups(trigger.newMap.keySet()); }
The second line, containing the call to the AddToGroups is the new part - this is the method that does the heavy lifting.
@future public static void AddToGroups(Set<Id> userIds) { List<User> users=[select id, Username from User where id in :userIds]; // set up the groups that the user should be added to List<String> groups=new List<String>{'All Users'}; List<CollaborationGroup> chatterGroups=[select id, Name from CollaborationGroup where name in :groups]; List<CollaborationGroupMember> chatterGroupMembers=new List<CollaborationGroupMember>(); List<FeedPost> feedPosts=new List<FeedPost>(); // loop the users that have been created for (User user : users) { // loop the groups for (CollaborationGroup chatterGroup : chatterGroups) { // add the user to the group CollaborationGroupMember cand = new CollaborationGroupMember( CollaborationGroupId=chatterGroup.id, MemberId = user.Id); chatterGroupMembers.add(cand); // announce this! FeedPost fpost = new FeedPost(ParentId=chatterGroup.id, Body = user.username + ' added to group via Bob Buzzard trigger'); feedPosts.add(fpost); fpost = new FeedPost(ParentId=user.id, Body = 'You have been added to the ' + chatterGroup.name + ' group via Bob Buzzard trigger'); feedPosts.add(fpost); } } insert chatterGroupMembers; insert feedPosts; }as before, the method has to be declared with the @future annotation, as the trigger is invoked when a User is inserted and I'll otherwise hit mixed mode DML problems.
The group name is hardcoded for the purposes of this blog - not a good idea in practice and for a production system I'd store this information in a custom setting. Once the groups have been retrieved, the inserted users are iterated and added to each of the groups. As an added flourish I've added a post to the group to notify members the new user has joined, and a post to the new user's feed to let them know this has happened. As I'm keen to grab all the credit that is going, I've also mentioned in each post that this was done via my trigger!
Now I can add a user via the UI:
Once the user is created, they are automatically added to the All Users group and a post announces this:
And navigating to the new users profile, shows a post on their chatter feed informing them this has taken place:
And finally, a shout out to my BrightGen colleage Zak Crammond who asked the question that led to this post.
Saturday, 29 October 2011
Creating Attachments in Apex Code
Capturing notes and attachments against records is standard Salesforce
functionality. Recently I had a requirement to capture some notes
and a subset of the record details at various points of the record's
life. Effectively the notes were specific to the record's current state
and viewing either in isolation wouldn't give the desired outcome. While it
would be possible to handle this with a combination of notes/attachments and
field history tracking, that puts far too much emphasis on the user to pull the
information from disparate places.
To satisfy this requirement I came up with a Visualforce page that is embedded in a record view and has a notes field to capture the free text plus a checkbox to indicate if the opportunity detail should be captured. The page embedded into an Opportunity record is shown below:
The fields that are included into the attachment are coded into the page and controller in this case, but it would be straightforward to use Visualforce field sets to reduce the maintenance overhead. The Visualforce markup is shown below:
The first part of the page simply pulls in the fields that are required by the controller. Note that these aren't rendered or used in the page, but without these components my controller would have to query the details from the database:
The only other aspect that isn't vanilla Visualforce forms is the following custom component:
This allows the entire record view page to be refreshed once a Boolean value from the controller is set to true - its the functionality from an earlier post embedded into a component to promote reuse.
Once the user has filled in the form, the following Apex action method is invoked:
The first part of the method simply builds up the text body of the attachment in a String, appending the user's notes and, if requested, a snapshot of the opportunity fields.
The interesting part is where the attachment is created:
As you can see there's not much to do - simply create a new attachment, convert the String containing the body to a Blob and store that in the attachment's body field, set up the name and which record its attached to and then insert. The final line sets the flag to refresh the record view page.
To satisfy this requirement I came up with a Visualforce page that is embedded in a record view and has a notes field to capture the free text plus a checkbox to indicate if the opportunity detail should be captured. The page embedded into an Opportunity record is shown below:
The fields that are included into the attachment are coded into the page and controller in this case, but it would be straightforward to use Visualforce field sets to reduce the maintenance overhead. The Visualforce markup is shown below:
<apex:page standardController="Opportunity" extensions="OpportunityAttachmentController"> <apex:outputText value="{!Opportunity.Account.Name}" rendered="false"/> <apex:outputText value="{!Opportunity.CloseDate}" rendered="false"/> <apex:outputText value="{!Opportunity.Amount}" rendered="false"/> <apex:outputText value="{!Opportunity.StageName}" rendered="false"/> <apex:outputText value="{!Opportunity.Probability}" rendered="false"/> <apex:outputText value="{!Opportunity.ExpectedRevenue}" rendered="false"/> <c:RefreshEmbeddedComponent id="refresher" location="/{!Opportunity.id}" active="{!refreshPage}"/> <apex:form > <apex:pageBlock title="Add Attachment" mode="mainDetail" id="pb"> <apex:pageBlockSection columns="2"> <apex:pageBlockSectionItem > <apex:outputLabel for="title" value="Title"/> <apex:inputText id="title" value="{!title}"/> </apex:pageBlockSectionItem> <apex:pageBlockSectionItem > <apex:outputLabel for="incsum" value="Include Opportunity Detail?"/> <apex:inputCheckbox id="incsum" value="{!includeOppDetail}"/> </apex:pageBlockSectionItem> </apex:pageBlockSection> <apex:pageBlockSection columns="1"> <apex:pageBlockSectionItem > <apex:outputLabel for="notes" value="Notes"/> <apex:inputTextArea rows="15" style="width:80%" id="notes" value="{!notes}"/> </apex:pageBlockSectionItem> </apex:pageBlockSection> <apex:commandButton value="Add As Attachment" action="{!addAttachment}" rerender="refresher"/> </apex:pageBlock> </apex:form> </apex:page>
The first part of the page simply pulls in the fields that are required by the controller. Note that these aren't rendered or used in the page, but without these components my controller would have to query the details from the database:
<apex:outputText value="{!Opportunity.Account.Name}" rendered="false"/> <apex:outputText value="{!Opportunity.CloseDate}" rendered="false"/> <apex:outputText value="{!Opportunity.Amount}" rendered="false"/> <apex:outputText value="{!Opportunity.StageName}" rendered="false"/> <apex:outputText value="{!Opportunity.Probability}" rendered="false"/> <apex:outputText value="{!Opportunity.ExpectedRevenue}" rendered="false"/>
The only other aspect that isn't vanilla Visualforce forms is the following custom component:
<c:RefreshEmbeddedComponent id="refresher" location="/{!Opportunity.id}" active="{!refreshPage}"/>
This allows the entire record view page to be refreshed once a Boolean value from the controller is set to true - its the functionality from an earlier post embedded into a component to promote reuse.
Once the user has filled in the form, the following Apex action method is invoked:
public void addAttachment() { String bodyStr=title + '\n' + notes; if (includeOppDetail) { bodyStr+='\n\nOpportunity Summary\n' + 'Account : ' + opp.Account.Name + '\n' + 'Close Date : ' + opp.CloseDate + '\n' + 'Amount : ' + opp.Amount + '\n' + 'Stage : ' + opp.StageName + '\n' + 'Probability : ' + opp.Probability + '\n' + 'Expected Rev : ' + opp.ExpectedRevenue; } bodyStr+='\n\n\n' + System.now(); Attachment att=new Attachment(); att.Body=Blob.valueOf(bodyStr); att.Name='Note_' + System.now().format('yyyy_MM_dd_hh_mm_ss') + '.txt'; att.parentId=opp.id; insert att; refreshPage=true; }
The first part of the method simply builds up the text body of the attachment in a String, appending the user's notes and, if requested, a snapshot of the opportunity fields.
The interesting part is where the attachment is created:
Attachment att=new Attachment(); att.Body=Blob.valueOf(bodyStr); att.Name='Note_' + System.now().format('yyyy_MM_dd_hh_mm_ss') + '.txt'; att.parentId=opp.id; insert att;
As you can see there's not much to do - simply create a new attachment, convert the String containing the body to a Blob and store that in the attachment's body field, set up the name and which record its attached to and then insert. The final line sets the flag to refresh the record view page.
Sunday, 23 October 2011
London Force.com Developer Meetup #3
This week saw the third London Force.com Developer meetup take place, at the Skills Matter eXchange in Clerkenwell. A slightly different format this time, with five lightning talks. Click on the links at the start of each section to view the podcasts.
First up was Amjad Khan on Schema Spy. Schema Spy is a Java tool that analyses databases and produces a graphical representation of the schema. A plugin links this to your Salesforce instance. This is a tool that I've used once or twice in the past, but not that frequently as one of the BrightGen deliverables from the scoping stage of a project is a diagram of the data model. Where it has come in very handy is when we've been engaged to extend an existing solution or provide a managed service - it saves a lot of analysis time in these cases.
The new Schema Builder functionality looks likely to replace this for us in the fullness of time, mainly as it is in the cloud, and anything that gets software off of my laptop gets my vote.
Next up was Simon Goodyear on interfaces. For some reason I'd convinced myself that this was going to be around user interfaces, rather than the programming construct as it turned out to be! Interfaces are something that I tend to use when developing products rather than carrying out a Salesforce implementation, on the assumption that I'm likely to want to swap out implementations as the platform moves on, but customers are unlikely to want (or to want to pay for!, even though the cost is usually pretty low) this level of flexibility. Usually Salesforce implementations are seen as a single-shot roll out with some additional maintenance work as time goes on.
A real-world example of interfaces for us was a custom calendar implementation. The standard Salesforce activity functionality didn't give the flexibility that was required, Initially there were three sobject types that could feature in calendars, which were people, rooms and equipment. It seemed highly likely that there would be other sobject types required to work in this way, and indeed this turned out to be the case. Thus the calendar functionality was implemented to work with a Calendarable interface, and wrapper classes to encapsulate the sobjects along with an implementation of the Calendarable interface were written. Supporting a new sobject type in the calendar was reduced to a small custom class with 10s of lines of code, rather than tweaking code all over the calendar implementation.
Next up was yours truly on the subject of testing. I'm planning to use this as the basis for a blog post dedicated to testing, so won't say anything more here.
Stony Grunow then spoke on effective B2C management. I found this a particularly useful talk, as I'm often having to field questions about why person accounts aren't suitable for an implementation, usually because additional, secondary contacts are required to be associated, for example spouse and children. I'll definitely be taking a look at the Contacts and Organizations package.
Finally, Bruce Durling spoke on using Selenium to automate deployments. Deploying configuration has always been a challenge, as there are some things that can be deployed via metadata - approval processes for example. Re-keying these is error-prone, even when using the four eyes principle. Using Selenium for this is an intriguing notion. A key factor in deciding whether to go this route would be the amount of times that the deployment will take place. If its a single shot, then I can't see there would be much benefit, as you would be effectively re-keying it through code that drives Selenium. Where it becomes much more useful is where changes have to be deployed through various developer sandboxes, QA and pre-production environments before ending up on production.
I can also see this being a useful technique for creating custom setting values, which aren't copied over for anything other than full copy sandboxes, but supply vital configuration information to allow the system to function correctly. It may even provide a solution to the perennial problem of determining the HTML element ids of fields to populate via URL parameters. We use custom settings to capture this information, but there's still manual intervention required to scrape the id out of the page. Something I'll be investigating in the near future.
With all this, excellent before and after networking, and free pizza and beer, its easy to see why these meetups are growing in popularity. Once again, thanks go to Wes Nolte (@wesnolte) and Bruce Durling (@otfrom) for organizing everything. If you are interested in coming along to the next one, follow any (or all!) of these twitter users and the details will be (re)tweeted as soon as they are confirmed: @bob_buzzard, @otfrom, @wesnolte. Look forward to seeing you there!
First up was Amjad Khan on Schema Spy. Schema Spy is a Java tool that analyses databases and produces a graphical representation of the schema. A plugin links this to your Salesforce instance. This is a tool that I've used once or twice in the past, but not that frequently as one of the BrightGen deliverables from the scoping stage of a project is a diagram of the data model. Where it has come in very handy is when we've been engaged to extend an existing solution or provide a managed service - it saves a lot of analysis time in these cases.
The new Schema Builder functionality looks likely to replace this for us in the fullness of time, mainly as it is in the cloud, and anything that gets software off of my laptop gets my vote.
Next up was Simon Goodyear on interfaces. For some reason I'd convinced myself that this was going to be around user interfaces, rather than the programming construct as it turned out to be! Interfaces are something that I tend to use when developing products rather than carrying out a Salesforce implementation, on the assumption that I'm likely to want to swap out implementations as the platform moves on, but customers are unlikely to want (or to want to pay for!, even though the cost is usually pretty low) this level of flexibility. Usually Salesforce implementations are seen as a single-shot roll out with some additional maintenance work as time goes on.
A real-world example of interfaces for us was a custom calendar implementation. The standard Salesforce activity functionality didn't give the flexibility that was required, Initially there were three sobject types that could feature in calendars, which were people, rooms and equipment. It seemed highly likely that there would be other sobject types required to work in this way, and indeed this turned out to be the case. Thus the calendar functionality was implemented to work with a Calendarable interface, and wrapper classes to encapsulate the sobjects along with an implementation of the Calendarable interface were written. Supporting a new sobject type in the calendar was reduced to a small custom class with 10s of lines of code, rather than tweaking code all over the calendar implementation.
Next up was yours truly on the subject of testing. I'm planning to use this as the basis for a blog post dedicated to testing, so won't say anything more here.
Stony Grunow then spoke on effective B2C management. I found this a particularly useful talk, as I'm often having to field questions about why person accounts aren't suitable for an implementation, usually because additional, secondary contacts are required to be associated, for example spouse and children. I'll definitely be taking a look at the Contacts and Organizations package.
Finally, Bruce Durling spoke on using Selenium to automate deployments. Deploying configuration has always been a challenge, as there are some things that can be deployed via metadata - approval processes for example. Re-keying these is error-prone, even when using the four eyes principle. Using Selenium for this is an intriguing notion. A key factor in deciding whether to go this route would be the amount of times that the deployment will take place. If its a single shot, then I can't see there would be much benefit, as you would be effectively re-keying it through code that drives Selenium. Where it becomes much more useful is where changes have to be deployed through various developer sandboxes, QA and pre-production environments before ending up on production.
I can also see this being a useful technique for creating custom setting values, which aren't copied over for anything other than full copy sandboxes, but supply vital configuration information to allow the system to function correctly. It may even provide a solution to the perennial problem of determining the HTML element ids of fields to populate via URL parameters. We use custom settings to capture this information, but there's still manual intervention required to scrape the id out of the page. Something I'll be investigating in the near future.
With all this, excellent before and after networking, and free pizza and beer, its easy to see why these meetups are growing in popularity. Once again, thanks go to Wes Nolte (@wesnolte) and Bruce Durling (@otfrom) for organizing everything. If you are interested in coming along to the next one, follow any (or all!) of these twitter users and the details will be (re)tweeted as soon as they are confirmed: @bob_buzzard, @otfrom, @wesnolte. Look forward to seeing you there!
Saturday, 15 October 2011
Dojo Charts Part 4 - Line Charts
This week the Dojo chart under the microscope is the line chart, where a series
of points are plotted on the graph and joined by lines. In my example
there are two series allowing comparison of made up information about the
English counties of Suffolk and Norfolk.
Following the now familiar pattern is a page with associated controller that pulls in the cross domain Dojo build, manages the data and delegates the chart production to a custom component:
The controller manages three String properties:
Updating the values in the chart and clicking the Update button causes the graph to be redrawn with the new values:
This example, like the others in this series, is available in the Google Code project
Following the now familiar pattern is a page with associated controller that pulls in the cross domain Dojo build, manages the data and delegates the chart production to a custom component:
<apex:page controller="DojoLineChartController"> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js" djConfig="parseOnLoad: true, modulePaths: { 'dojo': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo', 'dijit': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit', 'dojox': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox' } "> </script> <apex:form > <table> <tr> <td>Labels:</td> <td> <apex:inputText value="{!labels}" size="80"/> </td> </tr> <tr> <td>Series 1:</td> <td> <apex:inputText value="{!series1}"/> </td> </tr> <tr> <td>Series 2:</td> <td> <apex:inputText value="{!series2}"/> </td> </tr> </table> <apex:commandButton value="Update"/> </apex:form> <c:DojoLineChart id="linechart" miny="0" maxy="12" stepy="3" labelsx="{!xAxisLabels}" title1="Essex" series1="{!series1}" color1="#000" title2="Norfolk" series2="{!series2}" color2="#C00" /> </apex:page>
The controller manages three String properties:
- A comma separated list of labels, used to produce the x axis labels
- A comma separated list of values for the first series to plot
- A comma separated list of values for the second series to plot
<apex:component > <apex:attribute name="miny" description="Minimum Y Axis value" type="Integer"/> <apex:attribute name="maxy" description="Maximum Y Axis value" type="Integer"/> <apex:attribute name="stepy" description="Maximum Y Axis value" type="Integer"/> <apex:attribute name="labelsx" description="X Axis Labels" type="String" /> <apex:attribute name="title1" description="Plot series 1 title - displayed in legend" type="String" /> <apex:attribute name="series1" description="Plot series 1, comma separated values" type="String" /> <apex:attribute name="color1" description="Plot series 1 colour" type="String" /> <apex:attribute name="title2" description="Plot series 2 title - displayed in legend" type="String" /> <apex:attribute name="series2" description="Plot series 2, comma separated values" type="String" /> <apex:attribute name="color2" description="Plot series 2 colour" type="String" /> <apex:attribute name="title3" description="Plot series 3 title - displayed in legend" type="String" /> <apex:attribute name="series3" description="Plot series 3, comma separated values" type="String" /> <apex:attribute name="color3" description="Plot series 3colour" type="String" /> <apex:attribute name="height" description="Chart Height" type="Integer" default="300"/> <apex:attribute name="width" description="Chart Width" type="Integer" default="800"/> <apex:attribute name="showLegend" description="Show Legend" type="Boolean" default="true"/> <script type="text/javascript"> dojo.require("dojox.charting.Chart2D"); dojo.require("dojox.charting.widget.Legend"); makeCharts = function(){ var chart1 = new dojox.charting.Chart2D("simplechart", {fill:"transparent"}); chart1.addPlot("default", {type: "Lines", markers: "true", hAxis: "x", vAxis: "y"}); chart1.addAxis("x", { <apex:outputText rendered="{!NOT(ISBLANK(labelsx))}"> labels: [ {!labelsx} ] </apex:outputText> }); chart1.addAxis("y", { <apex:outputText value="min:{!miny}," rendered="{!NOT(ISBLANK(miny))}"/> <apex:outputText value="max:{!maxy}," rendered="{!NOT(ISBLANK(maxy))}"/> <apex:outputText value="majorTickStep:{!stepy}," rendered="{!NOT(ISBLANK(stepy))}"/> vertical: true, natural: false}); <apex:outputPanel layout="none" rendered="{!NOT(ISBLANK(title1))}"> chart1.addSeries("{!title1}", [{!series1}], {plot: "other", stroke: {color:"{!color1}"}}); </apex:outputPanel> <apex:outputPanel layout="none" rendered="{!NOT(ISBLANK(title2))}"> chart1.addSeries("{!title2}", [{!series2}], {plot: "other", stroke: {color:"{!color2}"}}); </apex:outputPanel> <apex:outputPanel layout="none" rendered="{!NOT(ISBLANK(title3))}"> chart1.addSeries("{!title3}", [{!series3}], {plot: "other", stroke: {color:"{!color3}"}}); </apex:outputPanel> chart1.render(); <apex:outputText rendered="{!showLegend}"> var legend1 = new dojox.charting.widget.Legend({chart: chart1, horizontal: false}, "legend1"); </apex:outputText> }; dojo.addOnLoad(makeCharts); </script> <table> <tr> <td> <div id="simplechart" style="width: 800px; height: 300px;"></div> </td> <td style="vertical-align:middle"> <div id="legend1"></div> </td> </tr> </table> </apex:component>The chart is created with a plot and x and y axes. The component can take up to 3 series - these are conditionally added to to chart via apex:outputPanel components, along with the line colour. The component can also display a legend for the chart - if the showLegend attribute is set to true. When first opened, the page appears as:
Updating the values in the chart and clicking the Update button causes the graph to be redrawn with the new values:
This example, like the others in this series, is available in the Google Code project
Sunday, 9 October 2011
Dojo Charts Part 3 - Stacked Bar Charts
Continuing the series of posts on Dojo charting, this week I'm looking at
stacked bar charts. This is nboStacked bar charts allow a single bar to
display more than one category of data. My example shows the SLA status
for a set of records.
As usual, the Visualforce page pulls in the cross domain Dojo build from Google, and delegates responsibility for the production of the chart to a custom component:
As usual, the Visualforce page pulls in the cross domain Dojo build from Google, and delegates responsibility for the production of the chart to a custom component:
<apex:page > <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js" djConfig="parseOnLoad: true, modulePaths: { 'dojo': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo', 'dijit': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit', 'dojox': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox' } "> </script> <apex:sectionheader title="Dojo Stacked Bar Chart"/> <c:DojoStackedBarChart name1="Inside SLA" series1="1,3,1" colour1="green" click1="http://www.google.com" name2="Escalated" series2="3,8,5" colour2="yellow" click2="http://www.google.com" name3="Breached SLA" series3="4,13,8" colour3="red" click3="http://www.google.com" divId="stackedBars" title="SLA Status" /> </apex:page>each stacked bar in the chart is set up via the series/colour/click/name indexed parameters. I haven't made this a live page, but it would be straightforward to store the series1-3 values in a custom controller and add apex:inputField components that allowed the user to update them. Where this differs from previous posts is the click parameters - this allows the user to click on one of the stacked bars and be taken to another page. I've simply sent the user to www.google.com, but when I've used this technique in a real project the user is taken to a drill down page or report that shows the detail behind that particular stacked bar. The component markup is shown below:
<apex:component > <apex:attribute name="name1" type="String" description="Name of first series (first stack)"/> <apex:attribute name="series1" type="String" description="Comma seperated values for first series"/> <apex:attribute name="colour1" type="String" description="Colour of first series"/> <apex:attribute name="click1" type="String" description="The destination if the user clicks on the first series"/> <apex:attribute name="name2" type="String" description="Name of second series (second stack)"/> <apex:attribute name="series2" type="String" description="Comma seperated values for second series"/> <apex:attribute name="colour2" type="String" description="Colour of second series"/> <apex:attribute name="click2" type="String" description="The destination if the user clicks on the first series"/> <apex:attribute name="name3" type="String" description="Name of third series (third stack)"/> <apex:attribute name="series3" type="String" description="Comma seperated values for first series"/> <apex:attribute name="colour3" type="String" description="Colour of first series"/> <apex:attribute name="click3" type="String" description="The destination if the user clicks on the first series"/> <apex:attribute name="divId" type="String" description="ID of the div to store the chart in"/> <apex:attribute name="title" type="String" description="Title of the chart"/> <script type="text/javascript"> dojo.require("dojox.charting.Chart2D"); dojo.require("dojo.colors"); dojo.require("dojox.charting.themes.Tufte"); dojo.require("dojox.charting.widget.Legend"); makeCharts = function(){ var chart2=new dojox.charting.Chart2D("{!divId}"); // use the Tufte theme as this gives transparent background chart2.setTheme(dojox.charting.themes.Tufte); chart2.addPlot("default", {type: "StackedBars"}); chart2.addSeries("{!name1}", [{!series1}], {fill: "{!colour1}"}); chart2.addSeries("{!name2}", [{!series2}], {fill: "{!colour2}"}); chart2.addSeries("{!name3}", [{!series3}], {fill: "{!colour3}"}); chart2.addAxis("x", {includeZero: true}); chart2.connectToPlot("default", this, function(evt) { if(!evt.shape||(evt.type != 'onclick')){ return; } var link=""; if (evt.run.name=="{!name1}") { link="{!click1}"; } else if (evt.run.name=="{!name2}") { link="{!click2}"; } else if (evt.run.name=="{!name3}") { link="{!click3}"; } if (""!=link) { window.open(link, "_blank"); } } ); chart2.render(); var legend1 = new dojox.charting.widget.Legend({chart: chart2}, "legend1"); }; dojo.addOnLoad(makeCharts); </script> <div id="{!divId}" style="width: 600px; height: 200px;"></div> <div id="legend1"></div> <div style="margin-top: 40px; font-size:14px; font-weight:bold; width: 400px; text-align:center">{!title}</div> </apex:component>Most of this will be familiar to regular readers - create the chart, define the series, add plots and axes etc. The interesting part (for this week at least) is the function that handles the event when the user clicks on a stacked bar.
chart2.connectToPlot("default", this, function(evt) { if(!evt.shape||(evt.type != 'onclick')){ return; } var link=""; if (evt.run.name=="{!name1}") { link="{!click1}"; } else if (evt.run.name=="{!name2}") { link="{!click2}"; } else if (evt.run.name=="{!name3}") { link="{!click3}"; } if (""!=link) { window.open(link, "_blank"); } } );The bar can be identified based on the run.name attribute of the event and is simply matched up with the associated click parameter. The generated chart is shown below:
Sunday, 2 October 2011
Dojo Charts Part 2 - Bar Charts
Following on from last weeks Pie Chart post, this week I'm presenting an example of a Bar Chart generated via
the Dojo JavaScript library.
As in the previous example, the Visualforce page pulls in the cross domain Dojo build from Google, has a couple of input fields (so that the data can be updated live) and responsibility for the production of the chart is delegated to a custom component:
The labels and series1 properties are simply comma separated lists of values, where the first entry in the series1 is associated with the first entry in labels and so on. The controller converts the labels into a suitable format for use in the JavaScript that sets up the bar chart.
The component takes a number of attributes, not all of which are supplied from the page. As an aside, this is by no means the full functionality available from a Dojo bar chart - there are click throughs, animations and many other features, some of which will be explored in later posts in this series.
Some of the values (minx, maxx) are optional - Dojo will calculate these if you don't provide them. The creation of the chart follows a simple path - create the chart, add a plot, add x and y axis, add the data series and render the chart. In this case the chart is rendered into the div with the id of "simplechart". If multiple charts were required on the same page, this should be passed as an attribute to the component.
The page with the generated chart is shown below:
As before, the chart is live, so updating the values in the Chart Data section and clicking "Update" regenerates the chart with the latest data:
The page, controller and component are available in the Google Code project, along with a number of additional examples. The next post will cover how to generate a stacked chart.
As in the previous example, the Visualforce page pulls in the cross domain Dojo build from Google, has a couple of input fields (so that the data can be updated live) and responsibility for the production of the chart is delegated to a custom component:
<apex:page controller="DojoBarChartController"> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js" djConfig="parseOnLoad: true, modulePaths: { 'dojo': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo', 'dijit': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit', 'dojox': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox' } "> </script> <apex:sectionheader title="Dojo Bar Chart"/> <apex:form > <apex:pageBlock title="Chart Data"> <table> <tr> <td>Labels:</td> <td> <apex:inputText value="{!labels}" size="80"/> </td> </tr> <tr> <td>Values:</td> <td> <apex:inputText value="{!series1}"/> </td> </tr> </table> <apex:commandButton value="Update"/> </apex:pageBlock> </apex:form> <c:DojoBarChart id="barchart" minx="0" stepx="2" labelsy="{!yAxisLabels}" title1="Data" series1="{!series1}" color1="#000" fill1="blue" /> </apex:page>
The labels and series1 properties are simply comma separated lists of values, where the first entry in the series1 is associated with the first entry in labels and so on. The controller converts the labels into a suitable format for use in the JavaScript that sets up the bar chart.
The component takes a number of attributes, not all of which are supplied from the page. As an aside, this is by no means the full functionality available from a Dojo bar chart - there are click throughs, animations and many other features, some of which will be explored in later posts in this series.
<apex:component > <apex:attribute name="minx" description="Minimum X Axis value" type="Integer"/> <apex:attribute name="maxx" description="Maximum X Axis value" type="Integer"/> <apex:attribute name="stepx" description="Step value for X axis" type="Integer"/> <apex:attribute name="labelsy" description="Y Axis Labels" type="String" /> <apex:attribute name="title1" description="Plot series 1 title - displayed in legend" type="String" /> <apex:attribute name="series1" description="Plotext> vertical: true, natural: false}); <apex:outputPanel layout="none" rendered="{!NOT(ISBLANK(title1))}"> chart1.addSeries("{!title1}", [{!series1}], {stroke: {color:"{!color1}"}, fill: "{!fill1}"}); </apex:outputPanel> chart1.render(); <apex:outputText rendered="{!showLegend}"> var legend1 = new dojox.charting.widget.Legend({chart: chart1, horizontal: false}, "legend1"); </apex:outputText> }; dojo.addOnLoadavascript"> dojo.require("dojox.charting.Chart2D"); dojo.require("dojox.charting.widget.Legend"); makeCharts = function(){ var chart1 = new dojox.charting.Chart2D("simplechart", {fill:"transparent"}); chart1.addPlot("default", {type: "Bars", gap:10}); chart1.addAxis("x", { <apex:outputText value="min:{!minx}," rendered="{!NOT(ISBLANK(minx))}"/> <apex:outputText value="max:{!maxx}," rendered="{!NOT(ISBLANK(maxx))}"/> <apex:outputText value="majorTickStep:{!stepx}," rendered="{!NOT(ISBLANK(stepx))}"/> }); chart1.addAxis("y", { <apex:outputText rendered="{!NOT(ISBLANK(labelsy))}"> labels: [ {!labelsy} ], </apex:outputText> vertical: true, natural: false}); <apex:outputPanel layout="none" rendered="{!NOT(ISBLANK(title1))}"> chart1.addSeries("{!title1}", [{!series1}], {stroke: {color:"{!color1}"}, fill: "{!fill1}"}); </apex:outputPanel> chart1.render(); <apex:outputText rendered="{!showLegend}"> var legend1 = new dojox.charting.widget.Legend({chart: chart1, horizontal: false}, "legend1"); </apex:outputText> }; dojo.addOnLoad(makeCharts); </script> <table> <tr> <td> <div id="simplechart" style="width: 800px; height: 300px;"></div> </td> <td style="vertical-align:middle"> <div id="legend1"></div> </td> </tr> </table> </apex:component>
Some of the values (minx, maxx) are optional - Dojo will calculate these if you don't provide them. The creation of the chart follows a simple path - create the chart, add a plot, add x and y axis, add the data series and render the chart. In this case the chart is rendered into the div with the id of "simplechart". If multiple charts were required on the same page, this should be passed as an attribute to the component.
The page with the generated chart is shown below:
As before, the chart is live, so updating the values in the Chart Data section and clicking "Update" regenerates the chart with the latest data:
The page, controller and component are available in the Google Code project, along with a number of additional examples. The next post will cover how to generate a stacked chart.
Wednesday, 21 September 2011
Dojo Charts Part 1 - Pie Charts
Dreamforce and Cloudforce are now over for another year, so its time to blog
some code for a change. This is the first in a short series of posts
detailing how to produce charts in Visualforce using the Dojo Javascript
toolkit.
The real work is done by the dojopiechart component. This takes three sets of parameters, one set per wedge of the pie. For each wedge, the colour (or color, as Dojo is clearly American!), the text to display in the legend and the value for the wedge - the values should add up to 100.
The component code is shown below.
The chart is rendered into the <div> element identified by the divId attribute of the component - this allows for multiple pie charts to appear in the same page. The wedges of the pie are supplied via the chart.addSeries function.
Below is a screenshot of the default page:
As the chart is "live", Changing the values in the Chart Data table and clicking Update generates an updated chart based on the new data:
I've made this available as a Google Code project, and also submitted it as a developer force code share, but it looks like it takes a while to get accepted and added to the list of projects.
I've been using Dojo Charting for around 18 months now on a variety of
projects. I don't propose to go into the minute detail in this post -
there's plenty of information on this at the Sitepen site - a good starting point is: A Beginners Guide to Dojo Charting. This post is concerned with integrating Dojo Charting into
Visualforce pages.
In view of the upcoming Winter 12 release, and the Developer Preview of
Visualforce Charting, I'm sure some people will be wondering if there's any
value to a Javascript charting solution. My view on this is that its
another tool in the box - plus Dojo Charting is available for production use
now.
And so to the code. The "parent" page provides the hooks into a cross
domain build of Dojo hosted by Google. There is always the option to
upload the Dojo library as a static resource into your Salesforce org, but I
prefer not to have to worry about managing this resource across every
Salesforce org. This page also contains sample data that is used to
generate the sample pie chart. The source for the page is shown below:
<apex:page controller="DojoPieChartController"> <script djconfig="parseOnLoad: true, modulePaths: { 'dojo': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo', 'dijit': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit', 'dojox': 'https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojox' } " src="https://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js" type="text/javascript"> </script> <apex:sectionheader title="Dojo Pie Chart"> <apex:form> <apex:pageblock title="Chart Data"> <table> <tbody> <tr> <td>Value</td> <td>Color</td> <td>Text</td> </tr> <tr> <td><apex:inputtext value="{!value1}"> </apex:inputtext></td> <td><apex:inputtext value="{!color1}"> </apex:inputtext></td> <td><apex:inputtext value="{!text1}"> </apex:inputtext></td> </tr> <tr> <td><apex:inputtext value="{!value2}"> </apex:inputtext></td> <td><apex:inputtext value="{!color2}"> </apex:inputtext></td> <td><apex:inputtext value="{!text2}"> </apex:inputtext></td> </tr> <tr> <td><apex:inputtext value="{!value3}"> </apex:inputtext></td> <td><apex:inputtext value="{!color3}"> </apex:inputtext></td> <td><apex:inputtext value="{!text3}"> </apex:inputtext></td> </tr> </tbody> </table> <apex:commandbutton value="Update"> </apex:commandbutton></apex:pageblock> </apex:form> <c:dojopiechart color1="{!color1}" color2="{!color2}" color3="{!color3}" id="piechart" text1="{!text1}" text2="{!text2}" text3="{!text3}" title="Example pie chart" value1="{!value1}" value2="{!value2}" value3="{!value3}"> </c:dojopiechart> </apex:sectionheader> </apex:page>
The real work is done by the dojopiechart component. This takes three sets of parameters, one set per wedge of the pie. For each wedge, the colour (or color, as Dojo is clearly American!), the text to display in the legend and the value for the wedge - the values should add up to 100.
The component code is shown below.
<apex:component > <apex:attribute name="value1" type="String" description="The value for slice 1"/> <apex:attribute name="color1" type="String" description="The colour for slice 1"/> <apex:attribute name="text1" type="String" description="Text to display against slice 1"/> <apex:attribute name="value2" type="String" description="The value for slice 2"/> <apex:attribute name="color2" type="String" description="The colour for slice 2"/> <apex:attribute name="text2" type="String" description="Text to display against slice 2"/> <apex:attribute name="value3" type="String" description="The value for slice 3"/> <apex:attribute name="color3" type="String" description="The colour for slice 3"/> <apex:attribute name="text3" type="String" description="Text to display against slice 3"/> <apex:attribute name="divId" type="String" description="ID of the div to store the chart in" default="piechart"/> <apex:attribute name="title" type="String" description="Title of the chart"/> <script type="text/javascript"> dojo.require("dojox.charting.Chart2D"); dojo.require("dojox.charting.plot2d.Pie"); dojo.require("dojox.charting.widget.Legend"); dojo.require("dojox.charting.themes.MiamiNice"); makeCharts = function(){ var chart=new dojox.charting.Chart2D("{!divId}"); chart.addPlot("default", {type: "Pie", font: "normal normal 11pt Tahoma", fontColor: "black", labelOffset: -40, radius: 80 }); chart.setTheme(dojox.charting.themes.MiamiNice); chart.addSeries("Series A", [ {y: {!value1}, text: "{!text1}", color: "{!color1}"}, {y: {!value2}, text: "{!text2}", color: "{!color2}"}, {y: {!value3}, text: "{!text3}", color: "{!color3}"} ]); chart.render(); }; dojo.addOnLoad(makeCharts); </script> <div style="font-family:Verdana; font-size:14px; font-weight:bold;">{!Title}</div> <div id="{!divId}" style="width: 300px; height: 300px; margin-left:50px;"></div> </apex:component>The dojo.require function calls pull in the Dojo modules that are needed to support generation of the chart. You can learn more about dojo.require at the Sitepen web site.
The chart is rendered into the <div> element identified by the divId attribute of the component - this allows for multiple pie charts to appear in the same page. The wedges of the pie are supplied via the chart.addSeries function.
Below is a screenshot of the default page:
As the chart is "live", Changing the values in the Chart Data table and clicking Update generates an updated chart based on the new data:
I've made this available as a Google Code project, and also submitted it as a developer force code share, but it looks like it takes a while to get accepted and added to the list of projects.
Tuesday, 13 September 2011
Dreamforce Day 3
Another day, another keynote. Excellent seats once again - a couple of rows back from Will.I.Am this time. Given that it's the same ballroom that Metallica played in last night, they've done an amazing job cleaning up and resetting the seats in a few hours. Got rid of the smell of booze too!
The highlight of day 3 for me was the session on Achieving Maximum Scale with Database.com Persistence Options. This turned into a deep dive on the Salesforce database architecture and confirmed (not that I doubted it for a moment) that there are some seriously smart people working for SFDC. Definitely worth checking out the recording/slide deck once available. Unfortunately I couldn't make the Eric Schmidt closing keynote as we were entertaining that night.
It all seems a long time ago now that we are setting up for Cloudforce London. Even worse, it's a whole year to the next Dreamforce rather than the usual 9 months.
The highlight of day 3 for me was the session on Achieving Maximum Scale with Database.com Persistence Options. This turned into a deep dive on the Salesforce database architecture and confirmed (not that I doubted it for a moment) that there are some seriously smart people working for SFDC. Definitely worth checking out the recording/slide deck once available. Unfortunately I couldn't make the Eric Schmidt closing keynote as we were entertaining that night.
It all seems a long time ago now that we are setting up for Cloudforce London. Even worse, it's a whole year to the next Dreamforce rather than the usual 9 months.
Sunday, 11 September 2011
Cloudforce London 2011
My company, BrightGen, is sponsoring Cloudforce London this year, and I'll be manning the stand. As well as showcasing our consultancy work, we'll have details of the some of the products that we've been working on over the last year or so.
So if you are in need of Salesforce assistance or just fancy shooting the breeze, stop by and see us in the expo.
Dreamforce Day 2
As mentioned on the day 1 blog entry, day 2 started with the keynote. Thanks to @ericakuhl, there were excellent seats for the MVPs - Neil Young was four seats ahead, and it doesn't get much better than that. A couple of items from the keynote that really caught my attention were:
1. Customers in private chatter groups. Here at BrightGen we aim to use google docs for collaboration with customers, but have found that a number of customers don't have access to this, even anonymous access. Looking at the pre-release notes there's not much to it - simply create the customer users, mark the group as allowing customers and add them to it. I've just signed up for a pre-release trial so that I can play around with this. I'll post how I get on.
2. Data residency option. This allows sensitive data to be encrypted before it is sent to Salesforce, using software installed inside your firewall. Thus even in the event of the unthinkable happening, and your Salesforce data being compromised, only you have the key to the encrypted data. Obviously there are some downsides to this - searching for short tex snippets is unlikely to be successful, although exact match search should work better, as the search text will be encrypted using the same mechanism as the original record. It will also mean that all connections route through this software, which could get interesting for mobile working. There's also the option of storing data in your own database and putting associated tokens into the Salesforce database, but unfortunately I couldn't find out much more about this option - as soon as I do I'll share!
The rest of the working day was streaming Api for me - an excellent live build from Pat Patterson in the second session.
And so to Metallica - this was the busiest I've seen the Moscone Center - thousands stuck outside trying to get in. Luckily I joined the lines under the street early so didn't miss too much. Nothing like the feeling when your whole body is vibrating to the bass! A short set from Will.I.Am rounded off the evening, ith keynote 2 just a few hours away.
Wednesday, 31 August 2011
Dreamforce Day 1
Dreamforce day 1 was a blast. Partner keynote threw up a few nuggets around certification and investment. I had to dip out of this a little early to attend the Force.com MVP networking lunch. A table chock full of superstars!
Afternoon was session time, including an excellent deep dive into single sign on.
I'm writing this while waiting for the keynote to start - MVP seating is in the VIP section, and I'm about 5 rows back from the action and Marc Benioff has just walked past looking pretty relaxed given there's 14000 excited people here to listen to him.
- Posted using BlogPress from my iPad
Tuesday, 30 August 2011
Dreamforce, baby!
So excited to be in San Francisco for Dreamforce 2011. This is my second
Dreamforce, but my first as an attendee. Last year BrightGen were part of
the Cloud Expo, so I was chained to a stand and trapped in a suit and tie. This
year I'm looking forward to some deep dive sessions and networking with my
fellow MVPs.
Due to late flight booking etc we didn't arrive until 7pm on Monday night after 20+ hours travelling and a hop through LAX. After a quick turn and burn at the hotel, we headed off to Union Square for dinner and within 30 minutes had bumped into about 25 people that we knew! Dreamforce takes over this town.
I'll be attempting some blog entries and twitter updates (along with 40,000+ others no doubt) as the week progresses, so stay tuned.
Due to late flight booking etc we didn't arrive until 7pm on Monday night after 20+ hours travelling and a hop through LAX. After a quick turn and burn at the hotel, we headed off to Union Square for dinner and within 30 minutes had bumped into about 25 people that we knew! Dreamforce takes over this town.
I'll be attempting some blog entries and twitter updates (along with 40,000+ others no doubt) as the week progresses, so stay tuned.
Friday, 26 August 2011
Force.com MVP
This week I was both delighted and honoured in equal measure to be chosen to join the Force.com MVP program as part of the Summer 11 intake. Looking at the other members I'm in some pretty stellar company - breathing the same rare air as the likes of Wes Nolte and Jeff Douglas still seems a little unreal.
The MVP term is one year, after which your contribution is re-assessed to make sure you still deserve your membership. I like this feature as it ensures that everyone stays at the top of their game.
I'd like to take this opportunity to thank all those that nominated me for the MVP program, but special thanks go to Abhi who has been unstinting with his encouragement.
Saturday, 13 August 2011
DML During Initialisation
I ran into a situation recently where I wanted to be able to insert a record as part of the construction of a Visualforce controller, which the user then edited. As we all know, DML is not allowed in the constructor, so I started investigating alternatives.
One mechanism is to create an interim Visualforce page with a page action attribute tied to a controller method that inserts the record and then redirects the user to the actual Visualforce edit page. The downside to this method is that I would end up with two pages and two controllers, as its not safe for this to be incorporated into a single controller due to the fact that order of execution cannot be guaranteed.
An alternative solution, and the route that I chose to go, is to use javascript to invoke an action method on the controller, and have this action method carry out the DML operation. In order to ensure that this doesn't happen every time that the page is refreshed, I have an "initialised" flag in my controller that is set to false in the constructor, and updated to true once the DML has taken place.
The controller is shown below:
On the page side, the main change was to put out a please wait spinner while the initialisation was going on, so that the user wasn't tempted to start entering data while the initialisation was taking place. The DML operation is executed via an actionfunction tied to the onload handler, and once this completes the main body of the page is rerendered, replacing the spinner with the newly inserted record detail (including the id of the account).
Note that it this will overwrite any existing onload handler - for details of how to add a function to the end of the existing onload handler see this blog post.
The page markup is shown below:
One mechanism is to create an interim Visualforce page with a page action attribute tied to a controller method that inserts the record and then redirects the user to the actual Visualforce edit page. The downside to this method is that I would end up with two pages and two controllers, as its not safe for this to be incorporated into a single controller due to the fact that order of execution cannot be guaranteed.
An alternative solution, and the route that I chose to go, is to use javascript to invoke an action method on the controller, and have this action method carry out the DML operation. In order to ensure that this doesn't happen every time that the page is refreshed, I have an "initialised" flag in my controller that is set to false in the constructor, and updated to true once the DML has taken place.
The controller is shown below:
public class InsertObjectController { public Boolean initialised{get; set;} public Account acc {get; set;} public InsertObjectController() { initialised=false; } public void init() { if (!initialised) { acc=new Account(); acc.Name='Blog Account'; insert acc; initialised=true; } } }
On the page side, the main change was to put out a please wait spinner while the initialisation was going on, so that the user wasn't tempted to start entering data while the initialisation was taking place. The DML operation is executed via an actionfunction tied to the onload handler, and once this completes the main body of the page is rerendered, replacing the spinner with the newly inserted record detail (including the id of the account).
Note that it this will overwrite any existing onload handler - for details of how to add a function to the end of the existing onload handler see this blog post.
The page markup is shown below:
<apex:page controller="InsertObjectController"> <apex:form > <apex:actionFunction name="doInit" action="{!init}" rerender="allPanel"/> <apex:outputPanel id="allPanel"> <apex:outputPanel rendered="{!NOT(initialised)}"> <p align="center" style='{font-family:"Arial", Helvetica, sans-serif; font-size:20px;}'><apex:image value="/img/loading.gif"/> Please wait</p> <script> window.onload=function() { doInit(); }; </script> </apex:outputPanel> <apex:outputPanel rendered="{!initialised}"> <apex:pageBlock title="Account Details"> <apex:pageBlockSection > <apex:inputField value="{!acc.id}"/> <apex:inputField value="{!acc.name}"/> <apex:inputField value="{!acc.type}"/> <apex:inputField value="{!acc.industry}"/> </apex:pageBlockSection> </apex:pageBlock> </apex:outputPanel> </apex:outputPanel> </apex:form> </apex:page>
Saturday, 6 August 2011
Second London Force.com Developer Meetup / Deploying
This week saw the second London Force.com Developer Meetup at the Skills Matter
eXchange, with a three handed talk on Team Development Strategies on the
Force.com Platform from Wes Nolte, Anup Jadhav and Laurent Delcambre. This
time the meetup was captured on video - you can view it at: http://skillsmatter.com/podcast/nosql/team-development-strategies. There were also prizes on offer for answering snap questions from the
presenters - I managed to come away with a Ruby on Rails book and a
Salesforce.com T-Shirt.
One of the more interesting topics of discussion for me was around deploying changes between sandbox and production. Like many Force.com developers I suspect, all my early deployments were carried out using the Force.com IDE, highlighting the elements I wanted to deploy, right-clicking and going through the deploy to server wizard. This gives a nice visual indicator of the changes that will be made on the target system:
However, there are a couple of downsides to this:
The next meetup is planned for October/November time - I'm looking forward to it already.
One of the more interesting topics of discussion for me was around deploying changes between sandbox and production. Like many Force.com developers I suspect, all my early deployments were carried out using the Force.com IDE, highlighting the elements I wanted to deploy, right-clicking and going through the deploy to server wizard. This gives a nice visual indicator of the changes that will be made on the target system:
However, there are a couple of downsides to this:
- If the deployment fails, you have to start over from scratch. Not too bad if there's only a couple of files like my example deployment above, but a bit tedious once you get into double figures.
- The changes aren't repeatable. If I want to deploy those same files to another org at a later date, that's only possible if no additional changes have been made.
For these reasons I tend to use change sets these days, although these only
work between connected organizations (e.g. sandbox to production or between
sandboxes associated with the same production org).
Change sets are persistent, so when working on a piece of development you can
add the new/changed elements as they are created. Once you are ready to
deploy, you upload the change set to the target org which locks the elements
in their current form. This means that if you need to deploy to a
separate org (a training sandbox for example), it doesn't matter when you do
this, the change set will be in exactly the same state as when it was first
uploaded. I've found this aspect to be particularly useful when I have a
large amount of changes to deploy to production - I can test out deploying to
a pre-production sandbox and once I've ironed out the issues, I can simply
"replay" the change set into production.
If the deployment of a change set fails, you can simply clone the change set,
fix the problem and then upload it again. An important point to note is
that when you clone the change set, you get the latest version of each
of the elements in the new change set.
Change sets aren't a silver bullet though - being limited to connected org
means that you must be developing in a sandbox to use them. If you are
working in a Developer Edition, they won't be available. They also tend
to be slower than using the IDE - something that can become important when you
have to wait 30 minutes for the latest attempt at the change set to be
uploaded at 1am in the morning. Less of a regular issue, but still an
issue nonetheless, is that they tend to be fragile around Salesforce.com
releases - sometimes the upload takes a couple of days to become available,
while at other times it doesn't make its way to the target org at all.
I'd imagine this is down to the source and target orgs being at
different versions (e.g. sandbox at Summer 11 but production at Spring 11).
Where possible, we always look to avoid major deployments to production
in the run up to these upgrade windows.
The next meetup is planned for October/November time - I'm looking forward to it already.
Friday, 22 July 2011
Managing a list of New Records in Visualforce
This week's post concerns bulk creation of an unknown number of sobject records
- Accounts, for example. In this situation I want to display a table containing
a set number of new records (5 in this case), enter the various pieces of
information and save to the database en-masse. However, I also want to be
able to add or remove rows prior to saving anything, so that if I need to I can
enter 1, 3, 10 etc at once. I promise this is the last post for a while
around list handling!
As the records haven't been added to the database yet, they don't have a unique identifier that I can use to locate them, so the first task is to create a wrapper class that encapsulates an Account and an identifier - I decided on an Integer.
As I'm only concerned with the uniqueness of the AccountWrapper within the context of my page, generating a unique identifier is as simple as defining a property in my controller:
and then post-incrementing this each time I create a new wrapper instance. In my constructor I create the initial five records:
In my page, I have an apex:pageBlockTable backed by the wrappers list. Each row of the table has a Delete button that if clicked, removes the entry from the list:
The unique identifier is passed to the controller via the apex:param component. If this is the first time that you've encountered this component, you may be interested in another blog post of mine explaining its use in detail.
The action method associated with the delete simply iterates the list of wrappers and deletes the entry associated with the supplied identifier:
The page also contains a couple more buttons, for adding one row, adding 5 rows and saving to the database.
Note that I'm using the apex:param technique for both the Add Row and Add 5 Rows buttons - this means I can have additional buttons for any number of rows without having to change the Apex code.
The full controller is shown below:
The only additional item of interest in the controller is the returned PageReference from the save method:
This sends the user to the Accounts tab. I originally had the three character key prefix hardcoded, but that always feels wrong. Thus, while its highly unlikely that Salesforce will change the prefix, if they do I'm covered.
Here's some screenshots of the page in action. First the initial page:
After I've clicked the Add 5 Rows Button
After I've deleted some entries at random:
And finally, after saving the records they are displayed in the recent list on the Accounts tab:
As the records haven't been added to the database yet, they don't have a unique identifier that I can use to locate them, so the first task is to create a wrapper class that encapsulates an Account and an identifier - I decided on an Integer.
public class AccountWrapper { public Account acc {get; private set;} public Integer ident {get; private set;} public AccountWrapper(Integer inIdent) { ident=inIdent; acc=new Account(Name='Bulk Acc ' + ident); } }Note that the wrapper class provides a useful constructor, that creates the wrapped record and automatically populates the Name of the Account based on the unique identifier. I added this as the Name is a required field when used as the value for an apex:inputfield in Visualforce, so this saved me some time when testing. I could just as easily have changed the component to an apex:inputText to work around this.
As I'm only concerned with the uniqueness of the AccountWrapper within the context of my page, generating a unique identifier is as simple as defining a property in my controller:
private Integer nextIdent=0;
and then post-incrementing this each time I create a new wrapper instance. In my constructor I create the initial five records:
wrappers=new List<AccountWrapper>(); for (Integer idx=0; idx<5; idx++) { wrappers.add(new AccountWrapper(nextIdent++)); }
In my page, I have an apex:pageBlockTable backed by the wrappers list. Each row of the table has a Delete button that if clicked, removes the entry from the list:
<apex:column headerValue="Action"> <apex:commandButton value="Delete" action="{!delWrapper}" rerender="wtable"> <apex:param name="toDelIdent" value="{!wrapper.ident}" assignTo="{!toDelIdent}"/> </apex:commandButton> </apex:column>
The unique identifier is passed to the controller via the apex:param component. If this is the first time that you've encountered this component, you may be interested in another blog post of mine explaining its use in detail
The action method associated with the delete simply iterates the list of wrappers and deletes the entry associated with the supplied identifier:
public void delWrapper() { Integer toDelPos=-1; for (Integer idx=0; idx<wrappers.size(); idx++) { if (wrappers[idx].ident==toDelIdent) { toDelPos=idx; } } if (-1!=toDelPos) { wrappers.remove(toDelPos); } }
The page also contains a couple more buttons, for adding one row, adding 5 rows and saving to the database.
<apex:page controller="ManageListController" tabstyle="Account"> <apex:form > <apex:pageBlock title="Bulk Account Create"> <apex:pageBlockTable value="{!wrappers}" var="wrapper" id="wtable"> <apex:column headerValue="Ident"> <apex:outputText value="{!wrapper.ident}"/> </apex:column> <apex:column headerValue="Name"> <apex:inputField value="{!wrapper.acc.Name}"/> </apex:column> <apex:column headerValue="Parent"> <apex:inputField value="{!wrapper.acc.ParentId}"/> </apex:column> <apex:column headerValue="Industry"> <apex:inputField value="{!wrapper.acc.Industry}"/> </apex:column> <apex:column headerValue="Type"> <apex:inputField value="{!wrapper.acc.Type}"/> </apex:column> <apex:column headerValue="Action"> <apex:commandButton value="Delete" action="{!delWrapper}" rerender="wtable"> <apex:param name="toDelIdent" value="{!wrapper.ident}" assignTo="{!toDelIdent}"/> </apex:commandButton> </apex:column> </apex:pageBlockTable> <apex:commandButton value="Add Row" action="{!addRows}" rerender="wtable"> <apex:param name="addCount" value="1" assignTo="{!addCount}"/> </apex:commandButton> <apex:commandButton value="Add 5 Rows" action="{!addRows}" rerender="wtable"> <apex:param name="addCount" value="5" assignTo="{!addCount}"/> </apex:commandButton> <apex:commandButton value="Save" action="{!save}"/> </apex:pageBlock> </apex:form> </apex:page>
Note that I'm using the apex:param technique for both the Add Row and Add 5 Rows buttons - this means I can have additional buttons for any number of rows without having to change the Apex code.
The full controller is shown below:
public class ManageListController { public List<AccountWrapper> wrappers {get; set;} public static Integer toDelIdent {get; set;} public static Integer addCount {get; set;} private Integer nextIdent=0; public ManageListController() { wrappers=new List<AccountWrapper>(); for (Integer idx=0; idx<5; idx++) { wrappers.add(new AccountWrapper(nextIdent++)); } } public void delWrapper() { Integer toDelPos=-1; for (Integer idx=0; idx<wrappers.size(); idx++) { if (wrappers[idx].ident==toDelIdent) { toDelPos=idx; } } if (-1!=toDelPos) { wrappers.remove(toDelPos); } } public void addRows() { for (Integer idx=0; idx<addCount; idx++) { wrappers.add(new AccountWrapper(nextIdent++)); } } public PageReference save() { List<Account> accs=new List<Account>(); for (AccountWrapper wrap : wrappers) { accs.add(wrap.acc); } insert accs; return new PageReference('/' + Schema.getGlobalDescribe().get('Account').getDescribe().getKeyPrefix() + '/o'); } public class AccountWrapper { public Account acc {get; private set;} public Integer ident {get; private set;} public AccountWrapper(Integer inIdent) { ident=inIdent; acc=new Account(Name='Bulk Acc ' + ident); } } }
The only additional item of interest in the controller is the returned PageReference from the save method:
return new PageReference('/' + Schema.getGlobalDescribe().get('Account').getDescribe().getKeyPrefix() + '/o');
This sends the user to the Accounts tab. I originally had the three character key prefix hardcoded, but that always feels wrong. Thus, while its highly unlikely that Salesforce will change the prefix, if they do I'm covered.
Here's some screenshots of the page in action. First the initial page:
After I've clicked the Add 5 Rows Button
After I've deleted some entries at random:
And finally, after saving the records they are displayed in the recent list on the Accounts tab:
Saturday, 9 July 2011
Passing Parameters to Apex Method from Visualforce Page
A question that often crops up when developers are introduced to Visualforce is
"how can I pass a parameter to a controller method?".
For example, consider the following page. This outputs the details of up to 10 contacts associated with an account:
The controller is as follows - note that I retrieve the contacts and store them in a list rather than relying on the Account.Contacts related list - there is a good reason for this and it will (hopefully) become clear later on:
The output of the page is shown below:
Next up I want to add a column that allows various actions to be performed on the particular contact instance. Its tempting to think that I can just add the following to the markup (assuming I have an appropriate method in the controller) and all will be well:
However, this refuses to compile with an error that delCont is an unknown function. As controller action methods can't take parameters in this way, the compiler assumes that this is a formula function, but no function with that name exists, hence the error.
So, how can we pass the id of the contact to the controller? The answer is straightforward, but requires a slight mindshift.
The <apex:param> standard component allows parameters to be defined for a parent component, which can be assigned to a controller property. I can therefore change my markup to the following to specify the parameter:
Note the rerender attribute of the commandbutton - if this attribute is not present, the parameter does not get sent back to the controller. I simply have an outputpanel around the entire page contents.
I then add the following property to my controller:
The key to this is the behaviour when the button is clicked - the id of the contact instance is assigned to my controller property before the action method is invoked. Thus the method that is actually carrying out the delete can simply access the contIdChosen property secure in the knowledge that it will contain the id of the contact associated with the button:
The fact that I have to rerender in order to get the parameter passing to work is the reason why I maintain the list of Contacts independent of the Account record from the standard controller - I can't make the browser refresh the page and thus rebuild the related contacts from scratch, so I have to rebuild the list server side.
For example, consider the following page. This outputs the details of up to 10 contacts associated with an account:
<apex:page standardController="Account" extensions="ParamBlogController"> <apex:outputPanel id="all"> <apex:form> <apex:pageBlock title="Account Detail"> <apex:pageBlockSection title="Account"> <apex:outputField value="{!Account.Name}"/> <apex:outputField value="{!Account.Description}"/> </apex:pageBlockSection> <apex:pageBlockSection title="Contacts" columns="1"> <apex:pageBlockTable value="{!conts}" var="cont" rows="10"> <apex:column value="{!cont.Id}"/> <apex:column value="{!cont.Name}"/> <apex:column value="{!cont.Email}"/> <apex:column value="{!cont.Phone}"/> </apex:pageBlockTable> </apex:pageBlockSection> </apex:pageBlock> </apex:form> </apex:outputPanel> </apex:page>
The controller is as follows - note that I retrieve the contacts and store them in a list rather than relying on the Account.Contacts related list - there is a good reason for this and it will (hopefully) become clear later on:
public class ParamBlogController { private ApexPages.StandardController stdCtrl {get; set;} public List<Contact> conts {get; set;} public ParamBlogController(ApexPages.StandardController std) { stdCtrl=std; setupContacts(); } private void setupContacts() { conts=[select id, Name, Email, Phone from Contact where AccountId=:stdCtrl.getId()]; } }
The output of the page is shown below:
Next up I want to add a column that allows various actions to be performed on the particular contact instance. Its tempting to think that I can just add the following to the markup (assuming I have an appropriate method in the controller) and all will be well:
<apex:column headerValue="Action"> <apex:commandButton value="Del" action="{!delCont(cont.id)}"/> </apex:column>
However, this refuses to compile with an error that delCont is an unknown function. As controller action methods can't take parameters in this way, the compiler assumes that this is a formula function, but no function with that name exists, hence the error.
So, how can we pass the id of the contact to the controller? The answer is straightforward, but requires a slight mindshift.
The <apex:param> standard component allows parameters to be defined for a parent component, which can be assigned to a controller property. I can therefore change my markup to the following to specify the parameter:
<apex:commandButton value="Del" action="{!delCont}" rerender="all"> <apex:param name="contIdParam" value="{!cont.id}" assignTo="{!contIdChosen}"/> </apex:commandButton>
Note the rerender attribute of the commandbutton - if this attribute is not present, the parameter does not get sent back to the controller. I simply have an outputpanel around the entire page contents.
I then add the following property to my controller:
public String contIdChosen {get; set;}
The key to this is the behaviour when the button is clicked - the id of the contact instance is assigned to my controller property before the action method is invoked. Thus the method that is actually carrying out the delete can simply access the contIdChosen property secure in the knowledge that it will contain the id of the contact associated with the button:
public PageReference delCont() { Contact toDel=new Contact(id=contIdChosen); delete todel; setupContacts(); return null; }
The fact that I have to rerender in order to get the parameter passing to work is the reason why I maintain the list of Contacts independent of the Account record from the standard controller - I can't make the browser refresh the page and thus rebuild the related contacts from scratch, so I have to rebuild the list server side.
Friday, 1 July 2011
Trust Salesforce Notifications with Blogtrottr
In addition to being a Salesforce implementation and ISV partner, my employers
(BrightGen) provide a managed service offering in the Salesforce space. As
part of this, we are always looking to keep on top of the current Salesforce
status so that we are aware of problems before our customers are affected.
A couple of releases ago, Salesforce added RSS feeds to the
trust.salesforce.com site, which provides a feed of status updates on a
per-instance basis. Useful information, but we needed a way to monitor this
information in an unattended fashion and receive updates when something
important happened.
A few googles later, I came across Blogtrottr. This is an online service where you supply the URL of an RSS feed and
an email address, and you are mailed feed updates as and when they are
published.
This means that the next time an issue occurs on Salesforce, the information
arrives in my mailbox without any effort on my part:
Saturday, 25 June 2011
London Force.com Developer Meetup
This Thursday saw the inaugural London Force.com Developer Meetup, kindly sponsored by Tquila and organized by Wes Nolte, a name familiar to most readers of this blog I'm sure. After years of reading about developer meetups all over the US, we finally got to have one of our own, and to top it all Jeff Douglas was attending.
There were three short talks - an overview of Talend, some thoughts on how to improve appplication development and a lively debate around whether Dynamic Visualforce Components are the Devil's work or a useful addition to the developer's tools.
There was also plenty of opportunity for networking and a chance to finally meet many people that I've had online exchanges with in the past.
Here's a picture from the event (courtesy of Ritesh Aswaney): L-R Bob Buzzard, Jeff Douglas, Wes Nolte:
The next meetup is scheduled for August 2nd.
There were three short talks - an overview of Talend, some thoughts on how to improve appplication development and a lively debate around whether Dynamic Visualforce Components are the Devil's work or a useful addition to the developer's tools.
There was also plenty of opportunity for networking and a chance to finally meet many people that I've had online exchanges with in the past.
Here's a picture from the event (courtesy of Ritesh Aswaney): L-R Bob Buzzard, Jeff Douglas, Wes Nolte:
The next meetup is scheduled for August 2nd.