JQuery delegates

Posted by Jack Altiere on April 8th, 2010

I’ve had a few projects where I needed to update information on a site at a regular interval, and it’s always a better user experience if you do it asynchronously.    I always like to come up with a functional example, so I’m going to write a quick MVC page that displays all the images in a directory.  The catch is that I want add any pictures to the page that are added to this directory without having to refresh the page.   Another thing I’d like to do is use the data capabilities in JQuery to store additional information about each image.

First I wanted to create a class to hold the additional data so that I can make a strongly typed view with it.  This is what I came up with:

[Serializable]
public class UploadedImage
{
    public int Id { get; set; }
    public string ImageName { get; set; }
    public string ImagePath { get; set; }
    public int Width { get; set; }
    public int Height { get; set; }
}


After creating my class, I created my strongly typed view to accept a List<UploadedImage>.  The next step was to add the controller method to display the view.  In this method, I wanted to load the initial list of images present in the directory and pass them to the view.

public ActionResult Timer()
{
    var photos = new List<UploadedImage>();
    var dir = new DirectoryInfo(ConfigurationParser.GetAppSettingString("PhotoDirectory"));
    var counter = 1;
    foreach (var file in dir.GetFiles())
    {
        var img = Image.FromFile(file.FullName);
        var photo = new UploadedImage
                        {
                            Id = counter,
                            ImageName = file.Name,
                            ImagePath = string.Format("../../Content/Images/Photos/{0}", file.Name),
                            Height = img.Height,
                            Width = img.Width
                        };
        photos.Add(photo);
        counter ++;
    }

    ViewData.Model = photos;
    return View();
}


This is pretty self explanatory, I just loaded the photos from the directory, gathered some information about them, and assigned each an arbitrary ID.  The last step was to put them all in a list and pass them to the view.

Since the focus of this exercise is JQuery, the bulk of our work is going to take place in the view.  I’m not doing anything complicated to display the images.  All I”m doing is using an unordered list to display all of the thumbnails, and creating a div that I will use to display additional information about the image in a pop-up when it is clicked.

<h2>JQuery Delegate Example</h2>

    <ul class="thumbnails">
        <% foreach (var item in Model) { %>
        <li><a href="#"><img id="img<%= item.Id %>" src="<%=Html.Encode(item.ImagePath) %>" alt="" /></a></li>
        <% } %>
    </ul>

    <div id="popupContact">
    <a id="popupContactClose">x</a>
    <span id="imgName"></span>
    <p id="contactArea">  

    </p>
</div>
<div id="backgroundPopup"></div>  


That leaves us with the client code.  The first obstacle we have to overcome is that we need a way to monitor the photo directory to figure out when new images are added.  I ended up using a JQuery plugin I found called doTimeout that works really well.  This sets up the framework for the polling operation, to close the loop I created a web service to pass back the list of images in the directory.

[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public List<UploadedImage> LoadImages()
{
    var photos = new List<UploadedImage>();
    var dir = new DirectoryInfo(ConfigurationParser.GetAppSettingString("PhotoDirectory"));
    foreach (var file in dir.GetFiles())
    {
        var img = Image.FromFile(file.FullName);
        var photo = new UploadedImage
        {
            ImageName = file.Name,
            ImagePath = string.Format("../../Content/Images/Photos/{0}", file.Name),
            Height = img.Height,
            Width = img.Width
        };
        photos.Add(photo);
    }

    return photos;
}


This method is in my .asmx web service.  The important thing to point out is that I specified that the response was going to be in JSON.  I can just return my List<UploadedImage> type, and it will be automatically be put into JSON where my client code can access it.

Before I worry about setting up the timer, I want to be able to attach data from the controller to my JSON objects.  My reasoning for this is because I don’t want to have to use unnecessary web service calls to retrieve this information if I already have it.  The reason I want to collect this data is that I want to display it in the pop up when someone clicks on an image.  To do this, I am going to use the jQuery.data() method.  When I wrote this code, it felt a little clunky, so if anyone has an idea for a better way to accomplish this leave me a comment.

var images = new Array();

$(document).ready(function() {
    // This is messy, I wonder if there is a better way to do this?
    <% foreach (var item in Model) { %>
        images[images.length] = "<%= item.ImageName %>";
        $("#img<%= item.Id %>").data('info', {
                Id: <%= item.Id %>,
                Name: "<%= item.ImageName %>",
                Width: <%= item.Width %>,
                Height: <%= item.Height %>
        });
    <% } %>
});


In addition to setting my data information, I’m populating an array that I had set up to keep track of what images are already displayed.  It’s easy to go off down a rabbit hole when you are coming up with an example, so rather than do it the right way and only bringing back images that were uploaded since the last time I checked, I’m just pulling  them all back for simplicity’s sake and adding ones that aren’t there yet.  The other thing I added that isn’t shown here is some code to display my pop up.  

The next thing to do is to set up the loop and poll the directory for new images, and then display any images that were added.

// Load the photos every 10 seconds.
$.doTimeout('tmrPhotoSearch', 10000, function() {
    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: "../DataService.asmx/LoadImages",
        data: "{}",
        dataType: "json",
        success: function(result) {
            ProcessImages(result.d);
        }
    });
    return true;
});

function ProcessImages(result)
{
    var max = 0;
    var data;
    $(".thumbnails a img").each(function() {
        data = $(this).data('info');
        if (data) {
            if (data.Id > max)
                max = data.Id
        }
    });
    max++;

    var item = '';
    var id = 0;
    $.each(result, function(key, value) { 

        if (jQuery.inArray(value.ImageName, images) == -1) {
            item = "<li><a href='#'><img id='img" + max + "' src='" + value.ImagePath + "' alt='' /></a></li>";
            $(".thumbnails").append(item);

            images[images.length] = value.ImageName;

            id = "#img" + max;
            $(id).data('info', {
                    Id: max,
                    Name: value.ImageName,
                    Width: value.Width,
                    Height: value.Height
            }); 

            max++;
        }
    });
}  

One thing about the web service call I should point out, be sure to include an empty data parameter if you don’t have any parameters.  If you don’t, the content-length header won’t be generated and you could run into problems.  Notice that my service is looking for a JSON result back, and on success it’s calling my ProcessImages method, passing in the web service response.   The first part of my processing method is hokey, it’s just figuring out what the max ID is that already exists and adding 1 to it.  I would definitely not do it this way for a real app, and I would certainly have more error handling built in.  With JQuery, I’m able to loop through the results directly with the .each method since my JSON has already been translated.  All I do is check each image, and add any that are not already present dynamically to the DOM at the end of the list with the append method.  I also update my array to put the new image in there so it’s not added more than 1 time, and I add all the meta data using the .data method like before.

The last bit of logic is to set up my event for displaying the pop up when an image is clicked.  I added this to my $(document).ready function to attach the click event.  I’m also retrieving the information from the JQuery cache about the clicked image and showing it in the popup.

$(".thumbnails a img").click(function() {
    var data = $(this).data('info');
    var html = "<b>" + data.Name + "</b><br/><br/>"
             + "<i>ID: " + data.Id + "</i><br/>"
             + "<i>Height: " + data.Height + "</i><br/>"
             + "<i>Width: " + data.Width + "</i><br/>";
    $("#imgName").html(html);
    loadPopup();
});

After putting a few sample images in my photo folder and running this, you can see that it grabs the 3 images that are there and shows them.

3photos

We can also see that clicking on one of these images successfully brings up our popup window with the attributes we put in the JQuery cache.

popup

The next thing to do is make sure that the timer is running.  The best way to do that in my opinion is with Firebug.  Firebug is a tremendous tool, and a great way to learn JQuery quickly. Using the “Net” tab, I can watch my asynchronous requests being sent off to the web service.

firebug

Now I’m going to keep the page up and drop an image into the directory.   You can see below that the image was added successfully to the page, but we have a problem.

4photos

When we click the new image, our pop up doesn’t work.  Although it took a little while to get here, this is actually the point of my example.  The problem is with our click event on the images.  When the event was wired up, the 4th image didn’t exist yet, so it obviously wasn’t selected by the JQuery selector statement, therefore no event gets attached to it. 

The solution to this problem is the JQuery delegate method.  Basically what this is doing is attaching the event to a parent DOM element, so any future elements that fit the selector that you add automatically get the event attached.  To get this to work, all we have to do is change our click event to look like this:

$(".thumbnails").delegate("a img", "click", function() {
    var data = $(this).data('info');
    var html = "<b>" + data.Name + "</b><br/><br/>"
             + "<i>ID: " + data.Id + "</i><br/>"
             + "<i>Height: " + data.Height + "</i><br/>"
             + "<i>Width: " + data.Width + "</i><br/>";
    $("#imgName").html(html);
    loadPopup();
});


After this change, I removed the 4th image from the directory and ran the page again. This time when I added the 4th image and clicked it, the popup appeared like we expected it to.

popup4

There are a few things I’d like to point out before wrapping up.  First, the delegate method is only available in JQuery 1.4.2 and later.  I think the version of JQuery that came with my MVC install was 1.4.1, so keep this in mind if you have a problem.  In fact, probably the best way to get JQuery is through a content delivery network. (I use the minified version specified here) The other thing that I’d like to point out is that the same capability is available in the JQuery .live method.  This method was introduced in version 1.3 of JQuery.  The difference is that the delegate method allows you to bind to a specific DOM element, where the live method binds to the root of the DOM tree.  This makes the delegate method faster, since it doesn’t have to bubble up as far.

kick it on DotNetKicks.com

Making web service calls with JQuery

Posted by Jack Altiere on April 5th, 2010

JQuery is an amazing Javascript library.   If you haven’t used it yet, you are REALLY missing out.  This article is going to talk about how you can use JQuery to make an AJAX call from an MVC application, and how you can receive JSON rather than XML from the web service.

First we’ll deal with the why.  Why would we want to receive JSON rather than XML to our web service?  To me, the answer to that starts with the fact that if we’re making an AJAX call, we’re going to do something with the results in client code.  It makes sense to use a format that is inherently understood by Javascript, which is what the client code is going to be written in most of the time.  The fact that we can send JSON data to our .NET web service allows us to pass objects back and forth from .NET and Javascript easily.

Our use case is going to be relatively simple, I want to be able to add a user to our database from an HTML form, and update the user interface when the task is done asynchronously.  I have a sample database with a very simple user table set up for this example.  I created a .dbml file with LINQ to SQL for my data layer:

data 

I created an index view for my form, the code looks like this:

<fieldset>
    <legend>Enter User Information</legend>

    <p>
    <label for="FirstName">First Name</label>
    <input type="text" id="FirstName" />
    </p>

    <p>
    <label for="LastName">Last Name</label>
    <input type="text" id="LastName" />
    </p>

    <p>
    <label for="Email">Email</label>
    <input type="text" id="Email" />
    </p>

    <p>
    <label for="Phone">Phone</label>
    <input type="text" id="Phone" />
    </p>

</fieldset>

<br />

<div class="buttons">
    <button type="submit" class="positive" name="save" id="btnSave">
        <img src='<%= Url.Content("~/Content/Images/apply.png") %>' alt=""/>
        Save
    </button>
</div> 

<br /><br />
<div id="result"></div>

The div at the bottom is where I plan to show if my service call was successful or not.  One thing I did here was use a tip I picked up by Dave Ward at Encosia.com and make the ID’s of my inputs match the property names on my user object.  This makes life easier on us later.  Then, to fix up the look and feel of the site a little, I just add a little CSS:

fieldset {
    border: 1px solid #226078;
    background: #62B1D0;
    width: 320px;
    padding: 2px;
}
fieldset legend {
    border: 1px solid #226078;
    font-family: Tahoma;
    font-size: 9pt;
    font-weight: bold;
    background: #0776A0;
    color: #FF8500;
    padding: 6px;
}

label {
    display: block;
    float: left;
    font-family: Tahoma;
    font-size: 8pt;
    font-weight: bold;
    color: #024C68;
    width: 150px;
}

label:after { content: ": " }


This makes my end UI look (slightly) more presentable.  I know, you envy my crazy user interface skills.

interface

The next thing to do is set up the JQuery code so that our button click even sends an asynchronous request to the web service.

<script language="javascript" type="text/javascript">
    $(document).ready(function() {
        $("#btnSave").click(function(event) {
            var user = {};

            // Use JQuery to iterate over the text fields and build the object.
            $(':text').each(function() {
                user[this.id] = this.value;
            });

            // Create a data transfer object (DTO) with the proper structure.
            var DTO = { 'user': user };

            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: "DataService.asmx/AddUser",
                data: JSON.stringify(DTO),
                dataType: "json",
                success: function(result) {
                    if (result.d)
                        $("#result").html("<span style='color:#529214'>User added successfully.</span>");
                    else {
                        $("#result").html("<span style='color:#d12f19'>Operation failed.</span>");
                    }
                }
            });
        });
    });
</script>


In the above code, you will see why we named the textboxes the same as the user object properties.  I used the JQuery each construct to loop through all the text boxes and populate the user object.  This is a much cleaner way to populate the object than manually setting every property.  The ajax call is pretty straight forward, I’m using the stringify method in the json2js library to turn my string into valid JSON, and lastly I’m wrapping my user object in a data transfer object to make the client code a little cleaner.  (another tip from Dave Ward)  On success of the asynchronous call, I’m parsing the result to determine whether the user was added successfully or not. 

Since I used LINQ to SQL to generate my objects, I decided to make it easy on myself by adding the “add user” logic in a partial user class.

public partial class User
{
    public void Add()
    {
        var db = new BlogSamplesDataContext();
        db.Users.InsertOnSubmit(this);
        db.SubmitChanges();
    }
}

The last step is to create a web service called DataService.asmx with a web method called AddUser that takes a parameter called user.

[System.Web.Script.Services.ScriptService]
public class DataService : WebService
{
    [WebMethod]
    public bool AddUser(User user)
    {
        try
        {
            user.Add();
        }
        catch (Exception ex)
        {
            // Error logging can happen here.
            return false;
        }

        return true;
    }
}


Make sure you uncomment the line in the web service that allows the service to be called from scripts..for whatever reason I forget to do that a lot.

I was trying to walk the line between “simple enough to get my point across” and “complex enough to show value” for this example.   The key is that you can pass full objects from client code to .NET web service code with the use of JSON, and you can make asynchronous calls to web methods fairly easily using JQuery.

kick it on DotNetKicks.com


Copyright © 2007 Jack Altiere. All rights reserved.