Using the Same Visualforce Page for both Browser and Salesforce1 Clients
- Posted in:
- visualforce
- salesforce
You have probably come up with a scenario where you need to create a custom Visualforce page for a custom object and have that page available for both the browser and the Salesforce1 application. Right now there’s no easy way to tell Salesforce to use a specific page for the browser version and another different page for SF1. Let’s focus the problem from the UI/UX perspective: how to leverage the UI and best practices associated to each specific platform. So basically, if we use the Visualforce tags to build for the browser then the SF1 experience will not be the desired one (it will actually be very ugly!), and if we decide to not use Visualforce tags and instead use pure HTML5 (as recommended by the Salesforce1 practices) then the page will look good in SF1 but for browser users it will look different. We want to create a Visualforce page that will change its look depending on the client used to display it. Here’s an example
On the image you can see how the UI for each client is maintained. Also notice that in the case of SF1 we are using the standard publisher buttons to work with the page. Same page, same controller, but different UI and UX depending on the client. How we achieve this? Read on.
An Example
Let’s use an example to illustrate the problem. In our example we’re developing a questionnaire application: users are able to create questionnaires which will have many questions associated. This is an example of a questionnaire:
The questionnaire could be associated to any object, and we want to be able to answer a specific questionnaire by using both the browser client and the Salesforce1 app. Since the number of questions may vary, we need to build a Visualforce page that dynamically displays the questions associated to a specific questionnaire (we could not do this with a standard layout). Our Visualforce page will have a controller extension associated that will drive the logic of the page. We will associate a questionnaire to a Contact for this example.
Here is our controller extension:
public with sharing class ceContactQuestionnaire { private final ApexPages.StandardController controller; private final Contact contactRecord; public ceContactQuestionnaire(ApexPages.StandardController controller) { this.controller = controller; controller.addFields(new String[] {sObjectType.Contact.fields.Questionnaire__c.Name}); this.contactRecord = (Contact)controller.getRecord(); } public String questionnaireName { get { if(questionnaireName == null && contactRecord.Questionnaire__c != null) { List<Questionnaire__c> q = [select Name from Questionnaire__c where Id = :contactRecord.Questionnaire__c]; if(q.size() > 0) questionnaireName = q[0].Name; } return questionnaireName; } private set { questionnaireName = value; } } public List<Contact_Question__c> questions { get { if(questions == null) { questions = new List<Contact_Question__c>(); for(Contact_Question__c question : [select Id,Question__c,Answer__c from Contact_Question__c where Contact__c = :contactRecord.Id order by Order__c]) { questions.add(question); } } return questions; } set { questions = value; } } public PageReference save() { upsert questions; return controller.save(); } }
You can see is actually a pretty simple controller: the contact has a questionnaire associated and an auxiliary object with all the questions for the questionnaire. We get those questions and we make it available as a property. The extension also overrides the save method to save the answers given to those questions.
Let’s first design the page by using the Visualforce tags that we’ll use on a browser version of the page:
<apex:page standardController="Contact" extensions="ceContactQuestionnaire" > <apex:pageMessages /> <apex:sectionHeader title="{!$ObjectType.Questionnaire__c.Label}: {!questionnaireName}" subtitle="{!Contact.Name}" /> <apex:form > <apex:pageBlock > <apex:pageBlockButtons > <apex:commandButton action="{!save}" value="Save" styleClass="btn btn-default" /> <apex:commandButton action="{!cancel}" value="Cancel" styleClass="btn btn-default" /> </apex:pageBlockButtons> <apex:pageBlockSection columns="1"> <apex:repeat var="question" value="{!questions}"> <apex:pageBlockSectionItem > <apex:outputLabel value="{!question.Question__c}" /> <apex:inputTextarea cols="80" rows="3" value="{!question.Answer__c}"/> </apex:pageBlockSectionItem> </apex:repeat> </apex:pageBlockSection> </apex:pageBlock> </apex:form> </apex:page>
This is a pretty simple Visualforce page, we just render each question we get from the controller as a label for the question and a text area for the answer. This is how it looks on the browser:
No surprises here: we’re using the familiar UI and look&feel of a Salesforce layout. Let’s make our Visualforce page available for SF1 (edit the page and select the option “Available for Salesforce mobile apps”) so we can open the page in SF1. You’ll notice that the UI and look&feel are pretty ugly:
If we present mobile users with this layout they will probably complain about it and not feel happy. So how do we fix this?
Determine what Client is Being Used
We need a way to know which client is using our Visualforce page so we can present the appropriate content and user experience. There is no official way to know this, but there are some tricks. Here I will use the parameters of the page to know whether I’m in a browser or in the SF1 app. We need a property in the controller extension:
public Boolean isSF1 { get { if(String.isNotBlank(ApexPages.currentPage().getParameters().get('sfdcIFrameHost')) || String.isNotBlank(ApexPages.currentPage().getParameters().get('sfdcIFrameOrigin')) || ApexPages.currentPage().getParameters().get('isdtp') == 'p1' || (ApexPages.currentPage().getParameters().get('retURL') != null && ApexPages.currentPage().getParameters().get('retURL').contains('projectone') ) ) { return true; }else{ return false; } } }
I will use this property to dynamically adjust the UI of the Visualforce page.
One Page for All
For the UI I will use a special version of Bootstrap for Salesforce1, which I have added as a static resource. This is how the page looks now:
<apex:page standardController="Contact" extensions="ceContactQuestionnaire" docType="html-5.0" tabStyle="Questionnaire__c"> <apex:stylesheet value="{!URLFOR($Resource.Bootstrap_SF1,'/css/bootstrap-namespaced.min.css')}"/> <apex:outputPanel rendered="{!!isSF1}"> <apex:pageMessages /> <apex:sectionHeader title="{!$ObjectType.Questionnaire__c.Label}: {!questionnaireName}" subtitle="{!Contact.Name}" /> <apex:form > <apex:pageBlock > <apex:pageBlockButtons > <apex:commandButton action="{!save}" value="Save" styleClass="btn btn-default" /> <apex:commandButton action="{!cancel}" value="Cancel" styleClass="btn btn-default" /> </apex:pageBlockButtons> <apex:pageBlockSection columns="1"> <apex:repeat var="question" value="{!questions}"> <apex:pageBlockSectionItem > <apex:outputLabel value="{!question.Question__c}" /> <apex:inputTextarea cols="80" rows="3" value="{!question.Answer__c}"/> </apex:pageBlockSectionItem> </apex:repeat> </apex:pageBlockSection> </apex:pageBlock> </apex:form> </apex:outputPanel> <apex:outputPanel rendered="{!isSF1}"> <div class="bootstrap" style="padding: 10px;"> <h1>{!$ObjectType.Questionnaire__c.Label}: {!questionnaireName} <small>{!Contact.Name}</small></h1> <apex:form > <apex:repeat var="question" value="{!questions}"> <div class="form-group"> <label>{!question.Question__c}</label> <apex:inputTextarea value="{!question.Answer__c}" rows="3" cols="80" styleClass="form-control"/> </div> </apex:repeat> <span id="submit"><apex:commandButton action="{!save}" value="Save" styleClass="btn btn-default" /></span> <span id="cancel"><apex:commandButton action="{!cancel}" value="Cancel" styleClass="btn btn-default" /></span> </apex:form> </div> </apex:outputPanel> </apex:page>
Notice on line 2 how I use the static resource for the Bootstrap for SF1 CSS library. Lines 4 and 25 create two outputPanels that will be rendered based on the property we created in the controller extension to determine if the page is being called from SF1. The first output panel will be rendered for the browser client, and the code and layout is the same we had before. The new part is the second output panel, which will be rendered for the SF1 app. Notice that the code for the SF1 layout doesn’t use page blocks or any of the standard layout tags, but instead uses standard HTML div tags to layout the content. Now the page in the browser still looks as before, but on SF1 it now looks like this:
Definitely better isn’t? But there’s still something that doesn’t fit right into the SF1 experience: the publisher bar. Notice that we still have two buttons for saving and canceling, and that save button on the publisher bar appears disabled. We need to take advantage of the publisher bar and provide a seamless user experience to our SF1 users.
Taking Care of the Publisher Bar
Using the publisher bar is pretty easy once you know what to do, of course. Here is the modified section of the page for the SF1 part:
<apex:outputPanel rendered="{!isSF1}"> <div class="bootstrap" style="padding: 10px;"> <h1>{!$ObjectType.Questionnaire__c.Label}: {!questionnaireName} <small>{!Contact.Name}</small></h1> <apex:form > <apex:repeat var="question" value="{!questions}"> <div class="form-group"> <label>{!question.Question__c}</label> <apex:inputTextarea value="{!question.Answer__c}" rows="3" cols="80" styleClass="form-control"/> </div> </apex:repeat> <apex:actionFunction action="{!save}" name="saveForm" /> </apex:form> </div> <script src='/canvas/sdk/js/publisher.js'></script> <script> if(sforce.one) { Sfdc.canvas.publisher.subscribe({name: "publisher.showPanel", onData:function(e) { Sfdc.canvas.publisher.publish({name: "publisher.setValidForSubmit", payload: "true"}); }}); Sfdc.canvas.publisher.subscribe({name: "publisher.post", onData:function(e) { saveForm(); Sfdc.canvas.publisher.publish( { name: "publisher.close", payload:{refresh:"false"}}); }}); } </script> </apex:outputPanel>
To work with the publisher bar we need to reference the publisher API (this is provided by the Salesforce platform). You can see we add a reference to the library in line 15. In line 18 we use the sforce.one property to know whether we are in SF1 or not (although this section will not be rendered if we’re not in SF1, it is a good practice to put this check). Notice that we removed the two buttons we had in the previous version for the save and cancel, as we don’t need them anymore since we will use the buttons on the publisher bar instead.
The publisher API works with a publisher/subscriber pattern: it exposes some events that we could subscribe to, or publish, as needed. In line 19 we subscribe to the publisher.showPanel event, which will get fired when the page is opened and finished rendering, in which case we need to enable the save button on the bar. We can tell the publisher API to enable the save button by firing the publisher.setValidForSubmit event.
In line 22 we subscribe to the publisher.post event, which gets fired when the user taps on the save button on the publisher bar. In this case we need to save the form, by calling the action function defined in line 11, which will call the save action on the controller extension. And finally we publish the publisher.close event to notify the API to dismiss the current page.
This is how the SF1 page looks now with the publisher bar enabled:
Users can now answer questions and when done tap on the save button to save their changes, and this will all work with the UI/UX of a SF1 application.