Go Back   SmartClient Forums > Smart GWT Technical Q&A
Wiki Register Search Today's Posts Mark Forums Read

Reply
 
Thread Tools Search this Thread
  #1  
Old 5th Jun 2010, 05:12
markok markok is offline
Registered Developer
 
Join Date: Feb 2009
Posts: 99
Default CanvasItem value management

Hello,

Could you please explain how dynamic form value management for form items works ? I tried going through the documentation but could not find a clear explanation how this works.

I've implemented a custom CanvasItem based form item and I'm trying to get the CanvasItem behave correctly in my dynamic form. I managed to get the canvasitem delegate the disabled state correctly to my custom canvas by overriding the "public void setDisabled(Boolean disabled)" method on CanvasItem.

I'm having trouble getting the value from my (datasource bound) dynamic form to be delegated to my custom canvas though. I've tried overriding the "public Object getValue()" and "public void setValue(String value)" methods on my CanvasItem but it seems I'm not getting any calls to these methods at all. Could you please explain the pattern that should be used here to

a) get the value from dynamic form's datasource to be displayed on my custom canvas(item) and
b) deliver the edited value from my custom canvas(item) back to the datasource when the dynamic form submits ?

I'm using the latest smartgwt 2.2 and gwt 2.0.3 on mac.

Thanks in advance,
Marko
Reply With Quote
  #2  
Old 7th Jun 2010, 10:05
markok markok is offline
Registered Developer
 
Join Date: Feb 2009
Posts: 99
Default

Anyone ? Is this even possible with the current set of APIs available ?
Reply With Quote
  #3  
Old 7th Jun 2010, 10:54
svjard svjard is offline
Registered Developer
 
Join Date: Oct 2009
Posts: 742
Default

It should work. I modified the showcase example, http://www.smartclient.com/smartgwt/showcase/#layout_form_databinding and it works without problem for me.

Code:
public class MyItem extends CanvasItem {
	public MyItem(String name) {
		setName(name);
		final DynamicForm f = new DynamicForm();
		TextItem s = new TextItem("theNewItem");
		s.addChangedHandler(new ChangedHandler() {
			@Override
			public void onChanged(ChangedEvent event) {
				setValue(event.getValue().toString());
			}
		});
		f.setItems(s);
		setCanvas(f);
	}

	@Override
	public void setValue(String value) {
		super.setValue(value);
	}
	
	@Override
	public Object getValue() {
		return super.getValue();
	}
};
Reply With Quote
  #4  
Old 12th Jun 2010, 02:19
markok markok is offline
Registered Developer
 
Join Date: Feb 2009
Posts: 99
Default

Thanks for your reply!

I have trouble undestanding the example code you sent. Is it a requirement to have a DynamicForm as canvas item's canvas for the values to be passed correctly ?

The code I'm trying is more like

Code:
public class TestCanvasItem extends CanvasItem {

    public TestCanvasItem(String name) {
        setName(name);
        Canvas myCustomCanvas = new Canvas();
        setCanvas(myCustomCanvas);
        setShouldSaveValue(true);
    }

    @Override
    public void setValue(String value) {
        SC.say("setValue called: " + value);
        super.setValue(value);
    }

    @Override
    public Object getValue() {
        SC.say("getValue called, returning " + super.getValue());
        return super.getValue();
    }

}
I have a custom canvas (currently a rich text editor) as the custom item's canvas. This canvas item is used in a DynamicForm which is bound to a datasource. I am sure the form itself works 100% because I have other fields (TextItems etc) in the form which work ok. When I use this TestCanvasItem with the form I never get any calls to getValue or setValue methods which I then could use to pass the values to myCustomCanvas. I don't really understand how your example MyItem could work in a DynamicForm which is bound to a datasource ? Any ideas ?

br,
Marko
Reply With Quote
  #5  
Old 12th Jun 2010, 03:34
markok markok is offline
Registered Developer
 
Join Date: Feb 2009
Posts: 99
Default

To follow up on my own post just to add that everything works if I _manually_ call the setValue before saving the form.

Code:
...
    myTestCanvasItem.setValue(((MyCustomRTECanvas)myTestCanvasItem.getCanvas()).getEditorValue());
    myForm.saveData();
...
And similarly when fetching the data:

Code:
...
    myForm.fetchData(criteria, new DSCallback() {
            public void execute(DSResponse response, Object rawData, DSRequest request) {
                ((MyCustomRTECanvas)myTestCanvasItem.getCanvas()).setEditorValue(response.getData()[0].getAttribute("editor"));
                ...
            }
    });
...
But surely there must be a way to do this automatically somehow on datasource bound forms ?

Last edited by markok; 12th Jun 2010 at 03:39..
Reply With Quote
  #6  
Old 2nd Jul 2010, 12:33
SiccoNaets SiccoNaets is offline
Registered Developer
 
Join Date: May 2010
Posts: 107
Default

I have the exact same problem. In fact, I flat out don't understand how SmartGWT does it's form submission. Consider the following example:



The application:

Code:
public class TomcatGWT implements EntryPoint
{

public TomcatGWT()
{

	  DataSource dataSource= DataSource.get("order");
	  
	  HLayout layout= new HLayout();
	  layout.setWidth(800);


         DynamicForm form = new DynamicForm(){
		  
		  @Override
		  public Object getValue(String fieldName)
		  {
			  Log.debug("Form.getValue(" + fieldName + ") called.");
			  return super.getValue(fieldName);
		  }
		  
		  @Override 
		  public String getValueAsString(String fieldName)
		  {
			  Log.debug("Form.getValueAsString(" + fieldName + ") called.");
			  return super.getValueAsString(fieldName);
		  }
		  
		  @Override 
		  public Map getValues(){
			  Log.debug("Form.getValues() called.");
			  return super.getValues();
		  }
		  
		  @Override
		  public Record getValuesAsRecord(){
			  Log.debug("Form.getValuesAsRecord() called.");
			  return super.getValuesAsRecord();
		  }
		  
		  @Override
		  public Boolean hasErrors()
		  {
			  Log.debug("Form.hasErrors() called.");
			  return super.hasErrors();
		  }
		  
		  @Override
		  public Boolean hasFieldErrors(String fieldName)
		  {
			  Log.debug("Form.hasFieldErrors(" + fieldName + ") called.");
			  return super.hasFieldErrors(fieldName);
		  }
	  };
	  form.setDataSource(dataSource);

          TextItem normalTextItem = new TextItem("type"){
		  
		@Override
		public Object getValue(){
			Log.debug("TextItem.getValue() called.");
			return super.getValue();
		}
	  };
	  normalTextItem.setTitle("Normal Text.");
	  
	  CustomTextItem textItem = new CustomTextItem("title");
	  textItem.setTitle("Title");
	  textItem.setShouldSaveValue(true);
	  textItem.setRequired(true);
	  
	  SubmitItem submit = new SubmitItem();
	  submit.setTitle("Submit");
	  form.setFields(textItem, submit);
	  
          layout.addMember(form);

         layout.draw();
}

}

The custom CanvasItem, again, really basic, Proof of Concept type stuff:

Code:
public class CustomTextItem extends CanvasItem {


        TextItem textItem;
	
	public CustomTextItem(String name)
	{
		super(name);
		
		DynamicForm form = new DynamicForm();
		 textItem = new TextItem();
		
		form.setFields(textItem);
		
		this.setCanvas(form);
	}
	
	@Override
	public Object getValue()
	{
		Log.debug("GetValue() called ");
		
		return textItem.getValue();
	}
	
	@Override
	public void setValue(String value)
	{
		Log.debug("Set value() called");
		
		textItem.setValue(value);
	}
         
        @Override
	public Boolean validate(){
		Log.debug("Validate() called");
		return super.validate();
	}


}

Now, when clicking the Submit button, the form never submits because CustomTextItem has been set as required and for some reason, SmartGWT always sees the CustomTextItem as empty. The interesting part is that the NormalTextItem works, but in NEITHER CASE DOES FormItem.getValue() or Form.getValue(fieldName) EVER GET CALLED. Nor do any of the other methods on the DynamicForm that I added debug statements too.

The only thing I can think of at this point is that the DynamicForm is doing some javascript magic under the hood that allows it to retrieve the entered values from the formitems which isn't exposed through the Java API. And that's basically the root of the problem with CanvasItem - you can override getValue() all you want, it never actually get invoked; so unless you manually invoke it and then add the value to the Record underlying the form, it will never get submitted. It also means that you cannot do validation against the CanvasItem because while you can "fake" adding the data to the Record in the way I just described, you cannot "fool" the validation - it will always fail on a CanvasItem, no matter how you implement validate() or getValue().

Someone from Isomorphic needs to take a look at this - I'm at the end of my rope.

Incidentally, while you're at it - can you explain to me the following inconsistencies in the API?

Record has the following methods (inherited from the DataClass):

Code:
Record.setAttribute(String propertyName, String[] values)
Record.setAttribute(String propertyName, int[] values)

but DynamicForm does not have matching methods for these; i.e. there are no:
Code:
DynamicForm.setValue(String fieldName, String[] values)
DymamicForm.setValue(String fieldName, int[] values)
You also cannot get directly at the Record underlying a Form; you only get a COPY of that record; i.e.:

Code:
Record record = form.getValuesAsRecord;

//do some stuff to the record, for instance add a multivalue array

String[] emailArray= new String[2];

emailArray[1]=email1@acmecorp.com;
emailArray[2]=email2@acmecorp.com;

record.setAttribute(myMultiValueStringField, stringArray);

form.editRecord(record);

form.saveData();
However, the call to form.editRecord() puts the form into OperationType=update; so you ALSO need to hack the form.getOperationType() method; for instance:

Code:
//check whether to call update or add prior to saving
Record record = this.getFormRecord();
			
			if(record.getAttributeAsInt("ID")!= null)
			{
				this.mainForm.setSaveOperationType(DSOperationType.UPDATE);
			}
			else
			{
				this.mainForm.setSaveOperationType(DSOperationType.ADD);
			}
			
			this.mainForm.saveData();

All this JUST to submit a multi-value property? In fact, if you look at the posts of the other people trying to use CanvasItem, most of them are doing this precise requirement: they're adding a ListGrid to a Form because they need to handle a multi-value property; something like a list of emails; for instance. That's a very common, basic requirement but the way SmartGWT wants you to handle this is by defining two different domain objects (one for your main record, one for the list of email addresses) and then use the foreign-key magic in the datasource to tie the two together. In other words; you now need to set up DataSource for every single multi-value property that you may have and, if you're using DMI, as I am, that's a HUGE amount of work.

I really feel there's times where SmartGWT is trying to be a little bit too smart, no pun intended. Some more basic functionality and more control over the atomic behavior would be better than all this magic-out-of-the-box stuff that never quite fits a real-world scenario and you can't actually use in a real application; i.e. something real, as opposed to idealized situations from the showcase.
Reply With Quote
  #7  
Old 6th Jul 2010, 16:09
Isomorphic Isomorphic is offline
Administrator
 
Join Date: May 2006
Posts: 38,662
Default

If you're using the latest, you'll notice the advice in the docs for CanvasItem to call setValue() rather than anything you've attempted with getValue() et al overrides. It's true that int[] and String[] convenience methods should be added, however, note the setValue() variant that takes a JavaScript Object - you can use this for now via creating a JSArray of int or String.
Reply With Quote
  #8  
Old 7th Jul 2010, 13:33
SiccoNaets SiccoNaets is offline
Registered Developer
 
Join Date: May 2010
Posts: 107
Default

Ok, so that works for submitting data; i.e. calling super.setValue(). I think it's terrible design, personally - you need to now add ChangedHandlers all over the place to ensure super.setValue() gets called (not to mention that ChangedHandlers and things like EditorValueFormatters don't work together). Frankly, override getValue() and actually have the form call getValue() on each FormItem during saveData() would be much better, but whatever: it works.

What about the other side of the binding equation? How do you actually populate a CanvasItem with data from a dataSource?

What I mean is this:

CanvasItem implementation (basically a CanvasItem wrapper around a TextItem):

Code:
public class CustomTextItem extends CanvasItem {
	
	TextItem textItem;
	
	public CustomTextItem(String name)
	{
		super(name);
		
		DynamicForm form = new DynamicForm();
		
		textItem = new TextItem();
		textItem.setShowTitle(false);
		
		textItem.addChangedHandler(new ChangedHandler()
		{

			@Override
			public void onChanged(ChangedEvent event) {
				CustomTextItem.super.setValue(event.getValue().toString());
				
			}
			
		});
		
		form.setFields(textItem);
		
		this.setCanvas(form);
	
		
	}
	
	@Override
	public void setValue(String value)
	{
		GWT.log("setValue called: " + value);
		
		textItem.setValue(value);
		
		super.setValue(value);
		
	}

}
Databound form:

Code:
public class BuiltInDS implements EntryPoint {
	private Layout layout;
	private DynamicForm form;


    public  void onModuleLoad () {
	DataSource dataSource= ClientOnlyDataSource.getInstance();
		
		layout= new HLayout();
		layout.setWidth(800);
		
		form = new DynamicForm();
TextItem textField = new TextItem("userId");
		CustomTextItem customTextField =new CustomTextItem("userId");
		
		form.setDataSource(dataSource);
		form.setFields(textField, customTextField);
		
		ListGrid listGrid = new ListGrid();
		listGrid.addSelectionChangedHandler(new SelectionChangedHandler(){

			@Override
			public void onSelectionChanged(SelectionEvent event) {
				form.editRecord(event.getRecord());
				
			}
			
			
		});
		
		listGrid.setDataSource(dataSource);
		listGrid.fetchData();
		
		layout.addMember(listGrid);
		layout.addMember(form);
		
		
		layout.draw();
		
		
		
    }
}

When you now select a record in the listGrid, the textField has data, but the customTextField does not - again, because apparently, DynamicForm.editRecord(Record record) never actually calls setValue() on any of the FormItems that are part of the form, but instead injects values under the hood through some proprietary javascript solution. Correct?

So what you're telling me is that inside the SelectionChangeHandler of the ListGrid; I now need to put this, right?

Code:
@Override
			public void onSelectionChanged(SelectionEvent event) {
				form.editRecord(event.getRecord());

                          customTextField.setValue(event.getRecord().getAttribute("userId"));


				
			}
Or, alternatively:

Code:
form = new DynamicForm(){
			
			@Override
			public void editRecord(Record record)
			{
				customTextField.setValue(record.getAttribute("userId");
				super.editRecord(record);

			}
			
		};
Both are terrible solutions; they go against every principle of black-boxing and reusable design. In the first solution, the ListGrid needs to know about the specific structure of the Form and what fields it has. In the second solution, the form has custom logic that deals with a specific FormItem. Basically, you now need to implement all this extra logic to make the CanvasItem work OUTSIDE the Canvasitem implementation itself.

Last edited by SiccoNaets; 7th Jul 2010 at 13:42..
Reply With Quote
  #9  
Old 7th Jul 2010, 17:50
Isomorphic Isomorphic is offline
Administrator
 
Join Date: May 2006
Posts: 38,662
Default

Central change handling (dynamicForm.addItemChangedHandler()) is among many reasons why items must signal the form of value changes rather than have the form call getValue().

FormItem.setValue() is called when the form is populated with values and an override here is one way for a CanvasItem to be notified of calls like dynamicForm.editRecord(). However, it's not set up as a SmartGWT override point yet. The reason it hasn't been prioritized is that the most common reason to use a CanvasItem is for a complex external editor dealing with singular or multiple subobjects, like this use case, where there is already a need for some special case code.

It's not clear why you've headed down this path. If you want a custom TextItem, you can subclass TextItem - is there a reason you want to use CanvasItem here?

As an aside, it would be nice if you could be a little less caustic with your remarks. You're throwing around phrases like "terrible design", but mostly, you have a few misconceptions about how the design actually works.
Reply With Quote
  #10  
Old 8th Jul 2010, 07:22
SiccoNaets SiccoNaets is offline
Registered Developer
 
Join Date: May 2010
Posts: 107
Default

I apologize if my comments gave offense; it's nothing personal, that's just the frustration talking.

The CustomTextItem was a hugely simplified example to try to figure out what's going on. Of course, this isn't something I would actually use.

I think perhaps I should show you the actual screens and widgets I'm working on, and I can try and show you why I think the current relationship between DynamicForm and FormItems isn't a good design - it's certainly caused me no end of headaches.

Here is a record detail screen from my application:

http://yfrog.com/05didorderdetailp

It contains several custom implementations of CanvasItem; I've numbered them in the graphic. All of them are fully functional at this point; but I'm not entirely happy with how I had to implement the underlying logic

1: NumberIncrementItem: this is a pretty simple CanvasItem implementation based on a textfield that ensures that the data entered is numeric. A plus and minus button allow you to increment or decrease the number that was entered.

2: EmailSelector: Basically a ListGrid that takes an array of Strings, where each String is supposed to be an email address. Adding a non-email address generates a validation error

3: DIDSelector: This is, by far, the most complex of the CanvasItems I've built. In fact, the actual implementation you're seeing here is what I call a ComplexDIDSelector. It allows users to add DIDs (a DID is basically a phone extension) to an order.

However, DIDSelectors come in different flavors. Here is an other example:

http://yfrog.com/jvdidtransferp

These are two implementations of the basic DIDSelector. It allows users to make a subselection of DIDs from the master DIDOrder and add them to the DIDs for a specific child-carrier order. The buttons in the middle are a "TransferStack", a configurable set of buttons that moves records back and forth between two listgrids.

Now, the ComplexDIDSelector I showed you in the first screen is actually an extension of the basic DIDSelector. It maintains the basic functionality; but adds a toolbar with two buttons (again, configurable) to the two. Click those buttons opens two sliding windows (with a nice little animation effect) which allow users two different ways of adding DIDs to the list. Either by searching from the pool of existing DIDs, like so:

http://yfrog.com/6bdidsearchp

Or by adding new DIDs to the pool and then automatically adding those DIDs to the order you were working on, like so:

http://yfrog.com/j7didwizardp

Now, why am I having problems with the CanvasItem value management for this widget?

First of all, DIDs can actually be added to a DIDSelector in a number of different ways, as you can see from the graphic:

1) They can be added through a wizard like interface which allows you create whole new ranges of DIDs and add them to the listgrid all at once.
2) They can be dragged and dropped, either from the did pool in a different window or from another did selector on the same screen.
3) They can be transfered by clicking a button on a two different types of TransferStacks (OneWayTransferStack and TwoWayTransferStack)
4) They can be added by double-clicking a record; either in the DID pool or in a different DID selector on the same screen.

They can also be removed through a number of different ways:
1) They can be deleted by clicking the delete graphic in the ListGrid
2) They can be dragged out again
3) They can be transferred out by clicking a button on one of the transferstacks


Now, if I really wanted to make the DIDSelector work the way the SmartGWT design demands it; I would have to add changeHandlers for all those different data-entry methods and each time ensure that CanvasItem.super.setValue() gets called, or, alternatively, DynamicForm.steValue(). Also, because neither FormItem nor DynamicForm right now does not take a int or String arrays as an argument; I'd have to construct and deconstruct JSArrays each time to wrap the ids of the DIDsin the ListGrid.

The problem with that approach is that there are a LOT of different components that can add data to the ListGrid. I've already determine that I can't just add handlers to the listgrid itself because not every form of data entry is detected by the listgrid. So I'd have to add it to the widgets that initiate the data-entry in the first place (the transferstack buttons, for instance). The problem THERE is that not all of them have a reference to the CanvasItem OR to the Form and it's not practical to throw those references around.

So instead, I did this:

Code:
public abstract class AbstractDIDOrderRecordView extends Tab
{
	protected Record record;
	
	protected DataSource dataSource;
	protected DataSource carrierOrderDS;
	protected DataSource accountDS;
	protected DataSource csaDS;
	protected DataSource userDS;
	
	protected final static int RELATED_CARRIER_ORDER_LIST_INDEX = 1;
	protected final static int CONTACT_EMAIL_SELECTOR_INDEX=2;
	protected final static int DID_SELECTOR_INDEX=1;
	
	protected HStack layout;
	protected final VStack leftColumn;
	protected final VStack rightColumn;
	protected final HStack carrierControls;
	protected final DynamicForm mainForm;
	protected final DynamicForm buttonForm;
	protected final DynamicForm emailForm;
	protected final StaticTextItem id;
	protected final TextItem didOrderTitle;
	protected final TextAreaItem notes;
	protected final StaticTextItem type;
	
	protected final SelectItem status;
	protected final DateItem requestedDate;
	protected final DateItem closedDate;
	protected final ComboBoxItem account;
	protected final ComboBoxItem csa;
	protected final TextItem rejectedReason;
	protected final StaticTextItem createdBy;
	protected final SelectItem assignedTo;
	protected final SpacerItem spacer;
	protected final Label relatedLabel;
	protected final ShortCarrierOrderListGrid relatedCarrierOrders;
	protected final EmailSelector emailSelector;
	protected final ComplexDIDSelectorView didSelector;
	protected final ButtonItem saveButton;
	protected final ButtonItem cancelButton;
	protected final ImgButton createCarrierOrderButton;
	
	public AbstractDIDOrderRecordView(HandlerManager eventBus, TabSet tabSet, Record record)
	{
		this(eventBus, tabSet);
		this.record = record;
		
	}
	
	public AbstractDIDOrderRecordView(HandlerManager eventBus, TabSet tabSet)
	{
		super();
	
		dataSource = DataSource.get("didOrder");
		carrierOrderDS = DataSource.get("carrierOrder");
		accountDS = DataSource.get("account");
		csaDS = DataSource.get("csa");
		userDS = DataSource.get("user");
		
		
		this.setCanClose(true);
		
		layout= new HStack(10);
		leftColumn = new VStack();
		leftColumn.setAlign(Alignment.RIGHT);
		leftColumn.setAlign(VerticalAlignment.TOP);
		rightColumn = new VStack();
		carrierControls = new HStack();
		carrierControls.setHeight(25);
		carrierControls.setAlign(VerticalAlignment.BOTTOM);
		
		
		mainForm = new DynamicForm();
		mainForm.setDataSource(dataSource);
		mainForm.setWrapItemTitles(false);
		mainForm.setNumCols(4);
		buttonForm = new DynamicForm();
		buttonForm.setNumCols(4);
		
		emailForm = new DynamicForm();
		emailForm.setNumCols(1);
		
		id = new StaticTextItem(DIDOrderField.ID.getName());
		id.setTitle(DIDOrderField.ID.getDisplayTitle());
		
		id.setValueFormatter(new FormItemValueFormatter(){

			@Override
			public String formatValue(Object value, Record record,
					DynamicForm form, FormItem item) {
				
				if(value==null)
				{
					return "Not saved.";
				}
				else
				{
					return value.toString();
				}
			}
			
			
		});
		
		didOrderTitle=new TextItem(DIDOrderField.TITLE.getName());
		didOrderTitle.setTitle(DIDOrderField.TITLE.getDisplayTitle());
		didOrderTitle.setWidth(420);
		didOrderTitle.setColSpan(4);
		didOrderTitle.setEndRow(true);
		
		
		type = new StaticTextItem(DIDOrderField.TYPE.getName());
		type.setTitle(DIDOrderField.TYPE.getDisplayTitle());
		type.setValueMap(DIDOrderType.getValueMap());
		
		
		status = new SelectItem(DIDOrderField.STATUS.getName());
		status.setTitle(DIDOrderField.STATUS.getDisplayTitle());
		status.setValueMap(DIDOrderStatus.getValueMap());
		
		requestedDate = new DateItem(DIDOrderField.REQ_DATE.getName());
		requestedDate.setTitle(DIDOrderField.REQ_DATE.getDisplayTitle());
		requestedDate.setUseTextField(true);
		
		closedDate = new DateItem(DIDOrderField.CLOSED_DATE.getName());
		closedDate.setTitle(DIDOrderField.CLOSED_DATE.getDisplayTitle());
		closedDate.setUseTextField(true);
		
		account = new ComboBoxItem(DIDOrderField.ACCOUNT.getName());
		account.setTitle(DIDOrderField.ACCOUNT.getDisplayTitle());
		account.setValueField(AccountField.ACCOUNT_ID.getName());
		account.setDisplayField(AccountField.ACCOUNT_NAME.getName());
		account.setOptionDataSource(accountDS);
		account.setWidth(175);
		
		
		csa = new ComboBoxItem(DIDOrderField.CSA.getName());
		csa.setTitle(DIDOrderField.CSA.getDisplayTitle());
		csa.setValueField(CSAField.CSA_ID.getName());
		csa.setDisplayField(CSAField.CSA.getName());
		csa.setOptionDataSource(csaDS);

		
		rejectedReason = new TextItem(DIDOrderField.REJECTED_REASON.getName());
		rejectedReason.setTitle(DIDOrderField.REJECTED_REASON.getDisplayTitle());
		rejectedReason.setStartRow(true);
		rejectedReason.setColSpan(4);
		rejectedReason.setEndRow(true);
		rejectedReason.setWidth(420);
		
		createdBy= new StaticTextItem(DIDOrderField.CREATED_BY.getName());
		createdBy.setTitle(DIDOrderField.CREATED_BY.getDisplayTitle());
		createdBy.setValueField(UserField.USER_ID.getName());
		createdBy.setDisplayField(UserField.FULL_NAME.getName());
		createdBy.setOptionDataSource(userDS);
		
		assignedTo = new SelectItem(DIDOrderField.ASSIGNED_TO.getName());
		assignedTo.setTitle(DIDOrderField.ASSIGNED_TO.getDisplayTitle());
		assignedTo.setValueField(UserField.USER_ID.getName());
		assignedTo.setDisplayField(UserField.FULL_NAME.getName());
		assignedTo.setOptionDataSource(userDS);
		
		notes = new TextAreaItem(DIDOrderField.NOTES.getName());
		notes.setTitle(DIDOrderField.NOTES.getDisplayTitle());
		notes.setTitleVAlign(VerticalAlignment.TOP);
		notes.setStartRow(true);
		notes.setColSpan(4);
		notes.setWidth(420);
		notes.setEndRow(true);
		
		didSelector = new ComplexDIDSelectorView(eventBus, layout, true);
		didSelector.init();
		didSelector.setTitle(DIDOrderField.DID_IDS.getDisplayTitle());
		didSelector.setTitleVAlign(VerticalAlignment.TOP);
		didSelector.setColSpan(4);
		
		saveButton = new ButtonItem("Save");
		saveButton.setWidth(100);
		saveButton.setStartRow(false);
		saveButton.setEndRow(false);
		cancelButton = new ButtonItem("Cancel");
		cancelButton.setStartRow(false);
		cancelButton.setWidth(100);
		spacer = new SpacerItem();
		spacer.setWidth(65);
		
		
		createCarrierOrderButton = new ImgButton();
		createCarrierOrderButton.setShowRollOver(true);
		createCarrierOrderButton.setShowDown(true);
		createCarrierOrderButton.setHeight(22);
		createCarrierOrderButton.setWidth(100);
		createCarrierOrderButton.setSrc("actions/newCarrierOrder.png");
		
		relatedLabel = new Label();
		relatedLabel.setContents("Related Carrier Orders");
		relatedLabel.setStyleName("sectionTitle");
		relatedLabel.setWidth(350);
		relatedLabel.setHeight(20);
		
		relatedCarrierOrders = new ShortCarrierOrderListGrid();
		relatedCarrierOrders.setDataSource(carrierOrderDS);
		relatedCarrierOrders.setHeight(225);
		relatedCarrierOrders.setWidth(450);
		
		emailSelector = new EmailSelector(DIDOrderField.CONTACT_EMAILS.getName(), eventBus, DIDOrderField.CONTACT_EMAILS.getName(), DIDOrderField.CONTACT_EMAILS.getDisplayTitle());
		emailSelector.setTitleOrientation(TitleOrientation.TOP);
		emailSelector.setTitle(DIDOrderField.CONTACT_EMAILS.getDisplayTitle());
		emailSelector.setTitleStyle("sectionTitle");
		emailSelector.init();
		emailForm.setFields(emailSelector);
		
		buttonForm.setFields(spacer, saveButton, spacer, cancelButton);
		buttonForm.setPadding(25);
		
		
		leftColumn.addMember(mainForm);
		leftColumn.addMember(buttonForm);
		
		carrierControls.addMember(relatedLabel);
		carrierControls.addMember(createCarrierOrderButton);
		
		rightColumn.addMember(carrierControls);
		rightColumn.addMember(relatedCarrierOrders);
		rightColumn.addMember(emailForm);

		
		layout.addMember(leftColumn);
		layout.addMember(rightColumn);
		
		this.setPane(layout);
		
		
	}
	
	public void setEmails(String[] emails)
	{
		this.emailSelector.setEmails(emails);
	}
	
	public String[] getEmails()
	{
		return this.emailSelector.getEmails();
	}
	
	public Canvas getTabBody()
	{
		return this.layout;
	}

	
	public String getPrincipal()
	{
		return "naetss";
		
	}
	
	public void clearFormField(String fieldName) {
		this.mainForm.clearValue(fieldName);
		
	}
	
	
	public void editRecord(Record record)
	{
		this.setTitle(this.getTabTitlePrefix() + record.getAttribute(DIDOrderField.TITLE.getName()));
		this.mainForm.editRecord(record);
	}
	
	public abstract String getTabTitlePrefix() ;

	public void editNewRecord()
	{
		this.mainForm.editNewRecord();
		this.setTitle(this.getTabTitlePrefix()+ "New");
	}

	
	
	
	public abstract class AbstractDIDOrderRecordController extends AbstractController implements CloseClickHandler
	{

		protected DIDOrder didOrder;
		protected TabSet tabSet;
	
		public AbstractDIDOrderRecordController(HandlerManager eventBus, TabSet tabSet)
		{
			super(eventBus);
	
			this.tabSet = tabSet;
		}
		
		@Override
		public void init()
		{
			
			super.init();
			
			if(record!= null)
			{
				editRecord(record);
				
				//get the csa value before filtering
				String csaValue = record.getAttribute(DIDOrderField.CSA.getName());
				
				//filter csa selector
				this.filterCSAPicker(account.getValue());
				
				//set csa value
				csa.setValue(csaValue);
				
				//populate DIDSelector
				int[] didIntArray = record.getAttributeAsIntArray(DIDOrderField.DID_IDS.getName());
				if(didIntArray != null)
				{
					didSelector.setDIDIds(didIntArray);
				}
				
				//populate EmailSelector 
				
				String[] emails = record.getAttributeAsStringArray(DIDOrderField.CONTACT_EMAILS.getName());
				if(emails != null)
				{
					setEmails(emails);
				}
				
				//populate relatedCarrierOrders
				this.filterRelatedCarrierOrderListGrid();
			}
			else
			{
				createdBy.setValue(getPrincipal());
			}
		}
		
		@Override
		public void bind()
		{
			super.bind();
			
			/* Binding for relatedCarrierOrder selection */
			
			HandlerRegistration carrierOrderSelectionReg= relatedCarrierOrders.addRecordDoubleClickHandler(new RecordDoubleClickHandler(){

				@Override
				public void onRecordDoubleClick(RecordDoubleClickEvent event) {
					Record record = event.getRecord();
					
					CarrierOrderType type = CarrierOrderType.valueOf(record.getAttribute(CarrierOrderField.TYPE.getName()));
					
					if(type != null)
					{
						eventBus.fireEvent(new OpenCarrierOrderEditorEvent(record, type));
					}
					else
					{
						Log.error("Selected record has unidentified CarrierOrderType.");
					}
					
				}
				
				
			});
			
			/* Binding for accountPicker */
			HandlerRegistration accountPickerReg = account.addChangedHandler(new ChangedHandler(){

				@Override
				public void onChanged(ChangedEvent event) {
					filterCSAPicker(event.getValue());	
				}
				
			});
		
			
			/* Binding to for closeClickHandler */
			HandlerRegistration closeClickReg = tabSet.addCloseClickHandler(this);
			
			/* Binding for changes to values in the title field */
			HandlerRegistration titleChangedReg = didOrderTitle.addChangedHandler(new ChangedHandler(){

				@Override
				public void onChanged(ChangedEvent event) 
				{
					
					String newValue;
					if(event.getValue()!= null && ((String)event.getValue()).length() > 0)
					{
						newValue = getTabTitlePrefix() + ((String)event.getValue());
					}
					else
					{
						newValue= getTabTitlePrefix() + " New";
					}
					
					
					tabSet.setTabTitle(AbstractDIDOrderRecordView.this, newValue);
				}
			});
			
			/* Binding for the save button */
			HandlerRegistration saveButtonReg = saveButton.addClickHandler(new com.smartgwt.client.widgets.form.fields.events.ClickHandler(){

				@Override
				public void onClick(com.smartgwt.client.widgets.form.fields.events.ClickEvent event) {
					doSave();
					
				}
				
			});
			
			
			/* Add HandlerRegistrations for unbinding */
			this.addHandlerRegistration(carrierOrderSelectionReg,accountPickerReg, titleChangedReg, closeClickReg, saveButtonReg);
		}
		
		@Override
		public void onCloseClick(TabCloseClickEvent event) {
			if(event.getTab() == AbstractDIDOrderRecordView.this)
			{
				this.destroy();
			}
		}
		
		public Record getFormRecord()
		{
			/* Clear non-canonical values */
			
			if(account.getValue() instanceof String)
			{
				try
				{
					new Integer((String)account.getValue());
				}
				catch(NumberFormatException exc)
				{
					Log.debug("Clearing account value.");
					account.clearValue();
				}
			}
			
			if(csa.getValue()instanceof String)
			{
				try
				{
					new Integer((String)csa.getValue());
				}
				catch(NumberFormatException exc)
				{
					Log.debug("Clearing csa value.");
					csa.clearValue();
				}
			}
			
			return mainForm.getValuesAsRecord();
		}
		
		private void filterRelatedCarrierOrderListGrid()
		{
			if(id.getValue() != null)
			{
				Criteria criteria = new Criteria();
				criteria.addCriteria(CarrierOrderField.DID_ORDER.getName(), ((Integer)id.getValue()).intValue());
				relatedCarrierOrders.fetchData(criteria);
			}
		}
		
		private void filterCSAPicker(Object accountIdValue)
		{
			//Clear any previous selections the user may have made in the picker.
			clearFormField("csa");
			
			if(accountIdValue == null)
			{
				filterCSAPicker(new Criteria());
			}
			else
			{
				if(accountIdValue instanceof Integer)
				{
					Long accountId = new Long((Integer)accountIdValue);
					Criteria criteria = new Criteria();
					criteria.addCriteria(AccountField.ACCOUNT_ID.getName(), accountId.intValue());
					csa.setOptionCriteria(criteria);
					csa.fetchData();
				}
				else if(accountIdValue instanceof String)
				{
					Log.debug("User was typing, do nothing");
				}
				else
				{
					Log.error("Unknown data type being entered into account picker.");
				}
				
			}
		}
		
		
		public void filterCSAPicker(Criteria criteria)
		{
			csa.setOptionCriteria(criteria);
			csa.fetchData();
		}

		protected void doSave() 
		{	
			
			/* Do value overrides before committing */
			Record recordCopy = getFormRecord();
			
			/* Get the list of email addresses */
			String[] emails = getEmails();
			
			recordCopy.setAttribute(DIDOrderField.CONTACT_EMAILS.getName(), emails);
			
			/* Get the list of DID Ids */
			int[] didIds = didSelector.getDIDIds();
			
			recordCopy.setAttribute(DIDOrderField.DID_IDS.getName(), didIds);
				
			editRecord(recordCopy);
			
			/* Set a default value on the didSelector to indicate it has contents */
			if(didIds.length!= 0)
			{
				didSelector.setValue(new Integer(1));
			}
			else
			{
				didSelector.clearValue();
			}
			
			//Perform validation
			if(mainForm.validate() && !emailSelector.getEmailListGrid().hasErrors())
			{
			
				//check whether to call update or add prior to saving
				Record record = this.getFormRecord();
				
				if(record.getAttributeAsInt(DIDOrderField.ID.getName())!= null)
				{
					mainForm.setSaveOperationType(DSOperationType.UPDATE);
				}
				else
				{
					mainForm.setSaveOperationType(DSOperationType.ADD);
				}
				
				mainForm.saveData();
			}
			else
			{
				SC.warn("Errors in the DID Order form need to be fixed before it can be saved.");
			}
			
		}
		

	}
	


}
Let me point out the salient parts:

When the form loads, it's controller's init() method gets called:

Code:
@Override
		public void init()
		{
			
			super.init();
			
			if(record!= null)
			{
				editRecord(record);
				
				//get the csa value before filtering
				String csaValue = record.getAttribute(DIDOrderField.CSA.getName());
				
				//filter csa selector
				this.filterCSAPicker(account.getValue());
				
				//set csa value
				csa.setValue(csaValue);
				
				//populate DIDSelector
				int[] didIntArray = record.getAttributeAsIntArray(DIDOrderField.DID_IDS.getName());
				if(didIntArray != null)
				{
					didSelector.setDIDIds(didIntArray);
				}
				
				//populate EmailSelector 
				
				String[] emails = record.getAttributeAsStringArray(DIDOrderField.CONTACT_EMAILS.getName());
				if(emails != null)
				{
					setEmails(emails);
				}
				
				//populate relatedCarrierOrders
				this.filterRelatedCarrierOrderListGrid();
			}
			else
			{
				createdBy.setValue(getPrincipal());
			}
		}
I check whether a record was passed to the form (these typically come wrapped in an event from the eventBus, because the actiion to open a form can come from a totally different part of the application). If this is the case, I tell the form to edit the record, which populates all the standard formItem fields. I then MANUALLY get data out of the record and populate the custom CanvasItem implementations.

Not graceful, but it works. My beef here is that you can't just stick a custom canvasItem on a form, because of it's own accord, the CanvasItem does not work. Instead, the Form needs specialized logic to make it work; as opposed to the standard FormItems (TextItem, SelectItem, etc) which you just sick on the form and they work, period. That's what I mean with the design violating the principle of encapsulation.

For saving data, it gets messier:

Code:
protected void doSave() 
		{	
			
			/* Do value overrides before committing */
			Record recordCopy = getFormRecord();
			
			/* Get the list of email addresses */
			String[] emails = getEmails();
			
			recordCopy.setAttribute(DIDOrderField.CONTACT_EMAILS.getName(), emails);
			
			/* Get the list of DID Ids */
			int[] didIds = didSelector.getDIDIds();
			
			recordCopy.setAttribute(DIDOrderField.DID_IDS.getName(), didIds);
				
			editRecord(recordCopy);
			
			/* Set a default value on the didSelector to indicate it has contents */
			if(didIds.length!= 0)
			{
				didSelector.setValue(new Integer(1));
			}
			else
			{
				didSelector.clearValue();
			}
			
			//Perform validation
			if(mainForm.validate() && !emailSelector.getEmailListGrid().hasErrors())
			{
			
				//check whether to call update or add prior to saving
				Record record = this.getFormRecord();
				
				if(record.getAttributeAsInt(DIDOrderField.ID.getName())!= null)
				{
					mainForm.setSaveOperationType(DSOperationType.UPDATE);
				}
				else
				{
					mainForm.setSaveOperationType(DSOperationType.ADD);
				}
				
				mainForm.saveData();
			}
			else
			{
				SC.warn("Errors in the DID Order form need to be fixed before it can be saved.");
			}
			
		}

Quite frankly, I'm not happy with this code at all. It's ugly and it's messy. First of all, I get a copy of the record backing the form (through a call to DynamicForm.getValuesAsRecord()). I'd rather have worked with the record directly, but there's no method exposed to allow you to get that.

I then manually extract data from the custom CanvasItems and add them to the record.

Next, I need to set the DIDSelector to some bogus value because apparently, the validation logic of the DynamicForm checks whatever the backing value is behind CanvasItem.getValue() - BUT WITHOUT EVER CALLING DIDSelector.getValue() or DIDSelector.validate(). It doesn't look at the Record behind the form -which is what you'd expect - it looks somehow inside the super class of your custom CanvasItem at whatever hidden variable stores the data.

Next, I need to manually validate the form and the emailselector. Again, because I have no control whatso-ever over the validation logic that happens inside a DynamicForm; I have no way of telling the form that the EmailSelector is part of that it needs to check the ListGrid inside the EmailSelector for validation errors. So I have to do that manually.

Finally, I tell the form to re-edit the copy of the record I just obtained and modified. Unfortunately, calls to DynamicForm.editRecord() automatically switch the Form from DSOperationType.ADD to DSOperationType.UPDATE, so I have to undo that and switch it back (after checking whether the ID for the item has been set; this tells me whether or not I'm dealing with a new or an existing record.


Now, imagine how much easier this would be if the DynamicForm, on saveData() called FormItem.getValue() and FormItem.getValidate() on each of it's fields. All you would have to do would be override those two methods on your custom CanvasItem implemenation and you'd be good to go. No messing with Handlers and no hacking the validation and data-binding logic.

I understand the point you made about the centralized value management benefits DynamicForm provides - for instance, this way, you would know if a user modified the form at all and you get set a "not-saved indicator". But there's other ways of doing this, namely:

Code:
DynamicForm form = new DynamicForm();
TextItem text1 = new TextItem();
TextItem text2  = new TextItem();
text1.addChangeHandler(new NotifyFormHandler());
text2.addChangeHandler(new NotifyFormHandler());

private class NotifyFormHandler extends ChangeHandler(){

@Override
onChange(ChangeEvent event)
{
 form.setUpdated(true);
}

}
That's a LOT less code and lot more straighforward than the amount of code it takes to get a custom CanvasItem() to submit data. Not to mention that I may not always care about whether a form has been modified or not but I will certainly care about the values in the FormItems each and every time.

You know, you may be right - I probably had some misconceptions around how the design worked. There's certainly parts of the SmartGWT that I still don't get. But let's be fair - it's not like there's a lot of documentation and what does exist leaves a lot unsaid. I mean, here's the javadoc for CanvasItem():

http://www.smartclient.com/smartgwtee/javadoc/com/smartgwt/client/widgets/form/fields/CanvasItem.html

I doesn't mention anything about the need to manage the value of the super-class. Nor does the Javadoc on DynamicForm or FormItem mention that in fact, the value management of data by forms is done top-down by the form to its form-items, rather than bottom-up by the formItems to their form.

Any logical person is simply going to look at the API doc for FormItem and assume they have to override getValue() and validate() - which is precisely what a lot of the threads on CanvasItem on this forum show other users as doing; and then discovering that that doesn't actually work.

SmartGWT is no doubt very very powerful. Don't get me wrong, you guys have built some very powerful and attractive widgets. But the documentation leaves a lot to be desired and the development process is painstackingly slow with huge amounts of trial and error.

For instance, when I built the NumberIncrementItem, I - naively - assumed that a TextItem could have BOTH an EditorValueFormatter AND a ChangedHandler. But apparently, it can't, because once you add an EditorValueFormatter to a TextItem, none of it's ChangeHandlers or ChangedHandlers() ever get notified. That is again not mentioned in the documentation, you simply have to discover it the hard way. The end-result is that what should have been a really simple widget to built (I mean, a textfield with two buttons to increment/decrement a number by 1 is about as basic as it gets) but between that issue and the general "how-the-hell-do-I-get-my-custom-canvas-item-to-actually-show-data-from-the-datasource) it took a day and a half.

When I started this smartGWT project, our initial estimate was 8 weeks. It's now been 4.5 months and counting, most of which has been learning all the undocumented and undescribed in-and-outs of the inner workings of SmartGWT.

I'm not saying this to be negative or to attack SmartGWT. I'm trying to provide feedback as a paying customer that my experience with SmartGWT has been painful, but rewarding in the end - but most importantly - far, far too time-consuming; time that most organizations cannot afford. From my perspective, SmartGWTs API either needs to be a lot of self-evident than it is OR the documentation needs to be a lot more comprehensive and explain some of these inner complexities. And I'm pretty sure that I'm not the first person to point that out.
Reply With Quote
Reply


Thread Tools Search this Thread
Search this Thread:

Advanced Search


© 2010,2011 Isomorphic Software. All Rights Reserved