2

I am having no luck trying to bind a collection of data to my custom control its property. I already have implemented the mechanism for a string property of this control (with a small help from here) and expected the collection type to be as easy. However I cannot make it work again.

Here is my custom control view

<UserControl x:Class="BadaniaOperacyjne.Controls.Matrix"
            mc:Ignorable="d" Name="CustomMatrix"
            d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <!-- ... -->
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <!-- ... -->
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ElementName=CustomMatrix, Path=Title}"/>
        <Grid Grid.Row="2" Grid.Column="1" Name="contentGrid">
            <ListBox ItemsSource="{Binding ElementName=CustomMatrix, Path=ItemsList}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding}"/>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    </Grid>
</UserControl>

and its code-behind

#region ItemsList Property
public static readonly DependencyProperty ItemsListProperty =
    DependencyProperty.Register("ItemsList", typeof(ObservableCollection<object>), typeof(Matrix), new PropertyMetadata(new ObservableCollection<object>(), new PropertyChangedCallback(ItemsListChanged)));
public ObservableCollection<object> ItemsList
{
    get { return GetValue(ItemsListProperty) as ObservableCollection<object>; }
    set { SetValue(ItemsListProperty, value); }
}
private void ItemsListChanged(object value)
{
    System.Diagnostics.Debug.WriteLine("matrix: items list changed " + value);
    if (ItemsList != null)
    {
        ItemsList.CollectionChanged += ItemsList_CollectionChanged;
        System.Diagnostics.Debug.WriteLine("got " + string.Join(",", ItemsList.ToList()));
    }
    else
    {
        System.Diagnostics.Debug.WriteLine("got null");
    }
}

void ItemsList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
    System.Diagnostics.Debug.WriteLine("matrix: current items list collection changed");
}
private static void ItemsListChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    // a breakpoint
    ((Matrix)d).ItemsListChanged(e.NewValue);
}
#endregion

// showing the Title property implementation just to state that
// it is done the same way as for ItemsList
#region Title Property
public static readonly DependencyProperty TitleProperty = 
    DependencyProperty.Register("Title", typeof(string), typeof(Matrix), new PropertyMetadata("", new PropertyChangedCallback(TitleChanged)));
public string Title
{
    get { return (string)GetValue(TitleProperty); }
    set { SetValue(TitleProperty, value); }
}
private void TitleChanged(string title)
{
    System.Diagnostics.Debug.WriteLine("matrix: title changed to: " + title);
}
private static void TitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    ((Matrix)d).TitleChanged((string)e.NewValue);
}
#endregion

And here's how I am trying to bind to that control

<custom:Matrix x:Name="customMatrix" DockPanel.Dock="Top" Title="{Binding Title}" ItemsList="{Binding Items}"/>

and the code-behind for the main page is

//internal ObservableCollection<List<int>> ItemsList { get; set; }
public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ViewModel()
    {
        Items = new ObservableCollection<int> { 1, 2, 3, 4, 5, 6};
    }

    void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("problem manager: items list changed " + e.NewItems.Count);
    }

    public ObservableCollection<int> Items { get; set; }

    protected string title;
    public string Title
    {
        get { return title; }
        set
        {
            if (title != value)
            {
                title = value;
                NotifyPropertyChanged("Title");
            }
        }
    }

    protected void NotifyPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
}

public ViewModel VM { get; private set; }

// this is the window constructor
private ProblemManager() 
{
    VM = new ViewModel();

    DataContext = VM;
    InitializeComponent();

    VM.Title = "title";
}

private int i = 0;
private void btnAddRow_Click(object sender, RoutedEventArgs e)
{
    // when doing either of these two lines below, 
    // the control breakpoint is never hit
    VM.Items.Add(++i);
    VM.Items = new ObservableCollection<int> { 2, 3 };

    // however, when directly assigning to the control's property, 
    // the event is raised and the breakpoint is hit and the UI is updated
    customMatrix.ItemsList = new ObservableCollection<object> { 1, 2, 3 };
    customMatrix.ItemsList.Add(66);
    // and this of course makes the control's title update
    VM.Title = (++i).ToString();
}

Both DependencyPropertys for the control's Title and ItemsList are, I believe, created the same way. Nonetheless, the binding is probably not working as the ItemsListChanged event is not raised by that binding. So, the problem is that I cannot bind my window's ViewModel.Items collection via XAML to the control's ItemsList collection. Is creating a DependencyProperty for a collection within a control any different from DependencyProperty for a simple string property?

patryk
  • 651
  • 12
  • 30
  • Binding with `Title` is working or that too isn't working? – Rohit Vats Dec 07 '13 at 21:12
  • Yes, it not only changes the property value but also refreshes the UI. The only not working thing for now is the `ItemsList` binding :( – patryk Dec 07 '13 at 21:13
  • check out the name difference one place its capital c and other its small c in CustomMatrix – yo chauhan Dec 07 '13 at 21:14
  • You mean the inner xaml name for the control and the Window's name for `custom:Matrix`? Does that matter? Still the other binding works. – patryk Dec 07 '13 at 21:17
  • i mean "{Binding ElementName=CustomMatrix should be "{Binding ElementName=customMatrix because the name is customMatrix – yo chauhan Dec 07 '13 at 21:19
  • 1
    These are not related I believe. The capital C is for the control's inner code while the lowercase is for the Window. Putting what @RohitVats told me to do makes it work after all. – patryk Dec 07 '13 at 21:23

2 Answers2

3

Problem is in your DependencyProperty registration. Co-variance is not applicable for generic lists i.e. you cannot do this -

ObservableCollection<object> objects = new ObservableCollection<int>();

You have declared type of DP as ObservableCollection<object> but binding it with list of type ObservableCollection<int>.

You should change either type of DP to ObservableCollection<int> OR change binding collection type to ObservableCollection<object>.

public ViewModel()
{
    Items = new ObservableCollection<object> { 1, 2, 3, 4, 5, 6};
}

public ObservableCollection<object> Items { get; set; }
Rohit Vats
  • 79,502
  • 12
  • 161
  • 185
  • This solved my problem but if I get this right, it is not possible to bind any of type to the control, I mean I cannot interchargeably bind to this a collection of strings or integers. Or is it somehow possible? – patryk Dec 07 '13 at 21:25
  • No `ObservableCollection` cannot be binded to `ObservableCollection`. Instead you can make return type to be `IEnumerable` which can be binded to any collection. – Rohit Vats Dec 07 '13 at 21:29
  • Even `ItemsSource` property of `ItemsControl` is of type `IEnumerable`. – Rohit Vats Dec 07 '13 at 21:29
  • But does that `IEnumerable` then need to be parametrized to `object` also? If so, then it's not much of a help, I'd rather I could have various collection content types in the same kind of collection :( – patryk Dec 07 '13 at 21:35
  • No you don't have to use parametrized version since all collections inherit from `IEnumerable` so it will work. – Rohit Vats Dec 07 '13 at 21:41
  • I meant I probably can't bind `IEnumerable` to `IEnumerable`, can I? Then how is this possible for built-in controls to be able to be bound to `List` and `List`? It is possible, isn't it? – patryk Dec 07 '13 at 21:45
  • I was saying to use `non-generic` version of `IEnumerable` and not generic version i.e. `IEnumerable`. And for another question yes `IEnumerable` binding will work with `IEnumerable` but its not true for list. However, in your property changed of property you are hooking onto CollectionChanged which sadly is provided by ObservableCollection only and not by IEnumerable. So, you have to trade-off some functionality in case you want it to use single list for all sort of collections. – Rohit Vats Dec 07 '13 at 21:50
  • Say we can drop the benefits of `ObservableCollection`. So having the control's list property of type `IEnumerable`, can I bind to that property something of e.g. `List` or `[any-collection]`? – patryk Dec 07 '13 at 21:56
  • Yeah you can since co-variance is supported for IEnumerable. So, that will work.. – Rohit Vats Dec 07 '13 at 21:58
  • Then it is pretty weird that it doesn't work with `ObservableCollection` being bound to `IEnumerable` :( I mean the binding only and nothing of `ObservableCollection` features that we drop. – patryk Dec 07 '13 at 22:01
  • No it should work. Can you post separate question with your relevant code replicating an issue? – Rohit Vats Dec 07 '13 at 22:07
  • http://stackoverflow.com/questions/20447404/binding-a-observablecollectionint-to-ienumerableobject-of-a-custom-control I just did – patryk Dec 07 '13 at 22:18
0

After such a long time, I kind of needed this to work in both ways:

  1. Be able to define a collection of any kind of items
  2. Be informed when an item was added/removed and so on.

I am working on a bread crumb like control to be more precise.

Indeed, the ItemsSource DP is defined with an IList type, to allow the "genericity" aforementioned:

public static readonly DependencyProperty ItemsSourceProperty =
  DependencyProperty.Register("ItemsSource", typeof(IList), typeof(BreadCrumbUserControl), 
    new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));

And here is how I use my BreadCrumb:

<views:BreadCrumbUserControl ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>

where Items is an ObservableCollection of a custom type:

public ObservableCollection<BaseItem> Items { get; set; }

Indeed it works fine, my ItemsSource local property points to the right collection but I need to be informed in my UserControl when an item is added to the ObservableCollection.

So, I've made use of the PropertyChangedCallback delegate on my ItemsSource DP.

private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  BreadCrumbUserControl thisUserControl = (BreadCrumbUserControl)d;
  (e.NewValue as INotifyCollectionChanged).CollectionChanged += thisUserControl.BreadCrumbUserControl_CollectionChanged;
}

This is the trick that worked for me, because my input reference is in fact the ObservableCollection defined in the upper layers.

Both goals are now achieved: work with collection of any custom types and in the same type, stay informed.

If a protocol is set, reflection can be used to change values on our custom type.

Here is just an example of iterating on my collection which has some unknown type in my UserControl's world:

foreach (var baseItem in ItemsSource)
{
   baseItem.GetType().GetProperty("IsActive").SetValue(baseItem, false);
}
Olaru Mircea
  • 2,570
  • 26
  • 49