I want to sort an Arraycollection by fieldName as ascending. Here's my code and I want to know whether it's right. Do you have any suggestions?
public static function arrayCollectionSort(ar:ArrayCollection, fieldName:String, isNumeric:Boolean):void
{var dataSortField:SortField = new SortField();
dataSortField.name = fieldName;
dataSortField.numeric = isNumeric;
var numericDataSort:Sort = new Sort();
numericDataSort.fields = [dataSortField];
arrCol.sort = numericDataSort;
arrCol.refresh();}
The code you have is correct, except for a type. arrCol should be ar. The code looks almost exactly like the code at the blog Flex Examples, which is also correct.
Just change is change arrCol to ar like below:
public static function arrayCollectionSort(ar:ArrayCollection, fieldName:String, isNumeric:Boolean):void
{
var dataSortField:SortField = new SortField();
dataSortField.name = fieldName;
dataSortField.numeric = isNumeric;
var numericDataSort:Sort = new Sort();
numericDataSort.fields = [dataSortField];
ar.sort = numericDataSort;
ar.refresh();
}
Not sure with numeric but otherwise everything else is correct.
Here is full example how to use sort in Array collection
http://blog.flexexamples.com/2007/08/05/sorting-an-arraycollection-using-the-sortfield-and-sort-classes/
Your code is fine, even so here are a couple of examples where a numeric and an alphabetical sort is applied on button clicks.
The alphabetical sort is a good example of sorting on 2 attributes. In this case, the primary sort is done on the 'firstname', the secondary sort is done on the 'lastname'.
The numerical sort is quite flexible, if you provide a boolean value of true for the numeric parameter of the sort field, the sort will cast the attribute to a number and sort by number. If you provide a boolean value of false, the built-in string compare function is used. Each of data items is cast to a String() function before the comparison. With the default value of null, the first data item is introspected to see if it is a number or string and the sort proceeds based on that introspection.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" minWidth="955" minHeight="600">
<mx:Button label="Sort by first then last name" click="sortItemsByName()"/>
<mx:Button label="Sort by number" click="sortItemsByNumber()"/>
<mx:DataGrid dataProvider="{items}"
width="300"
height="300">
<mx:columns>
<mx:DataGridColumn dataField="number"/>
<mx:DataGridColumn dataField="firstname"/>
<mx:DataGridColumn dataField="lastname"/>
</mx:columns>
</mx:DataGrid>
<mx:ArrayCollection id="items">
<mx:Object number="3" firstname="John" lastname="Brown"/>
<mx:Object number="1" firstname="Kate" lastname="Brown"/>
<mx:Object number="4" firstname="Jeremy" lastname="Ryan"/>
<mx:Object number="5" firstname="Joe" lastname="Wilson"/>
<mx:Object number="2" firstname="Greg" lastname="Walling"/>
</mx:ArrayCollection>
<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.collections.Sort;
import mx.collections.SortField;
/**
* Sort the arraycollection by the firstname and then the last name
* */
private function sortItemsByName():void{
var srt:Sort = new Sort();
srt.fields = [new SortField("firstname"), new SortField("lastname")];
items.sort = srt;
items.refresh();
}
/**
* Sort the arraycollection numerically
* */
private function sortItemsByNumber():void{
var srt:Sort = new Sort();
srt.fields = [new SortField("number", true, false, true)];
items.sort = srt;
items.refresh();
}
]]>
</mx:Script>
</mx:Application>
Also here is the language reference for the sortField...
http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/collections/SortField.html
...and the Adobe livedocs reference for data providers and collections...
http://livedocs.adobe.com/flex/3/html/help.html?content=about_dataproviders_2.html
...and here is a good livedocs reference for sorting and filtering...
http://livedocs.adobe.com/flex/3/html/help.html?content=about_dataproviders_4.html
Related
Is there a priority in the sorting algorithm when you provide two fields for a sort (ISort)?i.e. if you are sorting on date and time, will the sort only sort time if the dates are equal?
Sample Code Below:
private function sortXMLListCollection(listCollection:XMLListCollection, fields:Array):XMLListCollection
{
var descendingSort:Sort = new Sort();
descendingSort.fields = new Array();
for each( var field:String in fields)
{
descendingSort.fields.push(new SortField(field, true));
}
listCollection.sort = descendingSort;
listCollection.refresh();
return listCollection;
}
Function Call: sortXMLListCollection(patchCollection, ["date", "time"]);
Sample XML:
<patch>
<time>08:44:46</time>
<date>10/10/12</date>
</patch>
<patch>
<time>08:51:09</time>
<date>10/10/12</date>
</patch>
<patch>
<time>08:46:04</time>
<date>10/11/12</date>
</patch>
Somehow the above function does not work as I expected. I want it to compare the dates first and only sort time when dates are equal.
Thank you for your help
your problem isn't your code... it's your xml.
if you add a root node, your code will work as expected:
var xml:XML =
<root>
<patch>
<time>08:44:46</time>
<date>10/10/12</date>
</patch>
<patch>
<time>08:51:09</time>
<date>10/10/12</date>
</patch>
<patch>
<time>08:46:04</time>
<date>10/11/12</date>
</patch>
</root>
var patchCollection:XMLListCollection = new XMLListCollection(xml.patch);
sortXMLListCollection(patchCollection, ["date", "time"]);
As the pic show(This is drawn by PhotoShop, not implemented yet), I want to implemnt a List like this one. It has different column count, say the first row has only one item, and the others have two items . I tried to use itemRendererFunction to detect the different item(the first row treat as a rendererA, the others treat as another rendererB),but it didn't work.
The cleanest solution to this problem, is to create a custom layout (we've discussed in the comments how Romi's solution will eventually cause too many problems). However, this is usually not an easy thing to do.
I will give you a rough sketch of what this custom layout might look like, so you can use it as a starting point to create one that does exactly what you need.
To create a custom layout, you must subclass BaseLayout and override and implement its updateDisplayList and measure methods.
To make things easier (and in order not to dump 500 lines of code in here), I used some hardcoded variables for this example. It assumes there will always be two columns, the first item will always be 200x200 px, and the other items will always be 100x100 px. There is no horizontalGap or verticalGap.
The consequence is of course that you can use this custom layout (as it is now) only for this specific List and these specific ItemRenderers. If you want it to be more generic, you'll have to do a lot more calculations.
But now for the code:
public class MyCustomLayout extends LayoutBase {
//hardcoded variables
private var columnCount:int = 2;
private var bigTileWidth:Number = 200;
private var bigTileHeight:Number = 200;
private var smallTileWidth:Number = 100;
private var smallTileHeight:Number = 100;
override public function updateDisplayList(width:Number, height:Number):void {
var layoutTarget:GroupBase = target;
if (!layoutTarget) return;
var numElements:int = layoutTarget.numElements;
if (!numElements) return;
//position and size the first element
var el:ILayoutElement = useVirtualLayout ?
layoutTarget.getVirtualElementAt(0) : layoutTarget.getElementAt(0);
el.setLayoutBoundsSize(bigTileWidth, bigTileHeight);
el.setLayoutBoundsPosition(0, 0);
//position & size the other elements in 2 columns below the 1st element
for (var i:int=1; i<numElements; i++) {
var x:Number = smallTileWidth * ((i-1) % 2);
var y:Number = smallTileHeight * Math.floor((i-1) / 2) + bigTileHeight;
el = useVirtualLayout ?
layoutTarget.getVirtualElementAt(i) :
layoutTarget.getElementAt(i);
el.setLayoutBoundsSize(smallTileWidth, smallTileHeight);
el.setLayoutBoundsPosition(x, y);
}
//set the content size (necessary for scrolling)
layoutTarget.setContentSize(
layoutTarget.measuredWidth, layoutTarget.measuredHeight
);
}
override public function measure():void {
var layoutTarget:GroupBase = target;
if (!layoutTarget) return;
var rowCount:int = Math.ceil((layoutTarget.numElements - 1) / 2);
//measure the total width and height
layoutTarget.measuredWidth = layoutTarget.measuredMinWidth =
Math.max(smallTileWidth * columnCount, bigTileWidth);
layoutTarget.measuredHeight = layoutTarget.measuredMinHeight =
bigTileHeight + smallTileHeight * rowCount;
}
}
And you can use it like this:
<s:List dataProvider="{dp}" height="300">
<s:layout>
<l:MyCustomLayout />
</s:layout>
</s:List>
Whenever you want to change the defined behavior of an existing component, always check first if you can solve the problem with skinning. It is a really powerful feature i Flex, and can also provide a solution in this case.
So, let's begin, assuming you already have your List, you only need to create a custom skin which "splits" the data provider in two parts, the first item, and all the others. So, let's assume we have this initial setup:
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
[Bindable]
private var c:ArrayCollection = new ArrayCollection([
"String 1",
"String 2",
"String 3",
"String 4",
"String 5",
"String 6",
"String 7",
"String 8",
"String 9",
"String 10",
"String 11",
"String 12",
"String 13",
"String 14",
"String 15"]);
]]>
</fx:Script>
<s:List skinClass="CustomSkinList" dataProvider="{c}" />
As you can see, we define a custom list skin, which is just a copy of spark.skins.spark.ListSkin, the default skin for spark.components.List element.
Before we handle the data provider logic, we need to take a look at how the list items are rendered. This is done by using a DataGroup element, added to the skin, like so:
<s:Scroller left="0" top="0" right="0" bottom="0" id="scroller" minViewportInset="1" hasFocusableChildren="false">
<!--- #copy spark.components.SkinnableDataContainer#dataGroup -->
<s:DataGroup id="dataGroup" itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<!--- The default layout is vertical and measures at least for 5 rows.
When switching to a different layout, HorizontalLayout for example,
make sure to adjust the minWidth, minHeight sizes of the skin -->
<s:VerticalLayout gap="0" horizontalAlign="contentJustify" requestedMinRowCount="5" />
</s:layout>
</s:DataGroup>
</s:Scroller>
Here is the place where we will have to make the changes, in order to get the first element to render differently. What we need to do, is just add another DataGroup, for rendering the first element in a custom way (this of course means using a custom item renderer). Now, our scroller looks like this:
<s:Scroller left="0"
top="0"
right="0"
bottom="0"
id="scroller"
minViewportInset="1"
hasFocusableChildren="false">
<!--- #copy spark.components.SkinnableDataContainer#dataGroup -->
<s:VGroup width="100%" height="100%">
<s:DataGroup id="firstItemDataGroup"
width="100%"
itemRenderer="CustomItemRenderer"
height="20">
<s:layout>
<s:VerticalLayout />
</s:layout>
</s:DataGroup>
<s:DataGroup id="dataGroup" itemRenderer="spark.skins.spark.DefaultItemRenderer">
<s:layout>
<!--- The default layout is vertical and measures at least for 5 rows.
When switching to a different layout, HorizontalLayout for example,
make sure to adjust the minWidth, minHeight sizes of the skin -->
<s:TileLayout horizontalAlign="center" requestedColumnCount="2" />
</s:layout>
</s:DataGroup>
</s:VGroup>
</s:Scroller>
Notice the 'firstItemDataGroup' addition, also the fact that it uses a different item renderer, than the default dataGroup element. With this new container in place we can proceed to render the elements. The custom skin need to override the parent initializationComplete() method, like so:
override protected function initializationComplete():void
{
useChromeColor = true;
if (hostComponent.dataProvider && hostComponent.dataProvider.length > 0)
{
var allItems:Array = hostComponent.dataProvider.toArray().concat();
firstItemDataGroup.dataProvider = new ArrayCollection([hostComponent.dataProvider.getItemAt(0)]);
var remainingItems:Array = allItems.concat().reverse();
remainingItems.pop();
var reversed:Array = remainingItems.reverse();
dataGroupProvider = new ArrayCollection(reversed);
}
super.initializationComplete();
}
What was added was just the 'if' block, and a private variable, named dataGroupProvider. This is because we will set the new dataProvider, the one starting from the second element, to the dataGroup element, in the updateDisplayList() method. Here is what it looks like:
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
if (getStyle("borderVisible") == true)
{
border.visible = true;
background.left = background.top = background.right = background.bottom = 1;
scroller.minViewportInset = 1;
}
else
{
border.visible = false;
background.left = background.top = background.right = background.bottom = 0;
scroller.minViewportInset = 0;
}
// Here we assign the new data provider to the dataGroup element
if (dataGroupProvider)
dataGroup.dataProvider = dataGroupProvider;
borderStroke.color = getStyle("borderColor");
borderStroke.alpha = getStyle("borderAlpha");
super.updateDisplayList(unscaledWidth, unscaledHeight);
}
In conclusion, just by creating a custom skin for our List element, we can use two containers for rendering the first item in a different way, from the rest of the elements. You shouldn't underestimate the power of Flex Skinning :)
Hope this helps. Have a great day!
I'm stuck on following matter:
I have a datagrid with 10 items... I also added a extra row (row number 11) where I show a total of prevouis fields... But I always want to keep this last row (the totals) on the last line of the Datagrid (so always on position 11).
This means that when the datagrid is sorted on a column, the last row also changes in position according to the value in the datafield. Is there any easy and straightforward way of preventing it to be sorted with the other columns so that the column with the totals is always placed last? Or what approach would I use best?
Thanks for any help!
Please find below code Hope this may help you, i tried some workaround to achieve what you are looking for i am not sure how feasible is this code:-
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600"
>
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.events.FlexEvent;
[Bindable]
private var dpHierarchy:ArrayCollection= new ArrayCollection([
{name:"A", region: "Arizona", number:1},
{name:"B", region: "Arizona", number:2},
{name:"C", region: "California", number:3},
{name:"D", region: "California", number:4}
]);
private function creationComp():void
{
var totalValue:int = 0;
for(var i:int=0; i<dpHierarchy.length; i++)
{
totalValue = totalValue + dpHierarchy[i].number;
}
var obj:Object = new Object();
obj.name = "Total";
obj.region = "==";
obj.number = totalValue;
dpHierarchy.addItem(obj)
myADG.dataProvider = dpHierarchy;
}
private function sortHandler(obj1:Object, obj2:Object):int
{
var lastObj:Object = dpHierarchy.getItemAt(dpHierarchy.length-1);
if(lastObj.number == obj1.number || lastObj.number == obj2.number)
return 0;
if(obj1.number < obj2.number) {
return -1;
} else if(obj1 == obj2) {
return 0;
}
return 1;
}
]]>
</fx:Script>
<mx:AdvancedDataGrid id="myADG" x="50" y="50"
width="400" height="300"
variableRowHeight="true" creationComplete="creationComp()">
<mx:columns>
<mx:AdvancedDataGridColumn dataField="name" headerText="Name" sortCompareFunction="sortHandler"/>
<mx:AdvancedDataGridColumn dataField="region" headerText="Region" sortCompareFunction="sortHandler"/>
<mx:AdvancedDataGridColumn dataField="number" headerText="Number" sortCompareFunction="sortHandler"/>
</mx:columns>
</mx:AdvancedDataGrid>
</s:Application>
I'd consider a few options in this order.
Put your summary data somewhere else. That's really not what a row in a datagrid was intended for.
Not let your users sort by clicking on the column name (sortableColumns='false') if you really don't want them to sort the data.
Create a custom sort for each column that ensures you always put your summary data last. Here's a decent tutorial: http://www.switchonthecode.com/tutorials/flex-datagrid-custom-sorting. I'd like to stress that this would be a high maintenance idea because you'd have to build out a custom sort for every column.
I'm using the MVC model in my flex project.
What I'd like is to bind a value object's class properties to a view mxml, then change that view by altering the value object.
What happens:
Set the selected value to 'c' - index 2
Add 'x,y,z,' before 'c'
Hit enter -> now index 5
Hit enter -> now index is -1
See 4.
Why does only the first update work ? I know I'm probably missing something obvious...
Edit: Running Example
(P.S. first post and im not sure how to turn on MXML highlighting)
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
creationComplete="created(event)"
width="160" height="220">
<fx:Script>
<![CDATA[
import mx.collections.ArrayList;
import mx.events.FlexEvent;
import spark.events.IndexChangeEvent;
//===================================
// Pretend Value Object Class
[Bindable] private var list:ArrayList = null;
[Bindable] private var index:int = 0;
//===================================
protected function created(event:FlexEvent):void {
ddValues.addEventListener(FlexEvent.ENTER, update);
update();
}
private function update(... args):void {
//note selected item
trace("dropdown index: " + dd.selectedIndex);
var s:String = dd.selectedItem as String;
trace("selected item: " + s);
//build new list from csv
list = new ArrayList(ddValues.text.split(","));
trace("new list: " + ddValues.text);
trace("selected item: " + s);
//if exists in new list, set value object index
var newIndex:int = 0;
if(list)
list.toArray().forEach(function(ss:String, i:int, a:Array):void {
if(s == ss) newIndex = i;;
});
index = newIndex;
trace("new index: " + index + " (dropdown index: " + dd.selectedIndex + ")");
trace("===");
}
protected function ddChange(event:IndexChangeEvent):void
{
trace("selected item: " + (dd.selectedItem as String) + " (dropdown index: " + dd.selectedIndex + ")");
trace("===");
}
]]>
</fx:Script>
<s:Panel width="100%" height="100%" title="Drop Down Bug">
<s:layout>
<s:VerticalLayout gap="10" paddingLeft="10" paddingTop="10" paddingRight="10" paddingBottom="10"/>
</s:layout>
<s:DropDownList id="dd" dataProvider="{list}" selectedIndex="#{index}" change="ddChange(event)"></s:DropDownList>
<s:Label text="Label: {dd.selectedItem as String}" paddingTop="5" paddingBottom="5"/>
<s:Label text="Code Index: {index}" paddingTop="5" paddingBottom="5"/>
<s:Label text="DropDown Index: {dd.selectedIndex}" paddingTop="5" paddingBottom="5"/>
<s:TextInput id="ddValues" text="a,b,c,d,e"/>
</s:Panel>
</s:Application>
And heres the output
Edited code and added traces. Heres the output that shows my problem:
dropdown index: -1
selected item: null
new list: a,b,c,d,e
selected item: null
new index: 0 (dropdown index: 0)
===
selected item: c (dropdown index: 2)
===
dropdown index: 2
selected item: c
new list: a,b,x,y,z,c,d,e
selected item: c
new index: 5 (dropdown index: 5)
===
dropdown index: 5
selected item: c
new list: a,b,x,y,z,c,d,e
selected item: c
new index: 5 (dropdown index: 5)
===
dropdown index: -1
selected item: null
new list: a,b,x,y,z,c,d,e
selected item: null
new index: 0 (dropdown index: 0)
===
Oh, I see what's going on now. When you replace the whole list, the selected item will be null, because the item that was selected was in the old list. You'll need to store the selected item when it is selected, and then do the comparison against that stored item, not the current selected (null) item.
Note that what you're doing is in no way MVC, unless you've redefined MVC to mean "Model, View, and Controller are all the same thing." In MVC, the model has no idea of the view, and the view only has access to read as much of the model as it needs to to display the data. It does not have write access to the model. That is the function of the controller.
I'm setting selected element in s:List component with Actionscript, it works, but List doesn't scroll to selected item -- need to scroll with scrollbar or mouse. Is it possible to auto-scroll to selected item ? Thanks !
Try the s:List method ensureIndexIsVisible(index:int):void.
For Spark:
list.ensureIndexIsVisible(index);
This function will scroll to the top of the list in Flex 4+. It takes in account the height of the item, so it will work for lists with different items with different height.
private function scrollToIndex(list:List,index:int):void
{
if (!list.layout)
return;
var dataGroup:DataGroup = list.dataGroup;
var spDelta:Point = dataGroup.layout.getScrollPositionDeltaToElement(index);
if (spDelta)
{
dataGroup.horizontalScrollPosition += spDelta.x;
//move it to the top if the list has enough items
if(spDelta.y > 0)
{
var maxVSP:Number = dataGroup.contentHeight - dataGroup.height;
var itemBounds:Rectangle = list.layout.getElementBounds(index);
var newHeight:Number = dataGroup.verticalScrollPosition + spDelta.y
+ dataGroup.height - itemBounds.height;
dataGroup.verticalScrollPosition = Math.min(maxVSP, newHeight);
}
else
{
dataGroup.verticalScrollPosition += spDelta.y;
}
}
}
//try this
this.callLater(updateIndex);//where you want to set the selectedIndex
private function updateIndex():void
{
list.selectedIndex = newIndex;
list.ensureIndexIsVisible(newIndex);
}
In flex-3 there is a scrollToIndex method and hence you can call
list.scrollToIndex(list.selectedIndex);
I believe this should work in flex-4 too.
This worked for me. had to use the callLater.
list.selectedItem = "MyTestItem"; //or list.selectedIndex = 10;
this.callLater(updateIndex); //dispatch an update to list
private function updateIndex():void {
list.ensureIndexIsVisible(list.selectedIndex);
}
I saw this basic idea here...
http://arthurnn.com/blog/2011/01/12/coverflow-layout-for-flex-4/
public function scrollGroup( n : int ) : void
{
var scrollPoint : Point = theList.layout.getScrollPositionDeltaToElement( n );
var duration : Number = ( Math.max( scrollPoint.x, theList.layout.target.horizontalScrollPosition ) - Math.min( scrollPoint.x, theList.layout.target.horizontalScrollPosition )) * .01;
Tweener.addTween(theList.layout,{ horizontalScrollPosition: scrollPoint.x , time:duration});
}
protected function theList_caretChangeHandler(event:IndexChangeEvent):void
{
scrollGroup( event.newIndex );
event.target.invalidateDisplayList();
}
You'll probably want to access the List's scroller directly and do something like:
list.scroller.scrollRect.y = list.itemRenderer.height * index;
You can multiply the height of an element by its index and pass this value to:
yourListID.scroller.viewport.verticalScrollPosition
It is a bug - you can see the demonstration and a workaround at the https://issues.apache.org/jira/browse/FLEX-33660
This custom List component extension worked for me:
<s:List
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
valueCommit="callLater(ensureIndexIsVisible, [selectedIndex])">
</s:List>
I recently accomplished this in one of my projects by having a defined size for my items in the group..
<s:Scroller x="940" y="0" maxHeight="465" maxWidth="940" horizontalScrollPolicy="off" verticalScrollPolicy="off">
<s:HGroup id="tutPane" columnWidth="940" variableColumnWidth="false" gap="0" x="0" y="0">
</s:HGroup>
</s:Scroller>
Following this my button controls for manipulation worked by incrementing a private "targetindex" variable, then I called a checkAnimation function, which used the Animate class, in combo with a SimpleMotionPath and a comparison between tutpane.firstIndexInView and target index. This modified the "horizontalScrollPosition" of the group.
This allowed separate controls to essentially act as a scroll bar, but I had the requirement of sliding the control to view the selected item.. I believe this technique could work for automated selection of items as well