Friday, August 5, 2011

Binding ObservableCollection to Text Properties

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:

2 comments:

shashikamanoj said...

Hi

Thanks for the tutorial. You have lot of samples there. What is the correct one for this article..?

Thanks

Muhammad Shujaat Siddiqi said...

Hi shashikamanoj, You can use multibinding with count property.