Key things to optimize, speed up performance for your web app…

Whether you design, build, test, maintain, or manage applications, you need to consider performance. If your software does not meet its performance objectives, your application is unlikely to be a success. If you do not know your performance objectives, it is unlikely that you will meet them. The key things that you can do or focus before you start looking at performance of any web application. It is important to follow the orders:


  1. Minimize your TCP Round Trips / Server Requests from your page.
  2. Minimize client blocking / load blocking.
  3. Minimize latency / the distance from your content to your user.
  4. Server / Code optimization

ToolKits

What is Empty Cache?

When users come to your site, your images are loaded into their browser’s cache.

How often does this happen on your live site?

Do you know if/ how often images are cached on your site?

Loading images, css, js etc. from your server to a client browser takes time, bandwidth and resources.

More coming…stay tuned… 🙂

Advertisements

MVC Routes, Controllers: Actions and Parameters – Part 2

We know little bit  how routing works from Part 1. Lets get into RouteConfig.cs to find a new route. This is only something you need if the default route doesn’t work for you. Let me show you a scenario where new route can help. Lets expect the user comes into the application and search for book by its name. ex.: “books/MVC5” or “books/WCF” or “books/WebAPI”.

In this case the second entry in the url or the second segment in the path is not an action identifier it is a parameter. So it can be MVC5 or WCF or WebAPI or anything.

The default route wont work in this scenario and we don’t want to create an action to our controller for every book name we have, we just want to pass the action as a parameter.  So let us find a new route for this. This is also important that where we should put the new route to the route collection table, because the order is significant.

What the routing engine will do is evaluate each map route that we placed into that route collection and the first one that matched the url will win. The default route we have had is very greedy and it matches nearly any url. So let us add a new one that will satisfy our needs:

route

routes.MapRoute(
name: “Book”,
url: “Book/{name}”,
defaults: new {controller = “Book”, action = “Search”, name = UrlParameter.Optional}
);

Now let us examine our previous url:/Home/Contact

Does our new route satisfy the url needs? the answer is No. Because we have clearly mentioned the controller name is “Book”, so the url will go to next one and invoked the home controller. But if we type url like “/Book/MVC5”, our newly created route will satisfy the url need and invoked the controller. But we didn’t create any controller yet. So we will get 404 error. Let us create a controller named “BookController”.

1

2

If we build the application and try the same url we had before, it will still give us the error. Because according to route configuration it will look for an action / controller public method named “Search”, which is not currently available. Let us create that too and  as we didn’t create any view for this controller let us render text contents for testing purpose.

3

Now build and run the application and type /book, and we have a result, we will see the text on the browser that we have inside controller action.

Always try to avoid the fat controller.Now the question is : what is a fat controller, shortly we can say that:

  • If a controller has domain logic
  • If a controller serves too many requests / too many actions

Never, Ever let your controller suck the life-blood of your domain logic. Controller should be only responsible for:

  • Validating Input
  • Prepare the view
  • Calling Model
  • Return the view / Call another action

Let me point out that, Actions are nothing but public methods inside of a controller class. Anytime you add a public class or controller action, ask a question to yourself “is it url addressable?”. If your answers is No, then the code should not be inside your controller move it somewhere else.

Also don’t write any public method that you don’t expect to access via url. Just keep that in mind and move forward.

Let us try to pull the name value from the url, we can get that by using the following:

var name = RouteData.Values[“name”];

But ASP.NET MVC makes that even more easier to access because you add a parameter to an action. What the MVC framework will do is that try to find something that matches with the parameter name and will just give it to you. It will do everything to populate that parameter, it will look all around the request . It will look into the routing data, so things will be picked from the url. It will looked into the query string  and also at posted form values.

Let us add a parameter to that Action method:

public ActionResult Search(string name)

In the case to /Book/WCF , the MVC framework will see , I need a parameter called “name” , so extract something called name for  routedata/ querystring / etc..

Let us get that name by using

var bookName = Server.HtmlEncode(name); // to avoid some sort of malicious script, tag or something like that

That was all for today. As we move further, we will see more advanced features… Keep Smiling 🙂

MVC Routes and Controller- Part 1

Lets start with simple but important question,

How does asp.net knows how to deliver a request to a specific controller. Like /home/about to the Home Controller? The answer of the question is routing engine.

The routing engine is a core part of ASP.NET. Its not tide to the MVC framework. You can use the route request for Web Forms , WCF services , WebAPI etc. But in MVC we use the route engine to directly request to our controller.

Image

Here the first line “Default” is the friendly route name .

The second line “{controller}/{action}/{id}”  is URL with parameters or we can say this is a pattern for the route.

And the third or last line “new { controller = “Home”, action = “Index”, id = UrlParameter.Optional }”  represents the default parameters for the route.

Think about the goal of the routing engine. Its job is to examine the url and figure out where to send it for processing.

Capture2

In MVC we want to specify the URL. Here  Home points to {controller} and Contact points to {action}. So it will look into Home Controller and inside that controller it will invoke the contact action and perhaps other data. Following is the code inside home controller:

Capture3

So the words inside the curly braces are the URL parameter names. If the routing engine doesn’t find the specific data in the URL , like the controller name or the action name it can use one of the default values that we specify in the parameter defaults.

defaults: new { controller = “Home”, action = “Index”, id = UrlParameter.Optional }

Let us introduce the ASP.NET magical file “global.asax”. If I open it up, it will look like the following:

Capture4

You can see we have a class here derived from “HttpApplication” and this allow us to hook into some application events like “Application_Start”.  This method will be magically invoked by ASP.NET before you process your first http request. So when your application first running the code here will execute one time before any of your controller start executing and so there is where we put some configurations that the application needs like the routing configuration.

Lets make a summary, when an MVC application first starts, the Application_Start() method is called. This method, in turn, calls the RegisterRoutes() method. The RegisterRoutes() method creates the route table. The default route table contains a single route (named Default). The Default route maps the first segment of a URL to a controller name, the second segment of a URL to a controller action, and the third segment to a parameter named id.
Imagine that you enter the following URL into your web browser’s address bar:

/Home/Contact/3
The Default route maps this URL to the following parameters:
controller = Home
action = Contact
id = 3

When you request the URL /Home/Contact/3, the following code is executed:
HomeController.Contact(3)

The Default route includes defaults for all three parameters. If you don’t supply a controller, then the controller parameter defaults to the value Home. If you don’t supply an action, the action parameter defaults to the value Index. Finally, if you don’t supply an id, the id parameter defaults to an empty string.

That is all for today. Stay tuned for the next part…. Hope you enjoyed 🙂

Fluent API vs Data Annotations Working with Configuration – Part 2


I hope, you have already learned very basic on Fluent API and Data Annotations in Part 1. And hope you have learned how to work with code first using Author and Book classes on previous article. We have decided to change the class name from Author to Publisher as Publisher and Book is more appropriate for One to Many relationship. Ok let’s start, Code First allows you to override its conventions by applying additional configurations. You can choose either attribute based data annotation or strongly typed Fluent API for those configurations. You have seen how how to override the OnModelCreating method. The DbModelBuilder that is provided to the OnModelCreating method is the class for adding configurations.

public DbSet Publishers { get; set; }
public DbSet Books { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
            modelBuilder.Entity<Publisher>()
                .Property(n => n.PublisherName).IsRequired();
            modelBuilder.Entity<Publisher>()
                .Property(d => d.Description).HasMaxLength(500);
            modelBuilder.Entity<Publisher>()
                .Property(p => p.Photo).HasColumnType("image");
            modelBuilder.Entity<Book>()
                .Property(n => n.BookName).IsRequired();
            modelBuilder.Entity<Book>()
              .Property(n => n.BookName).HasMaxLength(200);
}

So you know how to work with Required, Column Type check, Max Length. How to configure properties of a class. You can also configure the class itself. You can also specify that which database table it should map to:

modelBuilder.Entity<Publisher> ().ToTable(“Your_Desired_Table_Name);

It is possible to use both Data Annotations and Fluent API. If you use both together your code will become inconsistent. It is always advised that try to keep your code consistent. So use any one, either Data Annotations Or Fluent API.

Now, if you need lot of configuration to perform, the OnModelCreating method mighty quickly become overwhelmed with code. You can group configuration by entity within individual EntityTypeConfiguration classes and then call them.  Let us separate Book and Publisher. Add two classes: 1) BookConfiguration 2) PublisherConfiguration. Your classes should look like the following:

 public class BookConfiguration : EntityTypeConfiguration<Book>
    {
        public BookConfiguration()
        {
            Property(n => n.BookName).IsRequired().HasMaxLength(200);
        }
    }
 public class PublisherConfiguration : EntityTypeConfiguration<Publisher>
    {
        public PublisherConfiguration()
        {
            Property(n => n.PublisherName).IsRequired();
            Property(d => d.Description).HasMaxLength(500);
            Property(p => p.Photo).HasColumnType("image");
        }
    }

Calling modelBuilder.Entity<Book>() will actually create an EntityTypeConfiguration<Book> and return it to you, so whichever approach you choose, you are accessing the same API.

Now change your OnModelCreating method that consumes those two classes  you created earlier. Your code should look like the following:

  public class LibraryDB:DbContext
    {
        public DbSet<Publisher> Publishers { get; set; }  
        public DbSet<Book> Books { get; set; }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new BookConfiguration());
            modelBuilder.Configurations.Add(new PublisherConfiguration());
        }
    }

Now you will learn how to recreate the database when the model change or to match the model to database. Before that, make sure you have installed EF latest version properly. You can use NuGet package manager for that. The Entity Framework can create, update, drop databases when application runs. The Entity Framework recommends to use “Database Migrations” as a prevention for loosing old records. But for production environment you always need to generate some test data. Entity Framework has “Seed” Method that allows you to seed some dummy data in the database. Lets have a look how to do that:

There are two methods for achieving your purpose , write any one of the following lines within Application_Start inside “Global.asax.cs” file. There is another good way to do that with dummy data. Add a new class named “DBInitializer” inherits from “DropCreateDatabaseIfModelChanges<LibraryDB>“.

   protected override void Seed(LibraryDB context)
            {
                //base.Seed(context);
                context.Publishers.Add(new Publisher
                {
                    PublisherName="O'Reilly",
                    Description="NA"
                });
                context.Publishers.Add(new Publisher
                {
                    PublisherName = "McGraw.Hill",
                    Description = "NA"

                });
               context.SaveChanges();
            }

Hope you all enjoyed. Have fun 🙂

Fluent API vs Data Annotations- Working with Configuration-Part1


If you consider the DDD architecture designs, one thing you must fulfill that is isolation our Domain Model Layer. To achieve that you must isolate Domain Entities from any other layers such as infrastructure layers, Data Persistence layers where you have a selected data technology. So you need to satisfy the  Persistence Ignorance Principle for domain Classes (Entities, Value-Objects, Domain Services, etc.). Here comes the question for right choice. This is the reason POCO entities are the right choice for DDD.

If you go further deep into Code First, you’ll see that the initial way to make the mappings from your POCO Domain Entities classes towards your final database tables is based on conventions. I have question ?? what will happen if those conventions are not ok (if you need to adjust to an existing database or whatever) ?? . So you need to customize those conventions. You can do that either in two ways. Let us start describing those two ways:

Code first leverages a programming pattern referred to as convention over configuration. Code first gives you two ways to add these configurations to your classes. One is using simple attributes called DataAnnotations within the ” System.ComponentModel.DataAnnotations “ namespace which is part of the .NET Frameworkand the other is using code first’s Fluent API, which provides you with a way to describe configurations imperatively, in code. Customizing it withFluent API is a way that fits much better with the PI (Persistence Ignorant), because our Domain Model will be ignorant of those mappings which will be defined into our Data Persistence Infrastructure layer, so, our Domain Model Layer will be better isolated from infrastructure implementation.

Everything what you can configure with DataAnnotations is also possible with the Fluent API. The reverse is not true. So, from the viewpoint of configuration options and flexibility the Fluent API is “better”. Learning the Fluent API is almost a Must , the DataAnnotations are a nice-to-have for simple applications.

We will describe the example for MVC4 Code First application.

public class Author
{
public int AuthorId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public byte[] Photo { get; set; }
public virtual ICollection<Book> Books { get; set; }

}

public class Book
{
public int BookId { get; set; }
public string Name { get; set; }
public DateTime PublishDate { get; set; }
public Author Author { get; set; }
}

public class LibraryDB:DbContext
{
public DbSet<Author> Authors { get; set; }
public DbSet<Book> Books { get; set; }
}

The Author and Book class describes a particular Author might have collection of Books. So Author object can have one or more Books associated with it. Our class LibraryDB, will inherit from DbContext in order to gain all of DbContext’s capabilities. Additionally, it will expose properties that return queryable sets, DbSets, of Author class and Book classe. The last class represents a complete data layer that you can use in applications. For DbContext you will be able to query, change, track and save Author and Book data. Thanks to DbContext :).

Let’s start with the Author Author class. There are three things I would like to change:

Ensure the Name is provided.

Limit the max length of Description field to 500 characters.

Store the Photo into SQL Server image type.

We need “System.ComponentModel.DataAnnotations” assembly that is part of .NET 4.0. Apply the following code:

public class Author
{
public int AuthorId { get; set; }
[Required]
public string Name { get; set; }
[MaxLength(500)]
public string Description { get; set; }
[Column(TypeName=”image”)]
public byte[] Photo { get; set; }
public virtual ICollection<Book> Books { get; set; }

}

public class Book
{
public int BookId { get; set; }
[Required]
public string Name { get; set; }
public DateTime PublishDate { get; set; }
public Author Author { get; set; }
}

The Required annotation needs no additional information, whereas the MaxLength and Column have parameter information that you need to provide. The parameter provided to the Column annotation is specific to the database that you are mapping to. We want to store Photo in a SQL Server image field. So we configured the data type. All three annotations will impact the databse schema. On the Book class we have only placed the required annotaion no character limit, so in the database schema you will find the length is nvarchar(max). Note that, we can also use MinLength which is an interesting annotation. While MaxLength has a database counterpart, MinLength does not. MinLength will be used for EF validation without impacting the database.

MinLength is the only configuration that can be achieved using Data Annotation but has no counterpart in the Fluent API configurations.

If you opened up the database table you will find the changes that we have made in our classes. We will discuss on EF DataBase Migration later.

Configuring with Fluent API:

The concern of a Fluent API isn’t specific to Code First or the EF. The basic is chained method calls to produce code that is easy for the developers to read. The return type of each call then defines the valid methods for the next call. There are more reasons why developers like Fluent API.

public class LibraryDB:DbContext
{
public DbSet<Author> Authors { get; set; }
public DbSet<Book> Books { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Author>()
.Property(n => n.Name).IsRequired();
modelBuilder.Entity<Author>()
.Property(d => d.Description).HasMaxLength(500);
modelBuilder.Entity<Author>()
.Property(p => p.Photo).HasColumnType(“image”);
modelBuilder.Entity<Book>()
.Property(n => n.Name).IsRequired();
}
}

Let us describe the mechanism above code, how it works actually. Julia Lerman & Rowan Miller describes nicely in their “Programming Entity Framework Code First” book. “When its time to build the model, the DBContext first looks at the classes and learns what it can from them. At this point, the context is ready to reason out the model, but there is an opportunity for the developers to interrupt the context and perform additional configuration. DbContext.OnModelCreating method called by the context just before the model is built. The method is virtual, so you can override it and insert your own logic. This is where the Fluent API configuration goes”.

Stay Tuned: Be Right Back with more advanced…

What is ViewData, ViewBag and TempData? – MVC options for passing data between current and subsequent request


ASP.NET MVC offers us three options ViewData, VieBag and TempData for passing data from controller to view and in next request. ViewData and ViewBag are almost similar and TempData performs additional responsibility. Lets discuss or get key points on those three objects:

Similarities between ViewBag & ViewData :

  1. Helps to maintain data when you move from controller to view.
  2. Used to pass data from controller to corresponding view.
  3. Short life means value becomes null when redirection occurs. This is because their goal is to provide a way to communicate between controllers and views. It’s a communication mechanism within the server call.

Difference between ViewBag & ViewData:

  1. ViewData is a dictionary of objects that is derived from ViewDataDictionary class and accessible using strings as keys.
  2. ViewBag is a dynamic property that takes advantage of the new dynamic features in C# 4.0.
  3. ViewData requires typecasting for complex data type and check for null values to avoid error.
  4. ViewBag doesn’t require typecasting for complex data type.

ViewBag & ViewData Example:

public ActionResult Index()
{
    ViewBag.Name = "Monjurul Habib";
    return View();
}

public ActionResult Index()
{
    ViewData["Name"] = "Monjurul Habib";
    return View();
} 

In View:

@ViewBag.Name 
@ViewData["Name"] 

TempData:

TempData is also a dictionary derived from TempDataDictionary class and stored in short lives session and it is a string key and object value. The difference is that the life cycle of the object. TempData keep the information for the time of an HTTP Request. This mean only from one page to another. This also work with a 302/303 redirection because it’s in the same HTTP Request. Helps to maintain data when you move from one controller to other controller or from one action to other action. In other words when you redirect, “Tempdata” helps to maintain data between those redirects. It internally uses session variables. Temp data use during the current and subsequent request only means it is use when you are sure that next request will be redirecting to next view. It requires typecasting for complex data type and check for null values to avoid error. generally used to store only one time messages like error messages, validation messages.

public ActionResult Index()
{
  var model = new Review()
            {
                Body = "Start",
                Rating=5
            };
    TempData["ModelName"] = model;
    return RedirectToAction("About");
}

public ActionResult About()
{
    var model= TempData["ModelName"];
    return View(model);
}

The last mechanism is the Session which work like the ViewData, like a Dictionary that take a string for key and object for value. This one is stored into the client Cookie and can be used for a much more long time. It also need more verification to never have any confidential information.

Regarding ViewData or ViewBag you should use it intelligently for application performance. Because each action goes through the whole life cycle of regular asp.net mvc request. You can use ViewData/ViewBag in your child action but be careful that you are not using it to populate the unrelated data which can pollute your controller.

IIS 7 deployment : ASP.NET 4.0 Routing and Ext.NET


Few days ago, i got a problem during deployment an ASP.NET 4.0 website on IIS7 with default app pool. The website includes asp.net 4 routing feature and ext.net in the website. But when tried to access the ext.net enable pages, we found nothing over there, it was just a blank page. The configuration was as follows:

For ext.net Install and Setup i used the following steps for visual studio 2010 sp1:

1) To start you will need to download the Ext.Net latest version from http://www.ext.net/.
2) Unzip the contents of the zip file into a new directory.
3) Open the directory and copy the following five files to your projects /bin directory:

a) Ext.Net.dll b) Ext.Net.Utilities.dll c) Ext.Net.xml d) Newtonsoft.Json.dll e) Newtonsoft.Json.xml

4 ) In the solution explorer make sure the view all files button is selected.
5) Expand the /bin directory to display all files and Click the refresh button at the top
6) select each of the new files you just added to the /bin directory (shift + click)
7) Right click and choose “Include in Project”

8) In solution Explorer Click the properties button for your project (top left button).
9) Click on References tab on the left. You should see all references in the center.
10) Click the Add button and then the browse tab.
11) Browse to your projects /bin directory (were you copied the DLL’s to) and select the 3 DLL’s and click ok.
12) Under Imported namespaces scroll to the bottom and click on the checkboxes for the following:

a) Ext b)Ext.Net c) Ext.Net.Utilities d) Newtonsoft e) Newtonsoft.Json

13) Open your web.config file. You will need to add the following to your config file.

<configuration>
<section name="extnet" type="Ext.Net.GlobalConfig" requirePermission="false" />
<configSections>
<system.web>
<httpHandlers>
<add path="*/ext.axd" verb="*" type="Ext.Net.ResourceHandler" validate="false" />
</httpHandlers>
<httpModules>
<add name="DirectRequestModule" type="Ext.Net.DirectRequestModule,Ext.Net" />
</httpModules>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules>
<add name="DirectRequestModule" preCondition="managedHandler"
type="Ext.Net.DirectRequestModule, Ext.Net" />
</modules>
<handlers>
<add name="DirectRequestHandler" verb="*" path="*/ext.axd" preCondition="integratedMode" type="Ext.Net.ResourceHandler" />
</handlers>
</system.webServer>

Now follow the steps to add Ext.Net in your visual studio toolbox:

# Open a .aspx file to exit in visual studios. We do this so items are displayed within your toolbox (if its a .vb, .cs, etc it will not display items).
# Next click on your toolbar and click add tab. Name this tab something (Ext.Net)
# Right click under this tab click choose items
# Click the Browse button under “.NET Framework Components”
# If no errors are displayed you should now see all the components under the new tab.

Before you begin I would suggest doing a Build Clean and Rebuild to confirm all the new changes are set.
To use Ext.Net within your page requires 2 changes to an existing page. A register tag and the resourcemanager tag.

At the top of your page (under the page directive) add the following which will give you access to the Ext.Net DLL :

<%@ Register Assembly="Ext.Net" Namespace="Ext.Net" TagPrefix="ext" %>

Next find your BODY tag and add the following:

<ext:ResourceManager ID="ResourceManager1" runat="server" />

And For routing : configure an ASP.NET Web site or Web application for routing, you first need to add a reference to the System.Web.Routing assembly. The SP1 installation for the .NET Framework 3.5 will install this assembly into the global assembly cache, and you can find the assembly inside the standard “Add Reference” dialog box. You’ll also need to configure the routing module into the ASP.NET pipeline. The routing module is a standard HTTP module. For IIS 6.0 and earlier and for the Visual Studio Web development server, you install the module using the <httpModules> section of web.config, as you see here:

<httpModules>
<add name=”RoutingModule”
type=”System.Web.Routing.UrlRoutingModule,
System.Web.Routing,
Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35″/>
</httpModules>

URL rewriting implementations typically perform their work during the BeginRequest event, which is the earliest event to fire during a request. With URL routing, the route matching and selection of a route handler occurs during the PostResolveRequestCache stage, which is after the authentication, authorization, and cache lookup stages of processing. I will need to revisit the implications of this event timing later in the column.

To run a Web site with routing in IIS 7.0, you need two entries in web.config. The first entry is the URL routing module configuration, which is found in the <modules> section of <system.webServer>. You also need an entry to handle requests for UrlRouting.axd in the <handlers> section of <system.webServer>. Both of these entries as follows:

<system.webServer>
<modules runAllManagedModulesForAllRequests=”true”>

<add name=”UrlRoutingModule”
type=”System.Web.Routing.UrlRoutingModule,
System.Web.Routing, Version=3.5.0.0,
Culture=neutral,
PublicKeyToken=31BF3856AD364E35″ />
</modules>
<handlers>

<add name=”UrlRoutingHandler”
preCondition=”integratedMode”
verb=”*” path=”UrlRouting.axd”
type=”System.Web.HttpForbiddenHandler,
System.Web, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a” />

</handlers>
</system.webServer>

The runAllManagedModulesForAllRequests attribute requires a value of true if you want to use the extensionless URLs as I’ve done in this sample. Also, it might seem strange to configure an HTTP handler for UrlRouting.axd. This is a small workaround that the routing engine requires in order for routing to work under IIS 7.0. The UrlRouting module actually rewrites the incoming URL to ~/UrlRouting.axd, which will rewrite the URL back to the original, incoming URL.

THE PROBLEM

The problem was ext.net worked fine after removing routing related code from the site. But we need both of them work together.

Then we we tried to find a workaround for that problem and got few links on google, we compiled them and apply one by one and finally we got the solution to use both of them together.

SOLUTION

The solution was as follows:

1) Remove httpHandlers and httpModules from system.web
2) Configure system.webServer like the following:

<system.webServer>
<modules runAllManagedModulesForAllRequests=”true”>
<add name=”DirectRequestModule” type=”Ext.Net.DirectRequestModule, Ext.Net”/>
</modules>
<handlers>
<add name=”DirectRequestModule” type=”Ext.Net.DirectRequestModule, Ext.Net”/>
</handlers>
</system.webServer>

3) Configure global.asax:
RouteTable.Routes.Ignore("{resource}.axd/{*pathInfo}");
RouteTable.Routes.Ignore("admin/{*pathInfo}");// this is because we used the ext.net in the admin module

That was all and the website is running without any conflict.

The reasons we use ext.net :

1) It has a *simple* design model that is followed consistently throughout the code
2) The upgrade path from v.0.33 to v.1.1 to v.2.02 was smooth and mostly uneventfull (i.e. it worked as it should!) with the exception of the grid component that was really a hard nut to crack (for the upgrades that is)
3) flexibility over choosing the best combination of extsjs+glue+custom code or full extsjs+custom code. For example, for a long time although I used extjs for almost everything I relied on YUI’s XHR calls for actually doing AJAX stuff and it worked as a charm
3) excellent UI with a lot of really helpfull examples