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

Community Builder funktioniert nicht

Nachdem ich eine in einer Sandbox konfigurierte und getestete Lightning Community per Changeset auf die Produktion übertrage und bereitgestellt hatte, stellte ich fest, dass sich die Community nicht konfigurieren lässt. Das Deployment lief fehlerfrei durch. Alle Komponenten der Lightning Community sind verfügbar. Jedoch erscheint die Fehlermeldung Cannot read property 'def' of undefined sobald ich auf den "Builder" - Link klicke. Folgendes Workaround löst das Problem: 1. Go to "All Communities" and click on "Workspaces" beside the problematic community. 2. Go to "Administration | Pages" and click on "Go to Site.com Studio". 3. Once site.com studio has finished loading, click on the "Site Actions" icon (small cog in top right of screen), and select "Export This Site". 4. When prompted, specify a local location to save the site export, and wait for the file download to complete. 5. After the d...

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(sUserTyp...

Crazy SOQL

Genauso habe ich heute geschaut, als ich den folgenden Code ausgeführt und das Ergebnis ausgewertet habe: CustomObj__c obj = [select LookupField__c from CustomObj__c where LookupField__c != NULL AND Id = 'hereisavalidid']; system.debug(' LookupField__c darf nicht NULL sein '); if(obj.LookupField__c == null){     system.debug(' Also doch NULL '); } Und was sehen meine müde Augen im Log... LookupField__c ist ein Lookup- und Pflichtfeld, somit darf eigentlich per Definition nicht NULL sein. Offensichtlich gibt es (alte) Daten im System mit dem  LookupField__c = NULL Habe erwartet, dass die SOQL Abfrage die NULL-Daten filtert.