To simplify the design, I have a ListBox alongside a Details View showing the details for the SelectedItem. When an item is selected, a web service call is made on a separate thread to populate the details for that item.
This has been working well, but when using the keyboard arrows to select an item towards the bottom of the list, things start to get nasty. As each item in the list gets temporarily selected as I move towards the item I actually want to select, those items all have their details loaded.
This behavior causes a bunch of unwanted web service calls to be made (a needless burden on the server), as well as a large number of threads to be consumed simultaneously on the client (causing thread starvation), such that the details for the actual item we want take much (much) longer to load. It can take minutes for the application to return to a usable state!
To solve this problem, I needed to introduce some new behavior to the loading of the selected item:
- When an item is selected:
- create a new Task but wait 500ms before proceeding to make the web service call;
- cancel any previous Tasks which are still waiting to proceed with the web service call.
- Once the 500ms has expired, if the Task has not been canceled, assume the selection is intended and go ahead with the web service call.
To implement this behavior, I created a new class named LagLoader. To use it, each selection scope (ie List) should have one LagLoader. LagLoader is initialized with an Action which should be called to load the selected item. LagLoader.LoadItem(SelectedItem) should be called whenever an item is selected. After half a second, if no other item has been selected, it will go ahead and call the loader function.
/// <typeparam name="T">The type of object which will be selected.</typeparam> public class LagLoader<T> { /// <param name="loader">The function which should be called when an item is selected.</param> public LagLoader(Action<T> loader) { _loader = loader; } private Action<T> _loader; private AutoResetEvent _waitHandle = new AutoResetEvent(false); private T _selectedItem; public void LoadItem(T item) { _waitHandle.Set(); //interrupt any Tasks which are waiting. _waitHandle = new AutoResetEvent(false); _selectedItem = item; Task.Factory.StartNew(() => { if (_waitHandle.WaitOne(500)) return; //Another selection interrupted the wait. _loader(item); }); } }
I'm pretty happy with the way this worked out, but if you know of a better way I'd love to hear about it.
No comments:
Post a Comment