mbaytas
mbaytas

Reputation: 3024

Binding an ObservableCollection<int> that holds indexes to a ListBox of CheckBoxes

In my C# / WPF / .NET 4.5 project, I have an ObservableCollection<int> which holds indexes. I also have a ListBox with its ItemsPanelTemplate set to a UniformGrid (with 20 rows and 20 columns) and its ItemTemplate set as a CheckBoxes.

I want the ObservableCollection<int> to hold the indexes of the CheckBoxes that are checked. This should be accomplished via a TwoWay binding: When an int is added or removed from the ObservableCollection, CheckBoxes in the UniformGrid should become checked or unchecked; and when I check or uncheck a CheckBox, its index should be added to or removed from the ObservableCollection.

I tried to accomplish this using a converter in the following manner:

XAML:

<ListBox
    x:Name="myListBox"
    SelectionMode="Multiple"
    ItemsSource="{Binding Cells, Converter={StaticResource cellsConverter}}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Rows="20" Columns="20"></UniformGrid>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <CheckBox IsChecked="{Binding}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

The class that contains the ObservableCollection<int>:

public class Frame : INotifyPropertyChanged {
    private ObservableCollection<int> _cells;

    public ObservableCollection<int> Cells {
      get { return _cells; }
      set {
        _cells = value;
        OnPropertyChanged("Cells");
      }
    }

    public Frame() {
      Cells = new ObservableCollection<int>();
    }

    // Event handler for INotifyPropertyChanged 
    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged(string propName) {
      if (PropertyChanged != null) {
        PropertyChanged(this, new PropertyChangedEventArgs(propName));
      }
    }
  }

Converter:

[ValueConversion(typeof(ObservableCollection<int>), typeof(ObservableCollection<bool>))]
public class CellsConverter : IValueConverter {

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
        ObservableCollection<int> ints = (ObservableCollection<int>)value;
        ObservableCollection<bool> bools = new ObservableCollection<bool> ();

        for (int i = 0; i < 400; i++) {
            bool b = new bool();
            if (ints.Contains(i)) b = true;
            else b = false;
            bools.Add(b);
        }

       return bools;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
        ObservableCollection<bool> bools = (ObservableCollection<bool>)value;
        ObservableCollection<int> ints = new ObservableCollection<int>();

        foreach (bool b in bools) {
            if (b == true) ints.Add(bools.IndexOf(b));
        }

       return ints;
    }
}

This is possibly not ideal method for accomplishing this, and in that case I would appreciate if anyone proposes a different method.

When I run this, I get a XamlParseException saying that "Two-way binding requires Path or XPath." I guess the issue is similar to the one posted here, but I could not figure out how to apply the solutions there to my case where I'm using a converter.

Questions:

EDIT

For clarity, an illustration of what I want to achieve:

ParentOfMyListBox.DataContext = new Frame();

The appearance of myListBox:

indexes.Add(0);
indexes.Add(3);

indexes.Remove(0);

Then if I manually check one of the cells...

Now indexes contains 3 and 8.

Upvotes: 0

Views: 2112

Answers (1)

Erti-Chris Eelmaa
Erti-Chris Eelmaa

Reputation: 26338

Remember, KISS - Keep it simple, stupid.

1) Create a wrapper class, that holds:

  • IsChecked boolean property
  • Value integer property

In XAML, bind against IsChecked property:

<ListBox
    SelectionMode="Multiple"
    ItemsSource="{Binding Cells}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Rows="20" Columns="20"></UniformGrid>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <CheckBox IsChecked="{Binding IsChecked}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Now, if you need to get ObservableCollection; you can use simple LINQ query:

new ObservableCollection<int>(Cells.Where(x= x.IsChecked).Select(x => x.Value));

The converter hack that shouldn't be used, but can give you ideas to implement it without one:

private void UpdateUnderlyingCollection(
    ObservableCollection<int> underlyingCollection , 
    int element, 
    bool isChecked)
{
    if (!isChecked)
        underlyingCollection.Remove(element);
    else if(!underlyingCollection.Contains(element))
        underlyingCollection.Add(element);
}

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    // todo; weak references
    var underlyingCollection = (ObservableCollection<int>)value;
    var lookup = underlyingCollection.ToLookup(x => x);

    var presentableCollection = new ObservableCollection<Wrap>();
    foreach (var i in Enumerable.Range(0, 400))
    {
        var damnLambdaYuNoCaptureByValue = i;
        var wrap = new Wrap { IsChecked = lookup.Contains(i)};
        wrap.PropertyChanged += (sender, args) =>
            {
                if(args.PropertyName == "IsChecked")
                    UpdateUnderlyingCollection(
                       underlyingCollection, 
                       damnLambdaYuNoCaptureByValue, 
                       wrap.IsChecked);
            };

        presentableCollection.Add(wrap);
    }

    return presentableCollection;
}

Upvotes: 3

Related Questions