Wednesday, October 30, 2013

Divorcing the UpdatePanel for Asynchronous Postbacks in Web Forms Isn't Easy

Point MVC vs. web forms here in the effort to make ajax calls to the server for asynchronous postbacks. This experience made me realize how nice MVC is from this respect I'm about to highlight.

For years I used ASP.NET UpdatePanels in web forms development and overall they have done the job. However, anyone that has used them on mildly complex pages knows, you can't just have 1 UpdatePanel. Nope, you need 2 with the 2nd one having a trigger pointing to the 1st so partial updates will work properly. The end result: a tangled web of UpdatePanel goo.

Recently I decided to use jQuery making AJAX calls to ASP.NET page methods instead as an alternative to UpdatePanels. The jQuery in it of itself may not be 100% cleaner but it works really well and is straight forward. So I create my ASP.NET page method, write some jQuery to make a call to my new page method asynchronously, and walla I have bypassed the UpdatePanel, yeah!

Not so fast...

Caveat #1: Page Method Must Be Static. This will by the way not be ideal for anyone doing Dependency Injection as you will have to Publicly resolve dependencies through methods like: Global.GetInstance()

Caveat #2: Can't see my server side controls 
from the static method called (because of #1 above and how it's called from the client).

Caveat #3: Normal postbacks later that access or post bound clientside elements complain of security and EnableEventValidation issue.

Caveat #4: Be prepared to query element values via Request.Form and not through control instances.

End result: A worse mess than using an UpdatePanel.

Recommendation: If you are doing ASP.NET web form development here are the (3) choices I recommend:

  1. Deal with full postbacks like it's 2002
  2. Use an UpdatePanel for asynchronous postbacks and don't mix client side data binding asynchronously with JavaScript.
  3. Ditch ASP.NET web forms and do MVC (hey no hater here, I stick up for web forms a lot - just know how the tool operates)
I want to show through an example all of what I just described. So 1st, lets look at a simple example to populate a ASP.NET ListBox server control's items using an AJAX call to the server to just see how this works. We'll make a DropDownList that contains the 'Periods' during a school day. Upon selection we want to have the classes populated in the ListBox. The goal is to do this without having a full postback or use an UpdatePanel.

First a simple set of server controls:

<form id="form1" runat="server">
    <div>
       <asp:DropDownList ID="DropDownList1" runat="server">
          <asp:ListItem Text="Please Select..." Value="0"></asp:ListItem>
          <asp:ListItem Text="1st Period" Value="1"></asp:ListItem>
          <asp:ListItem Text="2nd Period" Value="2"></asp:ListItem>
       </asp:DropDownList>
    </div>
    <div>
       <asp:ListBox ID="ListBox1" runat="server" Width="400px" SelectionMode="Multiple"></asp:ListBox>
    </div>
    <div>
       <asp:Label ID="Label1" runat="server" Text=""></asp:Label>
    </div>
    <div>
       <asp:Button ID="Button1" runat="server" Text="Submit" OnClick="Button1_Click" />
    </div>  
</form>


Next we need a Static server-side event with the [WebMethod] attribute added that our jQuery we will make can make an AJAX call to and will return a collection of 'School Classes' for the 'Period' selected:
public partial class Default : System.Web.UI.Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
   }

   [WebMethod]
   public static List<SchoolClasses> DropDownList1_SelectedIndexChanged(string id)
   {
 if (id == "0")
 {
    return null;
 }

 //Generic code representing call probably to retrieve values from the database
 var classesPeriod1 = new List<SchoolClasses>()
    {
  new SchoolClasses(){ID = 1, Name = "Math 101"},
  new SchoolClasses(){ID = 2, Name = "Science 101"},
  new SchoolClasses(){ID = 3, Name = "Social Studies 101"},
  new SchoolClasses(){ID = 4, Name = "Spanish 101"}
    };
 var classesPeriod2 = new List<SchoolClasses>()
    {
  new SchoolClasses(){ID = 5, Name = "Art 101"},
  new SchoolClasses(){ID = 6, Name = "Music 101"},
  new SchoolClasses(){ID = 7, Name = "English 101"},
  new SchoolClasses(){ID = 8, Name = "Global Studies 101"}
    };

 List<SchoolClasses> results = null;
 if (id == "1")
 {
    results = classesPeriod1.ToList();
 }
 if (id == "2")
 {
    results = classesPeriod2.ToList();
 }

 //this.Label1.Text == "You selected a id = " + id;

    return results;
 }

   protected void Button1_Click(object sender, EventArgs e)
   {
      var selectedItems = this.ListBox1.Items.Cast<ListItem>().Where(li => li.Selected).ToList();
      //The collection is empty?? There were selected items though...
   }
}

public class SchoolClasses
{
   public int ID { get; set; }
   public string Name { get; set; }
}

Lastly, we need the jQuery that is wired up to the 'change' event of the DropDownList that will call our new Static event server-side and populate the ListBox with the results.
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
   <script type="text/javascript">

      $(document).ready(function () {
         DropDownList1_OnChangeHandler();
      });

      function DropDownList1_OnChangeHandler() {
         // Add the page method call as an change handler for DropDownList1.
         // This will call the static page method, and use the returned results to populate ListBox1.
         $('#<%=DropDownList1.ClientID %>').change(function () {
            var val = $(this).val();
            var text = $(this).children("option:selected").text();
            var $timePeriod = $('#<%=ListBox1.ClientID %>');
            $timePeriod.attr('disabled', 'disabled');
            $timePeriod.empty();
            $timePeriod.append('<option value="0">< Loading Please Wait... ></option>');
            $.ajax({
               type: "POST",
               url: "Default.aspx/DropDownList1_SelectedIndexChanged",
               contentType: "application/json; charset=utf-8",
               dataType: "json",
               data: "{'id':'" + val + "'}",
               success: function (schoolClasses) {
                  $timePeriod.removeAttr('disabled');
                  $timePeriod.empty();
                  if (schoolClasses.d == null) {
                     return;
                  }
                  $.each(schoolClasses.d, function (i, schoolClass) {
                     $timePeriod.append('<option value="' + schoolClass.ID + '">' + schoolClass.Name + '</option>');
                  });
               },
               error: function () {
                  $timePeriod.empty();
                  $timePeriod.append('<option value="0">< Error Loading classes ></option>');
                  alert('Failed to retrieve classes.');
               }
            });
         });

      }
   </script>


If you built this example and ran it, the ListBox would populate as we would like. In fact it's really fast too. Originally I thought, "Home Run!" So what's the problem? Well if we have a simple example this might work, but as we need to add to the page, we begin to run into hurdles.



Let's try to add in (2) more features to highlight what I'm talking about:

  1. Update a label to state which 'Period' we selected when calling server-side Static method.
  2. Iterate through the 'Selected Items' collection on submittal of the form
OK, I know what you might be thinking thus far; EASY! I've done this a million times. Well, let's dig in and look at some of the challenges I listed at the beginning of this post.

First, add this line to our Static server-side method:
this.Label1.Text == "You selected a id = " + id;

Next, add this code to the server-side button click event to iterate through the selected items.
protected void Button1_Click(object sender, EventArgs e)
{
  var selectedItems = this.ListBox1.Items.Cast<listitem>().Where(li => li.Selected).ToList();
  //The collection is empty?? There were selected items though...
}


Now try to run this code. There is already an error with trying to access the Label's .Text property. Why? See #1 and #2 in the original list of issues. In a static method on the page, I can not see or have access to the Page's instance. Makes sense actually. I making a quick call to the server, but not in the context of how ASP.NET would expect, but rather from the client. Still completely legal to have the Static method, just will not behave as we might have hoped.

Comment out the the code to change the Label's .Text property. Now run the page, select some values in the dropdown and press the 'Submit' button. This time we have another issue; a hard error stating: "Invalid postback or callback argument." Why? See #2, #3, and #4. The server did not originally send the data to populate the ListBox, so it is unaware of it. This will cause a security error unless the page's EnableEventValidation="false" property is set. However I don't like to do this because it opens up a security risk and would not be something I recommend in production applications.

However if we do set to false EnableEventValidation, then we still can't see the values from our control, because the Server did not provide them and has no knowledge of them. How do we get them? Well you could use the Request.Form values and manually dig through the posted form to scrape out the elements. At this point, I'm thinking... Yuck.

So in order to make my AJAX call work to interact with the server using asynchronous postbacks and avoiding the UpdatePanel I have to deal with the following:
  • Reduce the page's security by setting EnableEventValidation="false"
  • Not be able to see any of the control values I manipulated through their server-side properties.
  • Have to dig through Request.Form to manually find updated client values
  • Not be able to use Dependency Injection as intended
  • Have a disorganized mismatch between the client and server state since the right-hand doesn't know what the left is doing.
At this point I think, is an UpdatePanel so bad compared to all of what I just discovered? Let's review the UpdatePanel code again:
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
    <ContentTemplate>
 <asp:ListBox ID="ListBox1" runat="server" Width="300px" SelectionMode="Multiple" AutoPostBack="True" OnSelectedIndexChanged="ListBox1_SelectedIndexChanged" />
    </ContentTemplate>
</asp:UpdatePanel>

Nope, that actually was not so bad. So the end result for a web forms applications was I stuck with the Update Panels and everything worked well in regards to asynchronous postbacks. As I mentioned at the beginning, I think this is where MVC really shines, because there is no state being tracked and updated by the server and causing at least a portion of this mess because of the mismatch between the client updates happening independent of the server knowing about it.

Hopefully you try all of this out and see the caveats of trying to do AJAX asynchronous postbacks to the server via jQuery before going to far down the road on a new project hoping this will work smoothly. There are as I've shown several 'pitfalls' to be aware of so just make sure you factor them in if going down this route.

1 comment:

  1. Thank you for the write-up, Allen! I've been working on an enterprise web forms app for a number of years and I've been trying to figure out how to remove the update panels and use jQuery page methods instead. I was never clear on how to do this with more complicated cases than just setting a label. Reading your article saved me a lot of time by convincing me not to go down this route. I still don't know if MVC is the right solution either since my app is very data-centric though...

    ReplyDelete