Tuesday, January 20, 2009

Submitting an IFramed Web App running within Microsoft CRM 4.0 - from CRM - a better approach

I published a post recently regarding submitting a ASP.NET application running within an IFRAME in CRM here.

This solution is ok but not brilliant.

I have found a better solution which is to call the click() method on a button in the ASP.NET form within the CRM Customization OnLoad event.

The advantage of this is we can call a specifc event handler - server side from CRM saving us the need to sumit the form on every postback - which could cause problems depending on how you have implemented your ASPX.

So how does CRM get a handle to the button? Well we render some JavaScript in a common class that each Web Form (that is interested in this behavior) inherits from. Calling this JavaScript returns a handle to the button for the specific form in question. Doing this saves us the need to duplicate code in every page.

So our base page class looks like this:
public abstract class CrmSupport : Page
{

protected CrmSupport()
{
Page.Load += OnPageLoad;
}

private void OnPageLoad(object sender, EventArgs e)
{

SaveButton.Attributes["style"] = "visibility:hidden";

if (SaveButton != null)
{
ClientScript.RegisterStartupScript(typeof (string), "saveButton",
@"<script language=javascript>function GetSaveButton(){return
document.getElementById('" + SaveButton.ClientID + "');}</script>");
SaveButton.Click += SaveButton_Click;
}
}

private void SaveButton_Click(object sender, EventArgs e)
{
OnSave();
}

protected virtual void OnSave()
{
//Do any common save process.
}

protected abstract Button SaveButton
{
get;
}
}
So the class is fairly light weight and there is no HTML, it's just a C# class. We have made it abstract and implemented a abstract property named SaveButton. You could make it default so instances where the save function is not required, but in this implementation it is mandatory for derived classes. In the derived class (the Web Form class) we implement the property like so:
public override Button SaveButton
{
get { return Save; }
}
Where Save is a reference to our button.

We also register a OnLoad event handler in the base form so that we can render some javascript at runtime. This javascript creates a function named GetSaveButton(). It will return an object to CRM so CRM customizations can call it. CRM needs to do this so that it can call the click() method on the button resulting in a server side Click event. We also set the style of the button to hidden. Note we must use styles as opposed to hidding the button so that we can get access to the ClientID property at runtime. Setting the server side visibility essentially removes it from the generated HTML.

Our web form HTML looks like the following:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="SampleInvokeButton._Default" %>


<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Button ID="Save" runat="server" Text="Button"/>
</div>
</form>
</body>
</html>
The code behind looks like so:
public partial class _Default : CrmSupport
{

protected override Button SaveButton
{
get { return Save; }
}

protected override void OnSave()
{
base.OnSave();
//Called from CRM to Save data. Most likely call our service layer passing a DTO.
}
}
So here we have our overrides property that returns an instance of our button to service our base forms request when it asks for it.

We have hooked up the OnSave event handler (server side) event in the base form that gets called when the button is clicked. Each implementation can override this event as desired. This gives the opportunity to process some common application logic for each page without haveing to duplicate this logic in each page.

Now all that remains is the javascript CRM side and all that is required is to place the following script in the OnSave event in the CRM customizations for the entity you are interested in where iframeName is the name of your iframe:
var button = document.getElementById(iframeName).contentWindow.GetSaveButton();
if (button != undefined)
button.click();
else
alert("Save is not permitted");
So CRM is driving the save here. We could, if we wanted to, let the form handle the click event by making the javascript more coarse grained ASPX end and finer CRM end - this is a debatable subject though. I opted for CRM doing most of the processing.

10 comments:

pen_2 said...

Code is not working...first and maybe not last error is here:

protected override Button SaveButton
{
get { return Save; }
}

What is 'Save'? Should it be ID of your button?

asp:Button ID="SaveButton" runat="server" Text="Button" OnClick="OnSave"

Your ID is 'SaveButton'...
How does OnSave event works without object sender and EventArgs e?

Please check it and correct me or your post.

Simon Hart said...

@pen_2:

Well spotted. The name of the button should be "Save" and also the OnInit in the derived class doesn't need to register a event handler as we are overriding the super class and the super class calls its implemented type.

Also...there is no need to register an event handler in the ASPX code as this is also done in the super class.

I've updated the post to reflect these changes.

--
Simon.

pen_2 said...

Thank you Simon for quick response and explaining whole thing. I have one more question for you. In what case can you get "Save is not permitted" popup? I've allowed XSS for my IFRAME but I'm getting this alert everytime I hit save button. It is really strange because while debugger is in GetSaveButton() function, document.getElementById('Save') really returns proper object but after debugger stepsout this funtion it seems that GetSaveButton() returned undefined object. If it's important I'm using CRM 4.0 with update rollup 2.

Simon Hart said...

@pen_2:

Have you set the IFrame url domain name the same as CRM host name. IE;

IFame might be on your local machine: http://localhost/Webapp/default.aspx

When navigating to CRM ensure you use localhost in this case. Using the NETBIOS name ie MYMACHINENAME will fail. This is due to cross scripting in IE. Because essentially you are calling a separate website from within CRM dynamics.

So if you are using load balancing ensure the iFrame is pointing to the load balanced URL.

Apart from that I can't think of anything else to try. It is working here ok on CRM 4.0 SP1.

--
Simon.

pen_2 said...

I have CRM on single server with IIS dedicated for CRM web app. My aspx page is in ISV folder (assembly in bin) and I'm using local paths. Only workaround that I have now is calling window.frames("IFRAME_NAME").document.getElementById(SaveButtonID);
in OnSave event - it returns proper object and I can call 'click'. I will install update rollup 3...maybe it will help. Anyway thanks for good article and help.

Anonymous said...

Can you please help?

your code seems to work perfectly except in the following scenario.

- i have an iframe inside a new tab section of the CRM form.

- if the tab and the iframe is loaded the code works. But if the tab is never loaded and i make changes to a general tab and try to save it, throws an error.

in line frame.GetSaveButton();

object doesnt support this method or property

Simon Hart said...

@Anon:

If the iFrame hasn't been loaded then you won't be able to call the javascript as it won't exist.

A work around to this perhaps is you'll need to check if you can query an object that you know exisits within the iFrame. If your query comes back as undefined, then you'll know the object doesn't exist.

SA123 said...

Hi,
I'm trying to get this code to work but when I add 'OnClick="OnSave"' to my button and build my site, I get the error "No overload for 'OnSave' matches delegate 'System.EventHandler'". Any help will be appreciated. Thanks.

Simon Hart said...

@SA123:

Could you please post your problem code, I'm not sure I understand you correctly.

Cheers
Simon

SA123 said...
This comment has been removed by the author.