PDA

View Full Version : Ext 2.0 - the the CRUD Form example I wish I had 6 months ago.



TheNakedPirate
28 Jan 2008, 7:40 PM
I call myself a C# programmer. I was introduced to all the fuss around AJAX around 6 months ago and found Jack's library pretty soon after that. I really like it BUT...

There is a lot of code and its not perfect but it works and it illustrates a lot of things that I wish I knew about EXT before I started.

I would appreciate your comments and improvements if you have them as I want to will make a tutorial one day to explain it all a bit better.

The core JSON call manager



Ext.apply(Application, {
SITE_ROOT: ''

/*
* Show Exception
*/
, showException : function(msg){
var exmsg = 'The last action failed with this message <br/>&nbsp;<br/><code>' + msg + '</code> <br/> If the problem persists please report it to application support.';

Ext.MessageBox.show({
title:'An Exception has Occurred'
, msg: exmsg
, buttons: Ext.Msg.OK
, width: 440
});
}

/*
* Show Are You Sure
*/
, showAreYouSurePrompt: function(container, form){

var dirt = form.isDirty();

if(dirt)
{
Ext.Msg.show({
title:'Save Changes?'
, msg: 'There could be unsaved changes. <br/>Would you like to save your changes?'
, buttons: Ext.Msg.YESNOCANCEL
, fn: function(button){
switch(button)
{
case 'yes':
Application.submitForm(container, form);
break;
case 'no':
container.close();
break;
}
}
, icon: Ext.MessageBox.QUESTION
});
}
else { container.close(); }
}

/*
* Submit Form
*/
, submitForm : function(container, form){
if (form.isValid()) {
form.submit({
waitMsg: 'Processing your request...'
, success: function (f, a) {
if(container){
container.onSuccess(a);
}
}
, failure: function (f, a) {
if(container){
container.onFailure(a);
}
var r = a.result;
if(r) {
if(r.exception !== null) {
Application.showException(r.exception.message);
}
else {
for(i = 0; i < f.items.length; i++)
{
for(v = 0; v < r.data.length; v++)
{
var it = f.items.items[i];
var val = r.data[v];
var fieldName = it.name;
var l = fieldName.indexOf(".");

if(l > 0){
fieldName = it.name.substring(l + 1);
}

if( fieldName == val.propertyname){
it.markInvalid(val.message);
}
}
}
}
}
else {
Ext.MessageBox.alert('Action Failed', 'A server-side problem occured while performing the last action. If the problem persists please report it to application support.');
}
}
, scope:this
});
} else {
Ext.MessageBox.alert('Errors', 'Please fix the errors noted.');
}
}
});

A base Edit Window class



EditWindow = function(config){
Ext.apply(this, config);

this.addEvents({
'save' : true
, 'fail' : true
});

EditWindow.superclass.constructor.call(this, {
modal: true
, plain: true
, shim: false
, layout:'fit'
, defaults: {
msgTarget: 'side'
}
, buttons:[

this.saveButton = new Ext.Button({
text:'Save'
, handler: this.onSaveButton
, scope: this
, minWidth: 80
})
, this.cancelButton = new Ext.Button({
text: 'Cancel'
, handler: this.onCancelButton
, scope: this
, minWidth: 80
})]
});

};

Ext.extend(EditWindow, Ext.Window, {
form : Ext.emptyFn
, onSuccess: function(action) {
this.fireEvent("save", action);
}
, onFailure: function(action) {
this.cancelButton.enable();
this.fireEvent("fail", action);
}
, onSaveButton: function(action) {
this.cancelButton.disable();
if(this.form){
Application.submitForm(this, this.form.getForm());
}
}
, onCancelButton: function(action) {
if(this.form){
Application.showAreYouSurePrompt(this, this.form.getForm());
}
}

});
an implementation of a window



MyEditWindow = function(config){
Ext.apply(this, config);

this.form = new Ext.form.FormPanel({
url : Application.SITE_ROOT + '/MyEditView/CreateUpdate.rails'
, height: 480
, trackResetOnLoad: true // Important to track isDirty()
, defaults: {
// applied to each contained item
width: 330
, msgTarget: 'side'
}
, reader: new Ext.data.JsonReader({
totalProperty: 'totalrecords'
, root: 'data'
, id: 'id'
}
, Ext.data.Record.create([
{name: 'id'}
, {name: 'Obj.Number', mapping: 'number'}
, {name: 'Obj.Type', mapping: 'type'}
, {name: 'Obj.Date', mapping: 'expirydate'}

])
)
, items: [
new Ext.form.Hidden({name: 'ParentId', value: this.parentId})
, new Ext.form.Hidden({name: 'Obj.Id', value: this.recordId})
, new Ext.Panel({
border:false
, baseCls : 'helpText'
, html:'<div class="res-block"><div class="res-block-inner">'
+ '<h3>Instructions</h3><ul>'
+ '<li>All fields on this page must be filled in before a record can be created.</li>'
+ '</ul></div></div>'
})
, new Ext.form.TextField({name: 'Obj.Number', fieldLabel: 'Number' })
, new Ext.form.ComboBox({store: Ext.StoreMgr.get('Options'), editable: false, name: 'Obj.Type', fieldLabel: 'Type'})
, new Ext.form.DateField({name: 'Obj.Date', fieldLabel: 'Date'})
]
});



MyEditWindow.superclass.constructor.call(this, {
title: 'Passport'
, items: this.form
, listeners :{
'render': function(c) {
if(this.recordId > 0){
this.form.getForm().load({
url: Application.SITE_ROOT + '/MyEditView/GetRecord.rails'
, params: {'id': this.recordId}
, waitMsg: 'Loading'
, success: function(f, a) {

}
, failure : function(f, a) {

}
, scope : this
});
}
}
, scope: this
}
});

Ext.StoreMgr.get('Options').load();

};

Ext.extend(MyEditWindow, EditWindow);
The call to create the edit window



, showWindow: function(recordId){

if(this.window){
this.window.close();
}

this.window = new MyEditWindow({
parentId: this.parentId
, recordId: recordId
});

this.window.on('save', function(action){
this.window.close();
this.refreshStore();
}, this);

this.window.show(this);
}

a typical success message would come back in the form




{"totalrecords":0,"success":true,"exception":null,"data":[]}

if success = true and exception is null the Transport object is serialised in the data array.

if success = false and exception is null a series of validation errors are serialised in the data array. this code then take the form and applies the server side messages to the form if it can.

if success = false and exception is true the exception is serialized in the data array

djfiii
6 Feb 2008, 1:25 PM
This is great - thanks for contributing. Would you be willing to package this with a real implementation example - including server side code, and sql structure of whatever data you are manipulating? That would really help my understanding (and I assume others as well). Part of what I'm missing is how to break all of that up into the appropriate files - for example, my mind still works like:

view.php - display data (with links to edit.php)
edit.php - add,edit,delete depending on how the page is called (i.e. edit.php?action=add, action=edit, etc.)
update.php - process submission from edit.php, redirect back to view.php

I can't figure out how this paradigm has changed when using EXT. Again, thanks for your post - this is the closest thing to what I've been looking for in this forum so far.

Edit: I use asp as well, so if you have code handy that is asp/sql server I can read and understand that as well.

TheNakedPirate
6 Feb 2008, 2:40 PM
Excuse English & grammar...I'm pretty busy at the moment so I won't proof read, I'll just write.

I my view. One of the central concepts with Ajax development and especially with Ext is that you application should be broken down into fully encapsulated "silos" of function.

Rule #1.
Your application should provide a fully encapsulated function on a single "page". You will see in my code I provide an Application Object that offers the equivalent of a client side controller for my client side operations. Core to this is the form posting methods with all the message boxes about dirty form etc.

If you have a look at the ext documentation application. Everything is self contained. All activity around looking up the API information occurs in ONE application with none of the traditional ASP.NET POSTBACKS when an event occurs. For a .Net programmer this is a particularly hard concept to implement because for years we have relied on the server side to control a lot of client side presentation.

Rule #2
Understand the Model View Controller pattern and how it extends to the web. Ext brings you closer to being able to implement a proper MVC as you can maintain a lot more state in the client (because we don't POSTBACK every time we hit a button now). The neatest way I have found to do this currently is (and some might argue this is overkill) to provide controllers on every application layer.

I'll digress a bit here. For a long time the vendor M$ has proposed n-tier architecture for web delivered applications in the enterprise. That is all well and good for all the reasons they have suggested but a lot of confusion exists over n-tier because (brace for flames) in practice it is essentially a way to sell more middle tier servers and it not a practical paradigm for planning software architecture. So just because we have an MVC doesn't mean we have to deploy it over 3 or more servers. And when I mention application layers I might use terminology used in the n-tier sales pitch but I am not talking about that.

So In rule 1 I mentioned the client controller which directs client calls to the business controller or service layer on the server side. Typically in the business controller I will populate a View Object (VO) or Transfer Object (TO) with the contents of the Post. A VO or TO is little more than a struct maybe with some clever initialisation perhaps but that's it.

Rule #3
Use a controller on each layer of activity in your application.

The business controller will go on the persist it or do what ever it has to do with it and then I return a DataPackage with a message payload. Akin to an Envelope. Here is my current one... I spose it could be written better but it works for now.

Its C#



namespace Web.TO
{

public class DataPackage
{

private bool success;
private SerializableException exception;
private List<object> data;
private int count;

public DataPackage(bool success)
{
data = new List<object>();
this.success = success;
}

public DataPackage()
{
data = new List<object>();
success = false;
}

public DataPackage(List<object> dataList, bool success)
{
if (dataList != null)
{
data = dataList;
}
else
{
data = new List<object>();
}
this.success = success;
}

public DataPackage(object data)
{
this.data = new List<object>();
AddData(data);
success = true;
}

public DataPackage(IEnumerable data)
{
this.data = new List<object>();
foreach (object o in data)
{
AddData(o);
}
success = true;
}

public DataPackage(object data, IList<ValidationResult> validationResults)
{
this.data = new List<object>();
AddData(data);
AddValidationErrors(validationResults);
}

private void ProcessValidationResults(IList<ValidationResult> results)
{
if (results.Count > 0)
{
data.Clear();
}
foreach (ValidationResult validationResult in results)
{
data.Add(new ValidationError(validationResult.Rule.InfoDescriptor.Name, FormatForMessageForUI(validationResult.ErrorMessage)));
}
}

private static string FormatForMessageForUI(string message)
{
return message.Replace("The property", "The form field");
}

public DataPackage(object data, Exception exception)
{
this.exception = new SerializableException(exception);
this.data = new List<object>();
this.data.Add(data);
success = false;
}

public DataPackage(List<object> dataList)
{
data = dataList;
success = true;
}

public DataPackage(List<object> dataList, Exception exception)
{
this.exception = new SerializableException(exception);
data = dataList;
success = false;
}

public int TotalRecords
{
get
{
return data.Count;
}
set
{
// read only
}
}

public int TotalCount
{
get
{
if (count < 1)
{
return data.Count;
}
else
{
return count;
}
}
set
{
count = value;
}
}

public bool Success
{
get
{
return success;
}
set
{
success = value;
}
}

public SerializableException Exception
{
get
{
return exception;
}
set
{
exception = value;
}
}

public List<object> Data
{
get
{
return data;
}
set
{
data = value;
}
}


public void AddValidationErrors(IList<ValidationResult> validationResults)
{
ProcessValidationResults(validationResults);
success = (validationResults.Count == 0 && data.Count == 0);
}

public void AddValidationError(string fieldName, string errorMessage)
{
data.Add(new ValidationError(fieldName, errorMessage));
success = false;
}

public void AddData(object newData)
{
if (newData != null)
{
data.Add(newData);
success = true;
}
}


public List<Type> GetStoredTypes()
{
List<Type> types = new List<Type>();
types.Add(GetType());
foreach (object o in data)
{
types.Add(o.GetType());
}
return types;
}

}


public class ValidationError
{
private string propertyName;
private string message;

public ValidationError()
{
}

public ValidationError(string propertyName, string message)
{
this.propertyName = propertyName;
this.message = message;
}


public string PropertyName
{
get
{
return propertyName;
}
set
{
propertyName = value;
}
}

public string Message
{
get
{
return message;
}
set
{
message = value;
}
}
}
}

Rule #4
Return data in a consistent message format. You won't believe how much time it will save in the long run.

I won't go into any more detail on the server side except to say I use Active Record which is a wrapper for nHibernate to manage our domain model persistence. An important note is that I separate Domain and Transport models so even though they may look exactly the same I still populate a TO every time I present data to the view.

Rule #5
Great wisdom and foresight is revealed where there is a separation of concerns.

I should also address the reason I posted my code. I am a firm believer in giving what I can back to the community. Ext has made a tough job easier in a lot of ways. I see a lot people in these forums post questions on how certain things are done and then post a few hours later with "don't worry I worked it out" with no code or explanation. It sh*ts me to tears. So I like to post FULL and COMPLETE answers to my problems with a context. I also make sure the Posts I start have meaningful titles that will make sense when someone is searching for an answer to some thing because so often the answer comes back "search the forums"

Rule #6
Give back QUALITY to the community to the best of your ability.


In Summary, and hopefully to answer you question as fully and completely as possible your VERY SIMPLE Web application might consist of this kind of structure

WEB-APP
view.htm
viewController.js
viewApplication.js

SERVER
->Dependencies // where all the framework class and other junk is kept
ServiceController.dll
ViewModel.dll
DomainController.dll
DomainModel.dll

view.htm would be the only page loaded by your browser.

When you write with EXT you are no longer writing web pages with the old CGI postback paradigm you are writing stateful web applications that use javascript's HTTP post capability to call a service layer in the background to update the state of your client.

Hope this clears things up for you.

TheNakedPirate
6 Feb 2008, 2:41 PM
Oh there are some architecture articles around the site have a look at those too

djfiii
7 Feb 2008, 7:35 AM
That helps a lot - thanks for such a detailed response :P

w_menzies
13 Feb 2008, 6:42 AM
I am a newbie to EXTJs and would like to use it. Very interested in approach MVC. How can I show the MyEditWindow. You have the code "the call to create the edit window" but it is incomplete.
I would very much appreciate it if you shared this.
Thanks in advance.

TheNakedPirate
15 Mar 2008, 8:27 PM
Sorry...it's been a while and I haven't had a lot of time to help you out here.

To help those still looking for a more of a clue...here is some IMPERFECT but demonstrative code to get you on your way.

Download the latest release of EXT v2.0.2

go to the examples directory and open the desktop sample

when you have finished playing with it tack the code here on the end of sample.js




MyDesktop.CRUDModule = Ext.extend(Ext.app.Module, {
form : Ext.emptyFn
, init :function(){

this.addEvents({
'save' : true
, 'fail' : true
});

}
, onSuccess: function(action) {
this.fireEvent("save", action);
}
, onFailure: function(action) {
this.cancelButton.enable();
this.fireEvent("fail", action);
}
, onSaveButton: function(action) {
this.cancelButton.disable();
if(this.form){
this.submitForm(this, this.form.getForm());
}
}
, onCancelButton: function(action, b, c) {
if(this.form){
this.showAreYouSurePrompt(this, this.form.getForm());
}
}
/*
* Show Exception
*/
, showException : function(msg){
var exmsg = 'The last action failed with this message <br/>&nbsp;<br/><code>' + msg + '</code> <br/> If the problem persists please report it to application support.';

Ext.MessageBox.show({
title:'An Exception has Occurred'
, msg: exmsg
, buttons: Ext.Msg.OK
, width: 440
});
}

/*
* Show Are You Sure
*/
, showAreYouSurePrompt: function(container, form){

var dirt = form.isDirty();

if(dirt)
{
Ext.Msg.show({
title:'Save Changes?'
, msg: 'There could be unsaved changes. <br/>Would you like to save your changes?'
, buttons: Ext.Msg.YESNOCANCEL
, fn: function(button){
switch(button)
{
case 'yes':
Application.submitForm(container, form);
break;
case 'no':
container.close();
break;
}
}
, icon: Ext.MessageBox.QUESTION
});
}
else { container.close(); }
}

/*
* Submit Form
*/
, submitForm : function(container, form){
if (form.isValid()) {
form.submit({
waitMsg: 'Processing your request...'
, success: function (f, a) {
if(container){
container.onSuccess(a);
}
}
, failure: function (f, a) {
if(container){
container.onFailure(a);
}
var r = a.result;
if(r) {
if(r.exception !== null) {
Application.showException(r.exception.message);
}
else {
for(i = 0; i < f.items.length; i++)
{
for(v = 0; v < r.data.length; v++)
{
var it = f.items.items;
var val = r.data[v];
var fieldName = it.name;
var l = fieldName.indexOf(".");

if(l > 0){
fieldName = it.name.substring(l + 1);
}

if( fieldName == val.propertyname){
it.markInvalid(val.message);
}
}
}
}
}
else {
Ext.MessageBox.alert('Action Failed', 'A server-side problem occured while performing the last action. If the problem persists please report it to application support.');
}
}
, scope:this
});
} else {
Ext.MessageBox.alert('Errors', 'Please fix the errors noted.');
}
}

});


/*
* Example CRUD window
*/
MyDesktop.CRUDWindow = Ext.extend(MyDesktop.CRUDModule, {
id:'crud-win',
init : function(){
this.launcher = {
text: 'CRUD Window',
iconCls:'icon-grid',
handler : this.createWindow,
scope: this
}
},

createWindow : function(){
var desktop = this.app.getDesktop();
var win = desktop.getWindow('crud-win');
if(!win){
win = desktop.createWindow({
id: 'crud-win',
title:'CRUD Window',
width:740,
height:480,
iconCls: 'icon-grid',
shim:false,
animCollapse:false,
constrainHeader:true,

layout: 'fit',
items: [
this.form = new Ext.form.FormPanel({
url : '/MyEditView/CreateUpdate.rails'
, height: 480
, trackResetOnLoad: true // Important to track isDirty()
, defaults: {
// applied to each contained item
width: 330
, msgTarget: 'side'
}
, reader: new Ext.data.JsonReader({
totalProperty: 'totalrecords'
, root: 'data'
, id: 'id'
}
, Ext.data.Record.create([
{name: 'id'}
, {name: 'Obj.Number', mapping: 'number'}
, {name: 'Obj.Type', mapping: 'type'}
, {name: 'Obj.Date', mapping: 'expirydate'}

])
)
, items: [
new Ext.form.Hidden({name: 'Obj.Id', value: this.recordId})
, new Ext.Panel({
border:false
, baseCls : 'helpText'
, html:'<div class="res-block"><div class="res-block-inner">'
+ '<h3>Instructions</h3><ul>'
+ '<li>All fields on this page must be filled in before a record can be created.</li>'
+ '</ul></div></div>'
})
, new Ext.form.TextField({name: 'Obj.Number', fieldLabel: 'Number' })
, new Ext.form.ComboBox({store: Ext.StoreMgr.get('Options'), editable: false, name: 'Obj.Type', fieldLabel: 'Type'})
, new Ext.form.DateField({name: 'Obj.Date', fieldLabel: 'Date'})
]
})
]
, buttons:[
this.saveButton = new Ext.Button({
text:'Save'
, handler: this.onSaveButton
, scope: this
, minWidth: 80
})
, this.cancelButton = new Ext.Button({
text: 'Cancel'
, handler: this.onCancelButton
, scope: this
, minWidth: 80
})]
, listeners :{
'render': function(c) {
// if(this.recordId > 0){
this.form.getForm().load({
url: '/MyEditView/GetRecord.rails'
, params: {'id': this.recordId}
, waitMsg: 'Loading'
, success: function(f, a) {

}
, failure : function(f, a) {

}
, scope : this
});
// }
}
, scope: this
}

});
}
win.show();
}
});



now you have done that find and change the code at the very top of samples.js to look like this.



getModules : function(){
return [
new MyDesktop.GridWindow(),
new MyDesktop.TabWindow(),
new MyDesktop.AccordionWindow(),
new MyDesktop.BogusMenuModule(),
new MyDesktop.BogusModule(),
[I]new MyDesktop.CRUDWindow()
];
},


Of course you are going to have to deal with all the backend plumbing and you should read all the stuff I have written in this post as well. I hope it helps you get over your first hurdle.

I'll repeat that I am away from my regular dev environment and there are PROBABLY some mistakes in the above...but hey its about as much quality as I can fit in for the moment.