http://www.engadget.com/2009/07/15/itunes-8-2-1-brings-pres-music-syncing-capability-to-a-halt/
(I’m not posting heavy content this week, due to vacation. This blog will be back next week with the continuation of my prior article.)
Moving from Coder to Craftsman
http://www.engadget.com/2009/07/15/itunes-8-2-1-brings-pres-music-syncing-capability-to-a-halt/
(I’m not posting heavy content this week, due to vacation. This blog will be back next week with the continuation of my prior article.)
Introduction
One of the most powerful and compelling reasons to develop something as a application that runs natively on the user’s machine, as opposed to a web application, is responsiveness. The ability to give feedback about an action the user is taking, even if it requires a fair amount of computational time, and even to allow the user to cancel that action is a pretty useful ability. Even as javascript and XML-HTTP have enabled us to craft more responsive user interfaces to the user, the issue of responsiveness to run-longing actions is one that needs to be addressed. Fortunately, those same tools have given us the ability to bridge even that gap.
This series of articles will show how to use ASP.NET AJAX and jQuery to take a long-running task that can be broken up into elements and use that to provide the user with a dialog that provides them feedback on their progress. The particular example used will be running a large number of queries against some slow data source – it is naturally preferable that a faster way of running the queries be found, and the reader is asked to please excuse me for solving this problem and not that one in this series.
The Task
In order to accomplish this, we need to be able to break the task up into chunks. Determining what these chunks are is a task in and of itself. The size of the chunks is important - you’ll be calling them multiple times, so you want to pick a size that is large enough to be meanginful and not be overwhelmed by the overhead of the web service calls you’ll be making, and that is small enough that the interface will still feel responsive. You’ll also need to build a way to determine what the total number of chunks to return are.
Once this is done, wrap this logic in your business layer in whatever way you want to do. In this example, we’ll assume that there is a class like the following:
public interface BusinessObject
{
// Returns the total number of operations required to
// finish this task.
public int GetNumberOfOperations();
// Performs some of the operations against a set of
// operations that may be empty. If nothing was done,
// return false. Otherwise, return true.
public bool PerformOperation();
}This code is an example interface, of course, your business object should more closely resemble your model.
Web Service
Having broken up your task, you’ll need to expose those two methods of the interface as a web service. One of the beautifal things about ASP.NET AJAX is that you can easily access web services that have been decorated with the special System.Web.Script.Services.ScriptService attribute through javascript, on the client. I’ll give more details on how to do that in the next article. For the meantime, you’re web session will look something like the following:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Web.Services;
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class TaskService : System.Web.Services.WebService
{
public TaskService()
{
}
[WebMethod(EnableSession=true)]
public int GetNumberOfSteps()
{
IBusinessObject obj = (IBusinessObject)Session["businessObject"];
return obj.GetNumberOfOperations();
}
[WebMethod(EnableSession = true)]
public bool PerformOperation()
{
IBusinessObject obj = (IBusinessObject)Session["businessObject"];
return obj.PerformOperation();
}
}
You’ll note that each of the methods above are decorated with EnableSession=true. This gives the service access to the same Session state as the rest of your ASP.NET application. In this case, I’m pretending an instance IBusinessObject has been stuffed into the session. In a real world application, you might want to make sure the current user actually has permissions to do the bulky task that they’re about to do.
Caveats
The caveats about this process are, actually, enough to make this only useful in some pretty rarified situations. For the one thing, you don’t actually generally want to do long-running tasks on your web server. Particularly if those long-running tasks are exposed to the outside world and might be called on by every person in the internet. Remember that these tasks, when run, are going to take up one of the worker threads of your ASP.NET pool and that there are a limited number of these. Thus, it is easily possible to take this to a level where your server will collapse underneath the load.
In the end, you might well be better served moving the operation off to another server (perhaps the database) as a scheduled task. You could then use this trick to refresh the status of that operation, instead.
Since I’m trying to solely concentrate on the trick in this series, I’m not going to go into that. But keep in mind the business object could easily get a StartOperation method to add the operation to the queue and a CheckOperation method to see how the operation is going.
Conclusion
I hope this at least whets your appetite. In the next article, I’ll show how to implement this into a web page using javascript and jQuery.
| Reactions: |
One thing I’ve been trying to do with this blog is to use it more frequently. This particular blog has always been meant as a means for discussing my thoughts on software development topics and to show what I’ve been doing in terms of actually writing code. So in many ways, I’m attempting to use this blog to serve as a portfolio of sorts of the work I’ve done and my thinking regarding that work. There’s a longer entry about software craftsmanship and maintaining a portfolio that may come in the future.
In the meantime, however, I’ve decided I want to keep posting to this blog on a fairly regular basis. In this case, I thought I would start out with weekly entries. As it happens, the next six entries I’d like to add to this blog have already been conceived of based on work I’m currently doing or work I will soon be doing. So, here’s the schedule I’m going to try to keep to. Note that all dates are Wednesdays – this seems to currently work as a good day to write an entry, but I reserve the right to post at any time during the weke.
After that, I’ll figure out what the next set of entries will be. Hopefully there’s stuff in there that people will find interesting to read.
Thanks,
John
One of the biggest challenges of developing for web applications has always been cross-browser javascript support. Different browsers would or would not support pieces of the DOM object model, or would implement them in different ways. The issues have been getting gradually better since the Netscape/Microsoft war ended and relevant W3C specifications came out. There are still, though, issues between Internet Explorer and Mozilla (and, presumably, other browsers). One example of this is in the way the browser divides up HTML content into the ChildNodes collection. Any element of the DOM that can support children supports the ChildNodes collection, which is meant to be a way to get at the children of a particular node.
The problem, though, is that Mozilla and Internet Explorer divide up content differently. The issue basically comes down to whitespace – Mozilla divides whitespace used in the markup into separate children, while Internet Explorer ignores it. I make no attempt to figure out which is, standards-wise, the correct approach. I will say that it seems more right to ignore whitespace, since when rendering HTML whitespace is pretty much ignored. And that treating it as significant seems to encourage compressed, poorly laid out code.
That said – I'm not in charge of the design of Mozilla's javascript rendering engine (and no, I'm not going to submit patches to an open source project just because I object to the way whitespace is dealt with. I don't have that much free time, thanks. And besides, the version which treats whitespace as significant is still out there in the wild and needs to be dealt with. In the past this has led to some complex javascript code. For instance, the following code block. In this case, I'm taking a collection of DIVs, each of which contain a SELECT and a TEXTBOX and pulling out the entered values. The values are then thrown to a web service, exposed through ASP.NET AJAX. Due to the fact that this DIV is cloned repeatedly on the page through user action, assigning ids to the nodes was not a particularly feasible option.
var newItemFormContainer = $get("newItemForm");
if (Sys.Browser.name == "Microsoft Internet Explorer") {
var newItemCount =
newItemFormContainer.childNodes.length;
} else {
var newItemCount =
newItemFormContainer.childNodes.length - 1;
}
for (var i = 0; i < newItemCount; i++) {
var itemText;
var scaleId;
if (Sys.Browser.name == "Microsoft Internet Explorer") {
itemText =
newItemFormContainer.childNodes[i].childNodes[0].
childNodes[1].value;
scaleId =
newItemFormContainer.childNodes[i].childNodes[4].
childNodes[1].value;
} else {
itemText =
newItemFormContainer.childNodes[i + 1].childNodes[0].
childNodes[2].value;
scaleId =
newItemFormContainer.childNodes[i + 1].childNodes[8].
childNodes[3].value;
}
if (scaleId == "") {
scaleId = null;
}
if (itemText != null && itemText.trim() != "") {
GeneseeSurvey.SurveyDesign.ManageSurveysService.
CreateItemAndAddToSurvey(itemText, scaleId, success,
surveyServiceCallFailure, i);
}
}
This is not particular a great solution. For one thing, it requires a conditional statement that is dependent on what browser the user is using. This can get messy and hard to maintain if there are other browsers with incompatibilities.
I would argue, though, that this code is bad from a design standpoint. Dipping into the childNodes collections as I was doing in the above example requires that you have extensive knowledge of the way the HTML page lays out user interface of the form DIVs. If that user interface is changed without the javascript being changed, the code will cease to function. Furthermore, the code is hard to follow – human eyes glaze over when they see you getting the second element of the zeroth element of the ith element. It would be very difficult to step back through the code and figure out what the developer was trying to do. So, refactoring this code would be a great thing. The problem being, though, that javascript will make it difficult to do this in a better fashion.
Lucky for us, a lot of other developers have had this problem in the past. And as developers are wont to do when they have a problem, a group of developers solved the problem by adding a new layer of abstraction. In this case they solved the issue by inventing jQuery. jQuery is a javascript library which adds a powerful set of querying capabilities which enable you to get at elements in an entirely new fashion. It also provides a powerful set of animation capabilities which enable you to make better user interfaces. That's for another blog entry, however.
With jQuery, you now have the capability to take an element of the DOM (or the entire document) and conduct a search based on a class name, an element name, an element id, or by the relationship of the target node to the current node. You are, further, not limited to creating one-level searches. For example, if you have a textarea underneath a div that has a class of 'textareacontainer', which is underneath a div which is the first child of the current div (which we'll assume has an id of 'firstDiv'), you can find it with the following piece of code:
var textarea = $("#firstDiv div:firstChild .textareacontainer textarea:first");
This will return a collection that has a single element. One gotcha of jQuery is that this will not be a collection of DOM objections. jQuery wraps all search results in its own object, which lets you then do further jQuery queries against the results. jQuery does provide a host of functions to manipulate common elements of the underlying DOM object. I could get the value of the textarea with textarea.val(), for instance. If I needed the DOM object, though, I could use textarea.get(0) and then do what I have to do.
Another thing that jQuery provides as part of its wrapper around DOM objects is the ability to iterate through the collection and carry out user code. This is provided through the each method, which takes as an argument a callback function which is executed once for each DOM element in the collection. Given this information, the following is the refactoring of the first javascript code block I had.
var newItemForms = $("#newItemForm > div");
newItemCount = newItemForms.length
newItemForms.each(function(i) {
var itemText = $("textarea:first", this).val();
var scaleId = $(".selectScaleDropdown:first", this).val();
if (scaleId == "") {
scaleId = null;
}
if (itemText != null && itemText.trim() != "") {
GeneseeSurvey.SurveyDesign.ManageSurveysService.
CreateItemAndAddToSurvey(itemText, scaleId, success,
surveyServiceCallFailure, i);
}
});
I maintain that this code is significantly easier to read and much, much easier to maintain than the prior code.
It should be noted at this point that Microsoft has committed themselves to delivering jQuery with Visual Studio. If you are developing with Visual Studio 2008 SP1, then you may already have access to it – I'm not, to be honest, entirely sure (see below). They have also delivered intellisense around it, though I haven't worked with that yet. For more information, see Scott Guthrie's announcement.
If you are developing with Telerik, they have also begun shipping jQuery with their components. This is how I ended up getting jQuery into my web application, since we make fairly heavy use of the Telerik 2009 Q1 controls. It does, require a bit of a patch to get the $() functionality to work. See Atanas Korchev's blog post regarding this.
I fully intend to refactor a good portion of our javascript code to use jQuery. This will take some time since I need to develop new features as well, but I think the savings in the end will more than make up for the cost. Naturally, newly developed javascript code will make use of jQuery.
So the project I'm currently working on relies fairly heavily on LINQ-to-SQL. Thus far it is has been an absolutely delightful experience, though one in which I have spent quite a lot of time passing DataContext derivatives into my business layer objects. I've been doing this because I was under the impression that the only way to register an object to be posted back to the database or removed from the database is through the InsertOnSubmit, InsertAllOnSubmit, DeleteOnSubmit, and DeleteAllOnSubmit methods on the DataContext object.
As it turns out, this is not entirely true. I discovered this when I attempted to do a copy of an object which has a property that points to another LINQ-to-SQL managed object. One possibility with this code is that the item being used as a conditional for the branch may not exist. In this case, we don't want to copy the Branching object.
The code, thus, was this:
Public Function CreateCopy(ByVal db As SteamDatabaseDataContext, _
ByVal surveyItemMapping As Dictionary(Of Integer, SurveyItem)) _
As Branching
Dim copy As New Branching()
copy.ExcludeFlag = ExcludeFlag
copy.ParameterValue = ParameterValue
If (surveyItemMapping.ContainsKey(SurveyItemID)) Then
copy.SurveyItem = surveyItemMapping(SurveyItemID) ' BAD!
Else
Throw New ArgumentException(String.Format("The survey item that branch {0} is" + _
"based on does not exist within the surveyItemMapping parameter.", BranchID))
End If
copy.ParamType = ParamType
If (ParamType = 1) Then
If (surveyItemMapping.ContainsKey(Convert.ToInt32(Parameter))) Then
copy.Parameter = surveyItemMapping(Convert.ToInt32(Parameter)).SurveyItemID
Else
Return Nothing
End If
Else
copy.Parameter = Parameter
End If
db.Branchings.InsertOnSubmit(copy)
Return copy
End Function
Thus, if the branch is not valid, it should return nothing and not reach the db.Branchings.InsertOnSubmit(copy) call. When I executed this against a survey with a branch, though, it became quickly apparent that the code was, in fact, registering the new Branch to be inserted into the database. This actually caused a crash, since Parameter would be null and the database schema is set to not allow this.
It turns out, that the problem is the assignment to the SurveyItem property above (see comment). Setting the SurveyItem property of the new Branching object immediately marks the object as to be inserted. When we change the code to the following:
copy.SurveyItemID = surveyItemMapping(SurveyItemID).SurveyItemID
The code works find and the Branching object is abandoned.
Thus, it appears that a side-effect of assinging such a property is that the object is registered for an Insert. I wasn’t aware of this side-effect and it cost me a lot of debugging time. This is definitely an object lesson in understanding the side effects of property sets. Its also a lesson to me to, when designing my own code, minimize those side effects.
$(targetRow).insertAfter($(targetRow).next())
if (targetRow.rowIndex > 1) {
$(targetRow).insertBefore($(targetRow).prev());
}
Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
Dim itemContainer = New HtmlGenericControl("div")
itemContainer.Attributes.Add("class", "radioButtonItemControl")
itemContainer.Attributes.Add("id", String.Format("item_{0}", Me.ItemId))
Dim textContainer = New HtmlGenericControl("div")
textContainer.Attributes.Add("class", "radioButtonItemControlText")
itemContainer.Controls.Add(textContainer)
If (FlagBlankItem And Not HasValidUserResponse) Then
Dim flagSpan = New HtmlGenericControl("span")
textContainer.Controls.Add(flagSpan)
flagSpan.Attributes.Add("class", "error")
flagSpan.InnerHtml = "*"
End If
textContainer.Controls.Add(New HtmlGenericControl("span") With {.InnerText = Label})
textContainer.Controls.Add(New HtmlGenericControl("span") With {.InnerText = Text})
' Figure out if we need to set one of the scales as checked.
Dim resp As Integer
If (UserResponses.Count > 0) Then
Int32.TryParse(UserResponses(0), resp)
End If
' Write out the scale
Dim scaleContainer = New HtmlGenericControl("div")
scaleContainer.Attributes.Add("class", "radioButtonItemControlScale")
itemContainer.Controls.Add(scaleContainer)
Dim idx As Integer = 1
For Each s As SerializedScale In Scale
Dim scaleItem = New HtmlGenericControl("div")
scaleContainer.Controls.Add(scaleItem)
Dim scaleRadioButton = New HtmlInputRadioButton() With {.Name = ID, .ID = String.Format("{0}_{1}", UniqueID, idx), .Value = s.Value.ToString(), .Checked = (resp = s.Value)}
scaleRadioButton.Attributes.Add("onclick", "doRadioButtonClick(this);")
scaleItem.Controls.Add(scaleRadioButton)
Dim scaleLabel = New HtmlGenericControl("label")
scaleItem.Controls.Add(scaleLabel)
scaleLabel.Attributes.Add("for", String.Format("{0}_{1}", UniqueID.Replace("$", "_"), idx))
scaleLabel.InnerText = s.Text
idx = idx + 1
Next
itemContainer.RenderControl(writer)
End Sub
vs
Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter)
Dim controlXhtml As XElement = _
<div class="hierarchyItemControl" id=<%= String.Format("item_{0}", ItemId) %>>
<div class="hierarchyItemControlText">
<span><%= Label %></span>
<span><%= Text %></span>
</div>
<%= GetScaleXHtml() %>
</div>
writer.Write(controlXhtml)
End Sub
Private Function GetScaleXHtml() As XElement
Dim scaleDiv As XElement = _
<div class="hierarchyItemControlScale">
<%= From s In Scale _
Select <div>
<input type="radio" id=<%= String.Format("{0}_{1}", UniqueID, s.Value) %> name=<%= ID %> value=<%= s.Value %>/>
<label for=<%= String.Format("{0}_{1}", UniqueID, s.Value) %>><%= s.Text %></label>
</div> %>
</div>
Return scaleDiv
End Function