Using Spring Security with ExtJS for Siteminder/LDAP authentication and authorization

Overview

I’ve been merrily creating my ExtJS 4 application when the inevitable came that we must secure the application. Of course it doesn’t stop at authentication, especially in a client-side where nothing should be assumed secure; the users of course want to show/hide, enable/disable things using role-based security.  In a previous life, I used Spring Security to do this sort of thing for Java applications, so I wondered why I couldn’t just do the same for ExtJS.  This article shares a complete example to do exactly that.  Here I will show you how you can use your existing IT infrastructure to secure your application (in my case LDAP and Siteminder) and how to integrate Spring Security 3.0 with ExtJS 4. Lastly, I would like to thank Dmitry and Rajesh for their contributions on this topic.

Authentication

The first step is to step up authentication to your application. The first approach I’ll show is using LDAP authentication (authn) followed by Siteminder authn. To set-up LDAP authn, you have to extend your HTML/JS/CSS3 app to a Java EE Spring application. There are already several articles on the Web on how to do this and most of today’s IDEs can go this for your automatically. Once you have created your Java EE app, you need to configure your web.xml. The basic gist of it is that you will be using a servlet filter to intercept URLs that invoke the application (see DelegatingFilterProxy). Here is an example one:

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>Your App</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>WEB-INF/applicationContext.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <filter>
        <filter-name>charsetFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter> 
    <filter-mapping>
        <filter-name>charsetFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
      <filter-name>springSecurityFilterChain</filter-name>
      <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
      <filter-name>springSecurityFilterChain</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

In your Spring applicationContext.xml you’ll want to create and import another spring context configuration that contains security specifics. In the example that follows, I show how you can do LDAP authentication:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:security="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
          http://www.springframework.org/schema/security
          http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">

    <security:http auto-config='true' use-expressions="true">
        <security:intercept-url pattern="/**" access="isAuthenticated()" />
        <!--access="hasRole('ROLE_LDAP_GROUP')"/>-->
        <security:http-basic/>
    </security:http>

    <security:global-method-security pre-post-annotations="enabled" />

    <security:ldap-server id="ldapServer" port="636" root="o=company"
                          url="ldaps://myldap.company.com"/>

    <security:authentication-manager alias="company">
        <security:ldap-authentication-provider
                group-search-base="ou=yourgroupou,ou=groups,o=company"
                group-role-attribute="cn"
                user-search-base="ou=people,o=company"
                user-search-filter="uid={0}"
                />

    </security:authentication-manager>
</beans>

Here I do an anonymous bind over the LDAPS port to authenticate based on the users’ credentials they provide (be sure you use SSL and take the time to set up your JVM to support it with our internal certs). The intercept-url includes a pattern (I did everything with “/**”) and the type of access (I did “isAuthenticated()” and defer authorization till later). You’ll see I have commented out access using hasRole(‘ROLE_LDAP_GROUP’) which can be used to limit the users able to open the application using an LDAP group, but I didn’t want to do that. In the next example, I show how you can do the same thing with Siteminder:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
          http://www.springframework.org/schema/security
          http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">

    <bean id="http403EntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint">
    </bean>

    <sec:http auto-config="false" entry-point-ref="http403EntryPoint">
        <!--<sec:intercept-url pattern="/**"
                           access="isAuthenticated()"/>-->
        <sec:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
    </sec:http>

    <bean id="siteminderFilter" class=
            "org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
        <property name="principalRequestHeader" value="SM_USER"/>
        <property name="authenticationManager" ref="authenticationManager"/>
    </bean>

    <bean id="preauthAuthProvider"
          class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
        <property name="preAuthenticatedUserDetailsService">
            <bean id="userDetailsServiceWrapper"
                  class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
                <property name="userDetailsService" ref="ldapUserDetailsService"/>
            </bean>
        </property>
    </bean>

    <sec:authentication-manager alias="authenticationManager">
        <sec:authentication-provider ref="preauthAuthProvider"/>
    </sec:authentication-manager>

    <!-- Example using LDAP -->
    <sec:ldap-server id="ldapServer" port="636" root="o=company"
                          url="ldaps://dir.company.com"/> 
    <sec:ldap-user-service id="ldapUserDetailsService" server-ref="ldapServer"
                           group-search-base="ou=yourgroupou,ou=groups,o=company"
                           role-prefix="ROLE_" group-role-attribute="cn"
                           user-search-base="ou=people,o=company" user-search-filter="uid={0}"/>
    <security:ldap-server id="ldapServer" port="636" root="o=company"
                          url="ldaps://myldap.company.com"/>
</beans>

While this has been all going on, it is necessary to block the Ext JS app until we have confirmed the identity of the user. In Ext JS we can set up an Ajax request to do that and use the success listener to block the viewport from being created until success is achieved. You’ll notice that my Ajax request is a HTTP POST to user/getUser. This is the mechanism I used to determine the principal from the server-side by parsing the JSON returned into the roles array for the application, which I’ll go into in a little bit.

Ext.application({
    name : 'App',
    controllers : [ 
        'dashboard.TabController' ],
    roles : [ ], 
    version : '1.0',
    onFailure : function(title, response, options) {
        Ext.MessageBox.show({
            title : title,
            msg : response,
            width : 400,
            icon : Ext.Msg.WARNING,
            buttons : Ext.Msg.OK
        });
    },
    hasRole : function(role) {
        return Ext.Array.contains(this.roles, role);
    },
    launch : function() {
        App.app = this;

    
        Ext.Ajax.request({
            url : 'user/getUser',
            method : 'post',
            timeout : '30000',
            defaultHeaders : {
                'Content-Type' : 'application/json; charset=utf-8'
            },
            success : function(response, opts) {
                try {
                    var result = Ext
                            .decode(response.responseText);
                    if (result !== null) {
                        var user = result.userName;
                        var perms = result.permissions;
                        for (var i = 0; i < perms.length; i += 1) {
                            if (perms[i].role
                                    .indexOf("ROLE_ADMIN") === 0) {
                                App.app.roles
                                        .push(perms[i].role);
                            }
                        }
                        if (user !== null) {
                            Ext.QuickTips.init();
                            Ext.create('Ext.container.Viewport',
                            {
                               // Do your normal stuff here
                            });
                        } else {
                            App.app
                                    .onFailure(
                                    'Authorization Failed',
                                    'User does not have the correct permissions',
                                    opts);
                        }
                    } else {
                        App.app
                                .onFailure(
                                'Authorization Failed',
                                'Response returned from service was null.',
                                opts);
                    }
                } catch (e) {
                    App.app.onFailure(
                            'User Serivce Failure', e, opts);
                }
            },
            failure : function(response, opts) {
                App.app.onFailure('User Service Failure',
                        response, opts);
            }
        });

    }
});

Now, let’s take a look at the UserController Java class. This is the class that is responsible for determining the user and retrieving his/her roles. What is really nice about Spring is that I can use their MVC controller mechanism and annotations to intercept URLs and do some sort of logic with a simple POJO as follows:

@Controller
@RequestMapping(value = "/user")
public class UserController {
    private static Logger log = Logger.getLogger(UserController.class);


    @RequestMapping(value = "/getUser", method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    public @ResponseBody String getUser() throws IllegalStateException {
        try {
            User user = new User(this.getUserName());


            Collection perms = this.getUserPermissions();
            user.setPermissions(perms);

            Gson gson = new Gson();
            String result = gson.toJson(user, User.class);
            log.info("Sending user object: " + result);

            return result;
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new IllegalStateException(e);
        }
    }


    private String getUserName() {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username = "";


        if (principal instanceof UserDetails) {
            username = ((UserDetails)principal).getUsername();
        } else {
            username = principal.toString();
        }
        return username;
    }


    private Collection getUserPermissions() {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        Collection perms = null;


        if (principal instanceof UserDetails) {
           perms = ((UserDetails)principal).getAuthorities();
        }


        return perms;
    }
}

Notice I use the Spring’s SecurityContextHolder to retrieve the principal as well as his/her authorities (i.e., roles). I populate those into a User bean, convert that to JSON and return the result to the client. Once this information is provided to the client, we can manage roles effectively on the client side.

Authorization

Now that we’ve authenticated the user, collected their roles and parsed them into the Ext.application’s roles array, how can we use that in the application to protect components. The answer is simple really, you just add some conditional logic for the components config for either “hidden” or “disabled”. For instance, I had a settings Tab on a tab panel that only administrators should see. I was able to accomplish that with this bit of JavaScript code:

{
    xtype : 'settingsTab',
    //Example of role-based hiding
    hidden : !App.app.hasRole('ROLE_UI.ADMIN')
}


// function to support above
hasRole : function(role) {
    return Ext.Array.contains(this.roles, role);
}

As you can see from above, I simply have hasRole return true or false based on whether the user has a given role established at the time we invoked the application and made the AJAX call to getUser() from the previous section. If the user is in the LDAP group “UI.ADMIN” (NOTE: Spring automatically prepends ROLE_), they are granted access to see the tab.

Now you might be saying, well I could write a greasemonkey script and hack the JavaScript so I could see the tab and indeed you can, but the approach I demonstrate is only used to change the behavior of the application, not the functionality. Let’s say you applied this to a “save” button instead, that save button is going to call some server-side functionality to, well, save. You should definitely check on the server-side that the user can execute save functions (in the example below, updateDriver). It would be folly to expect a client-side application to verify this. Again, Spring Security to the rescue with a simple annotation, @PreAuthorize, to the server-side method to check the role of the user as follows:

@RequestMapping(method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    @PreAuthorize("hasAuthority('ROLE_UI.ADMIN')")
    public void updateDriver(@RequestBody String json) throws IllegalStateException {
        try {
            // Using Gson to Serialize/Unserialize as example in case post-processing required
            Gson gson = new Gson();
            HosDriverEvent hde = gson.fromJson(json, HosDriverEvent.class);
            restTemplate.put(VEHICLE_DETAILS_SAVE_URL, gson.toJson(hde));
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new IllegalStateException(e);
        }
    }

Even before this method gets invoked, if the user does not have that role, they will get a 403 exception and thus preventing them from accessing the save operation.

Data Security

Another aspect of security for your application to consider should be the data itself. For instance, you may show a grid of data but one user is authorized to see more information than another. This is controlled with identity propagation to the underlying services. This means that with Siteminder, the users secure, time-based identity will be propagated in the HTTP header downstream to the underlying services. The services can validate the users authorities by parsing the header for the principal and querying data appropriately based on that user’s identity. Thus, the data returned will be unique for that user. It’s important to make sure your underlying services do this sort of checking.

Another approach is to use a secure token service (STS) and SAML assertions. This is especially popular in SOA’s. More information can be found in Thomas Erl’s seminal work on the topic.

Conclusion

Security is essential to all business-based Web applications and so it needs to be carefully thought out. Here I hope I’ve provided useful information on how you can implement your customer’s security requirements for an Ext JS application using Spring Security. You have to assume nothing is ever secure on a client-side application, so you must include a server-side component to ensure your application is secure.

Advertisements

SenchaCon 2011: ExtJS 4.1 Enhancements to Performance, Grid and API

Don Griffin, @dongryphon, from Sencha presents on ExtJS 4.1 features and performance improvements which I recently watched on vimeo here.  These are my notes from that session.

Main theme is performance, especially in the Grid.  On IE8, it takes ~4.7 seconds to fully render the Grid, with render and layout taking the most time and initalize and load taking the least. In 4.1, the load time is cut 2.4 seconds in IE8.  Focus was placed in all areas to improve on this to get it closer to the way it was in 3.4, but there are many more components in 4 which exacerbates the issue.

Regarding initialization, you can use the new class system and minimize the classes you load by using custom build.  This will then only load the classes (over the network) that you use in your app.  initComponent was also improved.

Rendering changes in 4.1 including removing createElement (createElement is fairly costly in 4.0); renderTpl manages this better by creating the elements in the hierarchy and then using renderSelectors during onRender to tie it back to the base element rather than doing this incrementally.  In 4.1, you now need to move custom code from onRender to beforeRender so that it gets called prior to be created in the DOM.

In 4.1, dockedItems config is available to add titles and other things to the edges of a container.

CSS calculations are expensive: Write + Read = Reflow because when you write to the DOM, the browser won’t cache it and thus causing an expensive reflow. In 4.1, “layout context” is moved to the global context so all the components are read at once and then writes after everything is read first. This minifies reflows and thus performance.

Other areas of investigation include converting layouts to CSS where possible, optimizing class creation and optimizing DOM Event binding.  This is being looked at for future releases.

In 4.1, Grid scrolling has gone back to “native scrolling” (or having true momentum when scrolling) especially when infinite scrolling.  This is a big deal since there were so many issues with the feel of scrolling in version 4.0; it wasn’t very smooth especially in IE.

API enhancements for 4.1 include:

  • Border Layout you can now do multiple regions (i.e., 2 west’s and 2 east’s) without nesting border layouts.  You can also set preferences to how regions layout with each others.  Regions can be added and removed dynamically too all within ONE border layout.  Splitters work as you’d expect, respecting the borders of its neighbors.
  • XTemplate compiles and runs faster and is now debuggable.  Now they compile to a function and support else, elseif and switch case control statements.  You can also add code right into the template.
  • Overrides will allow you to only bring in the components that you’re using in your build.

When porting from 4.0 to 4.1 you may need to check your custom layouts since now there is the global context for rendering as well as onRender which I mentioned earlier.

Increasing performance by 2 fold should certainly be welcome, but I especially like that attention was given to grid scrolling because I found it nearly unusable in 4.0 when using IE.  That will make my customers very happy.

For more information, I would encourage you to take a look at Ed Spencer’s blog on the subject.  An even more recent article is here that speaks to how to take advantage of the 4.1 performance enhancements in detail.

Using the Ext JS 4 MVC architecture and a few gotchas

I recently worked on a POC to integrate Solr search with the Ext JS 4 infinite scrolling grid. This allows you to scroll through 234k+ records without having the user page through the data; the scrolling does the data buffering automatically.  Other features include hit highlighting, wild card searching, resizing windows and word-wrapped columns.  However, the most interesting part to me was using the new MVC approach that Sencha introduced in this release to organize your project much like you would a Grails or Java Web project.  I’ll detail the approach I took to make that happen and point out some gotchas along the way.

First, let’s start from the model.  In the code below you can see I’ve defined a model and proxy which will be the piece that will pull the data.  There’s nothing too special here with the exception of the namespace I used to define the model, ESearch.model.EPart.  These are crucial that they are spelled correctly because they will be used later in other parts of the MVC.

Ext.define('ESearch.model.EPart', {
    extend: 'Ext.data.Model',
    idProperty: 'id',
    fields: [
        {name:'id', type:'int'}, 'description', 'item_number', 'part_number'
    ],
    proxy: {
        // load using script tags for cross domain, if the data in on the same domain as
        // this page, an HttpProxy would be better
        type: 'jsonp',
        url: 'http://solrdev1/solr-eti/select/',
        callbackKey: 'json.wrf',
        limitParam: 'rows',
        extraParams: {
            q: '*',
            wt:'json',
            hl:'on',
            'hl.fl': 'description',
            'json.nl':'arrarr'
        },
        reader: {
            root: 'response.docs',
            totalProperty: 'response.numFound'
        },
        // sends single sort as multi parameter
        simpleSortMode: true
    }
});

The next thing to consider is the store.  Arguably, this is not part of MVC per se, but it is used in conjunction with the model to define what type of store we want to use.  In this case, we want to use a buffered store of with a page size of 500.  You’ll also notice that in this code I create some listeners for beforeload and load so that I can allow sorting with Solr and to be able to do hit highlighting and query times.  You’ll also notice that I link the model to the store by using the namespace for the model parameter.  Let’s take a look:

// default to wildcard search
var query = '*';

Ext.define('ESearch.store.EParts', {
    extend: 'Ext.data.Store',
    model: 'ESearch.model.EPart',
    pageSize: 200,
    remoteSort: true,
    autoLoad:false,
    // allow the grid to interact with the paging scroller by buffering
    buffered: true,
    listeners: {
        beforeload:{ fn: function(store, options) {
            if (options && options.sorters) {
            var sorters = options.sorters;
            for (var i=0; i 1) {
                var queryParsed = query.replace(/\*/g,'').replace(/"/g, '').trim();
                var queries = queryParsed.split(' ');

                for (var i=0; i < queries.length; i++) {
                    if (queries[i]) {
                        var q = escapeRegExChars(queries[i]);

                        // Check to highlight text only in grid-body
                        var node = Ext.get("grid-inf").dom.childNodes;
                        for (var j=0; j < node.length;j++ ) {
                            if (node[j].className.contains("x-grid-body",true)) {
                                node = node[j];
                                break;
                            }
                        }

                        highlightText(node,
                                q + "+", 'HL', true);
                    }
                }
            }
            // temporary fix to address issue with scrollbars not resizing           
            var grid = Ext.getCmp('grid-inf');
            grid.resetScrollers();
          }
        }
    }
});

Now that I have the data being consumed the way I want it the next step is to put it into an infinity scrolling grid.  Here’s the code to do that:

Ext.define('ESearch.view.parts.List', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.partslist',
    store: 'EParts',
    initComponent: function() {

        var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
            groupHeaderTpl: 'Group: {name} ({rows.length})',
            startCollapsed: false
        });

        var selectFeature = Ext.create('qcom.grid.SelectFeature');

        var config = {
            name: 'qparts-grid',
            id: 'grid-inf',
            verticalScrollerType: 'paginggridscroller',
            loadMask: true,
            invalidateScrollerOnRefresh: false,
            disableSelection: false,
            features: [groupingFeature,selectFeature],
            viewConfig: {
                trackOver: false
            },
            // grid columns
            columns:[{xtype: 'rownumberer',width: 45, sortable: false},{
                id: 'id-col',
                header: "ID",
                dataIndex: 'id',
                width:60
            },{
                id:"descr",
                header: "Description",
                dataIndex: 'description',
                width: 300,
                renderer: columnWrap
            },{
                id:"itemnum",
                header: "Item Numbers",
                dataIndex: 'item_number',
                width: 100
            },{
                id: "partnum",
                header: "Part Numbers",
                dataIndex: 'part_number',
                flex: 1,
                renderer: columnWrap
            }]
            ,selModel:{
           selType:'rowmodel'
          ,allowDeselect:true
          ,mode:'MULTI'
         },
            tbar:
                ['Search:',{
                     xtype: 'textfield',
                     name: 'searchField',
                     hideLabel: true,
                     width: 250,
                     emptyText: "Enter search terms separated by space",
                     listeners: {
                         change: {
                            fn: function adjustQuery(field) {
                                // temporary fix to address issue with scrollbars not resizing
                                this.store.resetData();

                                // Regex query and add wildcards where appropriate
                                if (field.value.length >= 1) {
                                    var values = field.getValue().match(/[A-Za-z0-9_%\/\.\-\|]+|"[^"]+"/g),
                                        value =[];
                                    if (values && values.length > 1) {
                                        for ( var i=0; i < values.length; i++ ) {
                                            if (values[i].indexOf("\"") >= 0 ) {
                                                value.push(values[i].toLowerCase());
                                            }
                                            else {
                                                value.push("*" + values[i].toLowerCase() + "*");
                                            }
                                        }
                                        query = value.join(" ");
                                        if (Ext.isChrome) {
                                            console.log(query);
                                        }
                                    }
                                    else {
                                        if (field.getValue().indexOf("\"") >= 0 ) {
                                            value.push(field.getValue().toLowerCase());
                                            query = value.join(" ");
                                        }
                                        else {
                                            // temporary fix because regex not picking up 1 char
                                            var temp = values ? values[0] : field.getValue();
                                            query = "*" + temp.toLowerCase() + "*";
                                        }
                                        if (Ext.isChrome) {
                                            console.log(query);
                                        }
                                    }
                                    this.store.load({
                                        params: {q:query}
                                    });
                                }
                            },
                            scope: this,
                            buffer: 500
                         }
                     }
                },
                {
                     xtype: 'tbfill'
                },{
                     xtype: 'displayfield',
                     name: 'totalText',
                     id: 'totalText',
                     hideLabel: true,
                     baseCls: 'x-toolbar-text',
                     style: 'text-align:right;',
                     width:180
                }
            ]
        };
        // apply config object
     Ext.apply(this, config);

     // call parent initComponent
     this.callParent(arguments);
    }
});

So from the above code you see that I’m defining a Grid Panel and assigning an alias to it called “partslist” (more on that later), but one gotcha I found is that I could not use the full namespace for the store definition — I had to just simply call it “EParts”.  Finally, you’ll see me set-up the columns and create a top bar that will hold the search field.  I do a regex to process the search field to create a wildcard search and to preserve quotes.  I also set the buffer to 500 so they it will wait 500ms for keystrokes before firing the search again.

Now that we have the grid, we need to put it some where.  This is where I bring the window into the picture.  In the code below, I simply define my window size, where I want it in the browser, window capabilities like maximize, collapse and closable, and finally the items.  Notice for the items, I’m using the alias partslist from the previously defined grid as the xtype.  This allows me to insert a grid as I defined it before without having to instantiate it as a variable.  Let’s take a look:

Ext.define( 'ESearch.view.Portal', {
    extend: 'Ext.window.Window',
    alias: 'widget.portal',
        width: 800,
        height:600,
        x: 150,
        y: 80,
        layout:'fit',
        border: false,
        closable: true,
        maximizable: true,
        collapsible: true,
        title: 'EParts Search',
        items: [{
            xtype: 'partslist',
            itemId:'myPartList'
        }]
});

So to finish up the MVC portion, we need a controller.  In the code below you will see how we create the controller and then define the models, stores, views, and any references needed.  You’ll also see in the init function where I invoke the store for the initial load of data as well as an example of how we could listen for certain events and do something with that event.  Notice again, that the alias from the grid comes into play (partslist) so that we can capture button events from the grid.  This wasn’t completely implemented, but it gives an example how it might be implemented.

Ext.define('ESearch.controller.Search', {
    extend: 'Ext.app.Controller',
    models:[
        'EPart'
    ],
    stores:[
        'EParts'
    ],
    views:[
        'parts.List'
    ],
    refs:[{
         ref:'PartsList',
         selector:'partslist'
    }],
    init:function(app) {
            var store = this.getEPartsStore();
            store.guaranteeRange(0, 199);
            this.control({
                   'partslist button':{
                    click:this.onButtonClick
               }
          });
    },
    onButtonClick: function(btn, e) {
        if (btn.operation === 'newSearch') {
            //TODO need to find a nice way to instantiate a new window
        }
    }
});

The last little bit of code simply defines the application and sets some criteria as to what paths we should use and which pieces of Ext JS are required for this application to function.  One gotcha I noticed is that you must define the first part of your namespace as the the folder your app will fall under.  You’ll notice that I have mapped the path “app” to “ESearch” so thusly my directory structure for my application must follow something like this:

 

So for instance, ESearch.view.Portal, must live as a file called Portal.js under app/view and same for the other files you see there.  The App.js file that contains the following code will be under the “webapp” directory adjacent to “app” to maintain relative pathing.  All I do is create my viewport based on its namespace and fire .show() to kick the whole thing off.

Ext.Loader.setConfig({enabled: true,
        paths: {
            'Ext.ux':'lib/extjs4/ux/',
            'ESearch': 'app'
        }
});
Ext.require([
    'Ext.grid.*',
    'Ext.data.*',
    'Ext.util.*',
    'ESearch.view.Portal',
    'Ext.grid.PagingScroller',
    'Ext.ux.grid.FiltersFeature',
    'Ext.grid.feature.Grouping',
    'Ext.grid.plugin.CellEditing',
    'Ext.state.CookieProvider'
]);

Ext.application({
     name:'ESearch',
     appFolder:'app',
     autoCreateViewport:false,
     controllers:['Search'],
     launch:function() {
     Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
     this.viewport = Ext.create('ESearch.view.Portal', {
              stateId:'esearchWindow'
     });
     window[this.name].app = this;

      this.viewport.show();

    }
});

And finally, we have an HTML file that points to all the necessary JS files for this application to work, which is pretty standard stuff to bootstrap the application. However, with this approach, I only had to define the App.js file and not all the underlying JS files in the MVC portion. This is because the pathing we used in the previous section.

I hope this is useful for folks that would like to explore MVC in Ext JS 4 a little more.  I really find it useful because it helps break up a larger component much along the lines I’m used to.  In this way you could have multiple DEVs work on the same project pretty easily without walking over each other.

SenchaCon Day1: ExtJS 4 Overview

Ed Spencer from Sencha gives an exciting overview of ExtJS4 on how it will be faster, more stable and easier to use.

ExtJS4 will be faster.  Layout expends most of the time when rendering the application.  The new release boasts a vast improvement in this area.  They also have 4000+ unit tests to provide for a more stable framework boasting 90% code coverage that is integrated into their CI process so that they know if anything breaks far before it is released.

Visual QA demoed to playback a series of interactions with IE6!  Looks like a great functional testing tool for ExtJS that takes screenshots and compares them to reference screenshots to be sure that the application looks/acts as it should.  This makes for a high quality product.  This is something Selenium can’t do for advanced client-side testing (e.g., drag-n-drop).

Ease-of-use is paramount in the new release.  Here are some highlights:

  • ExtJS 4 will have the best documentation which include a clear explanation and examples on how to use it in their base API docs.
  • They will also have over 200 examples on how to use components and will be integrated right into the API docs!
  • Sample applications will be provided together with guides to show how to use and upgrade the product successfully.

API Improvements:

  • Standardized API with clear naming conventions and published style guide.
  • Simpler configuration will achieve the same thing with less lines of code.

New things in ExtJS4:

  1. Chart package completely rewritten to be completely JS driven rather than Flash driven as they are today.  Very nice animations included as well.
  2. Better support for ARIA and RTL for 508a compliance and right-to-left text read.
  3. New themes, such as Neptune, and uses SASS to make it easily customizable.  One line change to fit your custom color scheme.
  4. Upgraded Components particularly RowEditor.  This grid plugin makes it easy to update any row inline.  TreeGrid will also be easier to use in this release and both will be part of the standard release.
  5. FormLayout is gone!  You can use any layout to create forms.
  6. Huge update is the Data package.  This allows you to define a record with fields/data that you can describe your model.  Associations API will allow you connect objects on the client-side.  Proxy is responsible for loading/saving model data…this is now part of the package (think directly accessing REST/JSONP).  This greatly reduces the complexity.  You can store directly without a store to save into the cache directly off a model object. New Proxies: WebStorageProxy and WebSqlProxy allows you to save data offline.
  7. Addresses the issue of having all your JS in one file or multiple files back into one for production deployment.  ExtJS4 uses the MVC pattern to standardize the application architecture for better separation of concerns.  This will include a common file structure so it can built as 1 one file for rollout.  Unit tests are also going to be baked in. SenchaCommand are simple command line utilities to build out applications with generators (similar to Grails/Roo).

When will it be available:

First Beta in 6 weeks and final release on 2/28/11!!