How to properly fire off CollectionChanged Notification from contained object(s)?
Here is the long-winded explanation to what is essentially a simple problem. I am using a Telerilk RadDropDownButton, which displays a list Items with checkboxes.
<Controls:RadDropDownButton AutoOpenDelay="0:0:0.0" x:Name="Urgency" VerticalAlignment="Center" Width="150" Content="{Binding Path=ItemsSource, ElementName=UrgencyList, Mode=TwoWay, Converter={StaticResource ButtonTextConverter}}" HorizontalContentAlignment="Left">
<Controls:RadDropDownButton.DropDownContent>
<ListBox x:Name="UrgencyList">
<ListBox.ItemTemplate>
<DataTemplate>
<StackP开发者_StackOverflow中文版anel Orientation="Horizontal">
<CheckBox Content="{Binding Name}" ClickMode="Press" IsChecked="{Binding IsChecked, Mode=TwoWay}" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Controls:RadDropDownButton.DropDownContent>
</Controls:RadDropDownButton>
As you can see, I bound the Content property to a Converter. What I want is, if nothing is selected, for the content to read "All", and if something is checked, to show a list of the # of selected (Checked) items.
public class ButtonTextConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Debug.WriteLine("I'm Binding");
int numChecked = 0;
if (value != null)
numChecked = ((ObservableCollection<UrgencyItem>) value).Count(urgencyItem => urgencyItem.IsChecked);
return numChecked > 0 ? string.Format("{0} Items Selected", numChecked) : "All";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
The class I am binding to implements INotifyPropertyChanged, as required. Partial listing here:
public class UrgencyItem : INotifyPropertyChanged
{
private int _id;
private bool _isChecked;
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
NotifyPropertyChanged("Name");
}
}
And I bind the ListBox to the Data in the code-behind, like this:
private void SearchParamsVertical_Loaded(object sender, RoutedEventArgs e)
{
urgencyList = new ObservableCollection<UrgencyItem>
{
new UrgencyItem {ID = 1, IsChecked = false, Name = "Non Emergent"},
new UrgencyItem {ID = 2, IsChecked = false, Name = "Emergent"},
new UrgencyItem {ID = 3, IsChecked = false, Name = "Stat Emergent"},
new UrgencyItem {ID = 4, IsChecked = false, Name = "Stroke Protocol"}
};
urgencyList.CollectionChanged += urgencyList_CollectionChanged;
UrgencyList.ItemsSource = urgencyList;
}
SO HERE'S THE PROBLEM...
When a checkbox is checked, the value of Content should update. It is not.
The reason it's not is because, although the notification is firing that the IsChecked was changed, that notification is basically going nowhere. The UrgencyItem object has no idea that it is part of an ObservableCollection. And the thing about an ObservableCollection is that it only sends notifications to it's binding when items are ADDED/REMOVED to/from the collection. In other words, changing the property of an item in the collection does not fire the CollectionChanged event, because no objects were added/removed.
What I need to do is have the collectionChanged event fire when I modify a property of the collection. I used to know how to do this, but too much time away from Silverlight and I've forgotten how.
Anyone?
In brief, I think your diagnosis is correct: you don't normally get a CollectionChanged notification if an object in the ObservableCollection changes, even if that object implements INotifyPropertyChanged. So far as I'm aware, there isn't a straightforward way to get the behavior you're wanting through the built-in Silverlight classes.
There are three possible ways to address this that I'm aware of:
(1) One option would be to create your own collection for urgencyList, inheriting from ObservableCollection, that implements this behavior, i.e., it subscribes to the INPC notification of each object that is added to the collection, and fires a CollectionChanged event when that happens.
(2) A second alternative is to use something like the ReactiveUI framework, which has its own ReactiveCollection that implements this behavior.
(3) A third option is to create your urgencyList via something like Obtics or Continuous Linq. The collections they return implement this behavior automatically.
This is what I am using. What Ken suggested in nr (1) I suppose:
public class Person: INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set {
_name = value;
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
I have theese objects in a PLObservableNotifyList<Person>
which I set as ItemsSource
on the ItemsControl
. A soon as I update values (use the setter) the binding is updated automaticly.
public class PLObservableNotifyList<T> :
ObservableCollection<T> where T : INotifyPropertyChanged
{
public ItemPropertyChangedEventHandler ItemPropertyChanged;
public EventHandler CollectionCleared;
protected override void OnCollectionChanged(
NotifyCollectionChangedEventArgs args)
{
base.OnCollectionChanged(args);
if (args.NewItems != null)
foreach (INotifyPropertyChanged item in args.NewItems)
item.PropertyChanged += OnItemPropertyChanged;
if (args.OldItems != null)
foreach (INotifyPropertyChanged item in args.OldItems)
item.PropertyChanged -= OnItemPropertyChanged;
}
void OnItemPropertyChanged(object sender,
PropertyChangedEventArgs args)
{
if (ItemPropertyChanged != null)
ItemPropertyChanged(this,
new PLItemPropertyChangedEventArgs(sender,
args.PropertyName));
}
protected override void ClearItems()
{
foreach (INotifyPropertyChanged item in Items)
item.PropertyChanged -= OnItemPropertyChanged;
if (CollectionCleared != null)
CollectionCleared(this, EventArgs.Empty);
base.ClearItems();
}
}
You need my ObservableComputations library. Using that library you can code:
private Computing<string> _checkedUrgencyItemsText;
public Computing<string> CheckedUrgencyItemsText = _selectedUrgencyItemsText ??
Expr.Is(() => UrgencyItems.Filtering(urgencyItem => urgencyItem.IsChecked)
.Using(checkedUrgencyItems =>
checkedUrgencyItems.Count > 0
? string.Format("{0} Items Selected", checkedUrgencyItems.Count)
: "All")).Computing();
<Controls:RadDropDownButton Content="{Binding Path=CheckedUrgencyItemsText.Value}" AutoOpenDelay="0:0:0.0" x:Name="Urgency" VerticalAlignment="Center" Width="150" HorizontalContentAlignment="Left">
<Controls:RadDropDownButton.DropDownContent>
<ListBox x:Name="UrgencyList">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox Content="{Binding Name}" ClickMode="Press" IsChecked="{Binding IsChecked, Mode=TwoWay}" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Controls:RadDropDownButton.DropDownContent>
</Controls:RadDropDownButton>
精彩评论