Tuesday, November 1, 2016

Solved : ASP.Net form submit and page expire when navigate back

Each page we create in ASP.Net has at least one form. Those form can be submitted in many ways. Either a postback from a server control or an html button can submit a form using JavaScript.

Look at the below image.




















The page has expired due to a submission and user tries to navigate backward which the server needs the page to be submitted. Simulating the issue in a live environment would be like below;

1. You can have a filtering criteria as this.
2. The "Search" is an anchor tag that has a onClick event bind to it.




3. When user tick on each of the check boxes, the id values are stored in a hidden field and on "javascript:submitForm()" function the page (form) is resubmitted to the server to filter the product list. So the "submitForm" function looks like below;

 <input id="hdnFilterCriteria" name="hdnFilterCriteria" value="4339435,4339434,4339427," type="hidden">  


4. This causes the page to post back with the filtering criteria appended to the hidden field.
5. After the postback, if the user move in to another page and hit browser back button to see the previous page, the following error will display due to the document expiration.


















It is time to see how we can avoid this error. It is not that hard and all you need is to think outside the box. Simply avoid page postbacks / submissions. In this case we uses JQuery AJAX. Let's focus about the solution.

1. Create a class with the all properties included.
 public class SearchAttribute  
 {  
   public string idAttribute { get; set; }    
   public string strFilterCriteria { get; set; }    
   // You can have all the attributes as properties here  
 }  

2. Use the Cache / Session to maintain the search history. You can use Ajax to update the cache. See the web method below;

   [System.Web.Script.Services.ScriptMethod]  
   [WebMethod(EnableSession = true)]  
   public string catProductHiddenValueCaching(int idSearch,string idAttribute, string strFilterCriteria)  
   {  
     var cacheName = HttpContext.Current.Session.SessionID.ToString() + "_SearchAttribute";  
     Dictionary<int, SearchAttribute> searchHistory = new Dictionary<int, SearchAttribute>();  
     if (idSearch > 1)  
     {        
       var cacheSA = HttpContext.Current.Cache.Get(cacheName);  
       if (cacheSA != null)  
         searchHistory = (Dictionary<int, SearchAttribute>)cacheSA;  
     }  
     SearchAttribute sa = new SearchAttribute()  
     {  
       idAttribute = idAttribute,  
       strFilterCriteria = strFilterCriteria  
     };  
     searchHistory.Add(idSearch, sa);  
     HttpContext.Current.Cache.Insert(cacheName, searchHistory);      
     JavaScriptSerializer js = new JavaScriptSerializer() { MaxJsonLength = Int32.MaxValue };  
     return js.Serialize(idSearch.ToString());  
   }  

  • Use Session ID + Cache name to avoid conflicts with shared cache location.
  • Use Dictionary to store search attribute instances of each search
  • Use the Cache.Insert to overwrite the existing cache value.

3. Inside the onclick event of the "Search" button, call this function asynchronously.

     function submitForm(){  
          try{  
         var j = jQuery.noConflict();  
         var inSearch = getParameterByName('inSearch'); // to track the no of search histories  
         if(inSearch){  
             inSearch = parseInt(inSearch);  
             inSearch++;  
           }  
         else  
           inSearch = 1;  
         var urlAjax = 'http://'+window.location.hostname+'/your_service.asmx/setFilderValueCache'; // get the service path with full URL  
         var idAttribute = j("#idAttribute").val();  
         var strFilterCriteria = j("#strFilterCriteria").val();  
         var objPara = {  
           idSearch:inSearch,  
           idAttribute: idAttribute,  
           strFilterCriteria: strFilterCriteria  
         };  
         j.ajax({  
           type: "POST",  
           url: urlAjax,  
           data: JSON.stringify(objPara),  
           contentType: "application/json; charset=utf-8",  
           dataType: "json",  
           success: function (msg) {  
             var ret = JSON.parse(msg.d);     
             var url = window.location.href;    
             var newUrl = queryStringUrlReplacement(url, 'inSearch', inSearch); // append the inSearch as a query string parameter.  
             window.location.href = newUrl;               
           },  
           error: function (err) {  
             alert(err.responseText);  
           }  
         });  
          }catch(e){}  
     }  

  • idSearch is used to update the query string and reload the page with query string parameter.

 function queryStringUrlReplacement(url, param, value)   
     {  
       var re = new RegExp("[\\?&]" + param + "=([^&#]*)"), match = re.exec(url), delimiter, newString;  
       if (match === null) {          
         var hasQuestionMark = /\?/.test(url);   
         delimiter = hasQuestionMark ? "&" : "?";  
         newString = url + delimiter + param + "=" + value;  
       } else {  
         delimiter = match[0].charAt(0);  
         newString = url.replace(re, delimiter + param + "=" + value);  
       }  
       return newString;  
     }  

4. When "Search" is clicked, the relevant cache object is accessed and passed the search criteria to it's parameters inside "Page Load"

 protected void Page_Load(object sender, EventArgs e)  
   {      
     string strFilterCriteria= ((Request.Form["strFilterCriteria"] ?? Request.QueryString["strFilterCriteria"]) ?? string.Empty);  
     string inSearch = Request.QueryString["inSearch"];  
     if (!string.IsNullOrEmpty(inSearch))  
     {  
       int iSearch = int.Parse(inSearch);  
       var cacheName = HttpContext.Current.Session.SessionID.ToString() + "_SearchAttribute";  
       Dictionary<int, SearchAttribute> searchHistory = new Dictionary<int, SearchAttribute>();  
       var cacheSA = HttpContext.Current.Cache.Get(cacheName);  
       if (cacheSA != null)  
         searchHistory = (Dictionary<int, SearchAttribute>)cacheSA;  
       if (searchHistory.Count > 0)  
       {  
         SearchAttribute sa = searchHistory[iSearch];  
         if (sa == null)  
           return;  
         if (!string.IsNullOrEmpty(sa.idAttribute))  
           idAttribute = int.Parse(sa.idAttribute);  
         strFilterCriteria= sa.strFilterCriteria;               
       }  
     }  
    // Use 'strFilterCriteria' variable to store the currently filtered id list and pass it to the relevant method to get the result.  
 }  


Now each time the "Search" is clicked, it stores the search criteria in the cache and reload with inSearch as a query string parameter. There is no page postback and no document expiration happens. Each time it loads a fresh copy with the idSearch in the query string as "?inSearch=1" or "?inSearch=2" ..etc. The number increases on each Click to Search button. Navigate backward will retrieve the relevant result from the cached dictionary.

So, it is easy. Just a simple solution. :)

Happy coding... :)

No comments:

Post a Comment