How We Built the Private AppExchange App (Apex, Visualforce, RWD)
-
Upload
salesforce-developers -
Category
Technology
-
view
114 -
download
0
description
Transcript of How We Built the Private AppExchange App (Apex, Visualforce, RWD)
![Page 1: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/1.jpg)
How we built the Private AppExchange appHow we built the Private AppExchange appUsing Apex, VisualForce and RWD
Pratima NambiarTech Lead
AppExchange & Communities
Jochem GeerdinkProduct Designer
AppExchange & Communities
![Page 2: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/2.jpg)
Safe harborSafe harbor statement under the Private Securities Litigation Reform Act of 1995: This presentation may contain forward-looking statements that involve risks, uncertainties, and assumptions. If any such uncertainties materialize or if any of the assumptions proves incorrect, the results of salesforce.com, inc. could differ materially from the results expressed or implied by the forward-looking statements we make. All statements other than statements of historical fact could be deemed forward-looking, including any projections of product or service availability, subscriber growth, earnings, revenues, or other financial items and any statements regarding strategies or plans of management for future operations, statements of belief, any statements concerning new, planned, or upgraded services or technology developments and customer contracts or use of our services. The risks and uncertainties referred to above include – but are not limited to – risks associated with developing and delivering new functionality for our service, new products and services, our new business model, our past operating losses, possible fluctuations in our operating results and rate of growth, interruptions or delays in our Web hosting, breach of our security measures, the outcome of any litigation, risks associated with completed and any possible mergers and acquisitions, the immature market in which we operate, our relatively limited operating history, our ability to expand, retain, and motivate our employees and manage our growth, new releases of our service and successful customer deployment, our limited history reselling non-salesforce.com products, and utilization and selling to larger enterprise customers. Further information on potential factors that could affect the financial results of salesforce.com, inc. is included in our annual report on Form 10-K for the most recent fiscal year and in our quarterly report on Form 10-Q for the most recent fiscal quarter. These documents and others containing important disclosures are available on the SEC Filings section of the Investor Information section of our Web site. Any unreleased services or features referenced in this or other presentations, press releases or public statements are not currently available and may not be delivered on time or at all. Customers who purchase our services should make the purchase decisions based upon features that are currently available. Salesforce.com, inc. assumes no obligation and does not intend to update these forward-looking statements.
![Page 3: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/3.jpg)
Agenda
Intro
UI and RWD
Technical Deep Dive
![Page 4: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/4.jpg)
Our Team
AppExchange
https://appexchange.salesforce.com/
Success Community
https://success.salesforce.com/
![Page 5: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/5.jpg)
What is the Private AppExchange?
Private AppExchange is a private, corporate app store that
enables companies to distribute apps to their employees.
Closed ecosystem
Secure and easy access to apps
Web and mobile apps
![Page 6: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/6.jpg)
![Page 7: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/7.jpg)
![Page 8: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/8.jpg)
![Page 9: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/9.jpg)
Requirements
Desktop, tablet, phone
Professional, custom look & feel
Built using Apex and VisualForce
Managed Package
![Page 10: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/10.jpg)
UI – User Interface Design
Specs
IA – UX
VisD
HTML Mockups
![Page 11: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/11.jpg)
Mobile Technologies Considered
Native apps
Mobile version of the application
Web application using
Responsive Web Design
![Page 12: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/12.jpg)
Definition RWD
Responsive Web Design (RWD) is an approach to web design
in which a site is crafted to provide an optimal viewing
experience—easy reading and navigation with a minimum of
resizing, panning, and scrolling—across a wide range of devices
(from desktop computer monitors to mobile phones).
In short: the site should be usable on all devices and should feel
optimized for all devices.
![Page 13: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/13.jpg)
How to RWD? Media Queries!
Media queries allow the page to use different CSS styles based
on device capabilities.
For RWD, we will mostly look at browser width.
![Page 14: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/14.jpg)
Media queries code structure
![Page 15: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/15.jpg)
RWD and Images
Use background images when possible
Lazy loading for better page performance
![Page 16: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/16.jpg)
Use background images in sprites
Very useful for icons
Think about HD displays (Retina)
![Page 17: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/17.jpg)
Code example background images
<div class="msg-success">
<div class="msg-icon"></div>
<p>The store is online.</p>
</div>
![Page 18: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/18.jpg)
Lazy loading of images for better page performance
![Page 19: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/19.jpg)
Lazy loading of images for better page performance
<span id="phone-test"></span>
<span id="small-test"></span>
<span id="large-test"></span>
#phone-test, #small-test, #large-test {
width: 1px;
height: 1px;
display: none;
}
![Page 20: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/20.jpg)
Lazy loading of images for better page performance
getCurrentSiteState = function() {
var state = 'medium';
if (jQuery('#phone-test').css('display') === 'block') { state = 'phone'; }
else if (jQuery('#small-test').css('display') === 'block') { state = 'small'; }
else if (jQuery('#large-test').css('display') === 'block') { state = 'large'; }
return state;
};
![Page 21: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/21.jpg)
Lazy loading of images for better page performance
![Page 22: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/22.jpg)
![Page 23: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/23.jpg)
Tile - Example<apex:component id="tile" >
<apex:attribute name="tData" description="Data object" type="TileData" required="true" />
<div class="df-tile">
<div class="tile-img tile-img-brand">
<img src="{!$Resource.uilib}/img/p.gif" data-src="{!tData.bigImgURL}" class="desktop-img" />
</div>
<div class="tile-img tile-img-logo">
<a href="#"><img src="{!tData.imgURL}" /></a>
</div>
<div class="txt-primary">
<a href="#"><apex:outputText value="{!tData.description}" /></a>
</div>
</div>
</apex:component>
![Page 24: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/24.jpg)
![Page 25: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/25.jpg)
Search Framework Objectives Keyword Search
• Relevant keyword search results for all objects
Filtering• Ability to add filters easily to quickly meet requirements
Sorting• Ability to add sort options easily to quickly meeting requirements
![Page 26: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/26.jpg)
Keyword Search - SOSL Advantages
• Allows you to search in text, phone and email fields in multiple objects with one
simple query
Limitations• SOSL searches within all text fields and no one field or set of fields can be given
more importance
![Page 27: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/27.jpg)
Keyword Search – Our Solution
Our solution uses a combination of SOQL and SOSL Example Listing object
Field Type
Name Text
Features Text
Short Description TextArea
Long Description LongTextArea
……
![Page 28: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/28.jpg)
Keyword Search – Our Solution
Group fields and assign a score to each group
Field Group Score
Name 10
Features, Short Description 5
Long Description 2
![Page 29: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/29.jpg)
Keyword Search – Our Solution Cont. Decide whether to use SOQL or SOSL for searching within each group
of fields
Build a score map to track the keyword relevance score of each result
/* id to keyword relevance score map */
Map<ID,Integer> idToScoreMap = new Map<ID,Integer>();
Field Group SOQL/SOSL
Name SOSL
Features, Short Description SOQL
Long Description SOSL
![Page 30: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/30.jpg)
Keyword Search – Our Solution Cont. Execute SOSL on the Name field
FIND '*outlook integration*' IN NAME FIELDS RETURNING Listing__c (Id WHERE Public__c = true)
Execute SOQL using the “like” clause/* Execute this SOQL for every field group and update the score map */
For (Listing__c lst : [SELECT id FROM Listing__c WHERE (Features__c LIKE ‘%outlook%integration%’ OR
ShortDescription__c LIKE ‘%outlook%integration%’) AND Public__c = true]) {
Integer score = idToScoreMap.get(lst.id);
score += WEIGHT_FOR_THIS_FIELD_GROUP; /* 5 in this example */
idToScoreMap.put(lst.id,score);
}
Define a new object to store long text area fields and execute SOSL on
that object FIND '*outlook integration*' IN ALL FIELDS RETURNING ListingExtension__c (Listing__c WHERE
RecordType.Name=‘Description’ AND Listing__r.Public__c = true)
![Page 31: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/31.jpg)
Keyword Search – Our Solution Cont. Sort by keyword relevance score
/*Implement the Comparable interface to sort results by score*/
public class SearchResult implements Comparable
public Integer compareTo(Object compareTo) {
…….
}
}
![Page 32: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/32.jpg)
Search Framework - Filtering
Define a filter tree with a node to represent each filter you would
like to support.
• Data structure used to render filters UI and capture user’s selection
• Search engine uses the filter tree to execute SOQL and return filtered
results
![Page 33: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/33.jpg)
Search Framework - Filtering
Supports the following types of filters Filters based on a where clause (Eg. Type__c = ‘iOS’)
Filters based on pick list fields
List filters that are dependent on other list filters
Hierarchical set of filters
![Page 34: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/34.jpg)
Filter Tree - Example
![Page 35: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/35.jpg)
Filtering - Filters based on a where clausepublic class BuiltinFilterNode extends FilterNode {
public BuiltinFilterNode(String label, String clause, String filterNodeId) {
…
}
public override String getWhereClause(String objRef) {
if (getIsSelected()) {
return (objRef != null ? objRef + '.' : '') + predicate;
}
return null;
}
}
new BuiltinFilterNode (‘iOS’,‘Type__c = \‘iOS\’’, ‘ios’);
new BuiltinFilterNode (‘4 stars & up’,‘Rating__c >= 4’, ‘rt4’);
![Page 36: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/36.jpg)
Filtering - Filters based on pick list fields public virtual class ListFilterNode extends FilterNode {
public override void setSelectedValue(String val) {
for(ListOption lo : listValues) {
if (lo.val == selectedVal) {
selectedLabel = lo.label;
lo.isSelected = true;
break;
}
}
}
public virtual override String getWhereClause(String objRef) {
if (!String.isBlank(selectedVal)) {
if (isMultiSelectDataType)
clause = (objRef != null ? objRef + '.' : '') + fieldName + ' includes (\'' + selectedVal + '\')';
else
clause = (objRef != null ? objRef + '.' : '') + fieldName + ' = \'' + selectedVal + '\'';
}
return clause;
}
![Page 37: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/37.jpg)
Filtering – Generating the filter clause
![Page 38: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/38.jpg)
Search - Bringing it all together
Keyword ?
Keyword ?
Perform Keyword Search
Perform Keyword Search
Filter Results Based on user selection
Filter Results Based on user selection
yes
Filter results & sort based on user
selection. Construct list of ids of the
current page’s objects
Filter results & sort based on user
selection. Construct list of ids of the
current page’s objects
no
Sort by relevance score. Construct list
of ids of the current page results
Sort by relevance score. Construct list
of ids of the current page results
Keyword relevance sort ?
Keyword relevance sort ?
yesno
StartStart
Retrieve all data needed to render UIRetrieve all data
needed to render UI
![Page 39: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/39.jpg)
Filtering – Constructing the treeFilterNode filterRoot = new FilterNode.RootFilterNode();
GroupFilterNode appTypeGroup = new FilterNode.GroupFilterNode(Label.APP_TYPE,FilterNode.ShowAsType.TOP_FILTER);
filterRoot.add(appTypeGroup);
appTypeGroup.add(new FilterNode.BuiltinFilterNode(‘iOS Apps’,’Type__c = \’ios\’’, ‘ios’));
appTypeGroup.add(new FilterNode.BuiltinFilterNode(‘Android Apps’,’Type__c = \’android\’’, ‘android’));
appTypeGroup.add(new FilterNode.BuiltinFilterNode(‘Web Apps’,’Type__c = \’web\’’, ‘web’));
ListFilterNode langListNode = new ListFilterNode(LANGUAGE_FILTER_ID,sObjectType.App__c.fields.Languages__c.label,
System.Label.AllLanguages,'Languages__c‘,filterRoot,AppDO.languageSelectOptions,
FilterNode.ShowAsType.TOP_FILTER,true);
filterRoot.add(langListNode);
ListFilterNode catListNode = new ListFilterNode(CATEGORY_FILTER_ID,sObjectType.App__c.fields.Categories__c.label,
System.Label.AllCategories,'Categories__c‘, filterRoot,AppDO.categorySelectOptions,
FilterNode.ShowAsType.LEFT_NAV_FILTER,true);
filterRoot.add(catListNode);
![Page 40: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/40.jpg)
Configuring the search engineString listingWhereClause = ' Listing__c.Status__c = \'Live\' AND (Listing__c.Language__c = \'' + usersLanguage +
'\' OR Listing__c.isDefaultAppListing__c = true) ';
String appWhereClause = ' IsActive__c = true ' : ' LiveListings__c > 0 AND IsActive__c = true ';
String appNameSosl = 'FIND \'*{0}*\' IN NAME FIELDS RETURNING App__c (Id WHERE ' + appWhereClause + ')';
String descriptionSosl = 'FIND \'*{0}*\' IN ALL FIELDS RETURNING ListingExtension__c (App__c WHERE RecordType.Name
= \'Description\' AND ' + listingWhereClause.replace('Listing__c.', 'Listing__r.') + ')';
String requirementsSosl = 'FIND \'*{0}*\' IN ALL FIELDS RETURNING ListingExtension__c (App__c WHERE RecordType.Name
= \'Requirements\' AND ' + listingWhereClause.replace('Listing__c.', 'Listing__r.')) + ‘)';
List<KeywordGroupConfig> groupConfigs = new List<KeywordGroupConfig>();
groupConfigs.add(new KeywordSOSLGroupConfig(APP_NAME_FIELD_WEIGHTING, appNameSosl));
groupConfigs.add(new KeywordSOSLGroupConfig(DESCRIPTION_FIELD_WEIGHTING, descriptionSosl));
groupConfigs.add(new KeywordSOSLGroupConfig(REQUIREMENTS_FIELD_WEIGHTING, requirementsSosl));
![Page 41: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/41.jpg)
Configuring the search engine cont.
/* field group with field “TagLine” */
groupConfigs.add(new KeywordSOQLGroupConfig(new List<String>{'tagline__c'}, TAGLINE_FIELD_WEIGHTING, 'SELECT App__c
FROM Listing__c’, listingWhereClause));
/* field group with field categories */
groupConfigs.add(new KeywordSOQLGroupConfig('categories__c', CATEGORIES_FIELD_WEIGHTING, 'SELECT Id FROM App__c’,
appWhereClause, AppDO.categoriesLabelLookup));
super.initialize(new KeywordSearchConfig(groupConfigs,’App__c’, APP_FIELDS), null, appWhereClause);
![Page 42: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/42.jpg)
![Page 43: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/43.jpg)
![Page 44: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/44.jpg)
Other useful patterns Data Access Object
• Define a DAO class that acts as a layer between the business logic and the
database
– Code Reusability
– Easy Maintenance
Data Object• Define a DO class to encapsulate a SObject. This class has methods to create,
update, delete this SObject
– Add convenience methods
– Relate different DO objects to help with implementation of your business logic.
![Page 45: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/45.jpg)
Data Access Object - Example
![Page 46: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/46.jpg)
Data Object - Examplepublic class ListingDO extends BaseData {
private Listing__c listingObj;
private AppDO appObj;
public ListingDO(Listing__c listing) {
init(listing);
}
public Boolean getIsLive() {
return listingObj.Status__c == STATUS_LIVE;
}
public String getLanguageLabel() {
return langLabelLookup.get(listingObj.Language__c);
}
public AppDO getApp() {
if (appObj == null) appObj = new AppDO(listingObj.App__r);
return appObj;
}
public Boolean save() {
/* insert or update here */
}
...
}
![Page 47: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/47.jpg)
Enabling the Salesforce1 Experience Enable Visualforce pages for mobile.
Include the application’s tabs in the mobile navigation.
![Page 48: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/48.jpg)
![Page 49: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/49.jpg)
![Page 50: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/50.jpg)
Pratima NambiarPratima Nambiar
Tech LeadAppExchange & Communities
Jochem GeerdinkJochem Geerdink
Product DesignerAppExchange & Communities
![Page 51: How We Built the Private AppExchange App (Apex, Visualforce, RWD)](https://reader036.fdocuments.net/reader036/viewer/2022081414/54c660e54a7959a9678b4609/html5/thumbnails/51.jpg)