Direkt zum Hauptbereich

Email-Versand inklusive Attachments mit VisualForce

Elektronische Unternehmenskommunikation und die damit verbundenen Prozesse haben viel Optimierungspotenzial. Das am häufigsten genutzte Werkzeug in meisten Fällen ist die E-Mail. Insbesondere im Support-Bereich ist die E-Mail-Kommunikation ein integraler Bestandteil des Aufgabenbereichs. Mit Einsatz von VisualForce lassen sich Prozesse gerade in diesem Bereich deutlich optimieren (mehr dazu im Firmen-Blog: http://blog.factory42.com/blog/bid/341807/Neues-Rezept-f%C3%BCr-mehr-Effizienz ).
Entwicklung einer neuen VisualForce Seite zum Erfassen und Versenden von Emails ist kein Hexenwerk. Es wird aber dann etwas tricky, wenn Anhänge mitgesendet werden müssen.

Im aktuellen Projekt habe ich folgende Ansätze ausprobiert und nur eine funktionierende und in diesem Projekt akzeptable Lösung gefunden.

Ansätze:
1) Darstellung des Attachment-Blocks als Related List, ungefähr so

<apex:relatedList id="Attachments" subject="{!myObject}" list="NotesAndAttachments" title="Attached Documents"/>
endete mit einem "Internal Server Error".

2) Das Einbinden des entsprechenden Feldes direkt auf der VisualForce Seite, 
<apex:inputFile value="{!newAtt.body}" filename="{!newAtt.name}" id="file"/>
hat zwar funktioniert, erfordert aber den Refresh der gesamten Seite, was in einem komplexen Konstrukt zum Datenverlust führt. Versucht man statt dessen die Aktualisierung eines bestimmten Bereiches mit rerender="idAttachmentBlock"zu erzwingen, kommt die folgende Fehlermeldung zum Vorschein:
apex:inputFile can not be used in conjunction with an action component, apex:commandButton or apex:commandLink that specifies a rerender or oncomplete attribute

3) Die Attachments-Funktionalität wird ausgelagert. Es gibt eine zweite VisualForce Seite, die sich ausschließlich um Attachments kümmert, die als iFrame auf der ersten Seite erscheint.
Vorteil: keine Beeinträchtigung des Gesamtkonstrukts.
Nachteil: das iFrame hat eine feste Größe. Bedeutet, ich muss scrollen, falls viele Attachments angehängt werden. Aber auch dafür gibt es eine Lösung auf Basis von JavaScript. Aus Zeitgründen habe ich auf die Implementierung dieser Funktion verzichtet.
Beachte: die E-Mail wird als Entwurf gespeichert, bevor Dateien angehängt werden (da Attachments eine ParentID erwarten).

Hauptseite mit iFrame. Aufruf der Attachment-Seite im iFrame mit Übergabe der E-Mail ID als Parameter:
<apex:outputPanel id="attachBlock">
 <apex:outputPanel id="attachBlockInside" rendered="{!IF(attachmentParentId==null, false, true)}">
 <iframe src="/apex/f42_MultiAttachment?emailid={!attachmentParentId}" height="120" width="100%" border="0" frameborder="0" style="overflow-x: hidden;overflow-y: scroll;"></iframe>
 </apex:outputPanel>

</apex:outputPanel>

Seite mit Attachments:
<apex:page controller="f42_MultiAttachmentController" showHeader="false" sidebar="false" tabStyle="Case">
 <apex:stylesheet value="{!$Resource.SendEmailPageStyle}" />
 <style>
  body{background-color:#F8F8F8 !important; margin:0; padding:0;}
  .btn {margin:0 !important;}
  .inputText{width:250px !important;}
 </style>
 <apex:form >
  <apex:pageBlock >
   <apex:repeat value="{!newAttachments}" var="newAtt">
    <apex:pageBlockSection columns="3" >
     <apex:pageBlockSectionItem labelStyleClass="fieldBlockTopLabel" dataStyleClass="fieldBlockTopData">
      <apex:outputLabel value="{!$Label.SendEmail_lblFile}" for="file"/>
      <apex:inputFile value="{!newAtt.body}" filename="{!newAtt.name}" id="file"/>
     </apex:pageBlockSectionItem>
     <apex:pageBlockSectionItem labelStyleClass="fieldBlockTopLabel" dataStyleClass="fieldBlockTopData">
      <apex:outputLabel value="{!$Label.SendEmail_lblFileDescription}"/>
      <apex:inputText value="{!newAtt.Description}" styleClass="inputText" />
     </apex:pageBlockSectionItem>
     <apex:commandButton value="{!$Label.SendEmail_btnUpload}" action="{!saveAttachment}" style="margin:0; padding:0;"/>
    </apex:pageBlockSection>
   </apex:repeat>
   <apex:pageBlockSection columns="1" id="blockViewAttachments">
    <apex:pageBlockTable value="{!attachments}" var="attachment">
     <apex:column >
      <apex:outputLink value="{!URLFOR($Action.Attachment.Download, attachment.Id)}" style="color:#015ba7; text-decoration:none;" target="_blank">{!$Label.SendEmail_lblView}</apex:outputLink>
      <apex:outputLabel style="color:#999999" value=" | " />
      <apex:commandLink action="{!deleteAttachment}" style="color:#015ba7; text-decoration:none;">{!$Label.SendEmail_lblDelete}
       <apex:param value="{!attachment.Id}" name="idToDelete" assignTo="{!idAttachmentDelete}"/>
      </apex:commandLink>
     </apex:column>
     <apex:column value="{!attachment.Name}"/>
     <apex:column value="{!attachment.Description}"/>
    </apex:pageBlockTable>
   </apex:pageBlockSection>
  </apex:pageBlock>
 </apex:form>
</apex:page>

Attachment-Controller:
public without sharing class f42_MultiAttachmentController {
 public Id sobjId {get; set;} // the parent object id
 public List<Attachment> attachments; // list of existing attachments - populated on demand
 public List<Attachment> newAttachments {get; set;} // list of new attachments to add
 public static final Integer NUM_ATTACHMENTS_TO_ADD=5; // the number of new attachments to add to the list when the user clicks 'Add More'
 public id idAttachmentDelete{get;set;}


 // constructor
 public f42_MultiAttachmentController(){
  // instantiate the list with a single attachment
  newAttachments=new List<Attachment>{new Attachment()};
  sobjId = ApexPages.currentPage().getParameters().get('emailid');
 }

 // retrieve the existing attachments
 public List<Attachment> getAttachments(){
  // only execute the SOQL if the list hasn't been initialised
  if (null==attachments){
   attachments=[select Id, ParentId, Name, Description from Attachment where parentId=:sobjId];
  }
  return attachments;
 }

 // Add more attachments action method
 public void addMore(){
  // append NUM_ATTACHMENTS_TO_ADD to the new attachments list
  for (Integer idx=0; idx<NUM_ATTACHMENTS_TO_ADD; idx++){
   newAttachments.add(new Attachment());
  }
 }

 // Save action method
 public void saveAttachment(){
  List<Attachment> toInsert=new List<Attachment>();
  for (Attachment newAtt : newAttachments){
   if (newAtt.Body!=null){
    newAtt.parentId=sobjId;
    toInsert.add(newAtt);
   }
  }
  insert toInsert;
  newAttachments.clear();
  newAttachments.add(new Attachment());
  // null the list of existing attachments - this will be rebuilt when the page is refreshed
  attachments=null;
 }

 public void deleteAttachment(){
  List<Attachment> lstAttDelete = [Select id from Attachment where id = :idAttachmentDelete];
  if(!lstAttDelete.isEmpty())
   delete lstAttDelete;
  attachments=null;
 }

 // Action method when the user is done
 public PageReference done(){
  // send the user to the detail page for the sobject
  return new PageReference('/' + sobjId);
 }

 /******************************************************
 *
 * Unit Tests
 *
 ******************************************************/
 private static testMethod void testController(){
  Account acc=Test_Data_Generator.createTestAccount(1, true);
  f42_MultiAttachmentController controller=new f42_MultiAttachmentController();
  controller.sobjId=acc.id;
  System.assertEquals(0, controller.getAttachments().size());
  System.assertEquals(1, controller.newAttachments.size());
  controller.addMore();
  System.assertEquals(1 + NUM_ATTACHMENTS_TO_ADD, controller.newAttachments.size());
  // populate the first and third new attachments
  List<Attachment> newAtts=controller.newAttachments;
  newAtts[0].Name='Unit Test 1';
  newAtts[0].Description='Unit Test 1';
  newAtts[0].Body=Blob.valueOf('Unit Test 1');

  newAtts[2].Name='Unit Test 2';
  newAtts[2].Description='Unit Test 2';
  newAtts[2].Body=Blob.valueOf('Unit Test 2');
  controller.saveAttachment();
  System.assertEquals(2, controller.getAttachments().size());
  System.assertNotEquals(null, controller.done());
 }
}

Kommentare

Beliebte Posts aus diesem Blog

Zeitgesteuerter Flow blockiert Custom Leadkonvertierung

Die programmierte Konvertierung eines Leads bricht mit der Fehlermeldung "Unable to convert lead that is in use by workflow" ab. Der Grund ist ein Prozess, der automatisiert und zeitgesteuert ausgeführt wird. Dieser Prozess ruft zu einem späteren Zeitpunkt einen Flow auf. Während der Speicherung eines Leads wird dabei automatisch ein Flow Interview erstellt. Dieser Datensatz vom Typ "FlowInterview" blockiert die Leadkonvertierung. Lösung: Unmittelbar vor der Leadkonvertierung eine Checkbox auf dem Lead auf TRUE setzen. Da dieselbe Checkbox in den Process Builder Kriterien eingebunden ist und der Prozess nur auf den FALSE Wert reagiert, löscht das System automatisch das entsprechende Flow Interview.

Salesforce Community URL Settings

Ich habe mich in den letzten Tagen etwas ausführlicher mit Salesforce Communities in Kombination mit der API beschäftigt. Ein Problem dabei war, den richtigen Endpoint zu berechnen, wie im letzten Beitrag beschrieben API im Salesforce Partner Portal. Um die Weichen im Code für Community Benutzer einzubauen, muss während der Laufzeit berechnet werden, in welchem Context sich der aktuell eingeloggte Benutzer befindet. Dabei muss man sich zwangsweise mit den Fragen folgender Art beschäftigen: ist der eingeloggte Benuter ein Community Benutzer? ob und welche Community ist gerade aktiv? wie sieht die definierte Community URL aus? Antwort auf die Frage 1: private Boolean isCommunityUser(){         Boolean bIsCommunityUser = false;         String sUserType = UserInfo.getUserType();         sUserType = sUserType.toUpperCase();         if(sUserType == 'STANDARD')                 bIsCommunityUser = false;         if(sUserType == 'PARTNER')                  bIsCommunity

Bad value for restricted picklist field

Der Einsatz von "Restricted Picklists" bereitet spätestens im Deployment Kopfschmerzen. Basiert das Deployment auf Basis eines Drittanbietertools, dann sind die Kopfschmerzen noch intensiver. In meinem Fall habe ich versucht, ein neues Picklist-Feld mit Copado zu deployen. Während der Bereitstellung bekomme ich die folgende Fehlermeldung: System.DmlException: Insert failed. First exception on row 0; first error: INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST, bad value for restricted picklist field: Z012: [CountryGroup__c] Das neue Picklist-Feld übernimmt alle Werte aus einem Global Value Set. Das bedeutet, die Option "Restrict to the values defined in the value set" ist automatisch aktiv und lässt sich nicht deaktivieren. Eine APEX-Testklasse beschreibt ebenfalls die neue Pickliste. Mit dem folgenden Workaround konnte ich das Deployment-Problem lösen: 1) Global Value Set samt Pickliste per Changeset in die Zielorg übertragen und bereitstellen ggf. Prof