tags:

views:

518

answers:

2

Hi all,

I have this issue with ComboBox that uses IEnumerable<Brush> as ItemsSource; the problem lies when I (programmatically) try to set SelectedItem. Here is the code that describes the problem:

private readonly List<Brush> colors_;
private Brush white_;

ViewModelConstructor()
{
    colors_ = (from p in brushes_type.GetProperties()
               select (Brush)converter.ConvertFromString(p.Name)).ToList();

    white_ = colors_.Single(b => b.ToString() == "#FFFFFFFF");
}

public IEnumerable<Brush> Colors
{
    get { return colors_; }
}

public Brush White
{
    get { return white_; }
    set
    {
        if (white_ != value)
            white_ = value;
    }
}

And here is xaml code:

<ComboBox ItemsSource="{Binding Path=Colors}"
          SelectedItem="{Binding Path=White}">

    <ComboBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">

                <Border BorderThickness="1"
                        BorderBrush="Black"
                        Width="20"
                        Height="12"
                        SnapsToDevicePixels="True"
                        Margin="0,0,4,0">

                    <Border Background="{Binding}"/>

                </Border>

                <TextBlock Text="{Binding}"/>

            </StackPanel>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

After the get on the White property is called I get an Exception: Cannot set a property on object '#FFFFFFFF' because it is in a read-only state. If i leave White (white_) as null, everything works just fine.

A: 

Just a shot in the dark here (can't currently try this), but how about binding SelectedValue instead of SelectedItem?

MetalMikester
I tried SelectedValue; the same Exception is shown.
vladeck
Odd.. As sixlettervariables asked: The call stack of the exception may help.
MetalMikester
Yes, it is odd... I've put up the call stack, but every line (except one) is from the framework. And it is not just combobox, but listbox, too (i presume it would be the same with any ItemsControl).
vladeck
+1  A: 

Using Reflector I've traced down where the problem appears to be occuring. Inside Selector.ItemSetIsSelected, it takes the new SelectedItem and does the following:

  • If the container of the element is a DependencyObject it sets the IsSelectedProperty on the container to true.
  • Otherwise if the element is a DependencyObject it sets the IsSelectedProperty on the element to true.

That second part is where the failure comes into play, the Brush objects you've chosen are Read-Only. Because the Selector.ItemSetIsSelected is somewhat broken in this case, you have two options.

Option 1, you just call .Clone() on the Brush object returned from the Converter.

colors_ = (from p in typeof(Brushes).GetProperties()
    select ((Brush)converter.ConvertFromString(p.Name)).Clone()).ToList();


EDIT: You should go with Option 1...Option 2 is the longwinded way to fix the problem

Option 2, you could wrap the Brush objects into one more object:

public class BrushWrapper
{
    public Brush Brush { get; set; }
}

You then update the data template paths:

<Border Background="{Binding Path=Brush}" />

and

<TextBlock Text="{Binding Path=Brush}" />

Finally, you update the ViewModel:

private readonly List<BrushWrapper> colors_;
private BrushWrapper white_;

public ColorViewModel()
{
    colors_ = (from p in typeof(Brushes).GetProperties()
               select new BrushWrapper {
                   Brush = (Brush)converter.ConvertFromString(p.Name)
               }).ToList();

    white_ = colors_.Single(b => b.Brush.ToString() == "#FFFFFFFF");
}

public List<BrushWrapper> Colors
{
    get { return colors_; }
}

public BrushWrapper White
{
    get { return white_; }
    set
    {
        if (white_ != value)
            white_ = value;
    }
}
sixlettervariables
Thank you so much :)
vladeck
No problem. I highly recommend getting Reflector, as it has helped a ton with WPF oddities.
sixlettervariables
WPF can be perverse at times. :)
MetalMikester