0

I'm trying to keep a combo box in sync with a property:

<ComboBox SelectedItem="{Binding StrokeSwatch}"...

This work excepts the combo box keeps being empty (items are here if the box is opened, but there is no current/selected item) until I manually select a value.

enter image description here

It should display the Red swatch and name:

enter image description here
I can't find the reason: The property SelectedItem is bound to (StrokeSwatch) has a value, which is used by the line, but the combo box doesn't react to this value.

Learning WPF, would appreciate a little help to understand.

The code...

    <Window x:Class="WpfApp1.MainWindow"
        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"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="300">

        <Window.DataContext>
            <local:ViewModel/>
        </Window.DataContext>

        <StackPanel Margin="10">
            <StackPanel Orientation="Horizontal" Margin="10">
                <TextBlock Text="Stroke:"/>
                <ComboBox Margin="10,0,0,0" ItemsSource="{Binding SwatchesByName}" SelectedItem="{Binding StrokeSwatch}">
                    <ComboBox.ItemTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <Rectangle Width="25" Fill="{Binding Brush}"/>
                                <TextBlock Margin="10,0,0,0" Text="{Binding Name}"/>
                            </StackPanel>
                        </DataTemplate>
                    </ComboBox.ItemTemplate>
                </ComboBox>
            </StackPanel>

            <Line Margin="10" X1="0" Y1="0" X2="200" Y2="100"
                  Stroke="{Binding StrokeSwatch.Brush}"
                  StrokeThickness="2"/>
        </StackPanel>
    </Window>

C#:

    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Windows;
    using System.Windows.Media;

    namespace WpfApp1 {
        public partial class MainWindow : Window {
            public MainWindow () {
                InitializeComponent ();
            }
        }

        public class ViewModel : INotifyPropertyChanged {
            Swatch strokeSwatch;
            public IEnumerable<Swatch> SwatchesByName { get => Swatches.ByName; }
            public Swatch StrokeSwatch { get => strokeSwatch; set { strokeSwatch = value; RaisePropertyChanged (); } }
            public event PropertyChangedEventHandler PropertyChanged;

            public ViewModel () {
                StrokeSwatch = Swatches.ColorToSwatch (Colors.Red);
            }

            void RaisePropertyChanged ([CallerMemberName] string propertyName = null) {
                PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (propertyName));
            }
        }

        static class Swatches {
            public static IEnumerable<Swatch> ByName { get; }

            static Swatches () {
                ByName = from PropertyInfo pi in typeof (Colors).GetProperties ()
                         orderby pi.Name
                         select new Swatch (pi.Name, (Color) pi.GetValue (null, null));
            }

            public static Swatch ColorToSwatch (Color color) {
                return ByName.First (sw => sw.Color == color);
            }
        }

        public class Swatch {
            SolidColorBrush brush;
            public string Name { get; }
            public Color Color { get; }
            public SolidColorBrush Brush { get { if (brush == null) brush = new SolidColorBrush (Color); return brush; } }

            public Swatch (string name, Color color) {
                Name = name;
                Color = color;
            }
        }
    }
Pavel Anikhouski
  • 21,776
  • 12
  • 51
  • 66
mins
  • 6,478
  • 12
  • 56
  • 75
  • Make sure that the instance held by `StrokeSwatch` is actually contained in the `SwatchesByName` collection. If you can't assure that, overrride the Equals method of the Swatch class. – Clemens Sep 24 '19 at 13:00
  • Or set `SelectedValuePath="Name"` and `SelectedValue="{Binding StrokeSwatch.Name}"` (instead of binding SelectedItem). – Clemens Sep 24 '19 at 13:01
  • @Clemens: I see what you mean (and I suspect also something of this order), but the `StrokeSwatch` is actually a reference to an item of the list (`return ByName.First`) – mins Sep 24 '19 at 13:11
  • 1
    You may also force an initial enumeration by `ByName = ByName.ToList();` in the static Swatches constructor. – Clemens Sep 24 '19 at 13:12
  • 1
    LINQ expressions are always executed deferred. Only calling `ToList()`, `ToArray()` or `ToDictionary()` or any method that returns a single value e.g. `Count()`, `First()` or `Max()` or calling `IEnumerable.GetEnumerator` (e.g. `foreach`) will execute the query expression. – BionicCode Sep 24 '19 at 13:55
  • @BionicCode: Does that mean in the code above, `SwatchesByName` is empty until the combobox is opened (when foreach is needed), and when it is opened and combobox items actually exist, no item can match the object previously assigned to `StrokeSwatch`? If this is the case, that's an interesting one... but on the other end there is a call to `ByName.First` in `ColorToSwatch`, so I don't understand. – mins Sep 24 '19 at 14:04
  • 1
    The important point here is that `ByName.First(sw => sw.Color == color)` enumerates the expression independently from the enumeration done by the ComboBox. Thus you create two distinct sets of Swatch instances, hence the equality check for SelectedItem fails. – Clemens Sep 24 '19 at 14:09
  • @Clemens: Ah ok! That makes sense. Thanks for the helpful explanation. – mins Sep 24 '19 at 14:10
  • Add some debug output to the Swatch constructor. – Clemens Sep 24 '19 at 14:11

2 Answers2

1

Call ToArray() on the IEnumerable<Swatch> to materialize the ByName collection:

static Swatches()
{
    ByName = (from PropertyInfo pi in typeof(Colors).GetProperties()
              orderby pi.Name
              select new Swatch(pi.Name, (Color)pi.GetValue(null, null))).ToArray();
}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Exactly, do you mind providing a short explanation? – mins Sep 24 '19 at 13:14
  • 1
    The `Swatch` returned by `StrokeSwatch` must be present in the `ItemsSource` by the time the framework invokes the getter and performs the actual selection. If you are returning a sequence instead of an actual collection from `ByName`, it won't. – mm8 Sep 24 '19 at 13:16
-1

First of all you should use observableCollection property and return List in your Swatche class

public class ViewModel : INotifyPropertyChanged
{
    Swatch strokeSwatch;

    public ObservableCollection<Swatch> SwatchesByName
    {
        get => _swatchesByName;
        set { _swatchesByName = value; RaisePropertyChanged();}
    }

    private ObservableCollection<Swatch> _swatchesByName;

    public Swatch StrokeSwatch 
    { 
        get => strokeSwatch; 
        set { strokeSwatch = value; RaisePropertyChanged(); }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    public ViewModel()
    {
        SwatchesByName = new ObservableCollection<Swatch>(Swatches.ByName);
        StrokeSwatch = Swatches.ColorToSwatch(Colors.Red);
    }

    void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

static class Swatches
{
    public static List<Swatch> ByName { get; }

    static Swatches()
    {
        var list = from PropertyInfo pi in typeof(Colors).GetProperties()
            orderby pi.Name
            select new Swatch(pi.Name, (Color)pi.GetValue(null, null));
        ByName = list.ToList();
    }

    public static Swatch ColorToSwatch(Color color)
    {
        return ByName.First(sw => sw.Color == color);
    }
}

public class Swatch
{
    SolidColorBrush brush;
    public string Name { get; }
    public Color Color { get; }

    public SolidColorBrush Brush
    {
        get { if (brush == null) brush = new SolidColorBrush(Color); return brush; }
    }

    public Swatch(string name, Color color)
    {
        Name = name;
        Color = color;
    }
}