2

I've got a view model that exposes a CollectionViewSource built around my observable collection. I also have a command for deleting items:

public ObservableCollection<FooViewModel> Foos { get; set; }
public ICollectionView FooCollectionView { get; set; }
public ICommand DeleteFooCommand { get; set; }

private FooViewModel _SelectedFoo;
public FooViewModel SelectedFoo
{
    get { return _SelectedFoo; }
    set
    {
        if (value != _SelectedFoo)
        {
            _SelectedFoo = value;
            OnPropertyChanged();
        }
    }
}

public MyViewModel()
{
    Foos = new ObservableCollection<FooViewModel>();
    FooCollectionView = CollectionViewSource.GetDefaultView(Patches);
    FooCollectionView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
    DeleteFooCommand = new RelayCommand(DeleteFoo);
}

private void DeleteFoo(object parameter)
{
    var collection = parameter as IEnumerable;
    if (collection == null) return;

    var itemsToDelete = collection.OfType<FooViewModel>().ToList();
    if (!itemsToDelete.Any()) return;

    SelectedFoo = null;

    // TODO: Figure out what the next SelectedFoo should be after deleting) 

    foreach (var item in itemsToDelete)
        Foos.Remove(item);
}

In the spirit of MVVM, I don't want to know whether or not my CollectionViewSource was bound to a ListBox, ListView, etc. I simply expect one or more FooViewModels in my command parameter (via [control].SelectedItems). However, I want to set the new SelectedFoo to what I think would be the next logical item that should be selected. I know there is IndexOf() of my source collection, but that index won't match the index after the sort of my CollectionViewSource. Is there a way to get the index of the item on the view and not the source? I can do what I want if I pass the bound ListBox as my command parameter, but I'd rather not know about the bound control.

Jason Butera
  • 2,376
  • 3
  • 29
  • 46

2 Answers2

1

To answer your question directly (i.e. how to obtain the resulting index of an item in a collection view) - you can take advantage of the fact that the collection view enumerates the items in sorted order and then simply utilize Linq to count items preceding the one you're interested in. Since I'm not sure how to mix it into your view-model (you're not even checking if currently selected item is among the deleted ones?), here's an extension method that will return the resulting index of an item in a collection view:

public static int IndexOf(this ICollectionView view, object item)
{
    return view.Cast<object>().TakeWhile(x => !Equals(x, item)).Count();
}

Note though that this solution is not universal because it assumes that item is both in the source collection and is not filtered out by the collection view. Otherwise it will return the total count of items in the view (excluding the filtered ones). A universal solution would go something like this:

public static int IndexOf(this ICollectionView view, object item)
{
    var e = view.GetEnumerator();
    var idx = 0;
    while (e.MoveNext())
    {
        if (Equals(item, e.Current))
            return idx;
        else
            idx++;
    }
    //if we've got this far it means that the item is either filtered out
    //or is not in the source collection
    return -1;
}

Then of course there's another problem, namely you cannot access items by index in a collection view. So you have to enumerate the collection second time:

var idx = view.IndexOf(item);
var nextItem = view.Cast<object>().ElementAtOrDefault(idx + 1);

That should not be a problem for fairly small collections, but may become one for large collections (remember that the view enumerates items in sorted order, so unless the sorting result is cached, each enumeration would require to sort the whole collection).

To answer a slightly modified question ("How do I get an item following a particular one from a sorted collection view?") there's a simple solution that needs to enumerate the collection only once:

var nextItem = view.Cast<object>()
    .SkipWhile(x => !Equals(x, item)) //skip preceding items
    .Skip(1) //skip the item itself
    .FirstOrDefault();

Note that this will return null if the item is not present in the resulting collection or is its last item.

Grx70
  • 10,041
  • 1
  • 40
  • 55
  • But what if you need to find next item after **multiple** deleted items? Your solution is really nice but works for one deleted item only. Also it is not good to use `object` since you can use `Cast` or `OfType` with specific type. – Maxim Jun 08 '17 at 07:15
  • @Maxim Then it depends on what exactly do you mean by "next item after multiple deleted items". As for casting to `object` - I tried to abstract the problem and solution form OP's example so using for example `FooViewModel` would imply a lot of assumptions (e.g. that `FooViewModel` is a reference type and not a value type). – Grx70 Jun 08 '17 at 07:27
0

Try this code:

public static int IndexOf<TItem>(this ICollectionView collectionView, TItem item)
{
    int result = 0;

    foreach (var i in collectionView.OfType<TItem>())
    {
        if (i == item)
            return result;

        result++;
    }

    return -1;
}
Maxim
  • 1,995
  • 1
  • 19
  • 24
  • This solution is correct provided that **a)** the source collection only contains instances of `TItem` (does not contain `null`s or objects of other type) and **b)** the `==` operator is not overloaded for `TItem` (or is equivalent to default `IEqualityComparer`). – Grx70 Jun 08 '17 at 07:34