Monday, July 30, 2012

History and navigation with MVC and Ext 4.1 (Sencha)


I struggled for awhile with how to present the 'Are you sure you want to exit?' message when a user has unsaved data and attempts to close the browser or navigate elsewhere.  I also wanted pages in my application to be URL accessible so that a user could bookmark a URL to show a specific record or email a URL to another user.  I am using  Ext 4.1 and MVC.  

I decided on a two pronged approach.   First, I hooked directly into the window.beforeunload event for handling the user attempting to close the browser window or page away when they have unsaved data.  It was easy to setup a handler for the beforeunload.  The difficulty I ran into was finding the scope of my MVC application.

Secondly I used Ext.history to handle page navigation.



Here is an excerpt from my main MVC controller.

//-------------------------------------------------------
// function to handle the token that is passed in the URL for the page.
//-------------------------------------------------------
   dispatch: function (token) {

        if (token == "") {    // no parameters passed, go to home page.
            Ext.History.add("Main");
        }
        else if (token) {
            parts = token.split(':');

            if (parts[0] == "editCustomer") {  // Edit a specified customer
                this.editCustomer(parts[1]);
            } else if (parts[0] == "Main") {
                this.goHome();
            } else if (parts[0] == "tabpanel") {  // open a tab on main page
                this.goHome();
                var tab = this.getMainSearch().query('#tabs')[0];    //tabs
                tab.setActiveTab(parts[1]);  // set the main tab control to 'home'
            } else {
                this.goHome();
            }
        } else
            this.goHome();


    },
//-------------------------------------------------------
// Check if we need to ask if user needs to save.
//-------------------------------------------------------
    processHistoryToken: function (token, opt) {

        if (this.changesMade()) {  // if any dirty data that is not saved.
            this.token = token;
            Ext.Msg.show({
                title: 'Warning.',
                msg: "Save changes before exiting page?"  ,
                buttons: Ext.Msg.YESNO,
                fn: function (btn) {
                    if (btn == 'yes') {
// User has said yes so explicitly save the data to server.
                        if (this.changesMadeEvent())  // changes for my 'event' form
                            this.saveEvent(false);
                        if (this.changesMadeTicket())  // changes for my 'ticket' form
                            this.saveTicket(false);
                        }
// reset the forms so they are no longer dirty
                    this.getEventDetail().query('#EventDetailform')[0].getForm().reset();
                    this.getChangeDetail().query('#ChangeDetailform')[0].getForm().reset();
// proceed with doing the action
                    this.dispatch(this.token);
                },
                animEl: 'elId',
                icon: Ext.MessageBox.QUESTION,
                scope: this
            })
        } else {
            this.dispatch(token);
        }
    },
//-------------------------------------------------------
// Handler for the window.onbeforeonload.
//-------------------------------------------------------
 confirmExit: function () {

         // Using the previously save scope, call the 'changesMade' function to see if
        // any unsaved form data.
        if (this.MYscope.getController('AM.controller.smMain').changesMade())
            return "You will lose your changes if you leave this page !";
    },


//-------------------------------------------------------
// init function for my main MVC controller
//-------------------------------------------------------

   init: function () {
        // -------------------------------------------------
        // History management and navigation 'dispatcher' which handles some of the user navigation.
        // ----------------------------------------------------
        Ext.History.init();
        Ext.History.hasHistory = false;  // default to no history saved to prevent 'back' button completely out of the app.

        Ext.History.on('change', function (token, opt) {
            Ext.History.hasHistory = true;   // we have some history now so 'back' button is ok.
            this.processHistoryToken(token);
        }, this, this);


        //  Store our context as a property of the window.  I'm sure there are better ways to do this, but this works!
        window.MYscope = this;
        window.onbeforeunload = this.confirmExit;
......

// Support for URL.   Set the 'page' in the application based on the URL.
var token = document.location.hash.replace("#", "");
this.processHistoryToken(token);

...

Thursday, September 1, 2011

HoverButton

I found a nice hoverbutton extension at :


It extends the button class so that a mouseover will cause the menu to expand. I updated for Extjs 4.0. I also found that the expand would only work when mousing over the down-arrow icon so I modified it to use 'mouseover' and 'mouseout' events. This now allows the menu to expand when the mouse is anywhere over the button.

/**
* Add autoShow on mouseover option to buttons with menus
* @copyright LustForge.com 2011
* @author J.Lust
* @version ExtJs 3.3.4
*/
Ext.define('Ext.HoverButton', {
    extend: 'Ext.Button',
    alias: 'widget.hoverButton',


    // hide task properties and helpers
    hideTask: null,
    hideTaskMs: 250, // timeout in ms
    hideTaskFn: function () {
        if (this.hideTask !== null) {
            this.hideTask.cancel();
        }
        this.hideTask = new Ext.util.DelayedTask(this.hideMenu, this);
        this.hideTask.delay(this.hideTaskMs);
    },


    // extend init props
    initComponent: function () {


        // add hide/show, if this is a button with menu
        var config = {}, menuConfig = {};
        if (Ext.isDefined(this.initialConfig.menu)) {
            config = {
                listeners: {
                    //    menutriggerover: {
                    mouseover: {
                        fn: function (b) {
                            // console.log('menutriggerOver');
                            b.showMenu();
                        },
                        scope: this
                    },
                    mouseout: {
                        // menutriggerout: {
                        fn: function (b) {
                            //  console.log('menutriggerOut');
                            this.hideTaskFn();
                        },
                        scope: this
                    }
                }
            };
            // add listeners to see if user is over extended menu list
            menuConfig = {
                listeners: {
                    // if mousing over menu list, disable timeout
                    mouseover: {
                        fn: function (b) {
                            // cancel hide if they went away and came back
                            if (this.hideTask !== null) {
                                //       console.log('menu mouseOver');
                                this.hideTask.cancel();
                                this.hideTask = null;
                            }
                        },
                        scope: this
                    },
                    // on mousing out of menu list, resume timeout
                    mouseout: {
                        fn: function (b) {
                            //    console.log('menu mouseOut');
                            this.hideTaskFn();
                        },
                        scope: this
                    }
                }
            };


            Ext.apply(this.menu, menuConfig);
        }


        // apply config
        Ext.apply(this, Ext.apply(this.initialConfig, config));
        // call parent
        Ext.HoverButton.superclass.initComponent.apply(this, arguments);
    }
});

I use it as a 'more' dropdown box in my toolbar as follows:

, {
text: 'More',
xtype: 'hoverButton',
menu: {
     xtype: 'menu',
items: {
xtype: 'buttongroup',
columns: 1,
defaults: {
xtype: 'button',
scale: 'large',
iconAlign: 'left'
},
items: [
{
text: 'option1',
cls: 'x-btn-text-icon',
etc......

Buttongroup menu hiding

One unique behaviour of a buttongroup in a menu is that it will not 'hide' or collapse until the user clicks elsewhere. So how do you hide the buttongroup? Easy once you know how. Just do this call:

Ext.menu.Manager.hideAll();


This will hide all open menus.

Thursday, August 20, 2009

WPF binding to a typed dataset

I having been learning WPF and came across a problem trying to use typed datasets and the Visual Studio wizard for generating the xsd file. Once I created the dataset (xsd file) I couldn't find a way to actually bind it to anything or get the data. After some trial and error, the following seems to work well. I am populating an Xceed datagrid but the code is generic. I want to provide a 'blank' row for adding new records as well.


public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{

// Customers table (dataset is 'dsCustomers' as found in the ...Designer.cs file)
m_customer = new dsCustomers.TblCustomers();
//Create data adapter and fill from the source table
(new dsCustomersTableAdapters.TblCustomersTableAdapter()).Fill(m_customer );

m_customer.Rows.Add(m_customer.NewRow()); // Add a blank row

// Likewise for Address table (more verbose)

m_Addresses= new dsCustomers.TblAddresses();
dsCustomersTableAdapters.TblAddressesTableAdapter tblAdap = new dsCustomersTableAdapters.TblAddressesTableAdapter();

tblAdap.Fill(m_Addresses);
DataRow dr = m_Addresses.NewRow(); // generate a new row
m_Addresses.Rows.Add(dr); // append back to the table

base.OnStartup(e);
}

private dsCustomers.TblCustomers m_customer ;

public dsCustomers.TblCustomers Customers
{
get
{
return m_customer ;
}
}
private dsCustomers.TblAddresses m_Addresses;

public dsCustomers.TblAddresses Addresses
{
get
{
return m_Addresses;
}
}
}





Meanwhile the xaml is ..

< Grid.Resources>

< xcdg:DataGridCollectionViewSource
x:Key="Tblmycustomers"
Source="{Binding Source={x:Static Application.Current},
Path= Customers}"/ >

< xcdg:DataGridCollectionViewSource
x:Key="TblLocations"
AutoCreateForeignKeyDescriptions="true"
Source="{Binding Source={x:Static Application.Current},
Path= Addresses}"/ >
< /Grid.Resources >

Saturday, January 17, 2009

Cookies and the problems they cause

I often use the Ext state manager to automatically manage cookies for Ext widgets. This allows the application to 'remember' user settings such as window size, location etc. The only code required is :

Ext.state.Manager.setProvider(new Ext.state.CookieProvider());


This is very powerful and simple to add to an application BUT it can cause a mountain of problems. I've been bitten twice. What sometimes happens is the cookies can end up being applied to the incorrect objects. It seems that when Ext creates DOM objects, if there is no explicit dom id, an id is created. This id is used by the cookie 'manager' to reference the objects that are monitored. This is fine, except when items are added or removed from the application. The auto generated id numbers are changed and the cookies are now referencing different objects. This problem occurs during the development process or when new application versions are released (widgets are added and removed).

How do we avoid this? Its quite simple. Just assign explicit id properties to ALL items you create. Just do this as a 'best practice'. By doing this, there are no auto generated ids and no danger of the manager referencing the wrong objects.

Using Ext.extend to extend user classes.

While learning javascript, I've fallen into the copy/paste trap of code creation. Rather than using OO constructs, I often copy a block of code, paste it and modify it slightly. This is bad bad bad so I thought I would figure out how to use Ext.Extend to take my own base class and use inheritance to extend the base functionality. There is no rocket science here but I can use this now as a template for inheriting. Code ...

// ----------- Base class (extending 'object' )  ------------------
genericBase = Ext.extend(Object, {
basevar1: 1,
constructor: function(args) {
this.basevar1 = 2;
},

baseFunc1: function(testArg) {
alert("test Arg is " + testArg + " basevar = " + this.basevar1);
}
});

// ----------- Derived class (extending 'genericBase'class ) -------
genericDerived = Ext.extend(genericBase, {
derVar: 444, // class property
constructor: function(args) {
// Call the base constructor
genericDerived.superclass.constructor.call(this, args);
},

derivedFunc1: function(testArg) {
alert("derivedFunc1 called " + testArg );
},
getBaseval: function() {
return this.basevar1;
}

});


// -------- Code to instantiate and test the classes

// First, create the object
var myobj = new genericDerived({prop1: '123', prop2: 'abc' });

// displays basevar with a get method
alert("Derived class, method call ==>" + myobj.getBaseval());

// also displays basevar, but with direct property reference
alert("explicit reference to property basevar1 ==> " + myobj.basevar1);

myobj.baseFunc1('Calling base method');
myobj.derivedFunc1('Calling derived class method');


Tuesday, December 2, 2008

How to modify Ext tooltips dynamically

I have a requirement to dynamically change tool-tips as content gets refreshed and modified. There were a couple of suggestions in the forums, but the following works great for me:
var mydom = Ext.get('mydomelement');       // get element
var tip = Ext.getCmp(mydom.dom.id + '_tip'); //get element's tooltip

if ( tip ) {// tool tip already exists, so modify

tip.title = 'new title';
tip.html = 'new text';
}

else { // tip does not exist. Create it with unique id.

new Ext.ToolTip({
target: mydom.dom.id,
id: mydom.dom.id + '_tip',
title: 'title here', html: 'original text'
});
}
I am giving every tooltip a unique id, based on the id of the dom element it decorates.