I have noticed quite a few posts/projects out there that focus on using ASP.NET AJAX within custom MOSS web parts. Mike Ammerlaan has a great post on some of the background around AJAX and MOSS
This post describes what my team and I have been using for the past couple of months. It is a simple base class called AjaxBasePart.
Background:
I developed a .NET class in early November that allowed my team to take advantage of the MS AJAX Extensions from custom MOSS web parts. There were a number of problems that I did not resolve on my own though.
For the next month or so I worked on and off with the AJAX Extensions and MOSS product teams and with our powers combined we were able to come up with the AjaxBasePart class that custom MOSS web parts can derive from and fully enables all AJAX Extensions functionality within MOSS.
Very Simple Setup:
There is no need to modify master pages after the AJAX Extensions have been installed and the web.config is setup your web parts will manage the registration of a shared ScriptManager or in the case that one exists use that. If I'm missing anything please notify me in the comments and I will update.
- Download and install ASP.NET 2.0 AJAX Extensions 1.0
- Configure your MOSS web.config to support ASP.NET AJAX 1.0
Mike's post covers most of the configuration you need to support AjaxBasePart within your own MOSS environment. I've listed below a couple of points that you should be aware of when reading his article if you are using the AjaxBasePart- You can skip the "Adding a ScriptManager into a SharePoint MasterPage" section. No modification of the .master page is necessary with AjaxBasePart
- Leave the EnsureUpdatePanelFixups method out of your custom web parts, that is handled by the AjaxBasePart.
- Add the AjaxBasePart class (at the end of this post) to your solution and derive all custom web parts from CapDes.SharePoint.AjaxBasePart instead of Microsoft.SharePoint.WebPartPages.WebPart.
Support:
If you are looking for support I will do my best to assist via the comments on this post. This is in no way supported by Microsoft.
Credits:
The following Microsoft employees assisted me in the development of the AjaxBasePart. Thanks guys!
Bonus:
You will also notice a public RegisterError method on the AjaxBasePart. I find it to be a clean way to show users web part related error messages in a common way.
To use all you need to do is; from your custom web part call the RegisterError message and pass the string that you would like displayed to the user.
base.RegisterError("Some error message.");
Code:
using System;
using System.Xml.Serialization;
using System.Web.UI.WebControls;
using System.Web.UI;
using System.Drawing;
using System.Web;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebPartPages;
namespace CapDes.SharePoint
{
/// <summary>
/// AjaxBasePart allows Microsoft ASP.NET AJAX Extensions to work within Microsoft Office SharePoint Server 2007 webparts.
/// </summary>
[XmlRoot(Namespace = "CapDes.SharePoint.AjaxBasePart")]
[CLSCompliant(false)]
public abstract class AjaxBasePart : WebPart
{
private string _ValidationGroupId;
private ValidationSummary _ErrorContainer;
private ScriptManager _AjaxManager;
/// <summary>
/// Exposes the Page's script manager. The value is not set until after OnInit
/// </summary>
[WebPartStorage(Storage.None)]
public ScriptManager AjaxManager
{
get { return _AjaxManager; }
set { _AjaxManager = value; }
}
/// <summary>
/// Oninit fires before page load. Modifications to the page that are necessary to support Ajax are done here.
/// </summary>
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
//get the existing ScriptManager if it exists on the page
_AjaxManager = ScriptManager.GetCurrent(this.Page);
if (_AjaxManager == null)
{
//create new ScriptManager and EnablePartialRendering
_AjaxManager = new ScriptManager();
_AjaxManager.EnablePartialRendering = true;
// Fix problem with postbacks and form actions (DevDiv 55525)
Page.ClientScript.RegisterStartupScript(typeof(AjaxBasePart), this.ID, "_spOriginalFormAction = document.forms[0].action;", true);
//tag:"form" att:"onsubmit" val:"return _spFormOnSubmitWrapper()" blocks async postbacks after the first one
//not calling "_spFormOnSubmitWrapper()" breaks all postbacks
//returning true all the time, somewhat defeats the purpose of the _spFormOnSubmitWrapper() which is to block repetitive postbacks, but it allows MS AJAX Extensions to work properly
//its a hack that hopefully has minimal effect
if (this.Page.Form != null)
{
string formOnSubmitAtt = this.Page.Form.Attributes["onsubmit"];
if (!string.IsNullOrEmpty(formOnSubmitAtt) && formOnSubmitAtt == "return _spFormOnSubmitWrapper();")
{
this.Page.Form.Attributes["onsubmit"] = "_spFormOnSubmitWrapper();";
}
//add the ScriptManager as the first control in the Page.Form
//I don't think this actually matters, but I did it to be consistent with how you are supposed to place the ScriptManager when used declaritevly
this.Page.Form.Controls.AddAt(0, _AjaxManager);
}
}
}
/// <summary>
/// Needs to be called to ensure that the ValidationSummary control is registered on the page. Any child web parts will need to have base.CreateChildControls() at the top of their own CreateChildControls override.
/// </summary>
protected override void CreateChildControls()
{
base.CreateChildControls();
if (!this.Controls.Contains(_ErrorContainer))
{
_ValidationGroupId = Guid.NewGuid().ToString();
_ErrorContainer = new ValidationSummary();
_ErrorContainer.ID = "_ErrorContainer";
_ErrorContainer.ValidationGroup = _ValidationGroupId;
_ErrorContainer.BorderStyle = BorderStyle.Solid;
_ErrorContainer.BorderWidth = Unit.Pixel(3);
_ErrorContainer.BorderColor = Color.Red;
this.Controls.Add(_ErrorContainer);
}
}
/// <summary>
/// Used to provide a common way to display errors to the user of the current web part.
/// </summary>
/// <param name="message">Description of the error that occured.</param>
public void RegisterError(string message)
{
if (this.Controls.Contains(_ErrorContainer))
{
//this way of generating a unique control id is used in some of the OOB web parts
int uniqueCounter;
if (HttpContext.Current.Items["GetUniqueControlId"] != null)
{
uniqueCounter = (int)HttpContext.Current.Items["GetUniqueControlId"];
}
else
{
uniqueCounter = 0;
}
uniqueCounter++;
HttpContext.Current.Items["GetUniqueControlId"] = uniqueCounter;
//create a custom validator to register the current error message with the ValidationSummary control
CustomValidator cv = new CustomValidator();
cv.ID = string.Concat("_Error_", uniqueCounter);
cv.ValidationGroup = _ValidationGroupId;
cv.Display = ValidatorDisplay.None;
cv.IsValid = false;
cv.ErrorMessage = message;
this.Controls.Add(cv);
}
else
{
//if RegisterError is called before the CreateChildControls override in AjaxBasePart then transfer the user to an error page using the SPUtility
SPUtility.TransferToErrorPage("The CreateChildControls function of the AjaxBasePart has not been called. You probably need to add \"base.CreateChildControls()\" to the top of your CreateChildControls override.");
}
}
}
}
This is great work! One question - Are there any dependencies on MOSS or will this work with WSS as well. Thanks.
Posted by: Nick Christoff | 02/24/2007 at 04:53 PM
It should work fine with WSSv3. I am pretty sure there are no dependencies, but I have not personally tested it. Please post a followup comment if it works so others can benefit.
Thanks!
Posted by: Eric Schoonover | 02/24/2007 at 05:26 PM
Dear Eric,
I get the following error:
'Microsoft.SharePoint.Utilities.SPUtility' does not contain a definition for 'TransferToErrorPage'
Could you please help?
Posted by: Lilia | 02/26/2007 at 03:13 PM
Lilia,
Interesting, are you using MOSS or just WSSv3? I haven't run into this. One way to avoid that error would be to remove the functionality described in the "Bonus" section.
Delete from the AjaxBasePart.cs file the following items:
RegisterError method
CreateChildControls override
_ErrorContainer and _ValidationGroupId private variables
Let me know if that works for you.
Thanks!
Posted by: Eric Schoonover | 02/26/2007 at 08:10 PM
Hey Eric! Rember me? I used to work at VastNetworks with Derek B. Anyhow I was surfin the WSS blogs one day and found you on here. Its kind of funny that we are both SharePoint Devs! I've been running a blog too (sharethispoint.com), its pretty fun. Well I just wanted to say hi and let you know that this Ajax base part you did is pretty sweet. I've built a sample project that uses it to display a SPGridView in an UpdatePanel. Heres the link if you wanna check it out..
http://sharethispoint.com/archive/2007/02/28/Using-a-SPGridView-inside-an-ASP.net-Ajax-UpdatePanel.aspx
Hit me up on msn some time.
Take care,
Mark Collins
Posted by: Mark Collins | 03/01/2007 at 01:03 AM
Mark,
Good to hear from you, great example on your blog. Glad you are finding the AjaxBasePart useful.
I definately remember when you worked at VastNETWORKS. I also remember paintball and the Mountain Dew statue. Good times :)
Posted by: Eric Schoonover | 03/01/2007 at 10:47 PM
This is great! Works like a charm for an update panel. However, I'm not as successful in getting the Ajax Timer working (inside or outside an update panel). On its tick event it is throwing an, "Object reference not set to an instance of an object." exception. I've traced this back to a javascript error. I hypothisize that MOSS's own javascript is interfearing w/ the timer's javascript. Do you have any suggestions? Thanks!
Posted by: Justin | 03/08/2007 at 03:29 PM
The timer control works fine for me. That was actually the main reason for doing this work... so that I could create an UpdatePanel that is triggered by a Timer.
One of the main things that you will need to remember is to set your controls ID property.
Here is a small snippet from a sample I have, sorry that I don't have time to package it up for you (below class is incomplete):
public class TestWP : AjaxBasePart
{
private Timer tmr_RefreshTime;
private UpdatePanel up_ServerTime;
private UpdatePanel up_Grid;
protected override void CreateChildControls()
{
base.CreateChildControls();
tmr_RefreshTime = new Timer();
tmr_RefreshTime.ID = "tmr_RefreshTime";
tmr_RefreshTime.Enabled = true;
tmr_RefreshTime.Interval = 2500;
AsyncPostBackTrigger astrig_tmr_RefreshTime = new AsyncPostBackTrigger();
astrig_tmr_RefreshTime.ControlID = "tmr_RefreshTime";
up_ServerTime = new UpdatePanel();
up_ServerTime.ID = "up_ServerTime";
up_ServerTime.UpdateMode = UpdatePanelUpdateMode.Conditional;
up_ServerTime.ContentTemplate = new ServerTimeUpdateTemplate();
up_ServerTime.Triggers.Add(astrig_tmr_RefreshTime);
up_ServerTime.Load += new EventHandler(up_ServerTime_Load);
this.Controls.Add(tmr_RefreshTime);
this.Controls.Add(up_ServerTime);
up_Grid = new UpdatePanel();
up_Grid.ID = "up_Grid";
up_Grid.ChildrenAsTriggers = true;
up_Grid.UpdateMode = UpdatePanelUpdateMode.Conditional;
up_Grid.ContentTemplate = new GridUpdateTemplate();
up_Grid.Load += new EventHandler(up_Grid_Load);
this.Controls.Add(up_Grid);
}
}
Posted by: Eric Schoonover | 03/08/2007 at 07:35 PM
Thanks Eric. This helped a lot. I did have to change one line:
astrig_tmr_RefreshTime.ControlID = "tmr_RefreshTime";
because "tmr_RefreshTime" wasn't found in the client script to:
astrig_tmr_RefreshTime.ControlID = tmr_RefreshTime.ClientID;
Thanks again! Justin
Posted by: Justin | 03/09/2007 at 01:45 PM
Justin, thanks for following up so that others can benefit from your learning. Glad you were able to get it working.
Posted by: Eric Schoonover | 03/09/2007 at 05:49 PM
Could this be written as a normal asp.net web part instead of a sharepoint web part?
Posted by: kman | 04/10/2007 at 03:50 PM
kman,
There is some code in the AjaxBasePart that is specific to a MOSS implementation (around enabling postbacks).
The idea of a web part that uses a ScriptManager that exist on the page or creates one if none exists should be portable to normal ASP.NET web parts. I would recommend other methods though... such as ensuring that you don't have to dynamically place a ScriptManager.
Posted by: Eric Schoonover | 04/11/2007 at 07:27 AM
Of course, I read this article after I had just finished modifying 95% of my MasterPages in SharePoint.
I already had created a base class that did most of this functionality. I added a virtual property to set the padding on the control, and a few other things too
Posted by: Eric | 07/12/2007 at 05:08 PM
Eric,
Tried out your AjaxBasePart with a SP GridView Web Part and worked fine when inserting WebPart in standard Sharepoint administrative default page. However, when inserting it in an SP Designer aspx page in a WebPartPages:WebPartZone, it gave me the following error: "Cannot create an object of type 'System.Web.UI.ScriptManager' from its string representation for the 'AjaxManager property'". When inserting another standard WebPart into the same page there was no problem and the control worked okay. Have you seen this before and do you have any suggestions?
Thanks,
Frank
Posted by: Frank | 07/13/2007 at 10:04 PM
I was able to get everything wired up no problems into an existing SharePoint site. However, when I come to use an AJAX control (ie. calendar) into one of the pages, I receive the following error:
System.Web.HttpException: The Controls collection cannot be modified because the control contains code blocks (i.e. ).
With the following stack trace:
The Controls collection cannot be modified because the control contains code blocks (i.e. ).]
System.Web.UI.ControlCollection.Add(Control child) +2114095
AjaxControlToolkit.ScriptObjectBuilder.RegisterCssReferences(Control control) +371
AjaxControlToolkit.ExtenderControlBase.OnPreRender(EventArgs e) +163
System.Web.UI.Control.PreRenderRecursiveInternal() +77
System.Web.UI.Control.PreRenderRecursiveInternal() +161
System.Web.UI.Control.PreRenderRecursiveInternal() +161
System.Web.UI.Control.PreRenderRecursiveInternal() +161
System.Web.UI.Control.PreRenderRecursiveInternal() +161
System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +1360
Any ideas how to fix this problem?
Posted by: Ammar | 07/16/2007 at 10:33 PM
I'm trying to use the AjaxControlToolkit in a MOSS 2007 web part and it almost works... I'm using the tabcontrol and even though I've also tried using the class you provided I get the same results. My tabs are cut off on each tab panel on the bottom. I can't figure out why. Any thoughts would be helpful. Below is my code:
//snipped by Admin
Posted by: Steve | 07/21/2007 at 01:49 AM
Frank, that may be a problem with dropping an AjaxBasePart based control on a page using SharePoint Designer. It should be fairly easy to tweak the AjaxBasePart class to be aware of when it is being rendered to the page in design mode.
If you do and have an opportunity to post your modification on the internet somewhere or send it to me so that I can post it for you that would be great.
Thanks!
Posted by: Eric Schoonover | 07/24/2007 at 05:22 AM
Steve, I have not used the tab control within SharePoint using the AjaxBasePart but I have used other controls in the tool kit. My gut feeling is that you are having problems with styles being applied correctly (CSS).
Posted by: Eric Schoonover | 07/24/2007 at 05:23 AM
Ammar, I have not seen that error before. Let me know if you identify the problem and find a solution.
Posted by: Eric Schoonover | 07/24/2007 at 05:25 AM
Hi,
I have been trying to use this with sharepoint and webparts. Unfortunately, the ajaxbasepart doesnt seem to add the scriptmanager - during OnInit the Page.Form seems to be null.
i have alsoi been trying to use updatepanel with timer, would this even work? (i added scriptmanager to the masterpage to test this, dident quite work).
Posted by: Taavik | 10/10/2007 at 03:18 PM
hi...i copied the AjaxBasePart class to my solution ..i am getting the error "the type or namespace name "ScriptManager" could not be found"...pls tell me what is the solution
Posted by: vikas | 02/02/2008 at 08:13 AM