0 Comments

Every module in Pivotal is also known for its acronym. The most common modules, the base modules, are the Pivotal Customer Management (PCM) and the Customer Management System (CMS). These are probably the most known modules, but there are many others. There is no documentation or list that identifies all the acronyms, so I decided to summarize them here.

For developers it is very important to understand these acronyms, and also, to understand the effect that the installation of a module has on a development machine. So for example, a module installation creates an environment variable that points to the path where all the assemblies for such module are, which is relevant if we want to use Visual Studio to extend the module. I will explain this later in the article, but first let me explain all the modules available for a Pivotal system.

Pivotal modules

There are two possible base modules, the PCM and the CMS. The CMS is the old (5.x) base module containing Sales, Support and Marketing all in one package. Pivotal released PCM as the new base module, as a simpler and more elegant module for the pivotal 6.x version of the product. The following lists summarizes all the modules and their acronyms:

PCM

ModuleAcronymVersion
Pivotal Customer ManagementPCM6.0.8 HF2
Application WorkflowAWM6.0.9
Contact CenterCC6.0.9
Customer Service and SupportCSS6.0.8
Data ManagementDMM6.0.9
Document ManagementPDM6.0.9
eMarketingEM6.0.10
Marketing Resource ManagementMRM6.0.9
Sales Force AutomationSFA6.0.8 HF1
Social CRMSCRM6.0.10 HF2
Territory ManagementTM6.0.11
Web PortalWP6.0.11 HF1


CMS

ModuleAcronymVersion
Customer Management SystemCMS6.0.5
Application WorkflowAWM6.0
Contact CenterCC6.0
Data ManagementDMM6.0.7
Document ManagementPDM6.0
eMarketingEM6.0.4 HF5
Marketing Resource ManagementMRM6.0
Sales Force AutomationSFA6.0.8 HF1
Social CRMSCRM6.0


Notice that the CMS track doesn’t contain the Customer Service and Support, Sales Force Automation and Territory Management, but this is because, the CMS already contains these modules in the core. The only module that is not yet released for CMS is the Web Portal one (relevant if you use Thin Client). It is true that both PCM and CMS, although very similar, are not functionally equivalent, there might be some features in PCM that are not present in CMS (e.g. the integration with mapping services such as Google maps or Bing maps).

Environment variables

When you install a Pivotal module on a development machine, the following things occur:

  • The module is installed by default in C:\Program Files[ (x86)]\CDC Software\Pivotal CRM\Applications
  • Each module will create an Assemblies folder where all the .Net assemblies that implement the module will be placed. You will need these assemblies in case you want to extend the module and add references in Visual Studio.
  • A system environment variable is created. This variable is created by the acronym of the module concatenated with the word Path at the end. So for example, if you install the SFA module an environment variable with the name SFAPath will be created. This variable will point to the path where the assemblies for such module are installed. So for example, the SFAPath variable will point to C:\Program Files[ (x86)]\CDC Software\Pivotal CRM\Applications\Sales Force Automation\Assemblies

In addition to the environment variables created by each module, there are some generic environment variables that you need to consider as well. These variables are automatically created when you install the PBS, Pivotal Client and Smart Client Framework. The following is the complete list of such environment variables:

ComponentEnvironment Variable
Pivotal Business Server (PBS)LCSPath
Pivotal Integration for Microsoft OutlookOIPath
Pivotal ClientSCClientPath
Pivotal ClientSCCustomPath
Pivotal ClientSCPBSPath
Pivotal Smart Client FrameworkSCCPath
Pivotal Contact Management ModuleCOREPath
Pivotal Contact Management ModulePCMPath
Pivotal Sales Force Automation ModuleSFAPath
Pivotal Contact Center ModuleCCPath
Pivotal Customer Service & Support ModuleCSSPath
Pivotal Data Management ModuleDMMPath
Pivotal Application Workflow ModuleAWMPath
Pivotal Document Management ModulePDMPath
Pivotal Marketing Resource Management ModuleMRMPath
Pivotal Email Marketing ModuleEMPath
Pivotal Social CRM ModuleSCRMPath
Pivotal Territory Management ModuleTMPath
Pivotal Web Portal ModuleWPPath


So why you need to care about this? Because all the Pivotal source code, visual studio templates, etc. have references to these variables. If you open one of the projects (using a text editor) for one of the modules (the source code is installed as part of the module installation) you will notice that there are a bunch of references using this style:

<Reference Include="Applications.Core.Common, Version=6.0.6.0, Culture=neutral, PublicKeyToken=47e8a196c40562a5, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(COREPath)\Common\Applications.Core.Common.dll</HintPath>
  <Private>False</Private>
</Reference>
<Reference Include="Applications.Core.Data, Version=6.0.6.0, Culture=neutral, PublicKeyToken=47e8a196c40562a5, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(COREPath)\Common\Applications.Core.Data.dll</HintPath>
  <Private>False</Private>
</Reference>
<Reference Include="Applications.Core.Server, Version=6.0.6.0, Culture=neutral, PublicKeyToken=47e8a196c40562a5, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(COREPath)\Server Tasks\Applications.Core.Server.dll</HintPath>
  <Private>False</Private>
</Reference>
<Reference Include="Applications.Core.Server.InteractionServices, Version=6.0.6.0, Culture=neutral, PublicKeyToken=47e8a196c40562a5, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(COREPath)\Server Tasks\Applications.Core.Server.InteractionServices.dll</HintPath>
  <Private>False</Private>
</Reference>
<Reference Include="Css.Common.WorkingDaysCalendar, Version=6.0.8.0, Culture=neutral, PublicKeyToken=47e8a196c40562a5, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(CSSPath)\Common\Css.Common.WorkingDaysCalendar.dll</HintPath>
  <Private>False</Private>
</Reference>
<Reference Include="Css.Data, Version=6.0.8.0, Culture=neutral, PublicKeyToken=47e8a196c40562a5, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(CSSPath)\Common\Css.Data.dll</HintPath>
  <Private>False</Private>
</Reference>
<Reference Include="Css.Server.FormTasks.ServiceTicket, Version=6.0.8.0, Culture=neutral, PublicKeyToken=47e8a196c40562a5, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(CSSPath)\Server Tasks\Css.Server.FormTasks.ServiceTicket.dll</HintPath>
  <Private>False</Private>
</Reference>
<Reference Include="Pivotal.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=1ef445b59b9ece60, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(LCSPath)\Pivotal.Core.dll</HintPath>
  <Private>False</Private>
</Reference>
<Reference Include="Pivotal.Core.Common, Version=6.0.0.0, Culture=neutral, PublicKeyToken=1ef445b59b9ece60, processorArchitecture=MSIL">
  <SpecificVersion>False</SpecificVersion>
  <HintPath>$(LCSPath)\Pivotal.Core.Common.dll</HintPath>
  <Private>False</Private>
</Reference>

Notice the highlighted lines, they have references to the environment variables mentioned before. So, if your development machine doesn’t have the environment variables configured then the project will not compile since Visual Studio will not be able to find those references.

The reference paths in the Toolkit

So, now that you understand all the environment variables, and how to make your projects compile, comes the time when you need to upload the assembly into the BM using the toolkit. You use the Code File Importer. But when importing you get an error saying that it is not able to find a particular assembly. This is because the Toolkit analyses the assembly and checks its references, so it needs to know where those references are. You need to tell the Toolkit where to find those assemblies. You do this using the Reference Paths tab in the Code File Importer:

Code File Importer

You will need to add a reference path (a folder) to each of the assemblies’ directories you are referencing. The following is a list of all the folders you need to add:

$(LCSPath)
 $(COREPath)\Client Tasks
 $(COREPath)\Common
 $(COREPath)\Server Tasks
 $(PCMPath)\Client Tasks
 $(PCMPath)\Common
 $(PCMPath)\Server Tasks
 $(OIPath)
 $(SCCPath)
 $(SCCustomPath)
 $(SCClientPath)
 $(SCPBSPath)
 $(AWMPath)\Client
 $(AWMPath)\Common
 $(AWMPath)\Server
 $(CCPath)\Client
 $(CCPath)\Common
 $(CCPath)\Server
 $(CSSPath)\Client Tasks
 $(CSSPath)\Common
 $(CSSPath)\Server Tasks 
 $(DMMPath)\Client Tasks
 $(DMMPath)\Common
 $(DMMPath)\Data
 $(DMMPath)\Server Tasks
 $(EMPath)
 $(MRMPath)\Client Tasks
 $(MRMPath)\Data
 $(MRMPath)\Server Tasks
 $(PDMPath)\Application Pages
 $(PDMPath)\Client Tasks
 $(PDMPath)\Data
 $(PDMPath)\Server Tasks
 $(SCRMPath)
 $(SFAPath)\Client Tasks
 $(SFAPath)\Data
 $(SFAPath)\Server Tasks
 $(TMPath)
 $(WPPath)

Notice that each module specifies more than one folder. This is because some modules have different folders for client and server tasks.

Adding these folder one by one is a long task. The good news is that these folders are stored in the registry so you can create a .reg file to add them all at once. The registry path where these references are stores is the following:

For a 64bit OS: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Pivotal\Relationship\Connections\CRM CM\Code File Importer\Search Folders

For a 32bit OS: HKEY_LOCAL_MACHINE\SOFTWARE\Pivotal\Relationship\Connections\CRM CM\Code File Importer\Search Folders

To add all them at once, just import this file (remove the Wow6432Node part if you’re using a 32bit OS):

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Pivotal\Relationship\Connections\CRM CM\Code File Importer\Search Folders]
"Folder0"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Application Workflow Module 6.0.9\\Assemblies\\Client"
"Folder1"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Application Workflow Module 6.0.9\\Assemblies\\Common"
"Folder2"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Application Workflow Module 6.0.9\\Assemblies\\Server"
"Folder3"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Contact Center 6.0.9\\Assemblies\\Client"
"Folder4"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Contact Center 6.0.9\\Assemblies\\Common"
"Folder5"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Contact Center 6.0.9\\Assemblies\\Server"
"Folder6"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Customer Service and Support 6.0.8\\Assemblies\\Client Tasks"
"Folder7"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Customer Service and Support 6.0.8\\Assemblies\\Common"
"Folder8"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Customer Service and Support 6.0.8\\Assemblies\\Server Tasks"
"Folder9"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Data Management Module 6.0.9\\Assemblies\\Client Tasks"
"Folder10"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Data Management Module 6.0.9\\Assemblies\\Common"
"Folder11"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Data Management Module 6.0.9\\Assemblies\\Data"
"Folder12"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Data Management Module 6.0.9\\Assemblies\\Server Tasks"
"Folder13"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Document Management 6.0.9\\Assemblies\\Application Pages"
"Folder14"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Document Management 6.0.9\\Assemblies\\Client Tasks"
"Folder15"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Document Management 6.0.9\\Assemblies\\Data"
"Folder16"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Document Management 6.0.9\\Assemblies\\Server Tasks"
"Folder17"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Email Marketing 6.0.10\\Assemblies"
"Folder18"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Marketing Resource Management 6.0.9\\Assemblies\\Client Tasks"
"Folder19"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Marketing Resource Management 6.0.9\\Assemblies\\Data"
"Folder20"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Marketing Resource Management 6.0.9\\Assemblies\\Server Tasks"
"Folder21"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Pivotal Contact Management 6.0.8\\Assemblies\\Core\\Client Tasks"
"Folder22"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Pivotal Contact Management 6.0.8\\Assemblies\\Core\\Common"
"Folder23"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Pivotal Contact Management 6.0.8\\Assemblies\\Core\\Server Tasks"
"Folder24"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Pivotal Contact Management 6.0.8\\Assemblies\\PCM\\Client Tasks"
"Folder25"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Pivotal Contact Management 6.0.8\\Assemblies\\PCM\\Common"
"Folder26"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Pivotal Contact Management 6.0.8\\Assemblies\\PCM\\Server Tasks"
"Folder27"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Sales Force Automation 6.0.8\\Assemblies\\Client Tasks"
"Folder28"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Sales Force Automation 6.0.8\\Assemblies\\Data"
"Folder29"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Sales Force Automation 6.0.8\\Assemblies\\Server Tasks"
"Folder30"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Social CRM 6.0.10\\Assemblies"
"Folder31"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Territory Management 6.0.11\\Assemblies"
"Folder32"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Business Server"
"Folder33"="C:\\Program Files (x86)\\Grupo Lanka\\Pivotal CRM\\Framework Library\\Assemblies"
"Folder34"="C:\\Program Files\\CDC Software\\Smart Updater\\40\\Packages\\PivotalClient\\Client\\6.0.9.1\\bin\\standard"
"Folder35"="C:\\Program Files\\CDC Software\\Smart Updater\\40\\Packages\\PivotalClient\\Custom\\6.0.9.1\\bin\\standard"
"Folder36"="C:\\Program Files\\CDC Software\\Smart Updater\\40\\Packages\\PivotalClient\\PBS\\6.0.9.0\\bin\\standard"
"Folder37"="C:\\Program Files\\CDC Software\\Smart Updater\\40\\Packages\\PivotalOfficeIntegration\\OIRuntime\\6.0.9.2\\bin\\Standard"
"Folder38"="C:\\Program Files\\CDC Software\\Smart Updater\\40\\Packages\\Scc\\4.0.1.58"
"Folder39"="C:\\Program Files (x86)\\CDC Software\\Pivotal CRM\\Applications\\Web Portal 6.0.11\\Assemblies"

Notice that the paths I’m specifying above might not work in your particular case, so be careful when importing this file into your registry. At least check that the versions you have are the correct ones and if not modify the file to match your particular versions. Try to import it several times if it doesn’t work at the first try.

A healthy development machine

Although all the environment variables are automatically created by either the component or module installation (I’ve seen cases when this doesn’t happen though), the configuration of the development machine to take into consideration the right variables, the configuration of the Toolkit references, etc. is a hard and long task.

Fortunately, there is help. Grupo Lanka has created a set of development tools. One of these tools is the Environment Check tool, which takes care of checking that a development machine is well configured with all the environment variables. The cool thing about this tool is that it also creates and fixes the environment variables. The tool is smart enough to figure out the version of the component/module that is installed and creates the appropriate variables and reference paths in the Toolkit pointing to the right path. If you install a new version of the product (which changes the paths), the tool can automatically update your environment to match the new paths. I highly recommend it since it can save you a lot of headaches. Here is a screenshot of the tool:

Environment Check

0 Comments

There are times when we want to remove a field from a table. This usually is no problem, except when the field has already been added to a client form. If we remove the field from the table but we don’t remove it first from the form then we get an error saying the client form is wrong. Removing a field from a table doesn’t automatically remove it from the client form. We should always remove the field from the form first before removing it from the table. But what if we forget to do this? (or if we miss some forms, because there is no easy way to know in which forms the field is used).

Before I explain how to remove the field from the form I will give a little background on how fields are used in a client form. Client forms represent a view for data segments. Every client form has a primary data segment and zero or many secondary data segments. The primary data segment stores all the fields for the underlying table. Secondary data segments are used for secondary tables, tables with foreign keys to the primary table. When a field is added to a form, the form designer automatically adds the field to the corresponding data segment, and also adds it to the form definition (the view). So if we remove the field from the form, the form designer only removes it from the view, but it doesn’t remove it from the data segment. There are two ways to solve this problem.

formEditorThe first way is to use the form editor. Just go to any field in the form (on the primary or secondary segment), then go to properties and edit the PivotalBindings property. Click in the DataField property and you will see all the fields associated with the data segment. From here you just use the “Delete data field” link to remove the unwanted fields. This will remove the field from the data segment. You can check that the field is actually removed by looking at the data segment.

Data segments can be found in the toolkit under the menu “Data Templates, Active & Device Forms”. Primary segments will always have the 1 ordinal. The problem is that the toolkit doesn’t allow you to edit the data template, since this could make client forms corrupted.


However, sometimes you can’t even access the form using the form designer. In this case we need to solve it using the second way. You can work around this by editing the data template associated to the form using SQL on the BM database. You can use this query to remove the field from the corresponding segment.

delete from ng_form_field f where f.ng_form_field_id in (
select ff.ng_form_field_id 
from 
  sc_form cf inner join 
  ng_form af on cf.active_form_id = af.ng_form_id inner join
  ng_form_segment fs on fs.ng_form_id = af.ng_form_id inner join
  ng_form_field ff on ff.form_segment_id = fs.ng_form_segment_id inner join
  table_fields tf on tf.table_fields_id = ff.field_id
where 
  cf.form_name = '<formName>' and
  fs.form_segment_name = '<segmentName>' and
  tf.field_name = '<fieldname>'
)

In the above SQL, you need to know the name of the form, the name of the segment, and the field name. The form and field name are easily known, but the segment name might be difficult to know. If this is the case, use the toolkit to find out the name of the segment by going to the menu “Data Tempaltes, Active & Device Forms” and look at the data segments that conform the client form and write down the name of the segment that has the field you want to remove.

All this could be avoided of course if we remember to remove the field from the form (using the first way described above) before removing the field from the table.

0 Comments

When using the out-of-the-box Pivotal PCM module, there are missing LD Strings for the error messages related with non-existing fields/tables.

Missing LD string errorFor example, if you try to use the data access API to get a non-existing field, as shown in the following example, you will get an HRESULT exception. The thing is that this error doesn’t tell us anything and it hides the real error, which is that the field we’re trying to access doesn’t exist.


using(DataTable t = this.DefaultDataAccess.GetDataTable("Contact",new string[] { "NonExistingColumn"}))
{
    // do something
}

In the example above, we are trying to get a non-existing field called NonExistingColumn from the Contact table. The error is non-intuitive and it is hard to debug, since it will not show any debug information in the event viewer and developers think that it is some error related with the platform.

As mentioned before, the problem is due to the lack of some LD Strings in the BM. I have reported this error to Pivotal and we hope to have a PCM version that solves this in the near future. Meanwhile, the solution is to import the missing LD Strings.

My good friend Pablo Oliveira, from Grupo Lanka has created a file with the missing LD Strings:

APPLICATION ERROR CODE,NoType,NoType,Lanka system string,0x0409
APPLICATION ERROR CODE,Choice,Choice,Lanka system string,0x0409
APPLICATION ERROR CODE,ClientTask,ClientTask,Lanka system string,0x0409
APPLICATION ERROR CODE,Command,Command,Lanka system string,0x0409
APPLICATION ERROR CODE,CommandHandler,CommandHandler,Lanka system string,0x0409
APPLICATION ERROR CODE,DataSection,DataSection,Lanka system string,0x0409
APPLICATION ERROR CODE,DataTemplate,DataTemplate,Lanka system string,0x0409
APPLICATION ERROR CODE,Form,Form,Lanka system string,0x0409
APPLICATION ERROR CODE,InContextTableMenuItem,InContextTableMenuItem,Lanka system string,0x0409
APPLICATION ERROR CODE,InboundCommunicationAddress,InboundCommunicationAddress,Lanka system string,0x0409
APPLICATION ERROR CODE,LDGroup,LDGroup,Lanka system string,0x0409
APPLICATION ERROR CODE,LDLanguage,LDLanguage,Lanka system string,0x0409
APPLICATION ERROR CODE,LDString,LDString,Lanka system string,0x0409
APPLICATION ERROR CODE,LetterExpress,LetterExpress,Lanka system string,0x0409
APPLICATION ERROR CODE,List,List,Lanka system string,0x0409
APPLICATION ERROR CODE,Query,Query,Lanka system string,0x0409
APPLICATION ERROR CODE,Report,Report,Lanka system string,0x0409
APPLICATION ERROR CODE,Security,Security,Lanka system string,0x0409
APPLICATION ERROR CODE,Search,Search,Lanka system string,0x0409
APPLICATION ERROR CODE,SearchResultsList,SearchResultsList,Lanka system string,0x0409
APPLICATION ERROR CODE,ServerTask,ServerTask,Lanka system string,0x0409
APPLICATION ERROR CODE,SmartPortalCategory,SmartPortalCategory,Lanka system string,0x0409
APPLICATION ERROR CODE,SmartPortalContentItem,SmartPortalContentItem,Lanka system string,0x0409
APPLICATION ERROR CODE,SmartPortalPlugIn,SmartPortalPlugIn,Lanka system string,0x0409
APPLICATION ERROR CODE,SmartPortalPlugInAttr,SmartPortalPlugInAttr,Lanka system string,0x0409
APPLICATION ERROR CODE,Subject,Subject,Lanka system string,0x0409
APPLICATION ERROR CODE,Table,Table,Lanka system string,0x0409
APPLICATION ERROR CODE,TableField,TableField,Lanka system string,0x0409
APPLICATION ERROR CODE,Task,Task,Lanka system string,0x0409
APPLICATION ERROR CODE,TaskGroup,TaskGroup,Lanka system string,0x0409
APPLICATION ERROR CODE,TaskPad,TaskPad,Lanka system string,0x0409
APPLICATION ERROR CODE,Topic,Topic,Lanka system string,0x0409

You can copy and paste the above text in a file with a .csv extension, and then use the Toolkit to import it in the BM (use the agent “Import System Strings”). Once imported you will get the right error when trying to reference non-existing objects using the data access API.

Correct error for non existing objects


1 Comments

Recently I had to do a little project where users wanted to display a list of photos associated with a custom object. I decided to take advantage of what Salesforce already provides: attachments. I designed a custom visualforce page to show the attachment as photos. In this post I will show a little example of how to accomplish this using an extension controller and a visualforce page.

Let’s work with a simple example: suppose we want to implement a database of movies in Salesforce and, besides the typical movie attributes, we want to show photos of the movie (movie poster, scenes, etc.) The end result is something like this:

Movie with Photos

As you can see from the image, we are displaying a list of photos below the movie fields. The first thing we need to do is enable attachments for our custom object.

Object settingsWe will select the “Add Notes and Attachments related list to default page layout” on object creation. This will enable the object to have attachments. We can add as many attachments as we want (or as the Salesforce space limitations allow us). Once the custom object is created, we will be able to add attachments using the standard page layout that is created as part of the object.


Let’s us create a movie and add some attachments to it. In the example, I’ve set up some basic fields for a movie: name, genre, storyline and release date (this is enough for this example). I’ve created a sample movie (I’m a big fan of the Alien franchise) and added some attachments to it:

The movie layout with attachments

Now, for the fun part: we will create a custom visualforce page to show those attachments as photos. Let’s start by creating a new visualforce page by typing https://<salesforceurl>/apex/Movie?id=<someMovieId>. This will create a new Apex page called Movie, from scratch. Add the following markup to the visualforce page:

<apex:page standardController="Movie__c" extensions="MovieControllerExtension">
    <apex:detail relatedList="false"/>
    <apex:pageBlock >
        <apex:pageBlockSection title="Photos ({!totalPhotos})" collapsible="false">
            <apex:repeat value="{!photos}" var="photo">
                <apex:image url="{!URLFOR($Action.Attachment.Download, photo)}" width="50px" height="50px" />                    
            </apex:repeat>
        </apex:pageBlockSection>
    </apex:pageBlock>
</apex:page>

This is a very simple page: we’re leveraging on the apex:detail tag to have all the movie details and then we create a page block to list the photos (the attachments). The magic happens in line 6, where we use the special action Download to actually download the attachment at run time and display it as the source of the apex:image tag. We do all this inside the apex:repeat tag to cycle through all the attachments for the current movie object. Notice how we are using a controller extension called MovieControllerExtension. This extension provides two properties: totalPhotos and photos. The photos property is just a list of Ids for each of the attachments associated with the object. This is the code for the controller extension:

public with sharing class MovieControllerExtension {

    private ApexPages.standardController controller;
    
    private Movie__c movie;

    private List<Id> photoIds; 

    public MovieControllerExtension(ApexPages.StandardController controller) {
        this.controller = controller;
        
        this.movie = (Movie__c)controller.getRecord();
    }

    public List<Id> photos {
        get {
            if(photoIds == null) {
                photoIds = new List<Id>();
                for(Attachment att : [select Id from Attachment where ParentId = :movie.Id]) {
                    photoIds.Add(att.Id);
                }
            }
                            
            return photoIds;
        }
    }
    
    public Integer totalPhotos {
        get {
            return photos.size();
        }
    }
}

Lines 19 and 20 show how the attachment ids are retrieved from the database and added to the photos property.

And this is it! we get a nice list of all the photos for the movie by leveraging the attachment records associated to our custom object.

0 Comments

In the previous post (How to show a progress bar for a long operation in Pivotal) I talked about how to improve the user experience when executing long running operations. I explained how to show a progress bar to provide feedback to users that an operation is in progress and they should (patiently) wait. The user experience changes a lot when we provide feedback to users.

In this post I will talk about an alternative for long running operations that basically removes the wait time from the user. In this case, the long running operation will be executing on the server while the user gets back the control of the system. An example that shows a good use of an asynchronous operation is the following: suppose that when the user submits a new service ticket there are a bunch of emails being sent (to the supervisor, to the service queue, to the user acknowledging the submission, etc.) All these emails take time to be sent. If we do this on the server task associated to the service ticket, then the user will have to wait a (possibly) long time until all emails are being sent. All the user wants is to submit the incident, he/she doesn’t care if the emails are being sent at submission time or later.

CropperCapture[5]There are many ways to do this. You can set up an email queue and then have a background process (e.g. the scheduler script) to read the queue and send emails. In this case the submission task is responsible to submit the email jobs to the queue. In this post I will talk of a simpler way: a generic asynchronous task that executes server task method in the background.


For illustration purposes, let’s use a simple task to simulate our long running operation. The same example for our previous post will do:

[TaskExecute]
public virtual void LongRunningOperation(Id recordId)
{
    Trace.WriteLine(String.Format("Starting LongRunningOperation with recordId '{0}'", recordId));
    System.Threading.Thread.Sleep(20000);
    Trace.WriteLine(String.Format("Finishing LongRunningOperation with recordId '{0}'", recordId));
}

The client task makes a call to this method:

[ClientTaskCommand]
public virtual void LongRunningTask()
{
    try
    {
        Globals.Execute(this.DataTemplate, "LongRunningOperation", new object[] { this.RecordId });
        PivotalMessageBox.Show("Done", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Information);
    }
    catch (Exception e)
    {
        Globals.HandleException(e.InnerException ?? e, true);
    }
}

As it is right now, the user will have to wait 20 seconds for the task to execute, and during this time the client is freezing doing nothing. Let’s see how we can change this and make all this asynchronous. The client will remain the same, we don’t to change the client side code. The server side code, however, will suffer a little change:

[TaskExecute]
public virtual void LongRunningOperationSync(Id recordId)
{
    Trace.WriteLine(String.Format("Starting LongRunningOperation with recordId '{0}'", recordId));
    System.Threading.Thread.Sleep(20000);
    Trace.WriteLine(String.Format("Finishing LongRunningOperation with recordId '{0}'", recordId));
}

[TaskExecute]
public virtual void LongRunningOperation(Id recordId)
{
    TransitionPointParameter t = new TransitionPointParameter();
    t.SetUserDefinedParameter(1, "Ticket Service");
    t.SetUserDefinedParameter(2, "LongRunningOperationSync");
    t.SetUserDefinedParameter(3, recordId);

    this.SystemServer.ExecuteServerTask("Async Service", "ExecuteApp", t.ParameterList, false);
}

We have split our long running operation in two: the LongRunningOperation method now makes a call to a new service task called “Async Service”, executing the method “ExecuteApp”, and passing some transition point parameters. These parameters are: the name of the service task (in this example I’m assuming our service task is called “Ticket Service”), and the method that actually does the hard work, and the parameters for such method. Our second method, the LongRunningOperationSync, is the one that actually does the job. The body of this method is the same as we had before.

The Async Service task is a generic service task that executes another service task asynchronously. Let’s see how to implement our async service:

[TaskExecute]
public virtual void ExecuteApp(ParameterList parameterList)
{
    string serverName = TypeConvert.ToString(TransitionPointParameter.GetUserDefinedParameter(1));
    string methodName = TypeConvert.ToString(TransitionPointParameter.GetUserDefinedParameter(2));

    object[] parameters = new object[] { };
    if (this.TransitionPointParameter.UserDefinedParametersNumber > 2) // get the rest of parameters
    {
        List<object> passedParameters = new List<object>();
        for (int i = 3; i <= this.TransitionPointParameter.UserDefinedParametersNumber; i++)
        {
            object parameter = this.TransitionPointParameter.GetUserDefinedParameter(i);
            if (String.IsNullOrWhiteSpace(TypeConvert.ToString(parameter)))
                passedParameters.Add(null);
            else
                passedParameters.Add(parameter);
        }
        parameters = passedParameters.ToArray();
    }

    ExecuteApp(serverName, methodName, parameters);
}

/// <summary>
/// Executes an application server task method
/// </summary>
/// <param name="appName">The name of the application server task</param>
/// <param name="methodName">The name of the server task method</param>
/// <param name="methodParameters">An array of parameters to be passed to the server taks</param>
private bool ExecuteApp(string appName, string methodName, object[] methodParameters)
{
    try
    {
        XElement xmlCommand = BuildExecuteServiceTaskCommand(appName, methodName, methodParameters == null ? new object[] {} : methodParameters);
        SendCommandAsync(xmlCommand);

        return true;
    }
    catch (Exception e)
    {
        // log the error
        throw;
    }
}

The service exposes only one public method. This method is responsible to retrieve the required parameters: the service task and method name, and the parameters for such method. It parses the transition point parameters for such information and then it calls the second method, which is more interesting. This method calls two helper methods: BuildExecuteServiceTask (to build an XML document) and SendCommandAsync (to do the async call). Let’s see these methods are implemented.

The BuildExecuteServiceTaskCommand job is to build an XML document according to the PBS XML specification (see the Pivotal Composite API Reference). This document will be sent to the PBS XML interface.

private const string PivotalNamespace = "urn:schemas-pivotal-com/LifecycleServer60";

private XElement BuildExecuteServiceTaskCommand(string serverRuleName, string methodName, params object[] parameters)
{
    XElement commandXml = new XElement(PivotalNamespace + "executeAppServerRule",
        new XElement(PivotalNamespace + "appServerRuleName", serverRuleName),
        new XElement(PivotalNamespace + "appServerRuleMethod", methodName));

    XElement commandXmlDocument = new XElement(PivotalNamespace + "command",
        new XElement(PivotalNamespace + "systemName", SystemServer.SystemInformation.SystemName),
        new XElement(PivotalNamespace + "loginType", "Client"));

    commandXmlDocument.Add(commandXml);

    if (parameters != null)
    {
        XElement commandParametersXml = new XElement(PivotalNamespace + "commandParameters");
        commandXml.Add(commandParametersXml);

        for (int i = 0; i < 6; i++) // the first six parameters must be empty
            commandParametersXml.Add(new XElement(PivotalNamespace + "emptyParameter"));

        foreach (object parameter in parameters)
        {
            string type, value;
            GetParameterData(parameter, out type, out value);
            commandParametersXml.Add(new XElement(PivotalNamespace + type, value));
        }
    }

    return commandXmlDocument;
}

For our example, the resulting XML document will be something like this:

<command xmlns="urn:schemas-pivotal-com/LifecycleServer60">
    <systemName>CRM</systemName>
    <loginType>Client</loginType>
    <executeAppServerRule>
        <appServerRuleName>Async Service</appServerRuleName>
        <appServerRuleMethod>ExecuteApp</appServerRuleMethod>
        <commandParameters>
            <emptyParameter/>
            <emptyParameter/>
            <emptyParameter/>
            <emptyParameter/>
            <emptyParameter/>
            <emptyParameter/>            
            <stringParameter>Ticket Service</stringParameter>
            <stringParameter>LongRunningOperation</stringParameter>
            <binaryParameter>0000000000000A81</binaryParameter>
        </commandParameters>
    </executeAppServerRule>
    <collectStatistics/>
</command>

This XML will be sent using a .Net HttpRequest object in the SendCommandAsync method. There is one method that requires attention in the above code, the GetParameterData method. This method converts a parameter to the Pivotal equivalent using the following strings:

//binaryParameter
//booleanParameter
//stringParameter
//stringParameter
//floatParameter
//dateParameter
//floatParameter
//binaryParameter
//binaryParameter
//integerParameter
//binaryParameter
//stringParameter
//timeInstantParameter
//timeInstantParameter

Since this is just a matter of querying the parameter type and then decide which string of the above to use, I will leave this out in sake of space, since the implementation is more related to .Net than to Pivotal and is not difficult to do. Let’s us see how the XML document is sent asynchronously to the PBS.

In order to do an async operation we will use the BeginInvoke method defined by .Net delegates. Our delegate needs the XML document to send, and the URI to send to:

private delegate void SendAsyncDelegate(XElement xmlDocument, Uri url);
private const string PivotalUrl = "http://localhost/ePower/XMLServices.asp";

The PBS endpoint for XML documents is XMLServices.asp, located in the root of the ePower virtual directory (this endpoint is old, but still works and is used by many Pivotal services such as the EMS, the scheduled script, ePartner/eService, etc.).

And this is how we send the document:

private void SendCommandAsync(XElement xmlDocument)
{
    SendAsyncDelegate sendAsyc = new SendAsyncDelegate(SendXml2UrlAsyncDelegate);
    IAsyncResult result = sendAsyc.BeginInvoke(xmlDocument, new Uri(PivotalUrl), null, null);
}

protected void SendXml2UrlAsyncDelegate(XElement xmlDocument, Uri url)
{
    try
    {
        HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url);
        httpRequest.Method = "POST";
        httpRequest.ContentType = "text/xml";
        httpRequest.Credentials = CredentialCache.DefaultCredentials;

        StreamWriter writer = new StreamWriter(httpRequest.GetRequestStream());
        writer.Write(xmlDocument.ToString());
        writer.Flush();
        writer.Close();

        HttpWebResponse httpResponse = (HttpWebResponse)httpRequest.GetResponse();
        XmlTextReader reader = new XmlTextReader(httpResponse.GetResponseStream());
        XElement xmlResponse = XElement.Load(reader);

        reader.Close();
        httpResponse.Close();
    }
    catch (WebException e)
    {
        // handle error
    }
}

There are some interesting things here: notice how we create the HTTP request using the default network credentials, which will use NTLM credentials to call the endpoint. Also notice that we don’t care about the response (it’s an async method). Line 4 contains the most important part of our implementation: the asynchronous call.

This is all we need to improve the user experience when executing a long running operation: submit an async call to the server task method and return control to the user as soon as possible. Of courser there are some things that need to be considered before we can actually put this into a production system. Here is a list of things that need to be done:

  • Error handling: in the code above you’ve noticed that I’ve not done any error handling.
  • Security: the PBS endpoint might be configured in HTTPS. Also the default credentials might not be what we want. In this case we could do user impersonation to do the actual call
  • Logging: the async service doesn’t care about the result of the operation, but the user might. In this case we would need some kind of status report on the async tasks.
  • Form task methods: the example above works for service task, but sometimes we want to call methods in a form task.

All the above considerations are not difficult to do. Personally I prefer to use a solution that gives me all this and more: I personally use the Pivotal Development Tools created by Grupo Lanka, which include an async service with all that’s required for a good implementation of async tasks.

For now on, there is no excuse to leave the user wondering what happened with their application, you now have two tools to improve the user experience: progress bars and asynchronous calls. I hope you implement them!