Here's my solution. We can't use sidebar hacks, but we can communicate between Visualforce pages, because they all reside on the same domain anyways. Here's my implementation:
Common Controller
You could split these in two, but this is only demonstrative, so I combined the two:
public with sharing class AccountExtension {
public string severity { get; set; }
public string summary { get; set; }
public string detail { get; set; }
public AccountExtension(ApexPages.StandardController controller) {
}
public void showMessage() {
Map<String, ApexPages.Severity> levels = new Map<String, ApexPages.Severity> {
'info' => ApexPages.Severity.INFO,
'warn' => ApexPages.Severity.WARNING,
'err' => ApexPages.Severity.ERROR,
'confirm' => ApexPages.Severity.CONFIRM,
'fatal' => ApexPages.Severity.FATAL
};
ApexPages.Severity level = levels.get(severity);
if(level == null) {
level = ApexPages.Severity.ERROR;
}
ApexPages.addMessage(new ApexPages.Message(level, summary, detail));
}
public void createError() {
severity = 'err';
summary = 'Quick Summary';
detail = 'This is where I would show an error message.';
}
}
Visualforce Page for Viewing Account
Here's the page to view an account. We use a button override so that they'll see this page simply by clicking on an account:
<apex:page standardController="Account" extensions="AccountExtension" showChat="true">
<apex:form >
<apex:pageMessages id="messages" showDetail="true"/>
<apex:actionFunction name="showMessage" action="{!showMessage}" reRender="messages">
<apex:param name="severity" value="" assignTo="{!severity}"/>
<apex:param name="summary" value="" assignTo="{!summary}"/>
<apex:param name="detail" value="" assignTo="{!detail}"/>
</apex:actionFunction>
<apex:detail inlineEdit="true" relatedList="true" relatedListHover="true" />
</apex:form>
<script>
function messageHandler(event) {
var data = JSON.parse(event.data);
console.log(event);
if(data.severity && data.summary && data.detail && event.origin == window.location.protocol+'//'+window.location.host) {
showMessage(data.severity, data.summary, data.detail);
}
}
window.addEventListener('message', messageHandler, true);
</script>
</apex:page>
Now, we specify a second page that can communicate to the first:
<apex:page standardController="Account" extensions="AccountExtension">
<apex:form id="form">
<apex:commandButton action="{!createError}" reRender="form" oncomplete="sendMessageUp()" value="Show Error on Parent" />
<apex:inputHidden value="{!severity}" id="severity"/>
<apex:inputHidden value="{!summary}" id="summary"/>
<apex:inputHidden value="{!detail}" id="detail"/>
</apex:form>
<script>
function sendMessageUp() {
var data = {
severity: document.getElementById('{!$Component.form.severity}').value,
summary: document.getElementById('{!$Component.form.summary}').value,
detail: document.getElementById('{!$Component.form.detail}').value
};
window.parent.postMessage(JSON.stringify(data), window.location.protocol+'//'+window.location.host+'/');
}
</script>
</apex:page>
Finally, we override the view button, add this second page to the layout, and observe.
This method would also work using sessionStorage and the storage events in HTML5. We could send data in one side, and it'd pop out on the other side for consumption. sessionStorage would have the benefit of letting us place an error-displaying page in one section of our page layout, and an error-generating page elsewhere, and they could communicate without knowing about each other directly.
Note that both solutions require a relatively new browser to work (IE 9, FF, Chrome, Opera, Safari...).