views:

726

answers:

2

I'm currently working with a part of my application that uses Dynamic Web User Controls and I'm having a bit of trouble figuring out the best way to re-instantiate the controls on postback by using ViewState or some other method that doesn't require me to query a database on each postback.

Basically what I have on my page is a user control that contains a panel for holding a variable amount of child user controls and a button that says "Add Control" whose function is pretty self explanatory.

The child user control is pretty simple; it's just a delete button, a drop down, and a time picker control arranged in a row. Whenever the user clicks the Add Control button on the parent control, a new 'row' is added to the panel containing the child controls.

What I would like to do is to be able to add and remove controls to this collection, modify values, and perform whatever operations I need to do 'in-memory' without having to do any calls to a database. When I am done adding controls and populating their values, I'd like to click "save" to save/update all the data from the controls to a database at once. Currently the only solution I have found is to simply save the data in the database each post back and then use the rows stored in the db to re-instantiate the controls on postback. Obviously, this forces the user to save changes to the DB against their will and in the event that they want to cancel working with the controls without saving their data, extra work must be done to ensure that the rows previously committed are deleted.

From what I've learned about using dynamic controls, I know it's best to add the controls to the page during the Init stage of the lifecycle and then populate their values in the load stage. I've also learned that the only way to make sure you can persist the control's viewstate is to make sure you give each dynamic control a unique ID and be sure to assign it the exact same ID when re instantiating the control. I've also learned that the ViewState doesn't actually get loaded until after the Init stage in the life cycle. This is where my problem lies. How do I store and retrieve the names of these controls if I am unable to use the viewstate and I do not want to perform any calls to a database? Is this sort of in-memory manipulation / batch saving of values even possible using ASP.net?

Any help is greatly appreciated,

Mike

+3  A: 

I have done this in the past. I have not had to do this since the days of .NET 1.1, but the principal removes the same.

I did it on Page_Load not Init have to reload the controls that you created on the last page cycle.

First you need to keep track of the controls you have created on each page cycle. This includes type, name etc. . .

Then on each page load you need to rebuild them.

You do that by re-creating the control, assinging it the exact same id, add it to the sampe place on the page and finally in the ViewState["LoadedControl"] to the control type.

Here is the code I used, I only did this with User Controls that I created. I have not tried this with an ASP.NET control, but I think it would work the same.

In this case I have an ArrayList of Triplets (keep in mind this is .NET 1.1) adn the first item was a PageView ID. You might not need that for your application.

protected void Page_Load(object sender, System.EventArgs e)
{
    //**********************************************************
    //*  dynCtlArray will hold a triplet with the PageViewID,  *
    //*  ControlID, and the Control Name                       *
    //**********************************************************

    ArrayList dynCtlArray = (ArrayList)this.ViewState["dynCtlArray"];
    if (dynCtlArray != null)
    {

        foreach (object obj in dynCtlArray)
        {
            Triplet ctrlInfo = (Triplet)obj;

            DynamicLoadControl(ctrlInfo);
        }
    }
}

private void DynamicLoadControl(Triplet ctrlInfo)
{
    // ERROR HANDLING REMOVED FOR ANSWER BECAUSE IT IS NOT IMPORTANT YOU SHOULD HANDLE ERRORS IN THIS METHOD

    Control ctrl = this.LoadControl(Request.ApplicationPath
        + "/UC/" + (string)ctrlInfo.Third);

    ctrl.ID = (string)ctrlInfo.Second;

    // Create New PageView Item
    Telerik.WebControls.PageView pvItem = this.RadMultiPage1.PageViews[(int)ctrlInfo.First];
    pvItem.Controls.Add(ctrl);

    /******************************************************
     *  The ControlName must be preserved to track the    *
     *  currently loaded control                          *
     * ****************************************************/
    ViewState["LoadedControl"] = (string)ctrlInfo.Third;
}
private void RegisterDynControl(Triplet trip)
{
    ArrayList dynCtlArray = (ArrayList)this.ViewState["dynCtlArray"];

    if (dynCtlArray == null)
    {
        dynCtlArray = new ArrayList();
        this.ViewState.Add("dynCtlArray", dynCtlArray);
    }

    dynCtlArray.Add(trip);

}

In some method on your page

// Create new Control
Control ctrl = Page.LoadControl("../UC/MyUserControl.ascx");

// . . . snip .. . 

// Create Triplet
Triplet ctrlInfo = new Triplet(0, ctrl.ID, "MyUserControl.ascx");
// RegisterDynControl to ViewState
RegisterDynControl(ctrlInfo);

// . . . snip .. .

To access the controls to save there information you will have to do a this.Page.FindControl('');

David Basarab
David,Thanks so much for the response. I haven't yet had a chance to try your solution, but in looking it over I did have a question: I've used Page_Load in the past to load dynamic controls but that usually gives me problems since the Load event comes after the View State has been loaded (hence your ability to get to the array of control ID's). As a result, any user-entered values are lost on postback due to the fact that the control wasn't yet added to the page when the View State was loaded. Does the way you are adding these controls to the page manually load their View State?
Mike C
Page_Load should have access to the view state. But yes it will load there view state. For example say you are dynamically loading a text box. In my process you are telling the page 'Hey pay attention to these controls' so after they are loaded you can get the value of the textbox, or any of its view state information. You have to tell the page they were created manually otherwise the page will never know it.
David Basarab
+3  A: 

You could store the bare minimum of what you need to know to recreate the controls in a collection held in session. Session is available during the init phases of the page.

EDIT Ok... here is an example for you. It consists of:

Default.aspx, cs
- panel to store user controls
- "Add Control Button" which will add a user control each time it is clicked

TimeTeller.ascx, cs
- has a method called SetTime which sets a label on the control to a specified time.

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="DynamicControlTest._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Panel ID="pnlDynamicControls" runat="server">
        </asp:Panel>
        <br />
        <asp:Button ID="btnAddControl" runat="server" Text="Add User Control" 
            onclick="btnAddControl_Click" />
    </div>
    </form>
</body>
</html>

Default.aspx.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace DynamicControlTest
{
   public partial class _Default : System.Web.UI.Page
   {
      Dictionary<string, string> myControlList; // ID, Control ascx path

      protected void Page_Load(object sender, EventArgs e)
      {

      }

      protected override void OnInit(EventArgs e)
      {
         base.OnInit(e);

         if (!IsPostBack)
         {
            myControlList = new Dictionary<string, string>();
            Session["myControlList"] = myControlList;
         }
         else 
         {
            myControlList = (Dictionary<string, string>)Session["myControlList"];

            foreach (var registeredControlID in myControlList.Keys) 
            {
               UserControl controlToAdd = new UserControl();
               controlToAdd = (UserControl)controlToAdd.LoadControl(myControlList[registeredControlID]);
               controlToAdd.ID = registeredControlID;

               pnlDynamicControls.Controls.Add(controlToAdd);
            }
         }
      }

      protected void btnAddControl_Click(object sender, EventArgs e)
      {
         UserControl controlToAdd = new UserControl();
         controlToAdd = (UserControl)controlToAdd.LoadControl("TimeTeller.ascx");

         // Set a value to prove viewstate is working
         ((TimeTeller)controlToAdd).SetTime(DateTime.Now);
         controlToAdd.ID = Guid.NewGuid().ToString(); // does not have to be a guid, just something unique to avoid name collision.

         pnlDynamicControls.Controls.Add(controlToAdd);

         myControlList.Add(controlToAdd.ID, controlToAdd.AppRelativeVirtualPath);
      }
   }
}

TimeTeller.ascx

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="TimeTeller.ascx.cs" Inherits="DynamicControlTest.TimeTeller" %>
<asp:Label ID="lblTime" runat="server"/>

TimeTeller.ascx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace DynamicControlTest
{
   public partial class TimeTeller : System.Web.UI.UserControl
   {
      protected void Page_Load(object sender, EventArgs e)
      {

      }

      public void SetTime(DateTime time) 
      {
         lblTime.Text = time.ToString();
      }

      protected override void LoadViewState(object savedState)
      {
         base.LoadViewState(savedState);
         lblTime.Text = (string)ViewState["lblTime"];
      }

      protected override object SaveViewState()
      {
         ViewState["lblTime"] = lblTime.Text;
         return base.SaveViewState();
      }
   }
}

As you can see, I still have to manage the internal viewstate of my user control, but the viewstate bag is being saved to the page and handed back to the control on postback. I think it is important to note that my solution is very close to David's. The only major difference in my example is that it's using session instead of viewstate to store the control info. This allows things to happen during the initialization phase. It is important to note that this solution takes up more server resources, therefore it may not be appropriate in some situations depending on your scaling strategy.

Daniel Auger
I like that idea, because all I'm really concerned with is the id's (and perhaps the order) of the controls I'm trying to load. I'll work up a quick test bed for that solution and get back to you.Thanks!
Mike C
I hope you are able to get it to work. I've had to do something similar in the past. It's best to get the controls back on the page in the init phase because that's the way control lifecycle is expected to play out. I'm sure David's solution works, but I'd be concerned about long term maintainability. I've done things like that as well and it eventually turns into a "fighting the framework" scenario. You may eventually run into something that just won't work if you add it that late on in the lifecycle.
Daniel Auger
Daniel, I've made some progress with using your session solution but I've hit two snags. One is that despite the fact that the controls are being readded in the Init() stage the View State isn't being restored. I was under the impression that all I need to set on the dynamic control when it's being readded is the correct ID to get the View State data back, is there something I'm missing? Also, the session data persists even if I'm not using a post-back. I would just set the Session to null when !Page.IsPostBack, but the flag doesn't get set until after the Init() stage.
Mike C
I think you still need to internally manage viewstate for each or your user controls. Example coming right up.
Daniel Auger
One thing that I'm trying to do is learn how to implement the IPostBackdataHandler interface as Scott Mitchell lists it as something controls have to do if they need post back data. I'm not sure if this is only applicable to server controls, or if user controls need to do it too.
Mike C
IPostBackDataHandler is for "server controls". I don't think it has anything to do with "Web User Controls", but I could be wrong.
Daniel Auger
It's very confusing to identify what type of controls people are talking about. Web Control = Server Control. Web User Control is a different beast.
Daniel Auger
Daniel,I applied your View State example to my Web User Control and it worked! I also tried checking IsPostBack again in the Init() (I previously said it didn't get set yet) and that also works like expected, so I AM able to null the session dynamic control array on the initial page request! Thanks, your help has not only gotten me much farther along in this part of my application it has also deepened my understanding of how the View State works and how it relates to the ASP.net page and control lifecycle.
Mike C
Excellent! Glad to hear I was able to be of help to you.
Daniel Auger