I'm implementing a custom Flex component that provides a scrollable viewpoint onto a (possibly very large) data grid. I'm using the ItemRenderer pattern, such that I only have UIComponents for the elements visible on the screen at a given time. In other words, something similar to the standard DataGrid control.
One requirement I have is to dynamically size the grid cells to fit the provided data, such that the column widths and row heights are known up front. (The column widths and row heights can't fluctuate as the user scrolls and new cells come into view.)
This requirement implies a 'pre-measure' phase of the entire grid to be performed when the component's dataSource or itemRenderer is changed. I'd like to use the standard Flex component measuring operations to perform this pre-measurement phase.
My current strategy for this pre-measure phase is to:
Obtain an itemRenderer instance
Initialize the itemRenderer
For each cell in the data source:
Set the itemRenderer's 'data' object to that cell's data
'commitProperties()' on the component
'measure()' the component
Update column width/row height appropriately based on measurement results
I'd rather not attach the itemRenderer to the application's display list, but that means it won't be initialized by the framework. Also, I need the renderer's initialization and commitProperties/measurement phases to occur synchronously. I'm scared of how much of the Flex component lifecycle management framework I'll have to replicate to accomplish this.
So I call on those more experienced than I for words of wisdom:
Any thoughts on the feasibility of this strategy?
Any suggestions on how I could elegantly make use of the framework to perform this measurement for me?
Any better strategies to determine cell size?
I studied the framework code a bit, and if initial results are an indication, this isn't as painful as I feared. The guts of it:
var renderer:IListItemRenderer = getRenderer();
renderer.initialize();
for each (var cell:Object in cells) {
renderer.data = cell;
renderer.validateProperties();
renderer.validateSize(true);
// Access renderer's size properties here
}
Passing the 'recursive = true' flag to validateSize is the key that I was missing previously. Unfortunately there's no equivalent flag for validateProperties, so I'll probably have to implement that myself to make it reliable for arbitrary ItemRenderers.
I've never applied itemRenderers this way, so, not sure how much this would come into play with you're approach, but are you setting
super.data = data
at the top of your renderer's data setter? i.e:
override public function set data(value:Object):void
{
super.data = value;
...
}
If not, you might try adding that and see if it removes the need for a recursive flag in validateProperties().
Related
I have a TableView for which I've defined my own itemDelegate. Now, from within this delegate I can access the value for the column using styleData.value, but I'd also need to access the other properties in this same item but I can't find how to.
I need this, because the text styling needs to change depending on some other property of the item model.
Any ideas? thanks!
There is some documentation missing. Within the item delegate you can access the following (taken from the source code of TreeView.qml):
styleData (see documentation)
model (currently not documented)
modelData (currently not documented, not sure about this but I guess it's similar to ListView)
(By the way, what's also missing in the documentation but which is useful is styleData.role. Also, the documentation of the other delegates lacks some available properties too; the best is to peek into the source code of the QML file and have a look for the Loader element which instantiates your delegate. As a plus you learn how that creepy stuff works. ;))
With model and the row/column information you can then navigate to the item data. This code depends on the type of model.
If you're using QML's ListModel, then you can use model.get: model.get(styleData.row)[styleData.role] should then work (untested since I use it rarely, please give feedback).
If you're using a C++ QAbstractItemModel or friends, the best is to add a slot to the model class which takes just the row and role name, since that's the information the TableView works with (nor with role numbers nor with columns...).
However in both cases you shouldn't use the expression in a property binding! The notification system will not work since you don't use the property system for accessing the data. According to your question, I guess you wanted to use it in a expression with binding. I don't know how to properly listen to changes in the model manually.
An alternative approach is to access the other items of the row and provide a property there. Some hints:
From within one item, you can access other items of the same row by walking the object tree up twice (first to the Loader which instantiates your component, then to the actual row) and then down twice (first to the particular child object which is a Loader, then its instantiated item). You need to know the column number you want to access (not the role name), I assume you want to access the first column (index 0):
parent.parent.children[0].item
You can provide the model data using a property in each item. Assuming a simple Text element this might be:
Text {
property variant value: styleData.value // <-- Here you make it available
// your other stuff
}
Putting them together could look like the following. In this example I assume the first row contains an integer, and if it is zero, the second column should be red.
// (within TableView)
itemDelegate: Text {
property variant value: styleData.value
text: styleData.value
color: (styleData.column == 1 && parent.parent.children[0].item.value === 0)
"red" : "black"
}
I think it's pretty easy if you read the source code of TableViewItemDelegateLoader.qml (it is a private code in qtquickcontrol)
To access any role you use use : model[your_role_name] .
For exp: model["comment"]
Faced with same problem today, this is result of my investigations (Qt 5.2.x)
If you have hard limit to TableView, there is only one correct solution - use model.get(styleData.row)["roleForStyling"] as #leemes wrote. But it will very slow if you have big amount of data in model and using, for example, proxy model for sorting/filtering.
Direct solution from #leemes answer is great, but in general case not be working, because in TableView any Item wrapped in Loader and therefore independent from parent and other items:
When some item is created (where you want to change text style)
another element (from which to receive identity) cannot yet be
created
You may not have "parent" on item creation (i.e. binding will
be broken)
In my case, the best solution for deep customise was creation of the simple wrapper for ListView. In this case you have access for complete row data in delegate without the overhead. Highlights for making component ("My own ListView as table"):
Create standalone header (Rectangle or Item) - do not use header form ListView.This make it fixed for any amount of data.
Wrap ListView to ScrollView (if you need scrollbars)
Use Clip: true property in list for make correct
Set style for highlight and set highlightFollowsCurrentItem:true in ListView
As bonus in future this may be used for make "TreeTable" :)
How can I refresh view after a certain event?
I have a view which contains multiple groups. I want to show or hide some groups.
onCreationComplete() or initialize() method works only at the beginning of the view creation.
Try invalidateDisplayList() on the view
Let me know if that doesn't do the trick and we'll try some other tricks.
I personally don't like the answer that says to call invalidateDisplayList (sorry no offense Nate nothing personal). I feel it's too vague and doesn't explain what this does under the hood and furthermore you shouldn't have to call it directly in cases such as the one explained in the OPs question. You can simply create booleans that are bindable for each of the groups you'd like to show/hide then in the event handler set those booleans to the appropriate value and if they are bound to the visible and include in layout properties of the containers those containers will internally call invalidateDisplayList after calling set visible and consequently commitProperties.
This is basically what happens under the hood as I understand it: The way this works is values aren't committed or used to update the display until the next frame this way it doesn't get bogged down doing unnecessary layout calculations. So you update the bindable property which fires an event which triggers a notification in the listener (in this case a function that sets the property on your control), that in turn passes along the value to the control which sets an internal flag to update the property and calls invalidateProperties. When it hits the next frame redraw it sees that the properties flag is dirty (true) and then calls commitProperties, this computes/sets the appropriate values (possibly also invalidating then "fixing" the size using invalidateSize() and measure()) and calls invalidateDisplayList, then during the same frame it sees that the display list flag is dirty so it calls updateDisplayList, here it uses the values of the properties to draw appropriately.
You should also be able to achieve this using states, which add or remove children from the display list based on an array of "actions" for each state.
I'm trying to cycle through my list itemRenderers to change the data of a particular item. The code runs well until the for index becomes 7 (which is the maximum number of visible elements on my list). When the index is 7 or more, the getElementAt() function returns null. What could possibly be the problem?
var itemRenderer:ItemRenderer;
var numItems:int = list.dataGroup.numElements;
trace(numElements) // outputs 14
for(var i:int = 0; i < numItems; i++){
itemRenderer = list.dataGroup.getElementAt(i) as ItemRenderer;
if (itemRenderer.data.name == "bar") {
itemRenderer.data.option = "foo";
break;
}
}
If you want to change the data of a particular item, why not change it from the data you gave to the dataProvider? Changing it directly in the item renderer is ludicrous.
And for future reference, the reason why that fails is because of something called virtualization. Essentially, not all item renderers are created, only the ones visible.
Spark List uses virtual layout by default (useVirtualLayout = true). It allows to reuse item renderers and increase performance and decrease resources usage. In your case you can set useVirtualLayout=false for your List.
But it is very possible you do something wrong if you need to list List's item renderers from outside. Try to solve your problem some other way and remain using virtual layout.
You can also use the RendererExistenceEvent which is fired when the ItemRenderer of the DataGroup when it receives data and is rendered. Listen for the event rendererAdd on the DataGroup.
<s:DataGroup id="dataGroupList" dataProvider="{_listData}"
width="100%" height="100%"
rendererAdd="list_rendererAddHandler(event)"/>
For more information, check out the help document for RendererExistenceEvent.
Though some say it's not best practices for accessing and changing data in the ItemRenderer directly, there may be other use cases where you need to just that. For instance, if you want to toggle the selected or enabled properties of the list based on some user interaction or state change. Best practices only apply on the simplest use cases, other use cases require you to use these events or modify the control itself.
i have a Flex tree control and im trying to select a tree node 3 levels down right after the dataProvider is assigned with a collection object like the following.
basically treeItem1, treeItem2, treeItem3 are the nodes in the tree and treeitem3 is a child of treeItem2 which is a child of treeItem1. Assume these treeItem(1,2,3) are referenced correctly from the collection items.
my problem is that if i wait for the whole component to load completely then select the nodes, it open/select/scrolltoIndex correctly. However, if i were to select the node right after the dataProvider is assigned, then it doesn't even open or select (basically the this.treeService.selectedItem is always null).
can anyone point out what i did wrong? is there anything needs to happen after the dataProvider is assigned?
thanks
this.treeService.dataProvider = oPricingHelper.getCurrentPricingSercicesTreeSource();
this.treeService.expandItem(treeItem1, true);
this.treeService.expandItem(treeItem2, true);
this.treeService.selectedItem = treeItem3;
this.treeService.scrollToIndex(this.treeService.selectedIndex);
I have used the updateComplete event to know when a component (such as a DataGroup or List) has completed rendering after performing a simple task (such as updating the dataProvider reference). Of course, you have to be careful and remove listening to updateComplete because it can run a lot, unless you have a need for it to run.
Something like:
//...some function...
this.treeService.addEventListener(FlexEvent.UPDATE_COMPLETE, onTreeUpdateComplete);
this.treeService.dataProvider = oPricingHelper.getCurrentPricingSercicesTreeSource();
//...rest of some function...
private function onTreeUpdateComplete(event:FlexEvent):void {
this.treeService.removeEventListener(FlexEvent.UPDATE_COMPLETE, onTreeUpdateComplete);
this.treeService.expandItem(treeItem1, true);
this.treeService.expandItem(treeItem2, true);
this.treeService.selectedItem = treeItem3;
this.treeService.scrollToIndex(this.treeService.selectedIndex);
}
I'm not positive your experiencing the same issue but I seem to have the same type of problem with using the advanced data grid, it appears in these cases where the dataprovider is acceptable as multiple types, the components do some extra work in the background to wrap things up into something Hierarchical (HierarchicalData or HierarchicalCollectionView) and in doing so the dataprovider setter call is not synchronous (so it will return before actually having assigned the internal property storing the dataprovider). I've used callLater in this case with moderate success, callLater is generally a bad practice but basically adds a function to a list of functions to call once background processing is done, so this is assuming that something in the dataprovider setter called UIComponent.suspendBackgroundProcessing() and that it will subsequently call UIComponent.resumeBackgroundProcessing() and then it will execute the list of functions added by using callLater. Alternatively you could use setTimeout(someFunction,1000).
These are both "hacks" the real solution is to dig into the framework code and see what it's really doing when you tell it to set the dataprovider. Wherever you see that it actually has set the dataprovider you could extend that class and dispatch an event that you could listen for to run the function to do the selections after this point.
If anyone has a better solution please by all means correct me (I would love to have a better answer than this)
How can I get the width of my LinkButton object ?
myLinkButton = new LinkButton();
myLinkButton.label = "blabla";
myLinkButton.setStyle("fontSize", 24);
myContainer.addChild(myLinkButton);
trace (myContainer.width); //this doesn't work because I haven't directly set the attribute
thanks
First, what does that trace() show? Is it null or undefined or NaN or simply a wrong value?
Then, there are several ways I can think of how you could get around this problem:
Try using getBounds() or getRect(). These methods return a Rectangle object working as the DisplayObject's bounding box (including all coordinates and dimensions). Sometimes Flex behaves a bit weird and returns wrong/off results for the coordinates or dimensions of objects.
Try experimenting with validateSize() and/or measuredWidth. Perhaps you're trying to access the width property too soon so that Flex cannot do the measuring/layouting in time.
Similar idea: what happens if you use myContainer.callLater(trace, [myContainer.width]); (assuming your myContainer inherits from UIComponent)? If you do get a valid result using callLater() but not when accessing width directly then Flex just hasn't had a chance to layout and update the container.
You could also try using this method, which creates a Bitmap from the object and returns the Bitmap's height/width. This is especially useful if you have components with visible = false in your container, because Flex doesn't handle invisible components well in that regard.
Finally, you could try accessing $width in the mx_internal namespace and check that property's value. However, using mx_internal is sort of a very ugly hack because these properties and methods weren't meant for external use and are subject to change any time (so your component could stop working when a new version is released) - so use with caution.