如何通过扩展控制器将Visualforce生成的PDF保存至商机页面文件
问题分析与解决方案
核心问题
报错“QuotePDFSaveExt(ApexPages.StandardController ctrl)存在空引用”的原因是:生成PDF的Visualforce页面使用了自定义控制器QuoteOppProductCtrl,但同时指定了扩展QuotePDFSaveExt,而该扩展的构造函数仅支持StandardController类型。当访问PDF页面时,系统无法提供StandardController实例,导致ctrl为空,触发空引用错误。
另外,现有保存逻辑中直接调用pdfPage.getContentAsPDF()会再次触发PDF页面的action="{!generateAndStoreQuoteNumber}",可能导致重复生成报价编号,需要优化。
修复步骤
1. 调整生成PDF的Visualforce页面
移除不必要的扩展,因为该页面依赖自定义控制器,不需要QuotePDFSaveExt扩展:
<apex:page controller="QuoteOppProductCtrl" applyHtmlTag="false" showHeader="false" renderAs="PDF" action="{!generateAndStoreQuoteNumber}"> ----------------the body of the content of the PDF is here ------------ </apex:page>
2. 修正扩展控制器QuotePDFSaveExt
- 确保构造函数仅在保存页面(使用
StandardController)时生效 - 增加空值校验,防止商机ID为空
- 优化PDF存储的命名规则,更易识别
public with sharing class QuotePDFSaveExt{ private final Opportunity opp; // 仅支持StandardController构造,对应保存用的VF页面 public QuotePDFSaveExt(ApexPages.StandardController ctrl) { // 直接查询商机记录,同时校验ID是否有效 if(ctrl.getId() != null){ opp = [SELECT Id, Name FROM Opportunity WHERE Id = :ctrl.Id LIMIT 1]; } else { ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, '无法获取商机ID,请从商机页面操作')); } } public PageReference savePDF() { PageReference ret = null; // 校验商机是否存在 if(opp == null){ return ret; } try { // 构造PDF页面引用,传递商机ID PageReference pdfPage = Page.QuoteOppProductVfp; pdfPage.getParameters().put('Id', opp.Id); // 获取PDF内容,触发PDF页面的action方法生成报价编号 Blob pdfBlob = Test.isRunningTest() ? Blob.valueOf('Test PDF Content') : pdfPage.getContentAsPDF(); // 创建ContentVersion存储PDF ContentVersion cv = new ContentVersion( Title = '报价单_' + opp.Name + '_' + opp.Id, PathOnClient = '报价单_' + opp.Name + '_' + opp.Id + '.pdf', VersionData = pdfBlob, FirstPublishLocationId = opp.Id ); insert cv; // 查询对应的ContentDocument,跳转到文件详情页 ContentDocument doc = [SELECT Id FROM ContentDocument WHERE LatestPublishedVersionId = :cv.Id LIMIT 1]; ret = new ApexPages.StandardController(doc).view(); } catch (Exception e) { ApexPages.addMessages(e); } return ret; } }
3. 保留保存用的Visualforce页面(仅优化样式)
给iframe添加宽高样式,提升预览体验:
<apex:page standardController="Opportunity" extensions="QuotePDFSaveExt"> <apex:pageMessages /> <apex:form > <apex:pageBlock title="预览并保存报价单"> <apex:pageBlockButtons > <apex:commandButton value="保存报价单" action="{!savePDF}" /> </apex:pageBlockButtons> <apex:iframe src="{!URLFOR($Page.QuoteOppProductVfp, null, [id=Opportunity.Id])}" scrolling="true" style="width:100%;height:800px;" /> </apex:pageBlock> </apex:form> </apex:page>
4. 优化自定义控制器QuoteOppProductCtrl(必填)
确保控制器能正确从URL参数获取商机ID,避免空引用:
public with sharing class QuoteOppProductCtrl { public Date Today { get { return Date.today(); } } public Opportunity opportunity { get; set; } public String shipToAccountName { get; set; } public String prosthetistName { get; set; } public String fittingProsthetistId { get; set; } public String prosthetistEmail { get; set; } public Decimal subtotal { get; set; } public Decimal quoteTotal { get; set; } public String quoteNumber { get; set; } public QuoteOppProductCtrl() { // 从URL参数获取商机ID String oppId = ApexPages.currentPage().getParameters().get('Id'); if(oppId != null){ opportunity = [SELECT Id, Name, Account.Name FROM Opportunity WHERE Id = :oppId LIMIT 1]; // 其他初始化逻辑(比如汇总产品、生成报价编号)... } else { ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, '未传入有效商机ID')); } } // generateAndStoreQuoteNumber方法实现... }
操作流程
- 从商机页面点击自定义按钮,跳转到保存用的Visualforce页面
- 页面通过iframe自动加载并预览PDF内容
- 点击「保存报价单」按钮,系统将PDF存储为商机关联的文件,并自动跳转到文件详情页
内容的提问来源于stack exchange,提问作者amyverghese
相关产品推荐
相关产品推荐

