Archive for March, 2010

AJAX Accordion OutOfMemoryException

Posted: 29th March 2010 in AJAX, JQuery, WCF

Recently I came across a problem with the ASP.NET AJAX accordion control which caused my machine to have a System.OutOfMemoryException. This was caused by an attempt to bind a large result set returned from a WCF service  to the accordion control. The error never directly specified out of memory, but instead display the following webpage error: “Could not get the value property. Not enough storage is available to complete this operation”

image

This is actually a SOAP error, but its effectively the same thing. There is simply too much data for the machine to cope.

After some investigation I found that this problem was down to the fact that the accordion control not only loads the header detail for the bound list, but all the content detail too. In my case, the content control contained a lot of dropdown lists.  Firstly, I thought about implementing paging, but after consultation with the client I soon discovered that this was not an option.

So, to allow me to load the full list I had to change how the master and detail worked. Here’s how I went about it.

Firstly, I created a user control for the detail to be displayed and instead of loading the control in the html of the accordion content template I added a place holder as shown in Figure 1.

Figure 1
  1. <ContentTemplate>
  2. <asp:PlaceHolde

Now, I have to load this control dynamically. However, I only want to load it for the first item of the accordion. To do this I used the OnItemDataBound event of the accordion as can be seen below in Figure 2.

Figure 2
  1. protected void CircuitAccordion_ItemDataBound(object sender, AccordionItemEventArgs e)
  2. {
  3. if (e.ItemType != AccordionItemType.Content || ((Accordion) sender).Panes.Count > 1)
  4. {
  5. return;
  6. }
  7. var circuitDto = e.Item as CircuitDto;
  8. if (circuitDto == null)
  9. {
  10. return;
  11. }
  12. var circuitDetail = (CircuitDetail)LoadControl(“UserControls/CircuitDetail.ascx”);
  13. circuitDetail.Circuit = circuitDto;
  14. var circuitUserControlPlaceHolder =
  15. e.AccordionItem.FindControl(“circuitUserControlPlaceHolder”) as PlaceHolder;
  16. if (circuitUserControlPlaceHolder == null)
  17. {
  18. return;
  19. }
  20. circuitUserControlPlaceHolder.Controls.Add(circuitDetail);
  21. }

Great, now the first item loads, but not the rest.  To get the other items to load I needed to use a little jquery and a AJAX enabled WCF service (to get the data for each item).

To override the functionality of the accordion control I first need to create a client side event handler for  the on selected index changed event. This can be seen in figure 3 and attach the handler on the page load figure 4

Figure 3
  1. // handles the event which fires on the accordion selected index changing
  2. function onAccordionSelectedIndexChanged(sender, eventArgs) {
  3. var contentTemplate = $(‘#’ + sender._id.replace(‘_AccordionExtender’, ‘_Pane_’ + sender.get_SelectedIndex() + ‘_content’));
  4. contentTemplate.append($(“fieldset[id*=’DetailsControl’]:visible”));
  5. var headerTemplate = sender._id.replace(‘_AccordionExtender’, ‘_Pane_’ + sender.get_SelectedIndex() + ‘_header’);
  6. var id = $(‘#’ + headerTemplate + ‘_idHiddenTextBox’).val();
  7. var tabContainer = $get(‘<%=locationTabContainer.ClientID%>’).control;
  8. loadDetailControlData(tabContainer.get_activeTab().get_headerText(), id);
  9. }

In figure 3 I perform the following steps:

  • Line 3 – I get reference to the content template of the header which has been clicked.
  • Line 4 – I append the visible details user control to the associated content template of the selected index of the header template clicked.
  • Line 5 – Get reference to the header template to allow access to the hidden textbox control.
  • Line 6 – Retreive the value of the primary key, which is in a hidden textbox control named idHiddenTextbox in the header template pane.
  • Line 7- Get reference to the selected tab
  • Line 8 –  Pass the id and the selected tab control to the loadDetailControlData function in figure 5

To add the event handler to the control I use the javascript pageLoad() function as can be seen in figure 4.

Figure 4
  1. function pageLoad() {
  2. $find(‘<%= devicesAccordion.ClientID %>’ + ‘_AccordionExtender’).add_selectedIndexChanged(onAccordionSelectedIndexChanged);
  3. $find(‘<%= cabinetsAccordion.ClientID %>’ + ‘_AccordionExtender’).add_selectedIndexChanged(onAccordionSelectedIndexChanged);
  4. $find(‘<%= endBCircuitsAccordion.ClientID %>’ + ‘_AccordionExtender’).add_selectedIndexChanged(onAccordionSelectedIndexChanged);
  5. $find(‘<%= endACircuitsAccordion.ClientID %>’ + ‘_AccordionExtender’).add_selectedIndexChanged(onAccordionSelectedIndexChanged);
  6. $find(‘<%= nonNetworkDevicesAccordion.ClientID %>’ + ‘_AccordionExtender’).add_selectedIndexChanged(onAccordionSelectedIndexChanged);
  7. }

In this particular situation, I have a tab control which, in turn, contains the accordion controls. So, I created a JavaScript function to load the detail control data dependent on the selected tab, seen in figure 5 which call the WCF service shown in figure 6 and ultimately set the field values from the service response seen in figure 7.

figure 5
  1. function loadDetailControlData(tabHeaderText, id) {
  2. switch (tabHeaderText) {
  3. case ‘Cabinets’:
  4. getCabinet(id);
  5. break;
  6. case ‘Devices’:
  7. getDevice(id);
  8. break;
  9. case ‘End A Circuits’:
  10. case ‘End B Circuits’:
  11. getCircuit(id);
  12. break;
  13. case ‘Non-Network Devices’:
  14. getNonNetworkDevice(id);
  15. break;
  16. }
  17. }

Figure 6 shows the call to the WCF service using jquery. This function is called from the function above in Figure 5.

Figure 6
  1. function getCircuit(circuitId) {
  2. $.ajax({
  3. url: http://localhost:10785/LocationServiceAjax.svc/GetCircuit&#8221;,
  4. type: “POST”,
  5. contentType: “application/json”,
  6. data: ‘{“circuitId”:’ + circuitId + ‘}’,
  7. dataType: “json”,
  8. success: function(data) {
  9. setCircuitDetail(data.d);
  10. }
  11. });
  12. }

Seen below is the call-back function to set the control values of the details form which are returned from the WCF Ajax response seen in figure 6

Figure 7
  1. // Sets the circuit detail form callback from the ajax wcf call.
  2. function setCircuitDetail(circuit) {
  3. if (circuit == null) { alert(“Something is not configure correctly.”); return; }
  4. $(“input[id$=’circuitReferenceTextBox’]:visible”).val(circuit.CircuitReference);
  5. $(“select[id$=’circuitUsageDropDownList’]:visible”).val(circuit.Usage.Description);
  6. $(“select[id$=’circuitTypeDropDownList’]:visible”).val(circuit.Type.Name);
  7. $(“select[id$=’circuitOwnerDropDownList’]:visible”).val(circuit.Owner.Name);
  8. $(“select[id$=’circuitStatusDropDownList’]:visible”).val(circuit.Status.Status);
  9. $(“select[id$=’endACircuitLocationDropDownList’]:visible”).val(circuit.EndALocationCode);
  10. $(“select[id$=’endBCircuitLocationDropDownList’]:visible”).val(circuit.EndBLocationCode);
  11. }

Hopefully this post helps someone as it took me a while to work it out… 🙂

Advertisements

Scenario: You have build a user control with a button and now you wish to embed the control onto you page. However, you want to handle the click event of the button on the main page, not in the control.

Solution: To do this, you need to expose the event handler of the user control button’s click event and wire it up to internal working of the user control click event handler/delegate.

This can be done using the following code extracts:

First, add the control to the user control

Adding the button
  1. <%@ Control Language=”C#” AutoEventWireup=”true” CodeBehind=”EventHanderClick.ascx.cs” Inherits=”UserControlEventHandlerTest.EventHanderClick” %>
  2. <asp:Button ID=”Button1″ runat=”server” Text=”Button” OnClick=”AddNewButton_Click”/>

Next, we need to expose the events in the code behind of the user control.

User Control Code Behind
  1. protected void Page_Load(object sender, EventArgs e)
  2. {
  3. if (!string.IsNullOrEmpty(this.OnAddNewClientClick))
  4. this.Button1.Attributes.Add(“onclick”, string.Format(“{0}; return false;”, this.OnAddNewClientClick));
  5. }
  6. public string OnAddNewClientClick
  7. {
  8. get;
  9. set;
  10. }
  11. private static readonly object AddNewClickEvent = new object();
  12. public event EventHandler AddNewClick
  13. {
  14. add
  15. {
  16. Events.AddHandler(AddNewClickEvent, value);
  17. }
  18. remove
  19. {
  20. Events.RemoveHandler(AddNewClickEvent, value);
  21. }
  22. }
  23. protected void AddNewButton_Click(object sender, EventArgs e)
  24. {
  25. OnAddNewButtonClick(EventArgs.Empty);
  26. }
  27. protected virtual void OnAddNewButtonClick(EventArgs e)
  28. {
  29. var addNewEventDelegate =
  30. (EventHandler)Events[AddNewClickEvent];
  31. if (addNewEventDelegate != null)
  32. addNewEventDelegate(this, e);
  33. }

It can also be seen, above, that an OnAddNewClientClick property has been exposed as a string. This is to allow you to wire up a JavaScript function. Notice too, I have used string.Format to add the onclick attribute to the control and concatenated return false; onto the end. This is to prevent the control from posting back, if the on client click event is being used.

Now, we can go to the main page and add the newly defined user control.  If you now go to the source view you will notice that both events have been exposed and are picked up in the intellisense

User Control Source
  1. <uc1:ClickEventTestControl ID=”ClickEventTestControl1″ runat=”server” OnAddNewClick=”UserControlButtonEvent” OnAddNewClientClick=”alert();” />
User Control Event
  1. protected void UserControlButtonEvent(object sender, EventArgs e)
  2. {
  3. this.Label1.Text = “Fired by the user control click event”;
  4. }

Now, when you  click the button on the user control, which has been added to the page, the code on the page is used.