Using silverlight, I have a listbox with ItemsSource bound to an ObservableCollection which is updated asynchronously. I would like to automatically select the first item in the listbox as soon as the bindi开发者_开发知识库ng is finished updating.
I can't find a good way to make this happen. I don't see any useful events to handle on the listbox and if I bind to the collection's CollectionChanged event, the binding hasn't updated yet so if I set the listbox.selectedindex at that point I get an exception that the value is out of range. Any ideas? Maybe some way to hook the binding update?
I spent a long time searching the internet to the solution for this problem, and basically ended up stumbling on the solution.
What you want to do is bind your listbox to an ICollectionView. Then make sure that you DON'T have IsSynchronizedWithCurrentItem set to false.
Bad, won't work
IsSynchronizedWithCurrentItem="False"
This is the Silverlight default, don't waste your time typing it out
IsSynchronizedWithCurrentItem="{x:Null}"
This will throw an error at runtime, and I believe is the same as {x:null} anyways
IsSynchronizedWithCurrentItem="True"
ICollectionView has a method called MoveCurrentToFirst. That name seems a bit ambiguous, but it does actually move the CurrentItem pointer to the first item (I initally thought it reordered the collection by moving whatever item you had selected to the first item position). The IsSynchronizedWithCurrentItem property allows Listbox (or any control implementing Selector) to work magic with ICollectionViews. In your code you can call ICollectioView.CurrentItem instead of whatever you bound Listbox.SelectedItem to get the currently selected item.
Here is how I make my ICollectionView available to my view (I'm using MVVM) :
public System.ComponentModel.ICollectionView NonModifierPricesView
{
get
{
if (_NonModifierPricesView == null)
{
_NonModifierPricesView = AutoRefreshCollectionViewSourceFactory.Create(x => ((MenuItemPrice)x).PriceType == MenuItemPrice.PriceTypes.NonModifier);
_NonModifierPricesView.Source = Prices;
_NonModifierPricesView.ApplyFilter(x => ((MenuItemPrice)x).DTO.Active == true);
}
ICollectionView v = _NonModifierPricesView.View;
v.MoveCurrentToFirst();
return v;
}
}
Now, as you want to bind to an observable collection, you cannot use the default CollectionViewSource as it is not aware of updates to the source collection. You probably noticed that I am using a custom CVS implementation called AutoRefreshCollectionViewSource. If memory serves, I found the code online and modified it for my own uses. I've added extra functionality for filtering, so could probably clean these classes up even more.
Here is my version of the code :
AutoRefreshCollectionViewSource.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows.Data;
public class AutoRefreshCollectionViewSource : System.Windows.Data.CollectionViewSource
{
// A delegate for launching a refresh of the view at a different priority.
private delegate void NoArgDelegate();
private Predicate<object> MyFilter; // this is the filter we put on when we do ApplyFilter
private Predicate<object> BaseFilter; // this is the filter that is applied always
public AutoRefreshCollectionViewSource(Predicate<object> _baseFilter) : base()
{
BaseFilter = _baseFilter;
if (BaseFilter == null)
BaseFilter = x => true;
}
/// <summary>
/// A collection containing all objects whose event handlers have been
/// subscribed to.
/// </summary>
private List<INotifyPropertyChanged> colSubscribedItems = new List<INotifyPropertyChanged>();
// We must override the OnSourceChanged event so that we can subscribe
// to the objects in the new collection (and unsubscribe from the old items).
protected override void OnSourceChanged(object oldSource, object newSource)
{
// Unsubscribe from the old source.
if (oldSource != null)
SubscribeSourceEvents(oldSource, true);
// Subscribe to the new source.
if (newSource != null)
SubscribeSourceEvents(newSource, false);
base.OnSourceChanged(oldSource, newSource);
}
/// <summary>
/// Adds or Removes EventHandlers to each item in the source collection as well as the
/// collection itself (if supported).
/// </summary>
/// <param name="source">The collection to (un)subscribe to and whose objects should be (un)subscribed.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeSourceEvents(object source, bool remove)
{
// Make sure the source is not nothing.
// This may occur when setting up or tearing down this object.
if (source != null)
if (source is INotifyCollectionChanged)
// We are (un)subscribing to a specialized collection, it supports the INotifyCollectionChanged event.
// (Un)subscribe to the event.
if (remove)
((INotifyCollectionChanged)source).CollectionChanged -= Handle_INotifyCollectionChanged;
else
((INotifyCollectionChanged)source).CollectionChanged += Handle_INotifyCollectionChanged;
if (remove)
// We are unsubscribing so unsubscribe from each object in the collection.
UnsubscribeAllItemEvents();
else
// We are subscribing so subscribe to each object in the collection.
SubscribeItemsEvents((IEnumerable)source, false);
}
/// <summary>
/// Unsubscribes the NotifyPropertyChanged events from all objects
/// that have been subscribed to.
/// </summary>
private void UnsubscribeAllItemEvents()
{
while (colSubscribedItems.Count > 0)
SubscribeItemEvents(colSubscribedItems[0], true);
}
/// <summary>
/// Subscribes or unsubscribes to the NotifyPropertyChanged event of all items
/// in the supplied IEnumerable.
/// </summary>
/// <param name="items">The IEnumerable containing the items to (un)subscribe to/from.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeItemsEvents(IEnumerable items, bool remove)
{
foreach (object item in items)
SubscribeItemEvents(item, remove);
}
/// <summary>
/// Subscribes or unsubscribes to the NotifyPropertyChanged event if the supplied
/// object supports it.
/// </summary>
/// <param name="item">The object to (un)subscribe to/from.</param>
/// <param name="remove">Whether or not to subscribe or unsubscribe.</param>
private void SubscribeItemEvents(object item, bool remove)
{
if (item is INotifyPropertyChanged)
// We only subscribe of the object supports INotifyPropertyChanged.
if (remove)
{
// Unsubscribe.
((INotifyPropertyChanged)item).PropertyChanged -= Item_PropertyChanged;
colSubscribedItems.Remove((INotifyPropertyChanged)item);
}
else
{
// Subscribe.
((INotifyPropertyChanged)item).PropertyChanged += Item_PropertyChanged;
colSubscribedItems.Add((INotifyPropertyChanged)item);
}
}
/// <summary>
/// Handles a property changed event from an item that supports INotifyPropertyChanged.
/// </summary>
/// <param name="sender">The object that raised the event.</param>
/// <param name="e">The event arguments associated with the event.</param>
/// <remarks></remarks>
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// By default, we do not need to refresh.
bool refresh = false;
if (e.PropertyName == "Active" || e.PropertyName == "DTO.Active")
refresh = true;
if (refresh)
// Call the refresh.
// Notice that the dispatcher will make the call to Refresh the view. If the dispatcher is not used,
// there is a possibility for a StackOverFlow to result.
this.Dispatcher.BeginInvoke(new NoArgDelegate(this.View.Refresh), null);
}
/// <summary>
/// Handles the INotifyCollectionChanged event if the subscribed source supports it.
/// </summary>
private void Handle_INotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SubscribeItemsEvents(e.NewItems, false);
break;
case NotifyCollectionChangedAction.Remove:
SubscribeItemsEvents(e.OldItems, true);
break;
case NotifyCollectionChangedAction.Replace:
SubscribeItemsEvents(e.OldItems, true);
SubscribeItemsEvents(e.NewItems, false);
break;
case NotifyCollectionChangedAction.Reset:
UnsubscribeAllItemEvents();
SubscribeItemsEvents((IEnumerable)sender, false);
break;
}
}
public void ApplyFilter(Predicate<object> f)
{
if (f != null)
MyFilter = f;
this.View.Filter = x => MyFilter(x) && BaseFilter(x);
this.View.Refresh();
}
public void RemoveFilter()
{
this.View.Filter = BaseFilter;
this.View.Refresh();
}
}
AutoRefreshCollectionViewSourceFactory.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class AutoRefreshCollectionViewSourceFactory
{
private static List<AutoRefreshCollectionViewSource> Collections;
public static AutoRefreshCollectionViewSource Create()
{
if (Collections == null)
Collections = new List<AutoRefreshCollectionViewSource>();
AutoRefreshCollectionViewSource cvs = new AutoRefreshCollectionViewSource(null);
Collections.Add(cvs);
return cvs;
}
public static AutoRefreshCollectionViewSource Create(Predicate<object> p)
{
if (Collections == null)
Collections = new List<AutoRefreshCollectionViewSource>();
AutoRefreshCollectionViewSource cvs = new AutoRefreshCollectionViewSource(p);
Collections.Add(cvs);
return cvs;
}
public static void ApplyFilterOnCollections()
{
foreach (AutoRefreshCollectionViewSource cvs in Collections)
cvs.ApplyFilter(null);
}
public static void RemoveFilterFromCollections()
{
foreach (AutoRefreshCollectionViewSource cvs in Collections)
cvs.RemoveFilter();
}
public static void CleanUp()
{
Collections = null;
}
}
Another thing you can do is create a UserControl which inherits from ListBox, expose a ItemsChanged by overriding the OnItemsChanged method, and handle this event and set the ListBox's SelectedIndex to 0.
public partial class MyListBox : ListBox
{
public delegate void ItemsSourceChangedHandler(object sender, EventArgs e);
#region Override
protected override void OnItemsChanged(
NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
OnItemsChangedEvent(e);
}
#endregion Override
#region Class Events
public delegate void ItemsChangedEventHandler(object sender,
NotifyCollectionChangedEventArgs e);
public event ItemsChangedEventHandler ItemsChanged;
private void OnItemsChangedEvent(
NotifyCollectionChangedEventArgs e)
{
if (ItemsChanged != null)
{
ItemsChanged(this, e);
}
}
#endregion Class Events
}
XAML for the User Control:
<ListBox x:Class="CoverArtRefiner.MyListBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
</Grid>
</ListBox>
On your screen add the following:
xmlns:local="clr-namespace:<The Namespace which MyListBox is contained in>"
Now to add the custom control onto your window/usercontrol:
<local:MyListBox ItemsChanged="listBox_ItemsChanged" Background="Black" />
And finally to handle the event:
private void listBox_ItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
MyListBox listBox = (MyListBox)sender;
if (listBox.Items.Count > 0)
listBox.SelectedIndex = 0;
}
On the ListBox, bind the SelectedItem property to a property on your codebind or viewmodel. Then, in the async callback handler set the property to the first item in the collection and raise the PropertyChanged event for the property(unless you already raise the event in the setter of your property):
MySelectedListItem = _entitylist.FirstOrDefault();
RasisePropertyChanged("MySelectedListItem");
精彩评论