简体   繁体   中英

Binding a collection to a custom control property

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 DependencyProperty s 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?

Problem is in your DependencyProperty registration. Co-variance is not applicable for generic lists ie 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; }

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);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM