The myth of String.Format in .net

by Anthony 15. April 2012 07:44

Recently I was talking to a colleague of mine and the subject of string concatenation came up.  Now it is well known that for large string concatenation you should always use the StringBuilder object as it performs better than string concatenation.  But we were talking on a more micro level.  What is better for creating small strings String.Format or string concatenation?  I had run into the problem in the past and I knew string concatenation out performs String.Format hands down, but prior to that I was under the illusion Sting.Format was more efficient.  When I found out my colleague was under the same misconception I decided to write this article.

This article is not saying do not use String.Format.  Sometimes readability and maintainability outweigh small performance gains. Also, for formatting Dates, Times, Numbers, etc…  it is still the best choice.   However if you need to create thousands of small strings (Address concatenation, First Name Last Name, etc.) this may prove to be an eye opener.

So let’s look at the numbers for small string concatenation.  (These numbers are running on my machine your results may vary.)

Creating multiple single strings:

Creating 10000 separate sentences with string format.
Constructed Data: The quick, brown fox jumped over the lazy dog.
Elapsed Time: 00:00:00.0083951
 
Creating 10000 separate sentences with string concatenation.
Constructed Data: The quick, brown fox, jumped over the lazy dog.
Elapsed Time: 00:00:00.0000320
 
Creating 10000  separate sentences  with string builder.
Constructed Data: The quick, brown fox, jumped over the lazy dog.
Elapsed Time: 00:00:00.0011135

Obviously the clear winner here is string concatenation only taking .3 milliseconds.  But look at the difference between String.Format and StringBuilder.  Even with the cost of object creation in the string builder it is 754% faster than String.Format, huh…

Now let’s look at creating large strings:

Creating a 10000 string paragraph with string format.
Constructed Data: The quick, brown fox jumped over the lazy dog.  The quick, brown fox jumped over the lazy dog.  The ...
Elapsed Time: 00:00:05.2135822
 
Creating a 10000 string paragraph with string concatenation.
Constructed Data: The quick, brown fox, jumped over the lazy dog.  The quick, brown fox, jumped over the lazy dog.  Th...
Elapsed Time: 00:00:02.5202178
 
Creating a 10000 string paragraph with string builder.
Constructed Data: The quick, brown fox, jumped over the lazy dog.  The quick, brown fox, jumped over the lazy dog.  Th...
Elapsed Time: 00:00:00.0012484
 

This is where StringBuilder really shines.  There is no difference between creating a large concatenated string and 10000 small strings (results may vary slightly). However, String.Format is still the clear loser taking 207 % longer to complete then string concatenation.  This is when I decided it was time to find out why; ILSpy to the rescue.  I looked at the String.Format definition in mscorlib:

[SecuritySafeCritical]
public static string Format(IFormatProvider provider, string format, params object[] args)
{
	if (format == null || args == null)
	{
		throw new ArgumentNullException((format == null) ? "format" : "args");
	}
	StringBuilder stringBuilder = new StringBuilder(format.Length + args.Length * 8);
	stringBuilder.AppendFormat(provider, format, args);
	return stringBuilder.ToString();
}

WHAT THE HECK !?!?!?!?!  Every time I call string format a new string builder is created?  That is crazy…  But there you have it, code doesn’t lie.

So the next time you are thinking "I’ll use String.Format for performance reasons," think again.

Try it for yourself: 

StringConcat.zip (26.73 kb)

Tags: ,

CLR | Performance

Metrics That Matter

by Anthony 9. April 2012 14:07

Recently a member of a group of which I am enrolled posted a question, “How many lines of bug free code can a seasoned developer write in a day?”  This question tends to get the hackles of most developers to stand straight up (I am no exception).  But why do we become so offended by this question?  What has made this question a general taboo in the development community?  

I took some time to think about it and my thoughts turned to my grandfather.  He was a machinist for GE for 40+ years and his job was to work a 10-40 ton press and manufacture parts for various GE products.  He was a skilled laborer and I am under no delusions I could perform his job, however, he made the same part over the duration of his day.  His performance was measured by the number of parts he crafted minus the number of defective parts he created.  In machining this is a standard metric and one that is very easy to equate to the profit vs. expenses.

Trying to fit development into the same bucket is impossible.  We are not creating the same exact piece of code each day.  Equating average lines of code to throughput is analogous to judging the quality of a book based on the number of pages.  “If Book A has 200 pages and Book B has 400 pages, then obviously Book B is twice as good.”  Not really a metric used by most publishing houses.

But we are still left with the question, “what are good metrics for determining the effectiveness of a developer?”  For my projects I usually subscribe to the agile method of software development.  There are clear metrics for determining the success of a project and the effectiveness of a developer.

  • The team determines their low and high end effort/story points (most effort points are loosely based on the Fibonacci sequence 1, 2,3,5,8, 13, 20, 40, 100 and infinity).  An example of a 1 would be a label change, where as a 20 would be the creation of a workflow service to guide users through an application.
  • User Stories (feature requests) are examined and converted into effort points (this is usually done by consensus.)    40 and greater usually become “epics” and are broken down into smaller user stories.
  • User stories are added to a “Sprint” based on the number of effort points allowed in that given sprint (velocity). Example: Sprint 1 takes 2 weeks and has a maximum velocity of 100 points 
  • User stories are then converted to tasks in a sprint meeting. 
  • Tasks are given a duration and assigned to users.

After just a few sprints patterns start to emerge.  Effort points and durations start to have a clear correlation.  Based on this, management reports can be created that clearly show star developers vs developers that may need extra help from the more seasoned developers.

This type of metric does not implement a draconian policy of you need to write x number of characters today to get your food pellet.   But rewards developers for innovation and coming up with the best solution to a problem in a time-boxed situation.

 

Tags:

Agile | SDLC | Project Management

Dynamic Data: Skeptic Turned Believer

by Anthony 9. April 2012 12:35

I have to admit for the most part I loathe code generation tools.  The code generated is, in most cases, unsupportable, messy, uncommented and not optimized.  But that is the nature of code generators (otherwise most of us wouldn’t have jobs).  Recently I wrote an application that requires an administration piece for some of our database administrators.  Being technical people they do not need all of the bells and whistles of your average user.  I normally try to avoid these types of applications; they are not sexy, tedious and consume more time to develop then it appears they should.  

I had heard of Dynamic Data Web sites about 3 years ago and dismissed them as another code generation nightmare, however, in preparing for my Microsoft Web exam I ran across them again. I realized they are not code generated web sites, they are, in fact, a template with data hookups allowing developers to create a quick standard CRUD (Create, Read, Update and Delete) web site with a simple grid/detail page interface.  After looking into the details of how to implement, develop and configure the application I realized Dynamic Data is a perfect candidate for quickly implementing the application development task I had at hand.

There is a simple checklist I came up with to determine if an application is a good candidate for an out of the box (mostly) dynamic data implementation:

1.Are a relatively low number of users going to be accessing the application?

2.Can the application be created with a relatively simple UI

3.Is the application used for mainly administration (user creation/modification, updating type tables, updating contacts, etc.?

4.Is this not a user facing application?

5.There are no business requirements for the application

If the answers to all of those questions are yes, there is no reason not to create a dynamic data web site.  I would go into the details of how to create a Dynamic Data Website but Visual Studio 2010 makes it so easy I would just be regurgitating the steps provided for you.  I will say this, give it a try with one of your existing databases and you will see just how easy it is to create a fully functional application in minutes.

Tags: , ,

ASP.NET | Entity Framework | Dynamic Data

Entity Framework and Table per Type: You’ve been warned!!!

by Anthony 7. April 2012 16:23

Recently I was tasked with writing an application that uses a decorator pattern for its data model.  Since the model I worked on is proprietary I will use a simple “Contact” Data model to mimic this pattern.  In my “Contact” model Contact is the main table while Person, Employee, ThirdParty and Customer are the decorator tables.  The best possible choice for the implementation (or so I thought) of this model in Entity Framework (my preferred ORM) is a pattern called Table per Type.  

This seemed like the magic bullet, but being a developer I am always skeptical of magic bullets, so I did some performance testing. I noticed performance degraded exponentially for each decorator table I added to my model.  So I took a look at the generated SQL; that is when the magic bullet went through my conceptual model and shattered my design.  Ok that was a bit over dramatic.

Here is the crux of the problem:

A one table model works just fine.  I started with the Contact table.  The Lambda Expression used to select a list of items from the “Contact” table is simply:

IEnumerable<Contact> contactList = this._context.Contacts.ToList();

Which generates the following SQL:

SELECT 
[Extent1].[ContactId] AS [ContactId], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName], 
[Extent1].[ContactType] AS [ContactType]
FROM [dbo].[Contact] AS [Extent1]

Pretty simple, but when I add one table pictured below watch what happens to the SQL:

 

To use table per type you need to adjust some properties.  First, change the properties of the “Person” entity so that its bas type is “Contact”:

Second remove the ContactId property from the “Person” entity.  Now the “Person” decorator table is fully realized in the object model.  The model is now:

 

But look at what happens to the generated SQL (remember I changed nothing in the code):

SELECT 
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN '0X' ELSE '0X0X' END AS [C1], 
[Extent1].[ContactId] AS [ContactId], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName], 
[Extent1].[ContactType] AS [ContactType], 
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN CAST(NULL AS datetime2) ELSE [Project1].[Birthday] END AS [C2], 
CASE WHEN ( NOT (([Project1].[C1] = 1) AND ([Project1].[C1] IS NOT NULL))) THEN CAST(NULL AS varchar(1)) ELSE [Project1].[Title] END AS [C3]
FROM  [dbo].[Contact] AS [Extent1]
LEFT OUTER JOIN  (SELECT 
[Extent2].[ContactId] AS [ContactId], 
[Extent2].[Birthday] AS [Birthday], 
[Extent2].[Title] AS [Title], 
cast(1 as bit) AS [C1]
FROM [dbo].[Person] AS [Extent2] ) AS [Project1] ON [Extent1].[ContactId] = [Project1].[ContactId]

Ok a bit more overhead but still not horrible, watch what happens when I add all of the items to the object model:

 

Associated SQL:

SELECT 

CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN '0X' WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN '0X0X' WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN '0X1X' WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN '0X2X' ELSE '0X3X' END AS [C1], 
[Extent1].[ContactId] AS [ContactId], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName], 
[Extent1].[ContactType] AS [ContactType], 
CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN CAST(NULL AS datetime2) WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN [UnionAll3].[C2] WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN CAST(NULL AS datetime2) WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN CAST(NULL AS datetime2) END AS [C2], 
CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN [UnionAll3].[C3] WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) END AS [C3], 
CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN [UnionAll3].[C4] WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) END AS [C4], 
CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN CAST(NULL AS tinyint) WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN CAST(NULL AS tinyint) WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN [UnionAll3].[C5] WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN CAST(NULL AS tinyint) END AS [C5], 
CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN [UnionAll3].[C6] END AS [C6], 
CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN [UnionAll3].[C7] END AS [C7], 
CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) ELSE [UnionAll3].[C8] END AS [C8], 
CASE WHEN (( NOT (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL))) AND ( NOT (([UnionAll3].[C11] = 1) AND ([UnionAll3].[C11] IS NOT NULL))) AND ( NOT (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL))) AND ( NOT (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)))) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C10] = 1) AND ([UnionAll3].[C10] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C13] = 1) AND ([UnionAll3].[C13] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) WHEN (([UnionAll3].[C12] = 1) AND ([UnionAll3].[C12] IS NOT NULL)) THEN CAST(NULL AS varchar(1)) ELSE [UnionAll3].[C9] END AS [C9]
FROM  [dbo].[Contact] AS [Extent1]
LEFT OUTER JOIN  (SELECT 
[UnionAll2].[C1] AS [C1], 
[UnionAll2].[C2] AS [C2], 
[UnionAll2].[C3] AS [C3], 
[UnionAll2].[C4] AS [C4], 
[UnionAll2].[C5] AS [C5], 
[UnionAll2].[C6] AS [C6], 
[UnionAll2].[C7] AS [C7], 
[UnionAll2].[C8] AS [C8], 
[UnionAll2].[C9] AS [C9], 
[UnionAll2].[C10] AS [C10], 
[UnionAll2].[C11] AS [C11], 
[UnionAll2].[C12] AS [C12], 
[UnionAll2].[C13] AS [C13]
FROM  (SELECT 
[UnionAll1].[ContactId] AS [C1], 
[UnionAll1].[Birthday] AS [C2], 
[UnionAll1].[Title] AS [C3], 
[UnionAll1].[C1] AS [C4], 
[UnionAll1].[C2] AS [C5], 
[UnionAll1].[C3] AS [C6], 
[UnionAll1].[C4] AS [C7], 
[UnionAll1].[C5] AS [C8], 
[UnionAll1].[C6] AS [C9], 
[UnionAll1].[C7] AS [C10], 
[UnionAll1].[C8] AS [C11], 
[UnionAll1].[C9] AS [C12], 
[UnionAll1].[C10] AS [C13]
FROM  (SELECT 
[Extent2].[ContactId] AS [ContactId], 
[Extent2].[Birthday] AS [Birthday], 
[Extent2].[Title] AS [Title], 
CAST(NULL AS varchar(1)) AS [C1], 
CAST(NULL AS tinyint) AS [C2], 
CAST(NULL AS varchar(1)) AS [C3], 
CAST(NULL AS varchar(1)) AS [C4], 
CAST(NULL AS varchar(1)) AS [C5], 
CAST(NULL AS varchar(1)) AS [C6], 
cast(1 as bit) AS [C7], 
cast(0 as bit) AS [C8], 
cast(0 as bit) AS [C9], 
cast(0 as bit) AS [C10]
FROM [dbo].[Person] AS [Extent2]
UNION ALL
SELECT 
[Extent3].[ContactId] AS [ContactId], 
CAST(NULL AS datetime2) AS [C1], 
CAST(NULL AS varchar(1)) AS [C2], 
[Extent3].[CustomerId] AS [CustomerId], 
[Extent3].[CustomerRating] AS [CustomerRating], 
CAST(NULL AS varchar(1)) AS [C3], 
CAST(NULL AS varchar(1)) AS [C4], 
CAST(NULL AS varchar(1)) AS [C5], 
CAST(NULL AS varchar(1)) AS [C6], 
cast(0 as bit) AS [C7], 
cast(0 as bit) AS [C8], 
cast(0 as bit) AS [C9], 
cast(1 as bit) AS [C10]
FROM [dbo].[Customer] AS [Extent3]) AS [UnionAll1]
UNION ALL
SELECT 
[Extent4].[ContactId] AS [ContactId], 
CAST(NULL AS datetime2) AS [C1], 
CAST(NULL AS varchar(1)) AS [C2], 
CAST(NULL AS varchar(1)) AS [C3], 
CAST(NULL AS tinyint) AS [C4], 
CAST(NULL AS varchar(1)) AS [C5], 
CAST(NULL AS varchar(1)) AS [C6], 
[Extent4].[CompanyName] AS [CompanyName], 
[Extent4].[Location] AS [Location], 
cast(0 as bit) AS [C7], 
cast(1 as bit) AS [C8], 
cast(0 as bit) AS [C9], 
cast(0 as bit) AS [C10]
FROM [dbo].[ThirdParty] AS [Extent4]) AS [UnionAll2]
UNION ALL
SELECT 
[Extent5].[ContactId] AS [ContactId], 
CAST(NULL AS datetime2) AS [C1], 
CAST(NULL AS varchar(1)) AS [C2], 
CAST(NULL AS varchar(1)) AS [C3], 
CAST(NULL AS tinyint) AS [C4], 
[Extent5].[EmployeeId] AS [EmployeeId], 
[Extent5].[SSN] AS [SSN], 
CAST(NULL AS varchar(1)) AS [C5], 
CAST(NULL AS varchar(1)) AS [C6], 
cast(0 as bit) AS [C7], 
cast(0 as bit) AS [C8], 
cast(1 as bit) AS [C9], 
cast(0 as bit) AS [C10]
FROM [dbo].[Employee] AS [Extent5]) AS [UnionAll3] ON [Extent1].[ContactId] = [UnionAll3].[C1]

Now imagine this on a larger scale decorator implementation.  This issue is supposed to be addressed in the entity framework 5.0.

Hopefully this will prevent others from running into the same refactoring nightmare I ran into.

 

Tags: ,

Entity Framework | SQL Server

Update Panel: The Devil's toolkit

by Anthony 1. April 2012 06:27

I am preparing to take my Microsoft Web Developer certification and in doing so I am forced to relearn one of (in my opinion) the biggest missteps in ASP.NET history; the “Update Panel”.  Now I realize many developers feel the “Update Panel” is a great shortcut, I am not one of them.  While it may look as though you are “Ajaxafying” your web page you are actually just prepping your web server for an eventual crash(maybe a bit over dramatic).  Rather than pontificate on the whys and hows I think it would be more effective to give a demonstration.  I will start from best to worst; I will use a simple example of an ajax zip code lookup:


Example 1:

Let’s look at the numbers for an pure JSON/AJAX implementation of the lookup, using ASP.MVC :

Initial Page Size:

4513 Bytes

Lookup Request Length:

15 bytes

Request Body: 

{ 'zip': '08021'}

Lookup Response Length:

52 bytes

Response Body: 

{"Zip":"08021","City":"Laurel Springs","State":"NJ"}

All and all this is a very small amount of data being sent back and forth.  There is very little in the way of overhead in these calls.  The implementation is as follows:

Important Controls:

<div class="editor-label">
        @Html.LabelFor(model => model.City)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.City)
        @Html.ValidationMessageFor(model => model.City)
    </div>
    <div class="editor-label">
        @Html.LabelFor(model => model.State)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.State)
        @Html.ValidationMessageFor(model => model.State)
    </div>
    <div class="editor-label">
        @Html.LabelFor(model => model.Zip)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.Zip)
        @Html.ValidationMessageFor(model => model.Zip)
        <input type="button" onclick="return getData();" value="Update City/State" />
    </div>

Script:

function getData() {
        var zip = $("#Zip").val();
        $.ajax(
            { type: "POST",
            url: "@(Url.Content("~/Contact/_getZipData"))",
            data: "{'zip':'" + zip + "'}",
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            success: function(data){
                    $("#City").val(data.City);
                    $("#State").val(data.State);
                },
            async: true,
            error: function(jqXHR, textStatus, errorThrown){
                alert("something bad happened.");
            }
        });
        return false;
    }

Controller:

public class ContactController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult _getZipData(string zip)
        {
            ZipCode zipCodeData = zip.GetCityAndState();
            return Json(zipCodeData);
        }
    }

Example 2:

The following is a WCF AJAX enabled service.  In the world of ASP.NET this is the optimal method for the backend of an Ajax service.  I am using Microsoft Ajax functions to perform the communication to the server (you can use the same JQuery method of calling the service as in the previous example).  This is where we will notice some inefficiencies occur:

Initial Page Size:

5697 Bytes

Lookup Request Length:

15 bytes

Request Body: 

{"zip":"08021"}

Lookup Response Length:

100 bytes

Response Body: 

{"d":{"__type":"ZipCode:#MvcApplication1.Cache","City":"Laurel Springs","State":"NJ","Zip":"08021"}}

Important Controls:

<div class="editor-label">
            <asp:Label ID="lbCity" runat="server" Text="City" AssociatedControlID="tbCity"></asp:Label>
        </div>
        <div class="editor-field">
            <asp:TextBox ID="tbCity" ClientIDMode="Static" runat="server"></asp:TextBox>
        </div>
        <div class="editor-label">
            <asp:Label ID="lbState" runat="server" Text="State" AssociatedControlID="tbState"></asp:Label>
        </div>
        <div class="editor-field">
            <asp:TextBox ID="tbState" ClientIDMode="Static" runat="server"></asp:TextBox>
        </div>
        <div class="editor-label">
            <asp:Label ID="lbZip" runat="server" Text="Zip" AssociatedControlID="tbZip"></asp:Label>
        </div>
        <div class="editor-field">
            <asp:TextBox ID="tbZip" ClientIDMode="Static" runat="server"></asp:TextBox>
            <input type="button" onclick="return getData();" value="Update City/State" />
        </div>

Script:

function getData() {
    var zip = $("#tbZip").val();
    GetZipData.GetZipInfo(zip, onSucceed, onError);
    return false;
}

function onSucceed(result) {
    $("#tbCity").val(result.City);
    $("#tbState").val(result.State);
}

function onError(result) {
     alert("something bad happened.");
}

Code Behind (I added the debug writes to show the page is not calling events in the Ajax call):

public partial class WcfMethodTest : System.Web.UI.Page
    {
        protected void Page_Init(object sender, EventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("Page Init");
        }
        protected void Page_Load(object sender, EventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("Page Load");
        }
        protected void Page_PreRender(object sender, EventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("Page PreRender");
        }
        protected void Page_Unload(object sender, EventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("Page Unload");
        }
    }

Service:

[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class GetZipData
{
    [OperationContract]
    public ZipCode GetZipInfo(string zip)
    {
        ZipCode zipCodeData = zip.GetCityAndState();
        return zipCodeData;
    }
}

Example 3:

This is using the Microsoft AJAX JS libraries to call a Page method. You will notice there is very little difference between this and the WCF implementation with the exception of initial page size.

Initial Page Size:

9337 Bytes (a lot of scaffolding needed for this implementation)

Lookup Request Length:

15 bytes

Request Body: 

{"zip":"08021"}

Lookup Response Length:

99 bytes

Response Body: 

{"d":{"__type":"MvcApplication1.Cache.ZipCode","Zip":"08021","City":"Laurel Springs","State":"NJ"}}

Important Controls:

        <div class="editor-field">
            <asp:TextBox ID="tbCity" ClientIDMode="Static" runat="server"></asp:TextBox>
        </div>
        <div class="editor-label">
            <asp:Label ID="lbState" runat="server" Text="State" AssociatedControlID="tbState"></asp:Label>
        </div>
        <div class="editor-field">
            <asp:TextBox ID="tbState" ClientIDMode="Static" runat="server"></asp:TextBox>
        </div>
        <div class="editor-label">
            <asp:Label ID="lbZip" runat="server" Text="Zip" AssociatedControlID="tbZip"></asp:Label>
        </div>
        <div class="editor-field">
            <asp:TextBox ID="tbZip" ClientIDMode="Static" runat="server"></asp:TextBox>
            <input type="button" onclick="return getData();" value="Update City/State" />
        </div>

Script:

function getData() {
    var zip = $("#tbZip").val();
    PageMethods.GetZipData(zip, onSucceed, onError);
    return false;
}

function onSucceed(result) {
    $("#tbCity").val(result.City);
    $("#tbState").val(result.State);
}

function onError(result) {
    alert("something bad happened.");
}

Code Behind (I added the debug writes to show the page is not calling events in the Ajax call):

public partial class PageMethodTest : System.Web.UI.Page
{
    protected void Page_Init(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page Init");
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page Load");
    }

    protected void Page_PreRender(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page PreRender");
    }

    protected void Page_Unload(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page Unload");
    }

    [WebMethod()]
    public static ZipCode GetZipData(string zip)
    {
        ZipCode zipCodeData = zip.GetCityAndState();
        return zipCodeData;
    }
}

Example 4:

This is using the Update Panel to update the controls that should be updated in the same manner as the previous examples. This is by far the worst of all of the examples posted

Initial Page Size:

5538 Bytes (less than the previous example; less scaffolding required)

Lookup Request Length:

638 bytes (over a 4000% increase over previous examples!!!)

Request Body:

ctl00%24body%24smUpdate=ctl00%24body%24smUpdate%7Cctl00%24body%24lbUpdateCityState&__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=%2FwEPDwUKLTE0ODYwOTIxMWRkXgPL66ERIqtx3gVubvTQQrkVqfAVcjgDjOwCDEs4Z8k%3D&__EVENTVALIDATION=%2FwEWCwKC9ZT3DAK59tu9AgL%2F1ZrTAgLIuYntCwKZ9eKsCgKp742yBwLVs6yZCQKqxfb2DALOw%2ByZAwKu2fbCAQL%2FlNACnJEvyD3m7WcENitn%2FCghxG8SBhyLx5ZsHpmAJLC57q4%3D&ctl00%24body%24tbFirstName=&ctl00%24body%24tbLastName=&ctl00%24body%24tbStreetAddress1=&ctl00%24body%24tbStreetAddress2=&ctl00%24body%24tbCity=&ctl00%24body%24tbState=&ctl00%24body%24tbZip=08021&ctl00%24body%24tbPhone1=&ctl00%24body%24tbPhone2=&__ASYNCPOST=true&ctl00%24body%24lbUpdateCityState=Update%20City%2FState

Lookup Response Length:

1307 bytes (over a 2500% increase over the pure JSON implementation and over a 1300% increase over the WCF and Page Method examples!!!)

Response Body:

1|#||4|647|updatePanel|body_upUpdateCityState|
                <div class="editor-label">
                    <label for="body_tbCity" id="body_lbCity">City</label>
                </div>
                <div class="editor-field">
                    <input name="ctl00$body$tbCity" type="text" value="Laurel Springs" id="body_tbCity" />
                </div>
                <div class="editor-label">
                    <label for="body_tbState" id="body_lbState">State</label>
                </div>
                <div class="editor-field">
                    <input name="ctl00$body$tbState" type="text" value="NJ" id="body_tbState" />
                </div>
            |68|hiddenField|__VIEWSTATE|/wEPDwUKLTE0ODYwOTIxMWRkXgPL66ERIqtx3gVubvTQQrkVqfAVcjgDjOwCDEs4Z8k=|136|hiddenField| __EVENTVALIDATION|/wEWCwKC9ZT3DAKp742yBwLVs6yZCQK59tu9AgL/1ZrTAgLIuYntCwKZ9eKsCgKqxfb2DALOw+yZAwKu2fbCAQL/lNACjqb cQj2LhjBHPYGxcc/Oy+TYZxbWVj4w/u27ZVUFn84=|51|asyncPostBackControlIDs||ctl00$body$lbUpdateCityState,body_lbUpdateCityState|0|postBackControlIDs|||52|updatePanelIDs ||fctl00$body$upUpdateCityState,body_upUpdateCityState|0|childUpdatePanelIDs|||51|panelsToRefreshIDs ||ctl00$body$upUpdateCityState,body_upUpdateCityState|2|asyncPostBackTimeout||90|11|formAction ||UpdatePanel|4|pageTitle||Test|

As a side note: There is also a hidden issue to the Update Panel, the page must go through the entire page life cycle.  Meaning every event in the page is fired including all of the rendering and child creation events, even if the control is not updated.

Important Controls:

<asp:UpdatePanel ID="upUpdateCityState" runat="server" UpdateMode="Conditional" ChildrenAsTriggers="false">
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="lbUpdateCityState" EventName="Click"/>
    </Triggers>
    <ContentTemplate>
        <div class="editor-label">
            <asp:Label ID="lbCity" runat="server" Text="City" AssociatedControlID="tbCity"></asp:Label>
        </div>
        <div class="editor-field">
            <asp:TextBox ID="tbCity" runat="server"></asp:TextBox>
        </div>
        <div class="editor-label">
            <asp:Label ID="lbState" runat="server" Text="State" AssociatedControlID="tbState"></asp:Label>
        </div>
        <div class="editor-field">
            <asp:TextBox ID="tbState" runat="server"></asp:TextBox>
        </div>
    </ContentTemplate>
</asp:UpdatePanel>
<div class="editor-label">
    <asp:Label ID="lbZip" runat="server" Text="Zip" AssociatedControlID="tbZip"></asp:Label>
</div>
<div class="editor-field">
    <asp:TextBox ID="tbZip" runat="server"></asp:TextBox>
    <asp:Button ID="lbUpdateCityState" runat="server" Text="Update City/State" onclick="lbUpdateCityState_Click"></asp:Button>
</div>

Code Behind (I added the debug writes to show the page is calling every event in the Update Panel call):

public partial class UpdatePanelTest : System.Web.UI.Page
{
    protected void Page_Init(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page Init");
    }
    protected void Page_Load(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page Load");
    }
    protected void Page_PreRender(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page PreRender");
    }
    protected void Page_Unload(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page Unload");
    }

    protected void lbUpdateCityState_Click(object sender, EventArgs e)
    {
        string zip = tbZip.Text;
        ZipCode zipCodeData = zip.GetCityAndState();
        tbCity.Text = zipCodeData.City;
        tbState.Text = zipCodeData.State;
    }
}

While this example is fairly simplistic it illustrates a very important point, the update panel adds a tremendous amount of overhead for a minor saving on code.  Is it more maintainable?  Slightly; as new developers only need to know ASP.NET and not javascript (IMO not an options for web developers today).

So let’s look at the numbers:

 

Pure Ajax Json

WCF ASP.NET

Page Method ASP.NET

Update Panel

Request Length

15

15

15

638

Response Length

52

100

99

1307

Lines of Script

18

12

12

0

Lines of Markup

21

18

18

25

Lines of Code (just the update method)

5

10

5

7


I did not add execution times because as can vary, but the raw numbers speak for themselves.  There is very little value add in using the Update panel over standard AJAX calls.  I would go as far as saying there is no room for them in any web application/site in which load based performance is any kind of consideration.

UpdatePanelComparisons.zip (2.49 mb)

Tags:

AJAX | ASP.NET | jquery | JSON | MVC3

JQuery Tooltips with MVC3 and CSS 3.0 (Part 2)

by Anthony 25. March 2012 02:56

So in my previous post I explained all of the steps required to create a simple but effective CSS 3 JQuery tooltip implementation.  This was a good start, I had all I needed to fulfill my requirement of a consistent tooltip in my application.  But I needed it to be a little easier to use throughout the application.  A custom Html Helper Extension to the rescue. Creating an HTML Helper is a pretty straight forward task this one, however, needed a bit of special sauce to make it work the way I wanted.

First I wanted to make sure I could bind it to my model, in case in the future I wanted to default it to the [Display] property on the model.  Second I wanted to be able to use the @<text></text> syntax for the contents of the tooltip.  It made sense to work backwards I wanted the control to work like this: 

@Html.InfoTooltipFor(model => model.ToolTipTitle,
	@<text>
	<h4>@Model.ToolTipTitle</h4>
	<ul>
   		<li>
			<b>Scott Guthrie</b> – 
			<a href="http://weblogs.asp.net/scottgu/">
				ScottGu's Blog
			</a> One of the best resources for all things Microsoft Web
                </li>
		<li>
			<b>Scott Hanselman</b> – 
				<a href="http://www.hanselman.com/blog/">
					Scott Hanselman's Computer Zen
				</a> Another great resource very current and very informative.
		</li>
		<li>
			<b>ASP.NET</b> – 
				<a href="http://www.asp.net/mvc">
					ASP.NET MVC
				</a> The mother of all things ASP.NET MVC
		</li>
     </ul>
</text>)

 

Seemed simple enough so at first I create the Helper as follows:

 

public static MvcHtmlString InfoTooltipFor<TModel, TValue>(
    this HtmlHelper<TModel> html, 
    Expression<Func<TModel, TValue>> expression, 
    string tooltip)
{
...
}

 

Nope that won't work... 

 

And once I thought about it makes sense that a string won't work.  It isn't anything remotely like a string it contains functions, delegates and properties. I know this type of implementation exists I have seen it in other controls.  So how is it done?  After an exhaustive google search I found this article by Phil Haack.  It was close to what I needed, so, after some trial and error I came up with this:

public static MvcHtmlString InfoTooltipFor<TModel, TValue>(this HtmlHelper<TModel> html, 
		Expression<Func<TModel, TValue>> expression, 
		Func<object, HelperResult> tooltip)
{
	//Get the Model metadata
	ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
	string htmlFieldName = ExpressionHelper.GetExpressionText(expression);
	string tooltipId = metadata.PropertyName ? ? htmlFieldName.Split('.').Last();

	if (String.IsNullOrEmpty(tooltipId))
	{
		return MvcHtmlString.Empty;
	}
	//creates the span tag for the image
	TagBuilder spanTag = new TagBuilder("span");
	spanTag.MergeAttribute("class", "tooltipItem");
	spanTag.MergeAttribute("id", tooltipId);
	spanTag.MergeAttribute("data-tooltip", "tt" + tooltipId);

	TagBuilder imgTag = new TagBuilder("img");
	imgTag.MergeAttributes(new RouteValueDictionary(
		new
		{
			@class = "infoimage",
			src = UrlHelper.GenerateContentUrl("~/Content/info.png", html.ViewContext.HttpContext),
			alt = "info"
		}
		));
	spanTag.InnerHtml = imgTag.ToString(TagRenderMode.Normal);

	//creates the tooltip contents
	TagBuilder tooltipTag = new TagBuilder("div");
	tooltipTag.MergeAttribute("class", "hidden");
	tooltipTag.MergeAttribute("id", "tt" + tooltipId);
	tooltipTag.MergeAttribute("name", "tt" + tooltipId);
	tooltipTag.MergeAttribute("data-tooltip", "tt" + tooltipId);
	tooltipTag.InnerHtml = tooltip(null).ToString();

	return MvcHtmlString.Create(spanTag.ToString(TagRenderMode.Normal) + tooltipTag.ToString(TagRenderMode.Normal));
}

The key is Func<object, HelperResult> this allows templated items to be passed into helper method.  This was exactly what I needed and poof like magic it worked.  MVC is awesome, creating a control like that in standard ASP.NET would have been a major pain. 

The entire solution is in the attached zip...

EasyTooltips.zip (2.46 mb)

Tags: , , , ,

AJAX | css | jquery | MVC3

JQuery Tooltips with MVC3 and CSS 3.0 (Part 1)

by Anthony 24. March 2012 17:58

I am a developer and no graphic artist by any strech of the imagination, but I am often thrown into that roll.   Seriously, how many small projects have the budget for a Graphic artist and a developer, not to mention the fact management rarely knows the difference.  

Regardless, I needed to create some tooltips to be used throughout an internal web application I was creating.  There are about 30 trillion jquery tooltip plugins on the internet, and most of them are good, I felt a little plugin overloaded, so I decide to roll my own.  The CSS 3.0 implementation was pretty simple (only 2 classes):

.tooltipwindow
{
   border-bottom-left-radius: 15px;
   border-top-left-radius: 0px;
   border-bottom-right-radius: 15px;
   border-top-right-radius: 15px;
   border: 1px solid #0099FF;
   position:absolute;
   padding:5px;
   background-color:#fff;
   z-index: 20000;
   box-shadow: 3px 3px 2px #808080;
   font-size:smaller;
}
 
.hidden
{
   display:none;
}

Only a few CSS 3.0 items to mention box-shadow (for the drop shadow effect) and border-radius (for the rounded corners on all but the top right) other than that pretty standard.  The JavaScript was pretty simple as well:

A few global variables:

//A 1/2 Second fade-in/out
var toolTipHideDelay = 500;
//A 1/4 second before the tooltip begins to fade
var toolTipMouseoutDelay = 250;
//The timer to determine when the fade should start
var toolTipHideTimer = null;
//The variable containing the tool tip contents
var tooltipWindow = null;
//The current Id of the tool tip
var tooltipCurrentId = null;

Then the actual tool event handlers:

//standard doc ready event
$(document).ready(function () {
 
    initializeTooltips();
})
 
function initializeTooltips() {
    //there is only one window for all tooltips on the page.
    tooltipWindow = $("#tooltipWindow");
    //all tooltips implement the following class
    var tooltipItem = $(".tooltipItem");
 
    //tooltip mouse events (not using hover because I want the tooltip to follow the cursor
    tooltipItem.mousemove(function (e) {
        //check to see if the tooltip is already opened
        if (toolTipHideTimer) {
            //clear the timeout since the user is hovering over the item again
            clearTimeout(toolTipHideTimer);
            //retrieve the tooltip data item from the current element
            if (tooltipCurrentId != $(this).data("tooltip")) {
                setToolTipPosition(this, e);
                //set the tooltip inner content
                tooltipWindow.html($("#" + $(this).data("tooltip")).html());
            }
        } else {
            //show the tooltip
            setToolTipPosition(this, e);
            //the tooltip is hidden do fade it in
            tooltipWindow.fadeIn(toolTipHideDelay);
            //set the tooltip inner content
            tooltipWindow.html($("#" + $(this).data("tooltip")).html());
        }
    });
    tooltipItem.mouseout(function (e) {
        //get ready to hide the tooltip
        toolTipHideTimer = setTimeout(function () {
            toolTipHideTimer = null;
            tooltipWindow.fadeOut();
        }, toolTipMouseoutDelay);
    });
 
    //tooltip window mouse events
    tooltipWindow.mouseover(function () {
        //keep the tooltip open if the user is over the tooltip window
        if (toolTipHideTimer) {
            clearTimeout(toolTipHideTimer);
        }
    });
    tooltipWindow.mouseout(function () {
        //get ready to hide the tooltip
        toolTipHideTimer = setTimeout(function () {
            toolTipHideTimer = null;
            tooltipWindow.fadeOut();
        }, toolTipMouseoutDelay);
    });
}

Then just a simple postion helper:

function setToolTipPosition(item, mouseevent) {
    var tooltipitem = $("#tooltipWindow");
    //set the tool tip to the current mouse position the 
    //2+ is so the user can still click the element under the 
    //tooltip
    tooltipitem.css({
        left: 2+(mouseevent.pageX) + "px",
        top: 2+(mouseevent.pageY) + "px"
    });
}

So now we have all of the client pieces we need, infact the following html will generate a tooltip:

<h3 style="display:inline-block">For More info hover over the icon</h3>
<span class="tooltipItem" data-tooltip="ttToolTipTitle" id="ToolTipTitle">
  <img alt="info" class="infoimage" src="/Content/info.png" />
</span>
 
<div class="hidden" data-tooltip="ttToolTipTitle" id="ttToolTipTitle" name="ttToolTipTitle">        
            <h4>Learn More from these sites</h4>
            <ul>
                <li><b>Scott Guthrie</b> – <a href="http://weblogs.asp.net/scottgu/">ScottGu's Blog</a> One of the best resources for all things Microsoft Web</li>
                <li><b>Scott Hanselman</b> – <a href="http://www.hanselman.com/blog/">Scott Hanselman's Computer Zen</a> Another great resource very current and very informative.</li>
                <li><b>ASP.NET</b> – <a href="http://www.asp.net/mvc">ASP.NET MVC</a> The mother of all things ASP.NET MVC</li>
            </ul>
        </div>
</div>
 
<div id="tooltipWindow" class="hidden tooltipwindow">
</div>

 

That looks like this:

 

So we are good to go, I just didnt want to have to copy all of this HTML for every tooltip I created in the system.  In Part 2 I will talk about how to integrate this with an MVC Html Helper.

 

Tags: , , ,

css | jquery

MVC Makes AJAX easier

by Anthony 12. February 2012 19:07

MVC is a pretty new paridigm to me, I just started using it about 5 months ago, but one of the first things I noticed is how much easier it has made AJAX.  This is coming from someone who has had a very long career in ASP.NET.  There were a few different ways to provide Ajax functionality in ASP.NET the evil update panel being the easiest to implement.  My preference and, IMO, the more correct approach was to use the AJAX framework in conjunction with AJAX Aware WCF/Web Services.  The low framework overhead (no Eventing Model) in conjuction with the relatively small request/response package made these fairly easy to implement and perform very well.  The problem with these services is they were still tied up in all sorts of framework gook.

With MVC, all of that framework gook is gone, it is a much simpler PHP style implementation of AJAX.  There are several ways to utilize AJAX in the MVC world, there are the AJAX MVC Helper Class which is very easy to work with, but I perfer a pure JQuery ajax approach.

Enough talk here is an example of a simple implementation of ajax using 4 commonly used methods:

First the Controller (in this case I am using the Products table of the Northwind Database (kickin' it old school)):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
 
namespace JqueryAjax.Controllers
{
    public class ProductsController : Controller
    {
        //
        // GET: /Products/
 
        public ActionResult Index()
        {
 
            List<Product> prodList = null;
            using (NorthwindEntities ctx = new NorthwindEntities())
            {
                prodList = ctx.Products.OrderBy(p => p.ProductName).ToList();
 
            }
            return View(prodList);
        }
 
        public ActionResult _productInfoJSON(int? id)
        {
            Product prod = null;
            using (NorthwindEntities ctx = new NorthwindEntities())
            {
                prod = ctx.Products.Single(p => p.ProductID == id);
 
            }
            return Json(prod, JsonRequestBehavior.AllowGet);
        }
 
        public ActionResult _productInfo(int? id)
        {
            Product prod = null;
            using (NorthwindEntities ctx = new NorthwindEntities())
            {
                prod = ctx.Products.Single(p => p.ProductID == id);
 
            }
            return PartialView(prod);
        }
 
        [HttpPost]
        public ActionResult _productInfoJSONParams()
        {
            Product prod = null;
            int prodId = int.Parse(this.ValueProvider.GetValue("id").AttemptedValue);
            using (NorthwindEntities ctx = new NorthwindEntities())
            {
                prod = ctx.Products.Single(p => p.ProductID == prodId);
 
            }
            return Json(new { prod.ProductName, prod.UnitPrice });
        }
 
    }
}

I use the "_" syntax to denote partial actions and views.  So looking at the above code you will notice there is only one full action "Index", so lets look at its view:

@model IEnumerable<JqueryAjax.Product>
 
@{
    ViewBag.Title = "Products List";
}
 
@section script{
    <script src="@Url.Content("~/Scripts/Info.js")" type="text/javascript"></script>
}
<h2>Index</h2>
<table>
    <tr>
        <th>
 
        </th>
        <th>
            ProductName
        </th>
    </tr>
 
@foreach (var item in Model) {
    <tr>
        <td>
            <a class="prodInfo" href="#" data-product-id="@item.ProductID">
                <img src="../../Content/Images/info.jpg" width="20" height="20" alt="info" border="0" />
            </a>
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.ProductName)
            <div id="product-@item.ProductID" style="display:none;">
            </div>
        </td>
    </tr>
}
 
</table>
So in this view I am simply building a Table of product names with an info image, easy peasy.  The Ajax parts of the html are housed in the:
 
<div id="product-@item.ProductID" style="display:none;">
And the:
<a class="prodInfo" href="#" data-product-id="@item.ProductID">

The former is the place holder for our dynamic content and the latter is the link to our dynamic content.  Now that we have all of the page scaffolding in place we need to hook up the pieces.  There are several ways we can retrive the content.  We could retrieve a standard JSON object from a controller _productInfoJSON and _productInfoJSONParams or you can return a partial view.  This is where the power of MVC really hits home.  You can craft a fully functional page and return it to your AJAX container.  That is pretty sweet...  But first lets look at the JSON example:

I created 2 a JSON Get (useful for RESTful services) and a JSON Post (prefered for internal web site calls):

Here is the Java script for calling the JSON get in conjuntion with the previously crafted view and controller:

/// <reference path="jquery-1.7.1.min.js" />
$(document).ready(function () {
    $(".prodInfo").click(function (e) {
        //stop the href="#" from firing
        e.preventDefault();
 
        var prodId = $(this).data("productId");
        jsonGet(prodId);
 
    });
});

function jsonGet(prodId) {
    $.ajax({
        url: "/Products/_productInfoJSON/" + prodId,
        success: function (data) {
            $("#product-" + prodId).toggle();
            $("#product-" + prodId).html("<span> price:"+ data.UnitPrice +"</span>");
        }
    });
}
and the JSON parameterized post:

jsonPost(prodId);

function jsonPost(prodId) {
    $.ajax({
        url: "/Products/_productInfoJSONParams",
        type: "POST",
        data: { id: prodId },
        success: function (data) {
            $("#product-" + prodId).toggle();
            $("#product-" + prodId).html("<span> price:" + data.UnitPrice + "</span>");
        }
    });
}

This will get you a simple return value that you can format in JQuery.

Pretty simple right?  Well its about to get easier.  Now lets say we want to return a significant amount of data to the client and we don't want to do that messy JavaScript HTML formatting.  Partial Views to the rescue.  Create the following partial view from the _productInfo action:

@model JqueryAjax.Product
 
<fieldset>
    <legend>Product Info</legend>
 
    <div class="display-label"></div>
    <div class="display-field">
        QuantityPerUnit: @Html.DisplayFor(model => model.QuantityPerUnit)
    </div>
 
    <div class="display-field">
        UnitPrice: @Html.DisplayFor(model => model.UnitPrice)
    </div>
 
    <div class="display-field">
        UnitsInStock: @Html.DisplayFor(model => model.UnitsInStock)
    </div>
 
    <div class="display-field">
        UnitsOnOrder: @Html.DisplayFor(model => model.UnitsOnOrder)
    </div>
 
    <div class="display-field">
        ReorderLevel: @Html.DisplayFor(model => model.ReorderLevel)
    </div>
 
    <div class="display-field">
        Discontinued: @Html.DisplayFor(model => model.Discontinued)
    </div>
</fieldset>

Now change the JS click event to fire:

partialJqueryGet(prodId);

with the function definition of:

function partialJqueryGet(prodId) {
    $.get("/Products/_productInfo/" + prodId,
    function (data) {
        $("#product-" + prodId).toggle();
        $("#product-" + prodId).html(data);
    });
}

BA BAM done.  Now you are returning a partial view from an ajax call.  You can also use a pure jquery ajax implementation as opposed to the Get helper class:

function partialAjaxGet(prodId) 
{
    $.ajax({
        url: "/Products/_productInfo/" + prodId,
        success: function (data) {
            $("#product-" + prodId).toggle();
            $("#product-" + prodId).html(data);
        }
    });
 
}

 

All of the code and the northwind database can be found here:

JqueryAjax.zip (2.67 mb)

http://www.microsoft.com/download/en/details.aspx?id=23654

 

Tags: , , , , , ,

AJAX | ASP.NET | jquery | JSON | MVC3 | Razor

Noobie frustrated by nuGet

by Anthony 22. January 2012 20:44

So I am fairly new to MVC 3 but I did want to use the latest version of jQuery.  To my disappointment the default MVC 3 project comes with jQuery version 1.5.1.  Now, for my first MVC3 Razor project I just updated all of the script files manually.  That was quite annoying, then I found my savior (so to speak) nuGet.  NuGet should allow me to update the jQuery files via a simple simple Package Manager Console command "update-package jquery".  It can't be this simple...

Well guess what it isn't, again I was only given half the story.  So blissfully ignorant I proceeded to run the command:

PM> update-package jquery
Successfully installed 'jQuery 1.7.1'.
Update-Package : Conflict occurred. 'jQuery 1.5.1' referenced but requested 'jQuery 1.7.1'. 'jQuery.vsdoc 1.5.1, jQuery.Validation 1.8.0, jQu
ery.UI.Combined 1.8.11' depend on 'jQuery 1.5.1'.
At line:1 char:15
+ update-package <<<< jquery
+ CategoryInfo : NotSpecified: (:) [Update-Package], InvalidOperationException
+ FullyQualifiedErrorId : NuGetCmdletUnhandledException,NuGet.PowerShell.Commands.UpdatePackageCommand

 

Huh, success but failure.  So in my research I found the following command:

PM> get-package

Id Version Description
EntityFramework 4.1.10331.0 DbContext API and Code First workflow for ADO.NET Entity Framework.
jQuery 1.5.1 jQuery is a new kind of JavaScript Library....
jQuery 1.7.1 jQuery is a new kind of JavaScript Library....
jQuery.UI.Combined 1.8.11 jQuery UI is an open source library of interface components — interactions, full-featu...
jQuery.Validation 1.8.0 This jQuery plugin makes simple clientside form validation trivial, while offering lot...
jQuery.vsdoc 1.5.1 Includes vsdoc files for jQuery 1.5.1 that provide IntelliSense in Visual Studio 2010....
Modernizr 1.7 Modernizr adds classes to the <html> element which allow you to target specific browse...

Ok lets think about this, incompatable errors, a bunch of different pacakages of course .  After some trial and error I came up with the following order in which to run the updates:

update-package jQuery.UI.Combined
update-package jQuery.Validation
update-package Moderniz
update-package jQuery.vsdoc
update-package jQuery

This order allows jquery to update via nuGet correctly.  I hope this helps other frustrated nuGet noobs.

Tags: , , ,

jquery | MVC3 | nuGet | Razor

FAJAX Image Buttons

by Anthony 17. November 2010 15:40

I had an interesting question the other day.  "How can I use AJAX to stream images back from a database to the web browser?"  The answer is, you dont need to. This functionality has been around since .net 1.0.  You can create an HTTP Handler.  Http Handlers give you control over any request recieved from the client.  In the case of an Image streamed from... whatever, you can do it quite simply...  Here is all of the code you need:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="SlideShow.aspx.cs" Inherits="SlideShow" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Slide Show</title>
    <script>
        var index = 1;
        function getImage() {
            //increment counter
            if(index == 5){
                index = 1;
            }
            else{
                index ++
            }

            document.getElementById("sldImage").src = index + ".ipg";
        }
    </script>
</head>

<body>
    <form id="form1" runat="server">
    <div id="slidePanel">
        <img id="sldImage" src="1.ipg" alt="slideImg" />
    </div>
    <input type="button" value="Next" onclick="getImage();return false;" />
    </form>
</body>
</html>

In the code above I am I am using an "IPG" extension as a replacement for jpegs.  This code will also support gif as "IIF".  The Image Handler would look as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
/// <summary>
/// Summary description for Class1
/// </summary>
public class HttpImageHandler : IHttpHandler
{
    public void ProcessRequest(System.Web.HttpContext context)
    {
        long fileID = 0;
        long.TryParse(Path.GetFileNameWithoutExtension(context.Request.Path), out fileID);

        context.Response.ContentType = getContentType(context.Request.Path);
        FileInfo fi = null;
        switch (fileID)
        {
            case 1:
                fi = new FileInfo("C:\\Pics\\Chrysanthemum.jpg");
                break;
            case 2:
                fi = new FileInfo("C:\\Pics\\Desert.jpg");
                break;
            case 3:
                fi = new FileInfo("C:\\Pics\\Hydrangeas.jpg");
                break;
            case 4:
                fi = new FileInfo("C:\\Pics\\Jellyfish.jpg");
                break;
            case 5:
                fi = new FileInfo("C:\\Pics\\Koala.jpg");
                break;
        }
        context.Response.BinaryWrite(getImage(fi.OpenRead(), Path.GetExtension(context.Request.Path)));
        
        context.Response.End();
    }
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

    /// <summary>
    /// Gets the image steram as a byte array
    /// </summary>
    /// <param name="path">The </param>
    /// <returns></returns>
    private byte[] getImage(Stream imageData, string ImageType)
    {
        Bitmap imgIn = new Bitmap(imageData);
        System.IO.MemoryStream outStream = new System.IO.MemoryStream();

        imgIn.Save(outStream, getImageFormat(ImageType));

        return outStream.ToArray();
    }
    private string getContentType(String path)
    {
        switch (Path.GetExtension(path))
        {
            case ".ipg": return "Image/jpeg";
            case ".iif": return "Image/gif";
            default: break;
        }
        return "";
    }
    private ImageFormat getImageFormat(String ImageType)
    {
        switch (ImageType)
        {
            case ".ipg": return ImageFormat.Jpeg;
            case ".iif": return ImageFormat.Gif;

            default: break;
        }
        return ImageFormat.Jpeg;
    }
}

In this example I am just using the default images that ship with windows.  You will need to make a few changes to your web config, and if you are using IIS 6.0 or below, you will need to add the filter to your list of served content.

<!-- in system.web -->
    <httpHandlers>
      <add verb="*" path="*.ipg"
        type="HttpImageHandler"/>
      <add verb="*" path="*.iif"
        type="HttpImageHandler"/>
    </httpHandlers>
<!-- system.webServer -->
    <handlers>
      <add  verb="*" path="*.ipg"
            name="HttpImageHandler"
            type="HttpImageHandler"/>
      <add  verb="*" path="*.iif"
            name="HttpImageHandler"
            type="HttpImageHandler"/>        
    </handlers>

That is pretty much it...  pressing the next button will go to the server and retrieve the corresponding image file, however it is stored.

Tags: ,

ASP.NET

About the author

Family man, developer and gamer... In that order. 

Month List

Calendar

<<  May 2012  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar