I'm new to Flex and am having trouble understanding Events. I think Events are what I want to use for my situation. I have 2 components, addUser.mxml and listUsers.mxml. I access these from a ViewStack in my main application. When I load listUsers.mxml it shows a list of current users in a datagrid via a HTTPService call. When I add a user using the form on addUser.mxml I would like the datagrid in listUsers.mxml to refresh when I return to that view to show the new user. I've tried several different things with addEventListener and dispatchEvent but can't seem to get it working. Can someone help me with this logic?
--
Sample code for comment, I've parsed out the non-relative stuff.
adduser look like this:
<mx:HTTPService id="httpService"
url="{'http://someurl.com'}"
useProxy="false"
method="POST"
fault="faultHandler()"
result="resultHandler(event)"
/>
public function addUser():void{
if(validateForm()){
params = {};
params['action'] = 'adduser';
params['firstName'] = firstName.text;
params['lastName'] = lastName.text;
params['email'] = email.text;
params['isActive'] = isActive.selected;
httpService.cancel();
httpService.addEventListener("result", addUserResult);
httpService.send(params);
}
}
public function addUserResult(event:ResultEvent):void{
var result:Object = event.result;
//reset fields if add user was successful
if(result.returnXML.action=='adduser'){
var m:String = result.returnXML.message.toString();
if(result.returnXML.status=='fail'){
Alert.show(m, null, Alert.OK, null, null, Application.application.IconError);
}
if(result.returnXML.status=='success'){
firstName.text = "";
lastName.text = "";
email.text = "";
isActive.selected = true;
Alert.show(m, null, Alert.OK, null, null, Application.application.IconSuccess);
}
}
}
<mx:Button id="addButton" label="Add" click="addUser();" />
listUsers looks like this:
<mx:HTTPService id="httpListService"
url="{'http://someurl.com'}"
useProxy="false"
method="POST"
fault="faultHandler()"
result="resultHandler(event)"
/>
<mx:DataGrid id="dgUsers"
itemClick="dgClickEvent(event);"
width="85%"
maxHeight="500"
>
<mx:columns>
<mx:DataGridColumn headerText="First Name" dataField="fn" />
<mx:DataGridColumn headerText="Last Name" dataField="ln" />
<mx:DataGridColumn headerText="Email" dataField="email" />
<mx:DataGridColumn headerText="Active" dataField="active" />
</mx:columns>
</mx:DataGrid>
I don't think event listeners are necessarily the way to go. You use an event listener to do something upon detection of something else. ie) listening for a mouse down on a ui component = detect mouse down, do drag operation.
Given your example I think you just need to chain your functions together. It looks like your addUser function saves the user to the same source as your list users gets data from, so in your position I would call the listUsers httpService at the end of the add user result to refresh your data populating the datagrid.
httpListService.send()
I don't see your result handler for httpListService but that's where you update the data in your dataGrid.
good luck, and please post back with any complications.
Got it working. Here's what i did - basically everytime the parent viewstack brings the listUsers view into focus, it sends the httpListService and refreshes the datagrid regardless of any events (or non events) in the addUser component or any other component. it adds to the network traffic but, for the scope of my project, that is acceptable.
in listUsers.mxml:
<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="init()">
...
public function init():void{
//vsUsers is my view stack on the main application component
Application.application.vsUsers.addEventListener(IndexChangedEvent.CHANGE, refreshUsersGrid);
}
...
public function refreshUsersGrid(e:IndexChangedEvent):void{
//if the new viewable child is the list users view, then refresh the datagrid
if(Application.application.vsUsers.getChildAt(e.newIndex).name=='viewListUsers'){
//the sendListUsersRequest method fires the HTTPService send method and binds the results to the datagrid
sendListUsersRequest();
}
}
Related
I have a combobox in a view that receives information about application state changes, and then is supposed to show or hide it's children based on the whole application state.
It receives state change messages, it traces the correct values, it does what it's supposed to do, however, it just doesn't seem to work. Essentially, all it needs to do is hide a combobox during one state, and show it again during another state.
Here is the code:
public function updateState(event:* = null):void {
trace("Project Panel Updating State");
switch(ApplicationData.getSelf().currentState) {
case 'login':
this.visible = false;
break;
case 'grid':
this.visible = true;
listProjects.includeInLayout = false;
listProjects.visible = false;
trace("ListProjects: " + listProjects.visible);
listLang.visible = true;
break;
default:
break;
}
}
Here is the MXML:
<mx:HBox>
<mx:Button id="btnLoad" x="422" y="84" label="Load" enabled="true" click="loadProject();"/>
<mx:ComboBox id="listProjects"
x="652"
y="85"
editable="true"
change="listChange()"
color="#050CA8"
fontFamily="Arial" />
<mx:Label x="480" y="86" text="Language:" id="label3" fontFamily="Arial" />
<mx:ComboBox id="listLang"
x="537"
y="84"
editable="true"
dataProvider="{langList}"
color="#050CA8"
fontFamily="Arial"
width="107"
change="listLangChange(event)"/>
<mx:CheckBox x="830" y="84" label="Languages in English" id="langCheckbox" click='toggleLang()'/>
</mx:HBox>
It's not that clear form your code where and how the updateState function gets called, and to get any further into a solution I think I would need to see that. However, I think you may like to consider a different approach.
Have you tried using views instead of manually showing and hiding things and setting properties? I think you would have simpler code if you had a different view state for each of the cases in your switch, e.g. 'login' etc. Then all the showing hiding stuff becomes a design-time activity rather than run-time and all you have to do is set the current state.
If you matched your state names with your ApplicationData currentState values you may even be able to do away with the updateState function completely.
Have you tried changing
updateState(event:* = null):void
to this
updateState(event:Event = null):void
Im still looking into the event:* and everything I have found so far has Event instead of *. Will repost still looking
I have an ArrayCollection bound to an editable DataGrid, another component needs to know when the ArrayCollection changes (as a result of changes in the DataGrid) so it can also update itself, so is listening to the COLLECTION_CHANGE event of the ArrayCollection.
The problem is that the DataGrid only updates the ArrayCollection when the row being edited losses focus. This is not good for my app as a user could edit a column on a row and not click elsewhere on the table for a long time (causing the row to lose fucus), therefore the changes won't have propagated to the other parts of the application.
How can I make the data grid inform the ArrayCollection of a change every time there is a keyup event on a text input instead of every time a row looses focus?
Cheers,
Chris
I would add the handler to the component used to edit the value instead of to the ArrayCollection. Example:
<mx:DataGridColumn dataField="name" headerText="Name" itemEditor="{nameEditor}" editorDataField="selectedItem" />
Then this is used to edit the value:
<mx:Component id="nameEditor">
<mx:ComboBox dataProvider="{outerDocument.names}" change="outerDocument.setNameField(event)" close="outerDocument.setNameField(event)" />
</mx:Component>
And this is the handler for the change (and close) event:
public function setDestinationField(event:*):void {
var destination:String = (event.target as ComboBox).selectedLabel;
if (destination === '') {
delete _gridData[_currentlyEditedRowIndex].destination;
} else {
_gridData[_currentlyEditedRowIndex].destination = destination;
}
}
_currentlyEditedRowIndex is set by adding this to the grid:
itemEditBegin="beginEdit(event);"
Thanks Gabriel you put me on the right path. I will post here my final solution in case anyone else needs to do the same thing in the future.
<mx:DataGridColumn headerText="Title" width="300" dataField="title">
<mx:itemEditor>
<mx:Component>
<mx:TextInput change="data.title = text" />
</mx:Component>
</mx:itemEditor>
</mx:DataGridColumn>
Now whenever the text is changed in the input the ArrayCollection being used as the data provider is also updated, so other components listen for COLLECTION_CHANGE events.
I have various (read lots of..) flex forms in my app, and I now want to create a system by which the user is notified that he hasn't saved if he modifies anything in the form...
now.. I don't want to rewrite every form I have.. is there some nice way to implement this?
Extending the TextInput (and others..) classes somehow?
thanks
This is not really thought through but should work.
You could create a custom component, let's call it FormWatcher which you would than put next to your Form. What the form watcher would do is wait for the CreationComplete event from the form.
So now, as we have the Form ready you can use the getChildren() method of the form to get all the FormItems in it. Than look inside each of them, and you will get TextInputs, Comboboxes, etc. to which you can add event listeners (as individual components), eg.
// THIS IS WHERE THE COMPONENT SHOULD START
protected function changeHandler(event:Event):void
{
trace ("something is dirty");
}
protected function startWatching(passTheFormHere:Form):void
{
for each (var O:Object in passTheFormHere.getChildren())
{
if (O is FormItem)
{
// Let's assume you only have a single child in one FormItem
// and always one child for simplicity
addChangeHandlerFor((O as FormItem).getChildAt(0));
}
}
}
protected function addChangeHandlerFor(someComponent:Object):void
{
// Most regular flex components send a Event.CHANGE event
// when their value is changing
// keep in mind you should check stuff, this is a simple example
(someComponent).addEventListener(Event.CHANGE,changeHandler);
}
// THIS IS WHERE THE COMPONENT SHOULD END
Just paste this code next to some form, and call the startWatching(nameOfYourForm), you should see the changeHandler is being called.
A few more notes:
1) You should clean up the event listeners once you're done.
2) I would create a component out of it so that you would use it like this:
<mx:Form id="form1" >
[...]
</mx:Form>
<FormWatcher form="{form1}" />
Where FormWatcher would have a Boolean var called "clean" or something.
3) The example is very simple, so it will only work for forms similiar to this one:
<mx:Form id="myForm" >
<mx:FormItem>
<mx:TextInput id="someComponent1" />
</mx:FormItem>
<mx:FormItem>
<mx:CheckBox id="someComponent2" />
</mx:FormItem>
<mx:FormItem>
<mx:TextArea id="someComponent3" />
</mx:FormItem>
</mx:Form>
You could go into the TextInput class (and others) and add that event listener and function, but then you would be changing the SDK itself, which is kind of a bad idea. I would create custom classes extending the ones your using and do a find/replace to make it faster.
To illustrate the Q. I'll really over-simplify the example (in reality the code is much more convoluted).
Say you have a flex control, which underneath contains a datagrid. Something like
<mx:DataGrid id="grid" dataProvider="{document.items}">
<mx:columns>
<mx:DataGridColumn headerText="Column 1" dataField="#name"/>
<mx:DataGridColumn headerText="Column 2" dataField="#value"/>
</mx:columns>
</mx:DataGrid>
Where document is a Model object, containing some data. You provide a selection setter on the control, as the clients don't want to know anything about the underlying datamodel :
public function set selectedItem(title:String):Allocation
{
grid.selectedItem = null;
for each(var o:Object in grid.dataProvider)
{
var t:String = o.#title;
if( t == title )
{
grid.selectedItem = o;
return;
}
}
}
So far, so good. Provided document.items is prepopulated, the selection will work correctly.
However. What to do if you already know, at the startup of the application, what the selection ought to be - it's been passed (for example) on the URL? So, in the flex you might have something like
// Initialising now...
mycontrol.document = this.document; // give the control the document
// Fetch titles
new FetchTitlesEvent().dispatch(); // cairngorm-style event
// Set the selection
mycontrol.selectedItem = Application.application.parameters.title;
OOps. Because FetchTitlesEvent operates asynchronously, at the time mycontrol.selectedItem is unable to work. Somehow we need to (re)trigger that code to set the selection on the control. Now, there's several ways I can think to do this, but all have code smell stenches:
1) Do it in the FetchTitlesCommand, after the fetch has been completed
- This pollutes the command with knowledge of the view (or views) that need to know this. Feels like a maintenance nightmare waiting to happen, and means the views are totally bound to commands, and those commands aren't re-usable. Blech.
2) Have a callback from the event when it's complete that does it (either make some composite command that starts in FetchTitlesEvent and ends in a new command to do the set). Seems fragile to me - how does the maintainer know which callbacks are neccesarily required? And it's still binding UI control knowledge into commands. Badness.
3) Have some kind of timer, waiting for the event queue to have been quiescent for a number of seconds. Hackity hackhack.
4) Do it in the control. Bind to the collectionevents in mycontrol on document.items, monitor for changes. Once the row arrives that matches the selection, select it, and stop monitoring for changes.
- Control feels the right place to do it
- collection events sometimes throw exciting CHANGE or REFRESH events though
- seems an expensive monitor to have lying around
I'm pretty much leaning to (4). Are there any other options that flex-ers have used before to trap this issue - particularly are there any codified into libraries that I might use, as it must be a fairly general-purpose problem?
You could set the selectedItem property of the control to a Bindable Object variable:
<mx:Script>
<![CDATA[
[Bindable]
private var mySelectedItem:Object;
]]>
</mx:Script>
<mx:DataGrid id="grid" dataProvider="{document.items}" selectedItem="{mySelectedItem}">
<mx:columns>
<mx:DataGridColumn headerText="Column 1" dataField="#name"/>
<mx:DataGridColumn headerText="Column 2" dataField="#value"/>
</mx:columns>
</mx:DataGrid>
This way can you set or reset the selectedItem at any time, and the control will update the selectedItem based on the Object. For instance, you could set in a Handler for the initialize Event, or creationComplete Event, or in any other function. You may need to call validateNow() on the control when you set the variable, or on creationComplete of the control if you set the variable during initialization.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" initialize="initializeHandler();">
<mx:Script>
<![CDATA[
[Bindable]
private var mySelectedItem:Object;
private function initializeHandler():void
{
mySelectedItem = getSelectedItem(); // your logic to determine the initial item to select
}
....
private function grid_creationCompleteHandler():void
{
grid.validateNow();
}
]]>
</mx:Script>
<mx:DataGrid id="grid" dataProvider="{document.items}" selectedItem="{mySelectedItem}" creationComplete="grid_creationCompleteHandler();">
<mx:columns>
<mx:DataGridColumn headerText="Column 1" dataField="#name"/>
<mx:DataGridColumn headerText="Column 2" dataField="#value"/>
</mx:columns>
</mx:DataGrid>
</mx:Application>
The correct way to do this is with the creationComplete event if I am understanding your question correctly. All UI objects in flex broadcast a creationComplete event that lets you know when everything is done and it is ready to go. You can either listen for the creationComplete event on the grid itself or you can listen for it on the application since it will not broadcast until all of it's children (which is everything) is created.
Unfortunately it's not a creationcomplete issue - it's not the initialization of the control that's the problem, it's the initialization of the data. Since it's asynchronous, you don't know when it will eventually arrive.
Try using a ChangeWatcher
var cw:ChangeWatcher = ChangeWather.watch(this, ["document","items"],
function(e:Event):void
{
//check to see if the data we want to select has arrived
//once we've selected the data, we don't need this handler anymore
cw.unwatch();
});
Pardon any syntactical bugs, I don't have FB in front of me.
I have a ToggleButtonBar with a DataProvider setup like this:
<mx:ToggleButtonBar itemClick="clickHandler(event);" selectedIndex="0">
<mx:dataProvider>
<mx:String>{resourceManager.getString('dashboard','daily')}</mx:String>
<mx:String>{resourceManager.getString('dashboard','monthly')}</mx:String>
<mx:String>{resourceManager.getString('dashboard','quarterly')}</mx:String>
<mx:String>{resourceManager.getString('dashboard','yearly')}</mx:String>
</mx:dataProvider>
</mx:ToggleButtonBar>
To switch locale to Chinese, I have a combobox with this handler:
resourceManager.localeChain = "zh_CN";
My problem is that on locale change, while the labels for all the other controls on the screen dynamically reload for the new locale, the dataProvider values don't refresh.
I can manually reset them in code, but is there a cleaner solution?
I would abstract out the data for your data provider into a bindable variable, then just reset the data provider when you change locals.
<mx:Script>
<![CDATA[
[Bindable]
myArray:Array = new Array(
[resourceManager.getString('dashboard','daily')]
, [resourceManager.getString('dashboard','monthly')]
, [{resourceManager.getString('dashboard','quarterly')]
, [resourceManager.getString('dashboard','yearly')]);
]]>
</mx:Script>
<mx:ToggleButtonBar itemClick="clickHandler(event);"
selectedIndex="0" id="myToggleButtonBar" dataprovider="{myArray}" />
Then you can just say
myToggleButtonBar.dataProvider = myArray;
after you swap the locals and it should work.
Disclaimer, there may be some minor errors in my code, I obviously am not able to test it and I don't have flex builder available right now to even check my syntax so I hope I didn't make any spelling mistakes. But this should get you in the ballpark.
Maybe if you make a getter bindable to a custom event for ex: "langChange"
[Bindable("langChange")]
public function get dataProviderToggleB():ArrayCollection
{
var arr :ArrayCollection = new ArrayCollection();
arr.addItem(resourceManager.getString('dashboard','daily'));
arr.addItem(resourceManager.getString('dashboard','monthly'));
return arr;
}
and in your "resourceManager.localeChain" setter you dispatch:
dispatchEvent(new Event("langChange"));
and you can used like this:
<mx:ToggleButtonBar dataProvider="{dataProviderToggleB} itemClick="clickHandler(event);" selectedIndex="0">
I hope this would help you.
You should keep 'daily', ... in your array and use a labelFunction to translate the label.
When the resourceManager sends a change event you should do a combo.labelFunction = labelFunction
The trick is to add brackets around each element in the dataProvider array, that way it gets parsed correctly. Note that this also binds correctly to locale changes in flex, no custom event dispatching is needed.
<mx:ToggleButtonBar itemClick="clickHandler(event);" selectedIndex="0"
dataProvider="{[ (resourceManager.getString('dashboard','daily')),
(resourceManager.getString('dashboard','monthly')),
(resourceManager.getString('dashboard','quarterly')),
(resourceManager.getString('dashboard','yearly')) ]}">
</mx:ToggleButtonBar>