4
\$\begingroup\$

I have an ObservableCollection that is filled with custom elements of type Video runtime. These have the properties of Name, Rating and Number of Viewers. I want to display the Video elements in different orders based on whichever of the three criteria the user chooses during Application Runtime. I created the following Enum to store the possible choices:

public enum OrderBy
{
    [Description("Video Name")]
    Name = 0,

    [Description("Video Rating")]
    Rating = 1,

    [Description("Number of Viewers")]
    ViewerNumber = 2
}

These are fed into a ComboBox called OrderCombo allowing the user to change the order criteria (I'm displaying the Description to the user):

foreach (var item in Enum.GetValues(typeof(OrderBy)))
{
    OrderCombo.Items.Add(((OrderBy)item).GetDescription());
}

To avoid sorting, I used LINQ to get the order and I came up with the following:

IOrderedEnumerable<Video> GetOrder()
    {
        switch (OrderCombo.SelectedIndex)
        {

            case 0:
                return videos.OrderBy(x => x.Name);

            case 1:
                return videos.OrderByDescending(x => x.Rating);

            case 2:
                return videos.OrderByDescending(x => x.NumberOfViewers);

        }
        return videos.OrderBy(x => x.Name);
    }

Then, I call this function every time a new element is added or an element is delelted or the user changes the order:

VideoUIElment.ItemsSource = GetOrder();

So basically I keep re-calculating the enumerable every time, which workswpf but I'm wondering if there is a more efficient/elegant way of doing this. Maybe store the order in one or several Func<>s? I tried them, but they can only be of a (compile-time) fixed type as in Func<Video, string> for Name and I would rather avoid using implementations like Func<Video, dynamic>.

\$\endgroup\$
  • \$\begingroup\$ The is a way to sort in the XML but I forget right now. A default on the switch would look better there. \$\endgroup\$ – paparazzo Jul 16 '17 at 19:20
4
\$\begingroup\$

To sort your ObservableCollection you should be using data-binding and let WPF do the work.

First let's fix the ComboBox that you currently populate manually. For the sake of this review I assume you use your code-behind file as a data-source that is bound to the Window via:

DataContext="{Binding RelativeSource={RelativeSource Self}}"

Next you need to tell the ComboBox where to find the items and how to display them:

<ComboBox 
    ItemsSource="{Binding OrderOptions}" 
    DisplayMemberPath="Description" 
    SelectedValuePath="OrderBy" 
    SelectionChanged="Selector_OnSelectionChanged">
</ComboBox>

It receives the values from the code-behind property (you of course would read the descriptions here with your reflection helpers):

public IEnumerable<OrderOption> OrderOptions
{
    get
    {
        return
            Enum
                .GetValues(typeof(OrderBy))
                .Cast<OrderBy>()
                .Select(x => new OrderOption { OrderBy = x, Description = x.ToString() });
    }
}

I created a helper object for this combo-box that looks like this:

public class OrderOption
{
    public OrderBy OrderBy { get; set; }

    public string Description { get; set; }
}

Now we need a list of videos:

private ObservableCollection<Video> _videos = new ObservableCollection<Video>(new[]
{
    new Video { Name = "Rambo", Rating = 3},
    new Video { Name = "Rocky", Rating = 8},
    new Video { Name = "Turbo Interceptor", Rating = 1}
});

where the Video is implemented as:

public class Video
{
    public string Name { get; set; }

    public int Rating { get; set; }
}

and the code-behind file has this property:

public ObservableCollection<Video> Videos => _videos;

Now comes the tricky part. You need to add a new resource to the resource-dictionary. This is a CollectionViewSource that initially will sort the items by Name.

<Window.Resources>
    <ResourceDictionary>
        <CollectionViewSource x:Key="SortedVideos" Source="{Binding Videos}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Name"></scm:SortDescription>
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </ResourceDictionary>
</Window.Resources>

In order for this to work you need to add a new namespace:

xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"

Now you can bind a ListView to it:

<ListView 
    ItemsSource="{Binding Source={StaticResource SortedVideos}}" 
    DisplayMemberPath="Name">
</ListView>

You have probably noticed the combo-box has this Selector_OnSelectionChanged event handler that in code-behind is implemented like this to get the currently select OrderBy, then the switch is used to change the sorting by first clearing the current settings and adding a new one that changes the property of the Video object that is used for sorting. In this example I only used the Name and the Rating. Now each time you change the selection WPF will update the ListView and if you add or delete an item it will use the current setting to re-order it.

private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var colView = Resources["SortedVideos"] as CollectionViewSource;
    switch (((sender as ComboBox).SelectedItem as OrderOption).OrderBy)
    {
        case OrderBy.Name:
            colView.SortDescriptions.Clear();
            colView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
            break;
        case OrderBy.Rating:
            colView.SortDescriptions.Clear();
            colView.SortDescriptions.Add(new SortDescription("Rating", ListSortDirection.Ascending));
            break;
    }
}
\$\endgroup\$
  • \$\begingroup\$ To implement all this stuff in ideal way MVVM should be used. I recommend to place ICollectionView in view-model, bind SelectedItem of ComboBox to the property in view-model and change SortDescriptions when this property changed. It is the case where code-behind can be (and should be) avoided. Also it is preferable to use nameof operator instead of string literals with property names. \$\endgroup\$ – Maxim Jul 17 '17 at 4:24
  • \$\begingroup\$ @Maxim sure ;-) this example should just demonstrate that using WPF components like the ICollectionView might be a better approach then sorting the collection manually... and to some point I intentionally didn't do it the MVVM-way so there's something left for OP to implement :-] \$\endgroup\$ – t3chb0t Jul 17 '17 at 8:11
  • \$\begingroup\$ Looking at tons of questions on Stack Overflow marked with wpf tag where people do DataContext = this and set ItemsSource in code-behind I came to the decision that it is necessary to force them to use MVVM :) \$\endgroup\$ – Maxim Jul 17 '17 at 11:56
  • \$\begingroup\$ Thank you! I had something like this in mind. The CollectionViewSource, then, is a kind of intermediate layer between the data and the UI responsible for, say, the presentation of the data? \$\endgroup\$ – rTECH Jul 17 '17 at 11:59
  • \$\begingroup\$ @rTECH exactly, it can act between the WPF control and the underlying collection to let you present it with different view. You could for example have multiple views of the same collection and each of them in different order. I wasn't actually sure... should I go MVVM or better not yet so it's a mix of partial databinding and partial old event handling but it would make sense to implement it the way Maxim mentioned. \$\endgroup\$ – t3chb0t Jul 17 '17 at 12:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.