views:

708

answers:

2

I'm surprised that no one has asked this before here... well, at least I haven't found an answer here or anywhere else, actually.

I have a ComboBox that is databound to an ObservableCollection. Everything worked great until the guys wanted the contents sorted. No problem -- I end up changing the simple property out:

public ObservableCollection<string> CandyNames { get; set; } // instantiated in constructor

for something like this:

private ObservableCollection<string> _candy_names; // instantiated in constructor
public ObservableCollection<string> CandyNames
{
    get {
        _candy_names = new ObservableCollection<string>(_candy_names.OrderBy( i => i));
        return _candy_names;
    }
    set {
        _candy_names = value;
    }
}

This post is really two questions in one:

  1. How can I sort a simple ComboBox of strings in XAML only. I have researched this and can only find info about a SortDescription class, and this is the closest implementation I could find, but it wasn't for a ComboBox.
  2. Once I implemented the sorting in code-behind, it my databinding was broken; when I added new items to the ObservableCollection, the ComboBox items didn't update! I don't see how that happened, because I didn't assign a name to my ComboBox and manipulate it directly, which is what typically breaks the binding.

Thanks for your help!

+2  A: 

You can use a CollectionViewSource to do the sorting in XAML, however you need to refresh it's view if the underlying collection changes.

XAML:

<Window x:Class="CBSortTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
    Height="300" Width="300">

    <Window.Resources>
        <CollectionViewSource Source="{Binding Path=CandyNames}" x:Key="cvs">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>

    </Window.Resources>
    <StackPanel>
        <ComboBox ItemsSource="{Binding Source={StaticResource cvs}}" />
        <Button Content="Add" Click="OnAdd" />
    </StackPanel>
</Window>

Code behind:

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Data;

namespace CBSortTest
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            CandyNames = new ObservableCollection<string>();

            OnAdd(this, null);
            OnAdd(this, null);
            OnAdd(this, null);
            OnAdd(this, null);

            DataContext = this;

            CandyNames.CollectionChanged += 
                (sender, e) =>
                {
                    CollectionViewSource viewSource =
                        FindResource("cvs") as CollectionViewSource;
                    viewSource.View.Refresh();
                };
        }

        public ObservableCollection<string> CandyNames { get; set; }

        private void OnAdd(object sender, RoutedEventArgs e)
        {
            CandyNames.Add("Candy " + _random.Next(100));
        }

        private Random _random = new Random();
    }
}
Wallstreet Programmer
cool, that's definitely a possibility. I had just found this article http://bea.stollnitz.com/blog/?p=17 so it's good to hear that CollectionViewSource is my likely solution. However, I think this will work without refreshing... thanks for the tip!
Dave
can anyone explain the difference between ItemsSource="{StaticResource cvs}" and ItemsSource="{Binding Source={StaticResource cvs}}"? I always considered StaticResource to imply that there was a binding, so I just have a conceptual hurdle to jump regarding why Binding needs to be specified and is not implicit?
Dave
A: 

I have another question that has come up as a result of implementing the accepted answer. My ComboBox is databound so that the SelectedItem is bound to a property called CurrentCandy:

<ComboBox ItemsSource="{Binding Source={StaticResource cvs}}" SelectedItem="{Binding CurrentCandy, Mode=TwoWay}" />

But now that doesn't work like it used to. It always comes up empty when the GUI loads. I checked the output window and got this databinding error:

System.Windows.Data Error: 8 : Cannot save value from target back to source. BindingExpression:Path=CurrentCandy; DataItem='CandyControl' (HashCode=8676369); target element is 'ComboBox' (Name=''); target property is 'SelectedItem' (type 'Object') TargetInvocationException:'System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.ArgumentNullException: Value cannot be null.
Parameter name: key

Now, I am pretty sure that the reason for this is that the ComboBox now is binding to the StaticResource instead of what it was previously databound to -- the ViewModel. So I just need to figure out how to bind to CurrentCandy in the ViewModel. Obviously, it's no longer SelectedItem={Binding CurrentCandy} -- it needs to use something like

SelectedItem={Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type CandyControl}}, Path=CurrentCandy}

But that doesn't work... I then get:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='CandyControl', AncestorLevel='1''. BindingExpression:Path=CurrentCandy; DataItem=null; target element is 'ComboBox' (Name=''); target property is 'SelectedItem' (type 'Object')
Dave
try using the SelectedValue property instead of SelectedItem to define your binding.
Roel
Please post this as a separate question. CollectionViews have builtin current handling: http://msdn.microsoft.com/en-us/library/system.windows.data.collectionview.currentitem%28VS.85%29.aspx
Wallstreet Programmer
@Wallstreet: thanks for the link, that was really helpful.
Dave