ASP.NET custom control: when is LoadPostData() called?
Asked Answered
A

3

8

I have developed a custom control that extends ListBox. The idea is that the control 'remembers' modifications to its elements which occurred client-side, e.g. as a result of an AJAX request.

The way it works is that the control also renders a hidden input, and the result of the AJAX request is stored in the hidden input. This is posted back, and the control's LoadPostData() method looks for the hidden input, and if the hidden input has data, creates the ListItem collection from it.

This works perfectly so long as the user has made a selection from the list box. If they have not, the LoadPostData() method doesn't get called, and consequently the new ListItem collection is not created. (I've established this using the debugger.)

I assume that the LoadPostData method is only called if the POST data collection includes data corresponding to the control's UniqueID (i.e. 'name' attribute in HTML). If the user hasn't made a selection from the list box, nothing is included in the post data for the list box's UniqueID and LoadPostData() isn't called. Is that correct?

Can anyone suggest how I can ensure that my custom ListBox's LoadPostData() method is called every postback regardless of whether the user has made a selection?

Thanks in advance - I'm really stuck with this one.

David

Angelinaangeline answered 9/7, 2010 at 9:0 Comment(0)
A
1

I've established that the LoadPostData() method is not called unless the post data contains an item with the same name as the control's UniqueID. [Edit: calling Page.RegisterRequiresPostback during Init() overcomes this.] I can see why, but it is quite limiting.

I have overcome the problem by not handling it during the LoadPostData() method at all. Instead, I have handled it in a method which I call in OnLoad() instead.

Two things need to be borne in mind when using this approach:

1) You no longer have access to the postCollection NameValueCollection object which is passed in to the LoadPostData() method as an argument. This means you have to extract the post data from the Request.Form collection, which is slightly harder work. 2) Since OnLoad() occurs after the ViewState processing code, you will need to manually set the SelectedValue after you create the ListItems. If you don't, if the listbox is populated via AJAX and the user makes a selection, the selection will be lost.

I hope this helps someone in the future.

Angelinaangeline answered 9/7, 2010 at 9:55 Comment(0)
R
8

I am a little late jumping in on this but, just for future reference, here is how I accomplished something similar…

My control is a tree that uses templates for the nodes. The issue where I was dealing with this was how to capture the client side changes to the expanded/collapsed state of the nodes. What ended up working was:

In CreateChildControls add the hidden field to the controls collection of my root control.

protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
    ...
    _cdExpanded = new HiddenField();
    _cdExpanded.ID = "cdExpanded";
    this.Controls.Add(_cdExpanded);
    ...
}

In OnInit call

protected override void OnInit(EventArgs e)
{
    ...
    Page.RegisterRequiresPostBack(this);
    ...
}

In LoadPostData look for a value in the post collection that matches the UniqueID (not ClientID) of the hidden field:

public bool LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
{
    ...
    string cdExpanded = postCollection[_cdExpanded.UniqueID];
    ...
}

Within the classes for the individual nodes I have code which populates the onclick events of my toggle buttons with a call to a JavaScript function which takes the ID of the base control and the individual nodes as arguments.

    string ToggleScript
    {
        get
        {
            return "ToggleNode('" + this.ClientID + "', '" + _TreeRoot.ClientID + "');";
        }
    }
    protected override void Render(HtmlTextWriter writer)
    {
        ...
        if (this.HasChildren)
        {
            writer.AddAttribute("onclick", ToggleScript);
        }
        ...
    }

This makes it so that finding the hidden field is fairly easy via getElementById:

function ToggleNode(nodeID, treeID) {
var cdExpanded = document.getElementById(treeID + "_cdExpanded");
...
}

The JavaScript then modifies the value of the hidden field as needed for the event that occurred. When we get back to the server I am able to parse out the contents of this field and modify the control state as necessary before it gets rendered again. (Note: I actually use 3 hidden fields for tracking different events but the concept is the same)

Hope this helps others in the future…

Rainarainah answered 25/2, 2011 at 13:44 Comment(1)
Thanks for your contribution.Angelinaangeline
B
3

Sounds really odd to be working only when an item is selected. A quick way to check if LoadPostData is being invoked would to enable tracing and put the following in IPostBackDataHandler.LoadPostData(...).

Page.Trace.Write("My control", "LoadPostData");

If that is the case, you should make sure that you've got the following:

Page.RegisterRequiresPostBack(this) in OnInit

Here is a full sample control.

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel.Design;
using System.ComponentModel;
using System.Web.UI.Design;

namespace Controls
{
    public sealed class ExtendedListBoxDesigner : ControlDesigner
    {

        public override string GetDesignTimeHtml()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("<div>My designer</div>");
            return sb.ToString();
        }

    }

    [DesignerAttribute(typeof(ExtendedListBoxDesigner), typeof(IDesigner))]
    public class ExtendedListBox : ListBox, INamingContainer, IPostBackDataHandler 
    {
        bool IPostBackDataHandler.LoadPostData(string postDataKey, System.Collections.Specialized.NameValueCollection postCollection)
        {
            Page.Trace.Write("ExtendedListBox", "LoadPostData");
            return true;
        }


        protected override void OnInit(EventArgs e)
        {
            Page.RegisterRequiresPostBack(this);
            base.OnInit(e);
        }

        protected override void RenderContents(HtmlTextWriter writer)
        {
            base.RenderContents(writer);
            writer.Write(string.Format("<input type=\"hidden\" name=\"{0}_dummy\" value=\"alwaysPostBack\">", this.ID));
        }

    }
}

and the page looks like this.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="ControlSamlpe._Default" Trace="true" %>
<%@ Register Assembly="Controls" Namespace="Controls" TagPrefix="sample" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <sample:ExtendedListBox runat="server" ID="extListBox"></sample:ExtendedListBox>
    <asp:Button runat="server" ID="go" />
    </div>
    </form>
</body>
</html>

When you click on go, you should see "LoadPostData" in the trace.

Bedroll answered 9/7, 2010 at 9:45 Comment(9)
It's not at all odd that it works only when an item is selected. The LoadPostData() method is only called if the postdata contains an item with the same name as the control's UniqueID - which it won't if the user hasn't selected an item.Angelinaangeline
Well my first reaction was that it felt odd when I started writing it! :) Did you try adding the dummy field in RenderContents? That way LoadPostData will always get called.Bedroll
Thanks for your suggestion about creating an 'fake' element with the same name attribute as the UniqueID of the control. I sort of see what you mean. To my mind it's not quite as clean as the solution I've offered though. What do you think?Angelinaangeline
Although it is a piece of twisted genius. If I have any problems with my way, I'll certainly try it! :)Angelinaangeline
Hold on - what would happen if you added the fake element and the user made a selection? Wouldn't the post data contain two items with the same name? I'm not sure what would happen then.Angelinaangeline
You can have multiple hidden fields as long as they have different names. So in LoadPostData: postCollection[string.Format("{0}_value", this.ID)] and postCollection[string.Format("{0}_hidden", this.ID)] would refer to two different items! The presence of either when you post would fire LoadPostData for your control.Bedroll
But surely it's the name attribute of the posted element which fires LoadPostData - and it must match the UniqueID of the control perfectly. If the control's UniqueID is 'foo', why do you think elements which names of 'foo_hidden' or 'foo_value' would fire LoadPostData? Am I misunderstanding you?Angelinaangeline
You're right, you still need Page.RegisterRequiresBostBack(this). I've amended my answer with a full sample.Bedroll
Thanks for bringing my attention to Page.RegisterRequiresPostback. It is the way to ensure that LoadPostData() is always called, irrespective of whether there is an item in the post data with the same name as the control's UniqueID. However, I have found that using this method sometimes the selected value of the list box is not preserved. My solution (using a method called during OnLoad()) is the only one that consistently seems to work so I've marked that one as correct.Angelinaangeline
A
1

I've established that the LoadPostData() method is not called unless the post data contains an item with the same name as the control's UniqueID. [Edit: calling Page.RegisterRequiresPostback during Init() overcomes this.] I can see why, but it is quite limiting.

I have overcome the problem by not handling it during the LoadPostData() method at all. Instead, I have handled it in a method which I call in OnLoad() instead.

Two things need to be borne in mind when using this approach:

1) You no longer have access to the postCollection NameValueCollection object which is passed in to the LoadPostData() method as an argument. This means you have to extract the post data from the Request.Form collection, which is slightly harder work. 2) Since OnLoad() occurs after the ViewState processing code, you will need to manually set the SelectedValue after you create the ListItems. If you don't, if the listbox is populated via AJAX and the user makes a selection, the selection will be lost.

I hope this helps someone in the future.

Angelinaangeline answered 9/7, 2010 at 9:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.