DependencyProperty没有在NotifyCollectionChanged事件上通知UI

本文关键字:通知 UI 事件 NotifyCollectionChanged DependencyProperty | 更新日期: 2023-09-27 18:08:01

我正在使用一个自定义控件,它有一个选定的项目依赖属性,我已经连接了集合更改事件,但UI没有通知,PropertyChanged事件始终为空。通常我会说这是一个数据上下文问题。但是我不能更改控件上的数据上下文,因为没有数据将显示。

    public ObservableCollection<object> SelectedItems
    {
        get { return (ObservableCollection<object>)GetValue(SelectedItemsProperty); }
        set { SetValue(SelectedItemsProperty, value); }
    }
    // Using a DependencyProperty as the backing store for SelectedItems.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.RegisterAttached("SelectedItems", typeof(ObservableCollection<object>), 
        typeof(MultiSelectComboBox), 
        new System.Windows.PropertyMetadata(new ObservableCollection<object>(), new PropertyChangedCallback(SelectedItemsPropertyChanged)));
    private static void SelectedItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        MultiSelectComboBox relay = d as MultiSelectComboBox;
        if (e.OldValue != null)
        {
            var coll = (INotifyCollectionChanged)e.OldValue;
            coll.CollectionChanged -= relay.SelectedItemsCollectionChanged;
        }
        if (e.NewValue != null)
        {
            var coll = (INotifyCollectionChanged)e.NewValue;
            coll.CollectionChanged += relay.SelectedItemsCollectionChanged;
        }
    }

上面是属性声明,它在xaml中绑定到ViewModel上的ObservableCollection。我错过了什么?控件实现了INotifyPropertyChanged。

我在下面添加了更多的代码。这是原始代码,我想将相同的属性更改为依赖属性,以便绑定到集合。

    namespace Kepler.SilverlightControls.MultiSelectComboBox
    {
/// <summary>
/// MultiSelect ComboBox
/// </summary>
public class MultiSelectComboBox : Telerik.Windows.Controls.RadComboBox,    INotifyPropertyChanged
{
    #region Events
    /// <summary>
    /// Est appelé quand une propriété change
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion
    #region Constructor
    /// <summary>
    /// Initializes a new instance of MultiSelectComboBox
    /// </summary>
    public MultiSelectComboBox()
    {
        ClearSelectionButtonVisibility = Visibility.Collapsed;
        string xaml = @"<DataTemplate 
            xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
            xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
            xmlns:local=""clr-namespace:Kepler.SilverlightControls.MultiSelectComboBox;assembly=Kepler.SilverlightControls"">
            <TextBlock TextWrapping=""Wrap"" local:MultiSelectComboBoxService.SelectionBoxLoaded=""True"" />
            </DataTemplate>";
        var selectionBoxTemplate = (DataTemplate)XamlReader.Load(xaml);
        SelectionBoxTemplate = selectionBoxTemplate;
        EmptySelectionBoxTemplate = selectionBoxTemplate;
        xaml = @"<DataTemplate 
            xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
            xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""    
            xmlns:local=""clr-namespace:Kepler.SilverlightControls.MultiSelectComboBox;assembly=Kepler.SilverlightControls"">
            <CheckBox local:MultiSelectComboBoxService.ComboBoxItemLoaded=""True"" 
                IsChecked=""{Binding Path=(local:MultiSelectComboBoxService.IsChecked), Mode=TwoWay, RelativeSource={RelativeSource Self}}"" />
            </DataTemplate>";
        ItemTemplate = (DataTemplate)XamlReader.Load(xaml);
    }
    #endregion
    #region Propriétés
    /// <summary>
    /// IsCheckedBindingPath Property
    /// </summary>
    public string IsCheckedBindingPath
    {
        get { return (string)GetValue(IsCheckedBindingPathProperty); }
        set { SetValue(IsCheckedBindingPathProperty, value); }
    }
    // Using a DependencyProperty as the backing store for IsCheckedBindingPath.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsCheckedBindingPathProperty =
        DependencyProperty.Register("IsCheckedBindingPath", typeof(string), typeof(MultiSelectComboBox), new PropertyMetadata(null, (obj, e) =>
        {
            Telerik.Windows.Controls.TextSearch.SetTextPath(obj, e.NewValue as string);
        }));

    /// <summary>
    /// DisplayBindingPath Property
    /// </summary>
    public static readonly DependencyProperty DisplayBindingPathProperty = DependencyProperty.Register(
       "DisplayBindingPath", typeof(string), typeof(MultiSelectComboBox), new PropertyMetadata(null, (obj, e) =>
       {
           Telerik.Windows.Controls.TextSearch.SetTextPath(obj, e.NewValue as string);
       }));
    /// <summary>
    /// Gets or sets the display member path (we can't reuse DisplayMemberPath property)
    /// </summary>
    public string DisplayBindingPath
    {
        get { return GetValue(DisplayBindingPathProperty) as string; }
        set { SetValue(DisplayBindingPathProperty, value); }
    }
    private ObservableCollection<object> _selectedItems;
    /// <summary>
    /// Gets the selected items
    /// </summary>
    public ObservableCollection<object> SelectedItems
    {
        get
        {
            if (_selectedItems == null)
            {
                _selectedItems = new ObservableCollection<object>();
                _selectedItems.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedItemsCollectionChanged);
            }
            return _selectedItems;
        }
    }
    private ObservableCollection<object> _selectedValues;
    /// <summary>
    /// Gets the selected values
    /// </summary>
    public ObservableCollection<object> SelectedValues
    {
        get
        {
            if (_selectedValues == null)
            {
                _selectedValues = new ObservableCollection<object>();
                _selectedValues.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedValuesCollectionChanged);
            }
            return _selectedValues;
        }
    }
    #endregion
    #region Methods
    /// <summary>
    /// Called when the Items property changed
    /// </summary>
    /// <param name="e">change informations</param>
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);
        int idx;
        var selectedItems = SelectedItems;
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
            case NotifyCollectionChangedAction.Replace:
            case NotifyCollectionChangedAction.Reset:
                var items = e.NewItems;
                if (items == null)
                {
                    var selected = new List<object>();
                    foreach (var item in this.ItemsSource)
                    {
                        PropertyInfo isCheckedBindingPathProperty = this.IsCheckedBindingPath != null ? item.GetType().GetProperty(this.IsCheckedBindingPath) : null;
                        if (isCheckedBindingPathProperty != null
                            && (bool)isCheckedBindingPathProperty.GetValue(item,null) == true)
                        {
                            selected.Add(item);
                            SelectedValues.Add(item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
                        }
                    }
                    items = selected;
                }
                if (items != null)
                {
                    foreach (object value in SelectedValues)
                    {
                        foreach (object item in items)
                        {
                            if (GetSelectedValue(item).Equals(value) && !selectedItems.Contains(item))
                            {
                                selectedItems.Add(item);
                            }
                        }
                    } 
                }
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (object item in e.OldItems)
                {
                    idx = selectedItems.IndexOf(item);
                    if (idx >= 0)
                    {
                        selectedItems.RemoveAt(idx);
                    }
                }
                break;
        }
    }
    private void RemoveCollectionChangedEvents()
    {
        SelectedItems.CollectionChanged -= new NotifyCollectionChangedEventHandler(SelectedItemsCollectionChanged);
        SelectedValues.CollectionChanged -= new NotifyCollectionChangedEventHandler(SelectedValuesCollectionChanged);
    }
    private void AddCollectionChangedEvents()
    {
        SelectedItems.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedItemsCollectionChanged);
        SelectedValues.CollectionChanged += new NotifyCollectionChangedEventHandler(SelectedValuesCollectionChanged);
    }
    private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (SelectedValuePath != null)
        {
            RemoveCollectionChangedEvents();
            try
            {
                switch (e.Action)
                {
                    case NotifyCollectionChangedAction.Add:
                        AddSelectedValues(e.NewItems);
                        break;
                    case NotifyCollectionChangedAction.Remove:
                        RemoveSelectedValues(e.OldItems);
                        break;
                    case NotifyCollectionChangedAction.Replace:
                        RemoveSelectedValues(e.OldItems);
                        AddSelectedValues(e.NewItems);
                        break;
                    case NotifyCollectionChangedAction.Reset:
                        SelectedValues.Clear();
                        foreach (object item in Items)
                        {
                            UpdateSelectedItem(item, false);
                        }
                        AddSelectedValues(e.NewItems);
                        break;
                }
            }
            finally
            {
                AddCollectionChangedEvents();
            }
        }
        RaiseSelectedItemsPropertyChanged();
    }
    private void RemoveSelectedValues(IList items)
    {
        foreach (var item in items)
        {
            SelectedValues.Remove(GetSelectedValue(item));
            UpdateSelectedItem(item, false);
        }
    }
    private void AddSelectedValues(IList items)
    {
        if (items != null)
        {
            object selectedValue;
            foreach (var item in items)
            {
                selectedValue = GetSelectedValue(item);
                if (!SelectedValues.Contains(selectedValue))
                {
                    SelectedValues.Add(selectedValue);
                }
                UpdateSelectedItem(item, true);
            }
        }
    }
    private object GetSelectedValue(object item)
    {
        return DataControlHelper.GetPropertyInfo(item.GetType(), SelectedValuePath).GetValue(item, null);
    }
    private void SelectedValuesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        RemoveCollectionChangedEvents();
        try
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    AddSelectedItems(e.NewItems);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    RemoveSelectedItems(e.OldItems);
                    break;
                case NotifyCollectionChangedAction.Replace:
                    RemoveSelectedItems(e.OldItems);
                    AddSelectedItems(e.NewItems);
                    break;
                case NotifyCollectionChangedAction.Reset:
                    var selectedItems = SelectedItems.ToList();
                    SelectedItems.Clear();
                    foreach (object item in selectedItems)
                    {
                        UpdateSelectedItem(item, false);
                    }
                    AddSelectedItems(e.NewItems);
                    break;
            }
        }
        finally
        {
            AddCollectionChangedEvents();
        }
        RaiseSelectedItemsPropertyChanged();
    }
    private void RaiseSelectedItemsPropertyChanged()
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedItems"));
            // To update the selection box
        }
    }
    private void RemoveSelectedItems(IList values)
    {
        object item;
        foreach (var value in values)
        {
            item = SelectedItems.FirstOrDefault(e => GetSelectedValue(e).Equals(value));
            if (item != null)
            {
                SelectedItems.Remove(item);
                UpdateSelectedItem(item, false);
            }
        }
    }
    private void AddSelectedItems(IList values)
    {
        if (values != null)
        {
            object item;
            foreach (var value in values)
            {
                item = Items.FirstOrDefault(e => GetSelectedValue(e).Equals(value));
                if (item != null)
                {
                    SelectedItems.Add(item);
                    UpdateSelectedItem(item, true);
                }
            }
        }
    }
    private void UpdateSelectedItem(object item, bool select)
    {
        var obj = ItemContainerGenerator.ContainerFromItem(item);
        if (obj != null)
        {
            var cb = obj.FindChildByType<CheckBox>();
            if (cb != null && cb.IsChecked != select)
            {
                cb.IsChecked = select;
            }
        }
    }
    /// <summary>
    /// Create a new ComboBox item
    /// </summary>
    /// <returns>a new ComboBox item</returns>
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new MultiSelectComboBoxItem(this);
    }
    protected override void OnKeyDown(System.Windows.Input.KeyEventArgs e)
    {
        base.OnKeyDown(e);
        IsDropDownOpen = true;
    }
    #endregion
}

}

服务分类如下:

    namespace Kepler.SilverlightControls.MultiSelectComboBox
    {
/// <summary>
/// Service for the MultiSelect comboBox
/// </summary>
public static class MultiSelectComboBoxService
{
    /// <summary>
    /// IsChecked property
    /// </summary>
    public static DependencyProperty IsCheckedProperty = DependencyProperty.RegisterAttached("IsChecked",
        typeof(bool), typeof(MultiSelectComboBoxService), new PropertyMetadata(false, (obj, e) =>
        {
            MultiSelectComboBoxItem comboBoxItem = obj.GetVisualParent<MultiSelectComboBoxItem>();
            if (comboBoxItem != null)
            {
                MultiSelectComboBox comboBox = comboBoxItem.ParentComboBox;
                var selectedItems = (IList)comboBox.SelectedItems;
                object item = comboBoxItem.DataContext;
                PropertyInfo isCheckedBindingPathProperty = item.GetType().GetProperty(comboBox.IsCheckedBindingPath);
                isCheckedBindingPathProperty.SetValue(item, e.NewValue,null);
                if ((bool)e.NewValue)
                {
                    if (!selectedItems.Contains(item))
                    {
                        selectedItems.Add(item);
                    }
                }
                else
                {
                    selectedItems.Remove(item);
                }
            }
        }));
    /// <summary>
    /// Gets a value indicating if the object is checked or not
    /// </summary>
    /// <param name="obj">DependencyObject</param>
    /// <returns>a value indicating if the object is checked or not</returns>
    public static bool GetIsChecked(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsCheckedProperty);
    }
    /// <summary>
    /// Sets a value indicating if the object is checked or not
    /// </summary>
    /// <param name="obj">DependencyObject</param>
    /// <param name="value">the value indicating if the object is checked or not</param>
    public static void SetIsChecked(DependencyObject obj, bool value)
    {
        obj.SetValue(IsCheckedProperty, value);
    }
    /// <summary>
    /// SelectionBoxLoaded property called on SelectionBox load
    /// </summary>
    public static DependencyProperty SelectionBoxLoadedProperty = DependencyProperty.RegisterAttached("SelectionBoxLoaded",
        typeof(bool), typeof(MultiSelectComboBoxService), new PropertyMetadata(false, (obj, e) =>
        {
            TextBlock targetElement = obj as TextBlock;
            if (targetElement != null)
            {
                targetElement.Loaded += new RoutedEventHandler(targetElement_Loaded);
            }
        }));
    private static void targetElement_Loaded(object sender, RoutedEventArgs e)
    {
        TextBlock targetElement = (TextBlock)sender;
        targetElement.Loaded -= new RoutedEventHandler(targetElement_Loaded);
        MultiSelectComboBox comboBox = targetElement.GetVisualParent<MultiSelectComboBox>();
        if (comboBox != null)
        {
            targetElement.SetBinding(TextBlock.TextProperty, new Binding("SelectedItems")
            {
                Converter = new MultiSelectComboxConverter(),
                Source = comboBox,
                ConverterParameter = comboBox.DisplayBindingPath
            });
        }
    }
    /// <summary>
    /// Gets the value indicating if the object is loaded or not
    /// </summary>
    /// <param name="obj">DependencyObject</param>
    /// <returns>the value indicating if the object is loaded or not</returns>
    public static bool GetSelectionBoxLoaded(DependencyObject obj)
    {
        return (bool)obj.GetValue(SelectionBoxLoadedProperty);
    }
    /// <summary>
    /// Sets the value indicating if the object is loaded or not
    /// </summary>
    /// <param name="obj">DependencyObject</param>
    /// <param name="value">the value indicating if the object is loaded or not</param>
    public static void SetSelectionBoxLoaded(DependencyObject obj, bool value)
    {
        obj.SetValue(SelectionBoxLoadedProperty, value);
    }
    /// <summary>
    /// ComboBoxItemLoaded called on ComboBoxItem load
    /// </summary>
    public static DependencyProperty ComboBoxItemLoadedProperty = DependencyProperty.RegisterAttached("ComboBoxItemLoaded",
        typeof(bool), typeof(MultiSelectComboBoxService), new PropertyMetadata(false, (obj, e) =>
        {
            CheckBox targetElement = obj as CheckBox;
            if (targetElement != null)
            {
                targetElement.Loaded += new RoutedEventHandler(comboBoxItem_Loaded);
                targetElement.SetBinding(MultiSelectComboBoxService.DataContextProperty, new Binding());
            }
        }));
    private static void comboBoxItem_Loaded(object sender, RoutedEventArgs e)
    {
        FrameworkElement element = (FrameworkElement)sender;
        MultiSelectComboBox comboBox = GetComboBox(element);
        if (comboBox != null)
        {
            element.SetBinding(CheckBox.ContentProperty, new Binding(comboBox.DisplayBindingPath));
            //Binding binding = new Binding(comboBox.IsCheckedBindingPath);
            //binding.Mode = BindingMode.TwoWay;
            //element.SetBinding(CheckBox.IsCheckedProperty, binding);
        }
    }
    /// <summary>
    ///Gets the value indicating if the item is loaded or not
    /// </summary>
    /// <param name="obj">DependencyObject</param>
    /// <returns>the value indicating if the item is loaded or not</returns>
    public static bool GetComboBoxItemLoaded(DependencyObject obj)
    {
        return (bool)obj.GetValue(ComboBoxItemLoadedProperty);
    }
    /// <summary>
    /// Sets the value indicating if the item is loaded or not
    /// </summary>
    /// <param name="obj">DependencyObject</param>
    /// <param name="value">the value indicating if the item is loaded or not</param>
    public static void SetComboBoxItemLoaded(DependencyObject obj, bool value)
    {
        obj.SetValue(ComboBoxItemLoadedProperty, value);
    }
    private static MultiSelectComboBox GetComboBox(DependencyObject targetElement)
    {
        MultiSelectComboBoxItem item = targetElement.GetVisualParent<MultiSelectComboBoxItem>();
        if (item != null)
        {
            return item.ParentComboBox;
        }
        return null;
    }
    private static DependencyProperty DataContextProperty = DependencyProperty.RegisterAttached("DataContext",
        typeof(object), typeof(MultiSelectComboBoxService), new PropertyMetadata(null, (obj, e) =>
        {
            CheckBox checkBox = (CheckBox)obj;
            MultiSelectComboBox comboBox = GetComboBox(checkBox);
            if (comboBox != null)
            {
                checkBox.IsChecked = comboBox.SelectedItems.Contains(checkBox.DataContext);
            }
        }));
    private static object GetDataContext(DependencyObject obj)
    {
        return obj.GetValue(DataContextProperty);
    }
    private static void SetDataContext(DependencyObject obj, object value)
    {
        obj.SetValue(DataContextProperty, value);
    }
}

}

转换器如下:

    namespace Kepler.SilverlightControls.MultiSelectComboBox
    {
#region Méthodes
public class MultiSelectComboxConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string displayMemberPath = parameter as string;
        if (String.IsNullOrWhiteSpace(displayMemberPath))
        {
            return String.Empty;
        }
        PropertyInfo propertyInfo;
        return string.Join(", ", (value as IEnumerable<object>).Select(item =>
            {
                propertyInfo = DataControlHelper.GetPropertyInfo(item.GetType(), displayMemberPath);
                if (propertyInfo == null)
                {
                    return String.Empty;
                }
                return propertyInfo.GetValue(item, null);
            }).ToArray());
    }
    /// <summary>
    /// Not implemented
    /// </summary>
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
#endregion
}

}

DependencyProperty没有在NotifyCollectionChanged事件上通知UI

您不能将依赖属性注册为附加属性,而应将其注册为常规依赖属性。此外,您不应该使用new ObservableCollection<object>()作为默认属性值,因为这将在您的MultiSelectComboBox的所有实例上使用相同的集合实例作为属性的默认值。

public static readonly DependencyProperty SelectedItemsProperty =
    DependencyProperty.Register( // Register instead of RegisterAttached
        "SelectedItems",
        typeof(ObservableCollection<object>), 
        typeof(MultiSelectComboBox), 
        new PropertyMetadata(SelectedItemsPropertyChanged)); // no default value

我还建议不要使用ObservableCollection<object>作为属性类型,而是简单地使用ICollectionIEnumerable。这将允许在具体集合类型中实现INotifyCollectionChanged

public static readonly DependencyProperty SelectedItemsProperty =
    DependencyProperty.Register(
        "SelectedItems",
        typeof(ICollection),
        typeof(MultiSelectComboBox),
        new PropertyMetadata(SelectedItemsPropertyChanged));
public ICollection SelectedItems
{
    get { return (ICollection)GetValue(SelectedItemsProperty); }
    set { SetValue(SelectedItemsProperty, value); }
}
private static void SelectedItemsPropertyChanged(
    DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
    var comboBox = (MultiSelectComboBox)obj;
    var oldCollection = e.OldValue as INotifyCollectionChanged;
    var newCollection = e.NewValue as INotifyCollectionChanged;
    if (oldCollection != null)
    {
        oldCollection.CollectionChanged -= SelectedItemsCollectionChanged;
    }
    if (newCollection != null)
    {
        newCollection.CollectionChanged += SelectedItemsCollectionChanged;
    }
}
private static void SelectedItemsCollectionChanged(
    object sender, NotifyCollectionChangedEventArgs e)
{
    switch (e.Action)
    {
        ...
    }
}

还请注意,MultiSelectComboBox不需要实现INotifyPropertyChanged