In this post we will be discussing the issue when we bind a collection based (
ObservableCollection) property to some scalar
DependencyProperty e.g. Text property of a
TextBlock.
Let's create a sample MVVM Light based WPF application.
Note:
Yes, you would need an installation of MVVM Light in order to follow this example.
Let's update
MainWindow's definition as follows:
<Window x:Class="AppBindingScalarPropertiesCollections.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="clr-namespace:AppBindingScalarPropertiesCollections.Converters"
mc:Ignorable="d"
Height="386"
Width="514"
Title="MVVM Light Application"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Skins/MainSkin.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid x:Name="LayoutRoot">
<Grid.Resources>
<converters:StudentsListToStringConverter x:Key="studentConverter" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="42*" />
<RowDefinition Height="37*" />
<RowDefinition Height="189*" />
<RowDefinition Height="79*" />
</Grid.RowDefinitions>
<TextBlock FontSize="36"
FontWeight="Bold"
Foreground="Purple"
Text="{Binding Welcome}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" Margin="246,104,246,71" Grid.Row="2" />
<Label Content="New Student" Height="23" HorizontalAlignment="Left"
Margin="6,15,0,0" Name="label1" VerticalAlignment="Top" Width="97" />
<TextBox Height="25" HorizontalAlignment="Right" Margin="0,13,12,0"
VerticalAlignment="Top" Width="370"
Text="{Binding NewStudentName, UpdateSourceTrigger=PropertyChanged}" />
<GroupBox Header="Students List" Height="179" HorizontalAlignment="Left" Margin="6,2,0,0"
Name="groupBox1" VerticalAlignment="Top" Width="476" Grid.Row="2">
<Grid>
<ListBox ItemsSource="{Binding Students}" />
</Grid>
</GroupBox>
<GroupBox Header="Comma Separated Students List" Height="57" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="476" Grid.Row="3" Margin="0,8,0,0">
<TextBlock
Text="{Binding Students, Converter={StaticResource studentConverter}}"
Height="21" />
</GroupBox>
<Button Content="Add" Grid.Row="1" Height="26" HorizontalAlignment="Left"
Margin="382,7,0,0" VerticalAlignment="Top" Width="99"
Command="{Binding AddNewStudentCommand}" />
</Grid>
</Window>
The above view has some expectations from view model. Let's update the view model provided by MVVM Light's view model locator as follows:
namespace AppBindingScalarPropertiesCollections.ViewModel
{
using GalaSoft.MvvmLight;
using System.Collections.ObjectModel;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;
public class MainViewModel : ViewModelBase
{
public string Welcome
{
get
{
return "Welcome to MVVM Light";
}
}
string _newStudentName;
public string NewStudentName
{
get { return _newStudentName; }
set
{
_newStudentName = value;
RaisePropertyChanged("NewStudentName");
}
}
ObservableCollection<string> _students;
public ObservableCollection<string> Students
{
get
{
if (_students == null)
{
_students = new ObservableCollection<string>();
_students.Add("Muhammad");
_students.Add("Ryan");
_students.Add("Jim");
_students.Add("Brian");
_students.Add("Josh");
_students.Add("Jeremy");
}
return _students;
}
}
ICommand _addNewStudentCommand;
public ICommand AddNewStudentCommand
{
get
{
if (_addNewStudentCommand == null)
{
_addNewStudentCommand = new RelayCommand(
() =>
{
if (!Students.Contains(NewStudentName))
{
Students.Add(NewStudentName);
}
});
}
return _addNewStudentCommand;
}
}
}
}
Mainly it has three things.
NewStudentName property to be bound to the TextBox to enter the name of a new student.
AddNewStudentCommand
for the button's
Command property. Clicking this button should add the student's name as entered in the TextBox to Student's collection if it already doesn't exist. The ListBox and TextBlock showing this are automatically expected be updated as a new student is added to the collection.
Students collection to be bound to
ListBox's
ItemSource and
TextBlock's
Text properties.
We need the Student's list in the TextBlock to be comma separated names of all students. The view is using a converter for this purpose. It is a simple
IValueConverter. Let's see the definition of this converter.
namespace AppBindingScalarPropertiesCollections.Converters
{
using System.Windows.Data;
using System.Collections.ObjectModel;
using System.Linq;
class StudentsListToStringConverter : IValueConverter
{
public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string studentCommaSeparatedList = string.Empty;
ObservableCollection<string> studentList = value as ObservableCollection<string>;
if (studentList != null)
{
studentCommaSeparatedList = string.Join(", ",
(from string studentName
in studentList
select studentName).ToArray<string>());
}
return studentCommaSeparatedList;
}
public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new System.NotImplementedException();
}
}
}
In the
Convert method of this
IValueConverter, we are simple joining the elements of the collection with comma separation and returning it.
Let's run the application now. It appears as follows...
As we enter new student's name and hit
Add button, the new student is added to
Students collection. Since this collection is bound to the
ItemsSource of the ListBox, it appears in the list box. But the same does not appear in the TextBlock's comma separated list. This is weird!!!
Basically, the reason is very simple. When we bind to the Text property of TextBlock, it is just interested in the
PropertyChanged events. It handles this event to update its contents. But it seems that it doesn't handle CollectionChanged event of
ObservableCollection. That is why it is not able to update itself. Now since we know the problem how we can resolve this. Basically there might be two different solutions to this problem.
Solution # 1: [Raise PropertyChanged Event when collection is updated for items]
This is very simple. Since we are just adding items in the
Execute method of the
ICommand bound to the Add button, we can simply do it there. Let's update the
ICommand definition in the view model as follows:
public ICommand AddNewStudentCommand
{
get
{
if (_addNewStudentCommand == null)
{
_addNewStudentCommand = new RelayCommand(
() =>
{
if (!Students.Contains(NewStudentName))
{
Students.Add(NewStudentName);
RaisePropertyChanged("Students");
}
});
}
return _addNewStudentCommand;
}
}
Here
RaisePropertyChanged method raises
PropertyChanged event for the name of property provided as argument. This is available due to inheritence of this view model by
ViewModelBase from
MVVM Light. Let's run the application again.
As we entered
Sekhar and hit the button, it appears both in the ListBox and TextBlock. This is exactly what we desired.
Zindabad!!!
2nd Solution: [Use MultiBinding, adding binding for ObservableCollection.Count]
In this solution, we can simply add binding for Count property from the same collection. We can change the binding of TextBlock as follows:
<Window x:Class="AppBindingScalarPropertiesCollections.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:converters="clr-namespace:AppBindingScalarPropertiesCollections.Converters"
mc:Ignorable="d"
Height="386"
Width="514"
Title="MVVM Light Application"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Skins/MainSkin.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Grid x:Name="LayoutRoot">
<Grid.Resources>
<converters:StudentListToStringMultiConverter x:Key="studentMultiConverter" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="42*" />
<RowDefinition Height="37*" />
<RowDefinition Height="189*" />
<RowDefinition Height="79*" />
</Grid.RowDefinitions>
<TextBlock FontSize="36"
FontWeight="Bold"
Foreground="Purple"
Text="{Binding Welcome}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" Margin="246,104,246,71" Grid.Row="2" />
<Label Content="New Student" Height="23" HorizontalAlignment="Left"
Margin="6,15,0,0" Name="label1" VerticalAlignment="Top" Width="97" />
<TextBox Height="25" HorizontalAlignment="Right" Margin="0,13,12,0"
VerticalAlignment="Top" Width="370"
Text="{Binding NewStudentName, UpdateSourceTrigger=PropertyChanged}" />
<GroupBox Header="Students List" Height="179" HorizontalAlignment="Left" Margin="6,2,0,0"
Name="groupBox1" VerticalAlignment="Top" Width="476" Grid.Row="2">
<Grid>
<ListBox ItemsSource="{Binding Students}" />
</Grid>
</GroupBox>
<GroupBox Header="Comma Separated Students List" Height="57" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="476" Grid.Row="3" Margin="0,8,0,0">
<TextBlock
Height="21" >
<TextBlock.Text>
<MultiBinding Converter="{StaticResource studentMultiConverter}" >
<Binding Path ="Students"/>
<Binding Path ="Students.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</GroupBox>
<Button Content="Add" Grid.Row="1" Height="26" HorizontalAlignment="Left"
Margin="382,7,0,0" VerticalAlignment="Top" Width="99"
Command="{Binding AddNewStudentCommand}" />
</Grid>
</Window>
In addition of Binding update, we also need to update the converter to IMultiValueConverter. Let's see the definition of StudentListToStringMultiConverter used above.
class StudentListToStringMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, System.Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
string studentCommaSeparatedList = string.Empty;
ObservableCollection<string> studentList = values[0] as ObservableCollection<string>;
if (studentList != null)
{
studentCommaSeparatedList = string.Join(", ",
(from string studentName
in studentList
select studentName).ToArray<string>());
}
return studentCommaSeparatedList;
}
public object[] ConvertBack(object value, System.Type[] targetTypes, object parameter,
System.Globalization.CultureInfo culture)
{
throw new System.NotImplementedException();
}
}
Running the application should have the same result as the first option.
Any Questions??? Comments...
Download:
The sameple project can be downloaded from SkyDrive here: