WPF data binding with ObservableCollection to label
I have an ObservableCollection that I need to bind to two labels, first to show the number of items in the collection and then show the sum of the values.
The first label is bound to the counts property and the second label is bound directly to the ObservableCollection with a converter to calculate the total of all items
The XAML looks something like this:
<Grid>
<ListBox Name="itemList" ItemsSource="{Binding DataList}"/>
<Label Name="lblcount" Content="{Binding DataList.Count}" />
<Label Name="lblTotal" Content="{Binding DataList, Converter={StaticResource calculateTotalConvertor}" />
</Grid>
My virtual machine has a collection like this
ObservableCollection<int> data = new ObservableCollection<int>();
public ObservableCollection<int> DataList
{
get { return data; }
set { data = value; }
}
My converter code
public class CalculateTotalConvertor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<int> collection = value as ObservableCollection<int>;
return collection.Sum();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
The problem is adding new items to DataList, ListView and label showing item count is updating, but "lblTotal" is not getting updated with total.
Basically, how do you get your binding to evaluate on ObservableCollection changes? How does this work directly for the ListView or DataGrid, but not for the label?
I know this problem can be solved by creating a property in the VM to show the total and boost the property change when the collection is updated, but is there a better solution than that?
Of course this is a simplified form of my real problem, I don't have access to the ViewModel and the collection, its third party control. I am creating a custom wrapper item and has a relative binding to a view of its internal collection.
source to share
The other answers correctly explain why it is not updating. To force it to update, you can change your converter to IMultiValueConverter
:
public class CalculateTotalConvertor : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<int> collection = values.FirstOrDefault() as ObservableCollection<int>;
return collection.Sum();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Then change the binding to MultiBinding
, which will pull Count as well:
<Label Name="lblTotal">
<Label.Content>
<MultiBinding Converter="{StaticResource calculateTotalConvertor}">
<Binding Path="DataList"/>
<Binding Path="DataList.Count"/>
</MultiBinding>
</Label.Content>
</Label>
Now the second binding will tell you that the binding should update when items are added or removed, but you can simply ignore the counter value and not use it.
source to share
Not updated because its anchor to DataList
and DataList
did not change, the count label is updated because it is anchored to DataList.Count
, which is updated when an item is added to the list.
The only way I can update the label Sum
is to notify the UI that changed DataList
, but this will cause the ListBox to recheck the list and it will be much more expensive than just having a property in your model, update Sum
.
So, I think the best option would be to use a property of your model to calculate the sum using ObservableCollections
CollectionChangedEvent
or in logic that adds items to the list
source to share
It works for ListView
and DataGrid
because it is the ItemsControl
listener ObservableCollection
CollectionChangedEvent
that occurs when the collection itself changes by adding or removing items.
On the other hand, there Label
is ContentControl
that only listens PropertyChangedEvent
. Since your DataList is the same ObservableCollection
after insert as before, no events are generated.
Just saw your edit:
If you are creating a wrapper control, let a third party manage the name and connect to its internal collection CollectionChangedEvent
from your control code behind. This way you can still specify update notifications in your packaging type.
Go with an additional property, it will save you the code on the converter. From the code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
ObservableCollection<int> _list = new ObservableCollection<int>();
int _sum = 0;
Random rnd = new Random();
public MainWindow()
{
DataList = new ObservableCollection<int>();
DataList.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(DataList_CollectionChanged);
DataContext = this;
InitializeComponent();
}
void DataList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (object number in e.NewItems)
_sum += (int)number;
break;
case NotifyCollectionChangedAction.Remove:
foreach (object number in e.OldItems)
_sum -= (int)number;
break;
}
OnNotifyPropertyChanged("Sum");
}
public int Sum { get { return _sum; } }
public ObservableCollection<int> DataList { get; set; }
private void Add_Btn_Click(object sender, RoutedEventArgs e)
{
DataList.Add(rnd.Next(0, 256));
}
private void Remove_Btn_Click(object sender, RoutedEventArgs e)
{
if (DataList.Count == 0)
return;
DataList.RemoveAt(DataList.Count - 1);
}
public event PropertyChangedEventHandler PropertyChanged;
void OnNotifyPropertyChanged(string property)
{
if (PropertyChanged == null)
return;
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
source to share