简体   繁体   中英

WebForms Nested User Controls Data Binding

I'm implementing something using nested user controls and am having issues related to data binding, so I created a small application to attempt to debug the behavior.

I have a default page (aspx) that contains a single user control, 'ChildControl'. In the code behind, I am setting a couple of properties for ChildControl and calling DataBind() on it.

In the ChildControl user control (ascx), I have a ListView of 'GrandchildControl' ListViewItems, which has a datasource bound to a property that was set within the LoadPage() method of Default.aspx. I also am binding a property of GrandchildControl that will be used for a DropDownList within that user control. In the code behind for this user control, I have a click event handler that simply adds a new User object to a session variable and calls the LoadPage() method of Default.aspx.

In the GrandchildControl user control (ascx), I am displaying the information for each bound User object.

I have added logs to see the order in which the events are being called. When I navigate to the page (not postback), the events fire in the following order as expected:

DEFAULT PAGE LOAD
GRANDCHILD CONTROL ITEM DATA BOUND
GRANDCHILD CONTROL ITEM DATA BOUND
CHILD CONTROL PAGE LOAD
GRANDCHILD CONTROL PAGE LOAD
GRANDCHILD CONTROL PAGE LOAD

However, when I click the LinkButton within ChildControl to add a new User object, the events fire in this order:

DEFAULT PAGE LOAD
CHILD CONTROL PAGE LOAD
GRANDCHILD CONTROL PAGE LOAD
GRANDCHILD CONTROL PAGE LOAD
ADD INCOME CLICKED
GRANDCHILD CONTROL PAGE LOAD
GRANDCHILD CONTROL ITEM DATA BOUND
GRANDCHILD CONTROL PAGE LOAD
GRANDCHILD CONTROL ITEM DATA BOUND
GRANDCHILD CONTROL PAGE LOAD
GRANDCHILD CONTROL ITEM DATA BOUND

As you can see, the User objects are bound to the ListView after the call to the their respective page load's firing. Because of this, the dropdown selections are not set correctly (they all default to the 0 index).

So my question is, what causes ucChildControl.DataBind() to behave differently when the Page is initially loaded, versus when it is called from the context of the ChildControl's click handler?

I apologize for the long post but wanted to show as much detail as possible seeing as I have searched and have been unable to find a similar issue and resolution.

All code is below:

Model:

public class DropdownContainer
{
    public List<DropdownValue> DropdownValues { get; set; }
}

public class DropdownValue
{
    public string DisplayText { get; set; }
    public string ID { get; set; }
}

public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string AgeGroup { get; set; }
}

aspx front end:

<asp:Content runat="server" ID="BodyContent" ContentPlaceHolderID="MainContent">
    <uc1:ChildControl runat="server" ID="ucChildControl"></uc1:ChildControl>
</asp:Content>

aspx code behind:

public partial class Default : Page
{
    private DropdownContainer _ageGroupContainer = new DropdownContainer()
    {
        DropdownValues = new List<DropdownValue>()
        {
            new DropdownValue()
            {
                DisplayText = "18-25",
                ID = "1"
            },
            new DropdownValue()
            {
                DisplayText = "26-35",
                ID = "2"
            },
            new DropdownValue()
            {
                DisplayText = "36-45",
                ID = "3"
            }
        }
    };

    private List<User> _userDataSource = new List<User>()
    {
        new User()
        {
            FirstName = "Mickey",
            LastName = "Mouse",
            AgeGroup = "1"
        },
        new User()
        {
            FirstName = "Daffy",
            LastName = "Duck",
            AgeGroup = "3"
        }
    };

    public string Log { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {
        Log += "DEFAULT PAGE LOAD\n";

        if (!Page.IsPostBack)
        {
            LoadPage();
        }
    }

    public void LoadPage()
    {
        List<User> lstUserTemp = _userDataSource;

        if (Session["usersAdded"] != null)
        {
            lstUserTemp.AddRange((List<User>)Session["usersAdded"]);
        }

        ucChildControl.UserDataSource = lstUserTemp;
        ucChildControl.AgeGroupDataSource = _ageGroupContainer;
        ucChildControl.DataBind();
    }
}

Child ascx front end:

<div>
    <asp:ListView runat="server" ID="lvChild" DataSource='<%# UserDataSource %>' OnItemDataBound="lvChild_ItemDataBound">
        <ItemTemplate>
            <uc2:GrandchildControl runat="server" ID="ucGrandchildControl" User='<%# Container.DataItem %>' AgeGroupDataSource='<%# AgeGroupDataSource %>'></uc2:GrandchildControl>
            <br />
        </ItemTemplate>
    </asp:ListView>
</div>
<asp:LinkButton runat="server" ID="lbtnAddUser" OnClick="lbtnAddUser_Click" Text="Add User"></asp:LinkButton>

Child ascx code behind:

public partial class ChildControl : System.Web.UI.UserControl
{
    public List<User> UserDataSource { get; set; }

    public DropdownContainer AgeGroupDataSource { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {
        ((Default)Page).Log += "CHILD CONTROL PAGE LOAD\n";
    }

    protected void lvChild_ItemDataBound(object sender, ListViewItemEventArgs e)
    {
        ((Default)Page).Log += "GRANDCHILD CONTROL ITEM DATA BOUND\n";
    }

    protected void lbtnAddUser_Click(object sender, EventArgs e)
    {
        ((Default)Page).Log += "ADD USER CLICKED\n";

        List<User> usersAdded = (List<User>)Session["usersAdded"];

        if (usersAdded == null)
        {
            usersAdded = new List<User>();
        }

        usersAdded.Add(new User());

        Session["usersAdded"] = usersAdded;

        ((Default)Page).LoadPage();
    }
}

Grandchild ascx front end:

<div>
    <asp:Label runat="server" Text="User Name: "></asp:Label>
    <asp:TextBox runat="server" ID="txtUserName" Text='<%# string.Format(@"{0} {1}", User.FirstName, User.LastName) %>'></asp:TextBox>
</div>
<div>
    <asp:Label runat="server" Text="Age Group: "></asp:Label>
    <asp:DropDownList runat="server" ID="ddlAgeGroup" DataSource='<%# AgeGroupDataSource.DropdownValues.OrderBy(x => Convert.ToInt32(x.ID)) %>' DataTextField="DisplayText" DataValueField="ID" OnSelectedIndexChanged="ddlAgeGroup_SelectedIndexChanged" AutoPostBack="true"></asp:DropDownList>
</div>

Grandchild ascx code behind:

public partial class GrandchildControl : System.Web.UI.UserControl
{
    public User User { get; set; }

    public DropdownContainer AgeGroupDataSource { get; set; }

    protected void Page_Load(object sender, EventArgs e)
    {
        ((Default)Page).Log += "GRANDCHILD CONTROL PAGE LOAD\n";

        if (User != null)
        {
            ddlAgeGroup.SelectedValue = User.AgeGroup;
        }

        ddlAgeGroup.Items.Insert(0, new ListItem() { Text = "SELECT AN AGE GROUP", Value = string.Empty });
    }

    protected void ddlAgeGroup_SelectedIndexChanged(object sender, EventArgs e)
    {
        ((Default)Page).Log += "GRANDCHILD CONTROL SELECTED INDEX CHANGED\n";
    }
}

In case anyone comes across this question and is stuck on a similar issue, I have solved the issue.

The reason all dropdown selections revert back to the 0 index is because when data binding after adding an additional user, the page load event of each list item is called BEFORE the data bound event (as seen in the original question). Because of this, the following happens:

  1. GrandchildControl page loads. User property is null at this point as it is not set until AFTER page load. The only thing that happens at this point is adding the new list item to the dropdown.

  2. After page load, all front end properties are binded and the ItemDataBound event is called. When binding the dropdown datasource, the list item that was previously inserted is overwritten and the selected value is no longer set, therefore it defaults to the 0 index.

Therefore, the insertion of the default dropdown selection and assignment of the selected value needs to be done in the ItemDataBound event. I have attached all code changes below.

Child ascx code behind:

protected void lvChild_ItemDataBound(object sender, ListViewItemEventArgs e)
{
    ((Default)Page).Log += "GRANDCHILD CONTROL ITEM DATA BOUND\n";

    //Add call to 'InitializeControls' method of grandchild user control.
    ((GrandchildControl)e.Item.FindControl("ucGrandchildControl")).InitializeControls();
}

Grandchild ascx code behind:

//Remove initialization logic from page load.
protected void Page_Load(object sender, EventArgs e)
{
    ((Default)Page).Log += "GRANDCHILD CONTROL PAGE LOAD\n";
}

//This method has been added to be called from the ItemDataBound event.
public void InitializeControls()
{
    ddlAgeGroup.Items.Insert(0, new ListItem() { Text = "SELECT AN AGE GROUP", Value = string.Empty });

    if (User != null)
    {
        ddlAgeGroup.SelectedValue = User.AgeGroup;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM