Philip Hendry's Blog

February 10, 2011

Displaying ‘Unsaved Changes’ message for a web page using jQuery

Filed under: ASP.NET, jQuery, Web — philiphendry @ 10:48 am

I had a need to allow a user to cancel navigation to another page if changes to a form had not yet been saved – especially if they attempted to navigate to another page from the ever present menu. The code I used has a slight ‘hack’ which involves using the propertychange DOM event in IE rather than the change event which I would have usually expected.

Here’s the code that sets a global flag to indicate there’s an unsaved change :

var _changesMade = false;
$(document).ready(function() {

   $('form').bind($.browser.msie ? 'propertychange' : 'change', function() {
       _changesMade = true;
   });

   $(window).bind('beforeunload', function() {
       if (_changesMade)
           return 'There are unsaved changes which will be lost if you continue.';
   });
});

There are a couple of caveats. Any button that actually does the save will need a OnClientClick that sets the _changesMade to false to prevent the message from being displayed :

<asp:Button ID="btnSubmit" runat="server" Text="Submit" CssClass="button" OnClick="btnSubmit_Click" 
    OnClientClick="_changesMade=false; return true;" /> 

Also, if you have any code that causes the change event to fire (DOM manipulation after an Ajax call) then you might have to save the state of the _changesMade flag, make the DOM changes then reset the flag to it’s original value.

September 14, 2010

Implementing a jQuery Browser history Plugin using microsoft Ajax History

Filed under: ASP.NET, jQuery, Uncategorized — philiphendry @ 8:31 pm

This article is a summary of the steps I took to produce a jQuery plugin that would create history points in the browsers history programmatically. In particular I wanted to create as simple an interface in the implementing page as possible and primarily needed to integrate with ASP.NET.

I will also describe my workaround for a bug in Microsoft Ajax ASP.NET 3.5 where the onNavigate event would not fire when the page was too large since the _onIFrameLoad code injected by the ScriptManager would execute before the ajax initialisation had time to run.

The first thing I did was consider the interface I wanted to write in each of the aspx pages that required history. In particular I wanted to :

  1. Only have to enter the code to achieve history in one location
  2. Encapsulate as much of the complexities of maintaining browser history as possible.
  3. Allow the plugin to be extendable without over-engineering it!

I decided that keeping it client-side and written in javascript would be the best approach and using my favoured library, jQuery, would simplify integration. And this is the interface I came up with :

<script type="text/javascript" language="javascript">
   $(document).ready(function() {
       $.fn.ajaxHistory.registerCallbacks(getHistory, setHistory, refreshHistory);
       $("#<%=ddlStore.ClientID%>").ajaxHistory();
       $("#<%=btnSearch.ClientID%>").ajaxHistory();
   });
   
   function getHistory() {
       return {
           "store": $("#<%=ddlStore.ClientID%>").val(),
           "surname": $("#<%=txbSurname.ClientID%>").val()
       };
   }

   function setHistory(state) {
       $("#<%=ddlStore.ClientID%>").val(state.store || "0");
       $("#<%=txbSurname.ClientID%>").val(state.surname || "");
   }

   function refreshHistory() {
       $("#<%=btnSearch.ClientID%>").click();
   }
</script>

This code his very basic indeed and doesn’t really include an functionality whatsoever. The ready() function registers three callback with the refreshHistory callback optional. getHistory() is called to return a javascript object containing all the data that must be stored in order to return the page back to a the current state. setHistory() performs the reverse of getHistory(). And refreshHistory() is called upon to refresh the page using the restored state and therefore fetch any updates from a server. In this particular example, the page is a search screen where the user can select a store from a drop down list (ddlStore) which is configured for AutoPostBack and refreshes the search results or they can enter a surname and click the search button (btnSearch.) In the ready() I therefore tell .ajaxHistory(), my plugin, the controls that, when clicked or changed, are required to mark a history point.

The plugin itself is written using a standard pattern and over all looks like this :

(function($) {
    var _elements = new Array();

    $.fn.ajaxHistory = function() {
        return this.each(function() {
            var element = "#" + this.id;
            bindEvent(element);
            _elements.push(element);
        });
    };

    $.fn.ajaxHistory.initialise = function() {
        Sys.Application.add_navigate(onNavigate);
        Sys.WebForms.PageRequestManager.getInstance().add_endRequest(onEndRequest);
    };


    // onEndRequest will be called each time an ajax call is completed and therefore we can rebind all the events.
    function onEndRequest(sender, e) {
        $.each(_elements, function(index, item) {
            bindEvent(item);
        });
    };

})(jQuery);

$(document).ready(function() {
    $.fn.ajaxHistory.initialise();
});

The plugin initialises a couple of Microsoft Ajax events in its own ready() method and the ajaxHistory() method binds the passed elements (specified by a selector) to events that inform the plug in when the element is clicked (buttons, links) or changed (textboxes, drop downs). Since my plugin is used within UpdatePanels the onEndRequest() will be fired at the end of each Ajax call and re-bind the controls (I would use the jQuery .live() binding however binding the change event in IE using jQuery 1.4.2 seems to be broken at the moment and therefore I have to revert to jQuery 1.3.2 instead.)

To remember history points I’m making use of the history functionality built into Microsoft Ajax. This requires that the ScriptManager control is set with the attribute EnableHistory turned on :

<asp:ScriptManager ID="ScriptManager1" runat="server" EnableHistory="true" />

The Microsoft Ajax history functionality is then used in the plugin when the events bound to each of the elements is fired and simply adds a history point calling the getHistory() callback. This code store the state as a query string preceeded with a # which forces the browser to log it in it’s history and can even be bookmarked :

Sys.Application.addHistoryPoint(_getHistory(), document.title);

When the user clicks Back in the browser window, Microsoft Ajax will automatically detect the state after the # and raise the onNavigate event passing the state to it. The plugin simply passes that state to the setHistory() callback and then calls the refresh callback.

Microsoft Ajax Bug

Josh Close has already blogged about the bug I had whilst writing this plugin. My initial testing didn’t have any problems but when I tried to integrate my plugin to a larger page I suddenly found the onNavigate event would not fire and therefore my page would not return to a previous state.

My own debugging in the Microsoft Ajax javascript lead me to the _onIFrameLoad javascript that loads the state and it was Josh’s article that helped me understand that it was running too early. To solve this problem I made sure that the history was initialised as soon as possible rather than last in the page and therefore I made the call to _enableHistoryInScriptManager() myself immediately after the ScriptManager. Thankfully this method doesn’t do much anyway but ensures the write state is set up for the onNavigate event to be fired as expected.

<asp:ScriptManager ID="ScriptManager1" runat="server" EnableHistory="true" />
<script type="text/javascript" language="javascript">
    Sys.Application._enableHistoryInScriptManager();
</script>

Hopefully this problem will go away in ASP.NET 4!

January 6, 2010

IE8 Caches jQuery Ajax results

Filed under: ASP.NET MVC, jQuery — philiphendry @ 10:07 am

In my client code I have a small chunk of javascript that fetches the data for my form asynchronously using Ajax :

$.getJSON(_config.getUrl, null, function(data) {
    _shiftPattern = data;
    renderModel();
});

However, my server code was only being called on the first visit to the page and on subsequent attempts the browser seemed to be fetching cached results. Since my server side code is ASP.NET MVC the solution to this is very simple indeed and simply involves decorating the action method with a OutputCache attribute :

[OutputCache(Duration = 0, VaryByParam = "None")]
public ActionResult GetJson(int id)
{
    return Content(
        JsonHelper.Serialize(ShiftPatternBL.Get(id)), 
        "application/json; charset=utf-8"
        );
}

ASP.NET MVC is proving very elegant indeed :)

December 22, 2009

Sending and Receiving JSON between jQuery and ASP.NET MVC using Ajax.

Filed under: ASP.NET MVC, jQuery — philiphendry @ 9:17 pm

It’s taken me a while to get this working without any problems so it’s worth jotting down how it all works. I don’t think I was trying to do anything particularly difficult – just receive Json from an ASP.NET MVC application using jQuery then submitted but the APIs had to be coerced into just the correct way to get it functional.

Receiving Json from ASP.NET MVC using jQuery

First up, here’s the Json format I’m trying to send and receive :

var _shiftPattern = {
  'employee': {'name' : 'Jason Wright', 'number': 1234, 'contractedHours': 39.0 },
  'stepSize': 0.5,
  'days': [
      { 'day': 'Sunday',    'pattern': ['', '', '', '', '', '', '', 'p', 'b', 'w', 'w', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'p', 'p', 'p', 'p', 'p', 'b', 'p', 'p', 'p', '', '', '', '', '', '', '', '', '', '', '' ] },
      { 'day': 'Monday',    'pattern': ['', '', '', '', '', '', '', 'p', 'b', 'w', 'p', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'p', 'p', 'p', 'p', 'p', 'b', 'p', 'p', 'p', '', '', '', '', '', '', '', '', '', '', '' ] },
      { 'day': 'Tuesday',   'pattern': ['', '', '', '', '', '', '', 'p', 'b', 'w', 'b', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'p', 'p', 'p', 'p', 'p', 'b', 'p', 'p', 'p', '', '', '', '', '', '', '', '', '', '', '' ] },
      { 'day': 'Wednesday', 'pattern': ['', '', '', '', '', '', '', 'p', 'b', 'w', 'w', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'w', 'w', 'p', 'p', 'p', 'b', 'p', 'p', 'p', '', '', '', '', '', '', '', '', '', '', '' ] },
      { 'day': 'Thursday',  'pattern': ['', '', '', '', '', '', '', 'p', 'b', 'w', 'p', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'w', 'w', 'p', 'p', 'p', 'b', 'p', 'p', 'p', '', '', '', '', '', '', '', '', '', '', '' ] },
      { 'day': 'Friday',    'pattern': ['', '', '', '', '', '', '', 'p', 'b', 'w', 'b', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'w', 'w', 'p', 'p', 'p', 'b', 'p', 'p', 'p', '', '', '', '', '', '', '', '', '', '', '' ] },
      { 'day': 'Saturday',  'pattern': ['', '', '', '', '', '', '', 'p', 'b', 'w', 'w', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'w', 'w', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '' ] }
  ]
};

Note that I’ve opted to maintain Javascript standards by using camel casing – that caused a bit of trouble and I’ll show the problem and work-around later.

The jQuery to fetch this Json is trivial :

$(document).ready(function() {
  $.getJSON("/willingtowork/shiftpattern/1234", null, function(data) {
      _shiftPattern = data;
      renderModel();
  });
});

…and ASP.NET MVC controller action looks like this :

public ActionResult ShiftPattern(int id)
{
    return Content(JsonHelper.Serialize(
        new WillingToWork
        {
            Employee = new Employee { Name = "Jason Wright", Number = id, ContractedHours = 39.0f },
            StepSize = 0.5f,
            Days = new List<Day> {
                new Day { DayName = "Sun", Pattern = new List<string> {"", "", "", "", "", "", "", "p", "p", "b", "p", "p", "p", "p", "w", "w", "", "", "", "", "", "", "", "", "p", "p", "p", "p", "p", "p", "p", "w", "w", "w", "w", "w", "", "", "", "", "", "", "", "", "", "", "", "" }},
                new Day { DayName = "Mon", Pattern = new List<string> {"", "", "", "", "", "", "", "p", "p", "b", "p", "p", "p", "p", "w", "w", "", "", "", "", "", "", "", "", "p", "p", "p", "p", "p", "p", "p", "b", "p", "p", "p", "p", "w", "w", "w", "", "", "", "", "", "", "", "", "" }},
                new Day { DayName = "Tue", Pattern = new List<string> {"", "", "", "", "", "", "", "p", "p", "b", "p", "p", "p", "p", "w", "w", "", "", "", "", "", "", "", "", "p", "p", "p", "p", "p", "p", "p", "b", "p", "p", "p", "p", "w", "w", "w", "", "", "", "", "", "", "", "", "" }},
                new Day { DayName = "Wed", Pattern = new List<string> {"", "", "", "", "", "", "", "p", "p", "b", "p", "p", "p", "p", "w", "w", "", "", "", "", "", "", "", "", "p", "p", "p", "p", "p", "p", "p", "b", "p", "p", "p", "p", "w", "w", "w", "", "", "", "", "", "", "", "", "" }},
                new Day { DayName = "Thu", Pattern = new List<string> {"", "", "", "", "", "", "", "p", "p", "b", "p", "p", "p", "p", "w", "w", "", "", "", "", "", "", "", "", "p", "p", "p", "p", "p", "p", "p", "b", "p", "p", "p", "p", "w", "w", "w", "", "", "", "", "", "", "", "", "" }},
                new Day { DayName = "Fri", Pattern = new List<string> {"", "", "", "", "", "", "", "p", "p", "", "", "", "w", "w", "w", "w", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }},
                new Day { DayName = "Sat", Pattern = new List<string> {"", "", "", "", "", "", "", "", "", "", "", "", "w", "w", "w", "w", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "" }}
            }
        }
    ), "application/json; charset=utf-8");
}

In this test code I’ve hard-coded some object initialisations but the real issue for me here was originally choosing to use a JsonResult by calling Json() on the Mvc Controller Base but I’ve replaced it with a ContentResult and serialised the Json manually – more on that later.

The code below defines the data transfer objects I’m using to strongly type the Json in C#. Note I’ve implemented IExtensibleDataObject so that any additional data added to the Json in the javascript that I haven’t accounted for is ignored but still maintained if necessary (you may choose to remove this if you want to be strict but any extra data will cause an exception during deserialising.) I might also implement all properties as virtual to enable mocking frameworks (an alternative to defining interfaces which would be a bit complicated for DTOs) but that’s not shown here.

The main point to notice with the DataContract below is the use of DataMemberAttribute and its ability to rename properties – this is how I’m maintaining Pascal casing in C# and Camel casing for the serialised Json.

[DataContract]
public class WillingToWork : IExtensibleDataObject
{
   [DataMember(Name="employee")] public Employee Employee { get; set; }
   [DataMember(Name="days")] public List<Day> Days { get; set; }
   [DataMember(Name="stepSize")] public float StepSize { get; set; }

   public virtual ExtensionDataObject ExtensionData { get; set; }
}

[DataContract]
public class Day : IExtensibleDataObject
{
   [DataMember(Name = "pattern")] public List<string> Pattern { get; set; }
   [DataMember(Name = "day")] public string DayName { get; set; }

   public virtual ExtensionDataObject ExtensionData { get; set; }
}

[DataContract]
public class Employee :IExtensibleDataObject
{
   [DataMember(Name="contractedHours")] public float ContractedHours { get; set; }
   [DataMember(Name="name")] public string Name { get; set; }
   [DataMember(Name="number")] public int Number { get; set; }

   public virtual ExtensionDataObject ExtensionData { get; set; }
}

… and here lies a problem. When I was using the ASP.NET MVC JsonResult, the Json simply would not serialise using the names I’d provided in the DataMember and instead used the property name.

I’d already seen how to use the DataContractJsonSerializer() to manually create test code which correctly used the DataMemberAttribute() name property so I was wondering why JsonResult() wasn’t doing the same. Taking a look with reflector showed the following code in the JsonResult():

public override void ExecuteResult(ControllerContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException("context");
    }
    HttpResponseBase response = context.HttpContext.Response;
    if (!string.IsNullOrEmpty(this.ContentType))
    {
        response.ContentType = this.ContentType;
    }
    else
    {
        response.ContentType = "application/json";
    }
    if (this.ContentEncoding != null)
    {
        response.ContentEncoding = this.ContentEncoding;
    }
    if (this.Data != null)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        response.Write(serializer.Serialize(this.Data));
    }
}

The problem in this code is the use of the JavaScriptSerializer() which is now marked obsolete (although that seems to be questionable) and doesn’t handle the DataMemberAttribute(). Rather than try to work around this too cleverly I’ve used the ContentResult() instead passing a Json string serialized from a helper using the DataContractJsonSerializer() and making sure to pass the content type. Here’s the serializer helper (not exactly rocket science):

public class JsonHelper
{
   public static string Serialize<T>(T obj)
   {
       var serializer = new DataContractJsonSerializer(obj.GetType());
       var ms = new MemoryStream();
       serializer.WriteObject(ms, obj);
       return Encoding.Default.GetString(ms.ToArray());
   }
}

Posting Json using jQuery back to an ASP.NET MVC Controller

The next step was finishing the loop and allowing the client to post back the Json data in order for it to be persisted. Here’s the jQuery in the client:

function saveModel() {
  $.ajax({
      url: "/willingtowork/save",
      type: "POST",
      data: JSON.stringify(_shiftPattern),
      dataType : "json",
      contentType: "application/json; charset=utf-8",
      success: function(data, textStatus) {
          alert(data.result);
      },
      error: function(request, textStatus, errorThrown) {
          alert(request.responseText);
      }
  });
}

And the controller action that receives this:

[JsonFilter(Param = "data", RootType = typeof(WillingToWork))]
public ActionResult Save(WillingToWork data)
{
  return Json(new {result = "Success" });
}

The key here is the JsonFilterAttribute()

public class JsonFilter : ActionFilterAttribute
{

   public string Param { get; set; }
   public Type RootType { get; set; }

   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
       if (!(filterContext.HttpContext.Request.ContentType ?? string.Empty).Contains("application/json")) return;
       filterContext.ActionParameters[Param] = JsonHelper.Deserialize<WillingToWork>(filterContext.HttpContext.Request.InputStream);
   }
}

public class JsonHelper
{
   public static T Deserialize<T>(Stream sr)
   {
       var serializer = new DataContractJsonSerializer(typeof (T));
       return (T) serializer.ReadObject(sr);
   }
}

I sourced the JsonFilter from an article online although I’ve lost the reference now but I’m very grateful to whoever it was!!! I’ve also renamed the filter itself and changed it to use a helper for the deserializing. However, that filter does turn a Json string into a typed object with very little plumbing.

And that’s it!! Phew, it took a while and in the end the code needed to complete it is fairly simple. At some point I’ll probably create a new ActionResult() that wraps up the serializing using DataContractJsonSerializer() so that it’s not necessary to remember to use a ContentResult(), serializer and content type!!! Yeuck!

Theme: Shocking Blue Green. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.