1

I have a simple list and a background refresh protocol.

When the list is scrolled down, the refresh scrolls it back to the top. I want to stop this.

I have tried catching the COLLECTION_CHANGE event and

validateNow();   //  try to get the component to reset to the new data

list.ensureIndexIsVisible(previousIndex);    //  actually, I search for the previous data id in the IList, but that's not important

This fails because the list resets itself after the change (in DataGroup.commitProperties).

I hate to use a Timer, ENTER_FRAME, or callLater(), but I cannot seem to figure out a way.

The only other alternatives I can see is sub-classing the List so it can catch the dataProviderChanged event the DataGroup in the skin is throwing.

Any ideas?

Richard Haven
  • 1,122
  • 16
  • 31
  • Any update to the collection may change the length of the dataProvider, so staying in the same scroll position is not consistently practical. Are you replacing the dataProvider, or just updating it by adding items / removing items / adding a filter? You could try setting saving / resetting the veritcalScrollPosition. I'm not sure if that would cause a visual "bump" or not. – JeffryHouser Jun 30 '11 at 20:52
  • 1
    Why are you refreshing in the first place? what are you doing to the collection? – J_A_X Jun 30 '11 at 21:08
  • Have you looked at adding an event listener to something like updateComplete? I have previously solved this type of problem by subclassing List and overriding `set scrollPosition`. – Jacob Eggers Jun 30 '11 at 21:15
  • The model refreshes its data in the background to keep it current. I'll look into updateComplete. – Richard Haven Jun 30 '11 at 21:38
  • I solved that issue once. Sadly, I don't have the code lying around. It's in the repository at work. I'll get back to you in the morning (Brussels time) if no one else can provide you with an answer in the meantime. – RIAstar Jun 30 '11 at 21:46
  • Jacob: using list.dataGroup.updateComplete (it's public!) and carefully setting flags only when the CollectionEvent.kind == REFRESH, your answer seems to be working. If you want credit, post it as an Answer. Thanks – Richard Haven Jun 30 '11 at 22:36

4 Answers4

4

Actually MUCH better solution to this is to extend DataGroup. You need to override this.

All the solutions here create a flicker as the scrollbar gets resetted to 0 and the it's set back to the previous value. That looks wrong. This solution works without any flicker and the best of all, you just change DataGroup to FixedDataGroup in your code and it works, no other changes in code are needed ;).

Enjoy guys.

public class FixedDataGroup extends spark.components.DataGroup
{
    private var _dataProviderChanged:Boolean;
    private var _lastScrollPosition:Number = 0;

    public function FixedDataGroup()
    {
        super();
    }

    override public function set dataProvider(value:IList):void
    {
        if ( this.dataProvider != null && value != this.dataProvider )
        {
            dataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE, onDataProviderChanged);
        }
        super.dataProvider = value;

        if ( value != null )
        {
            value.addEventListener(CollectionEvent.COLLECTION_CHANGE, onDataProviderChanged);
        }
    }

    override protected function commitProperties():void
    {
        var lastScrollPosition:Number = _lastScrollPosition;

        super.commitProperties();

        if ( _dataProviderChanged )
        {
            verticalScrollPosition = lastScrollPosition;
        }
    }

    private function onDataProviderChanged(e:CollectionEvent):void
    {
        _dataProviderChanged = true;
        invalidateProperties();
    }

    override public function set verticalScrollPosition(value:Number):void
    {
        super.verticalScrollPosition = value;
        _lastScrollPosition = value;
    }


}
  • This does not work if you are using `useVirtualLayout="true"` which is crucial for performance reasons in many cases. – citizen conn Jun 18 '12 at 23:25
2

I have faced this problem before and solved it by using a data proxy pattern with a matcher. Write a matcher for your collection that supports your list by updating only changed objects and by updating only attributes for existing objects. The goal is to avoid creation of new objects when your data source refreshes.

When you have new data for the list (after a refresh), loop through your list of new data objects, copying attributes from these objects into the objects in the collection supporting your list. Typically you will match the objects based on id. Any objects in the new list that did not exist in the old one get added. Your scroll position will normally not change and any selections are usually kept.

Here is an example.

for each(newObject:Object in newArrayValues){
  var found:Boolean = false;
  for each(oldObject:Object in oldArrayValues){
    if(oldObject.id == newObject.id){
      found = true;
      oldObject.myAttribute = newObject.myAttribute;
      oldObject.myAttribute2 = newObject.myAttribute2;
    }
  }
  if(!found){
    oldArrayValues.addItem(newObject);
  }
}
TroyJ
  • 1,620
  • 1
  • 10
  • 7
  • sadly, sometimes the entire dataProvider is new instances of the same data. I dread adding that much complexity. – Richard Haven Jun 30 '11 at 21:37
  • this can be done with a for loop with something like this: for each(newObject:Object in newArrayValues){ var found:Boolean = false; for each(oldObject:Object in oldArrayValues){ if(oldObject.id == newObject.id){ found = true; oldObject.myAttribute = newObject.myAttribute; oldObject.myAttribute2 = newObject.myAttribute2; } } if(!found){ oldArrayValues.addItem(newObject); } } – TroyJ Jul 05 '11 at 16:18
2

I ll try to explain my approach...If you are still unsure let me know and I ll give you the source code as well.

1) Create a variable to store the current scroll position of the viewport. 2) Add Event listener for Event.CHANGE and MouseEvent.MOUSE_WHEEL on the scroller and update the variable created in step 1 with the current scroll position; 3) Add a event listener on your viewport for FlexEvent.UpdateComplete and set the scroll position to the variable stored.

In a nutshell, what we are doing is to have the scroll position stored in variable every time user interacts with it and when our viewport is updated (due to dataprovider change) we just set the scroll position we have stored previously in the variable.

-1

My solution for this problem was targeting a specific situation, but it has the advantage of being very simple so perhaps you can draw something that fits your needs from it. Since I don't know exactly what issue you're trying to solve I'll give you a description of mine:

I had a List that was progressively loading data from the server. When the user scrolled down and the next batch of items would be added to the dataprovider, the scrollposition would jump back to the start.

The solution for this was as simple as stopping the propagation of the COLLECTION_CHANGE event so that the List wouldn't catch it.

myDataProvider.addEventListener(
    CollectionEvent.COLLECTION_CHANGE, preventRefresh
);

private function preventRefresh(event:CollectionEvent):void {
    event.stopImmediatePropagation();
}

You have to know that this effectively prevents a redraw of the List component, hence any added items would not be shown. This was not an issue for me since the items would be added at the end of the List (outside the viewport) and when the user would scroll, the List would automatically be redrawn and the new items would be displayed. Perhaps in your situation you can force the redraw if need be.

When all items had been loaded I could then remove the event listener and return to the normal behavior of the List component.

RIAstar
  • 11,912
  • 20
  • 37