isatin
isatin

Reputation: 189

How to get PropertyChanged events work with bindings using converters?

I have a ListBox bound to a list of objects and for each item in the ListBox there is a TextBlock bound with a converter but when the properties of the objects change, I can't get those TextBlock updated while I can get them updated if not bound with a converter.

The xmal of the window is as follows:

<Window x:Class="BindingPropertyChanged.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:local="clr-namespace:BindingPropertyChanged"
        mc:Ignorable="d"
        Title="MainWindow" Height="400" Width="600">
    <Window.Resources>
        <local:LocationToTextConverter x:Key="LocationToTextConverter" />
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="2*"/>
        </Grid.ColumnDefinitions>
        <ListBox
            Grid.Row="2"
            HorizontalContentAlignment="Stretch"
            ItemsSource="{Binding Items}"
            SelectedItem="{Binding SelectedItem}"
          >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="auto"/>
                            <RowDefinition Height="auto"/>
                        </Grid.RowDefinitions>
                        <TextBlock x:Name="NameText" 
                            Text="{Binding Converter={StaticResource LocationToTextConverter}, UpdateSourceTrigger=PropertyChanged}" />
                        <TextBlock x:Name="DescriptionText"
                            Grid.Row="1" Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}" />
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Grid Grid.Column="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto"/>
                <RowDefinition Height="auto"/>
            </Grid.RowDefinitions>
            <Label HorizontalAlignment="Right" Content="Place" />
            <ComboBox Grid.Column="1"
                ItemsSource="{Binding Places}"
                SelectedValue="{Binding SelectedItem.PlaceId}"
                SelectedValuePath="Id"
                DisplayMemberPath="Name"
                IsEditable="False"/>
            <Label Grid.Row="1" HorizontalAlignment="Right" Content="Description" />
            <TextBox Grid.Row="1" Grid.Column="1"
                Text="{Binding SelectedItem.Description, 
                    UpdateSourceTrigger=PropertyChanged, 
                    ValidatesOnNotifyDataErrors=True}"
                VerticalAlignment="Center"/>
        </Grid>
    </Grid>
</Window>

On the left is the aforementioned ListBox and on the right are a TextBox and a ComboBox for authoring the selected item in the ListBox. In ListBox.ItemTemplate, DescriptionText can get updated if I alter the text of TextBox on the right while NameText cannot get updated because it uses a converter in the binding.

The following code is the classes of the domain model:

public class DomainBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberNameAttribute] string propertyName = "None")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

public class Location : DomainBase
{
    private int _placeId;
    private string _description;

    public int PlaceId
    {
        get { return _placeId; }
        set
        {
            if (_placeId == value) return;
            _placeId = value;
            OnPropertyChanged();
        }
    }

    public string Description
    {
        get { return _description; }
        set
        {
            if (_description == value) return;
            _description = value;
            OnPropertyChanged();
        }
    }
}

public class Place : DomainBase
{
    public int Id { get; set; }
    public string Name { get; set; }
}

There is a Repository class for dummy data:

public class Repository
{
    public static ICollection<Place> _places;
    public static ICollection<Place> Places
    {
        get { return _places; }
    }

    public static ICollection<Location> _locations;
    public static ICollection<Location> Locations
    {
        get { return _locations; }
    }

    static Repository()
    {
        _places = new List<Place>();
        _places.Add(new Place() { Id = 1, Name = "Downtown Center" });
        _places.Add(new Place() { Id = 2, Name = "Headquarter" });

        _locations = new List<Location>();
        _locations.Add(new Location() { PlaceId = 1, Description = "Room 101" });
        _locations.Add(new Location() { PlaceId = 2, Description = "B06" });
    }
}

And the ViewModel class set to the DataContext of the window:

public class ViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Location> _items = new ObservableCollection<Location>(Repository.Locations);
    private Location _selectedItem;

    public ObservableCollection<Location> Items
    {
        get
        {
            return _items;
        }
        private set
        {
            if (_items == value) return;
            _items = value;
            OnPropertyChanged();
            OnPropertyChanged("SelectedItem");
        }
    }
    public Location SelectedItem
    {
        get
        {
            return _selectedItem;
        }
        set
        {
            if (_selectedItem == value) return;
            _selectedItem = value;
            OnPropertyChanged(); 
        }
    }
    public ObservableCollection<Place> Places
    {
        get
        {
            return new ObservableCollection<Place>(Repository.Places);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "None")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Finally, the converter:

class LocationToTextConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string result = "";
        Location location = value as Location;
        if (location != null)
        {
            Place place = Repository.Places.Single(o => o.Id == location.PlaceId);
            if (place != null)
            {
                string placeName = place.Name;
                if (!String.IsNullOrWhiteSpace(placeName))
                {
                    result = placeName + ", ";
                }
            }

            result += location.Description;
        }
        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

What I want is to display the Place's Name with the Location's Description in the ListBox and when I alter the selected Location's Description, the display name in the ListBox should be changed instantly as well. Does anyone know how to achieve that?

Upvotes: 0

Views: 77

Answers (3)

paparazzo
paparazzo

Reputation: 45096

why not just have a property and bind to it?

public class Location : DomainBase
{
    private int _placeId;
    private string _description;

    public int PlaceId
    {
        get { return _placeId; }
        set
        {
            if (_placeId == value) return;
            _placeId = value;
            OnPropertyChanged();
        }
    }

    public string Description
    {
        get { return _description; }
        set
        {
            if (_description == value) return;
            _description = value;
            OnPropertyChanged();  // make sure  this Notify RepPlusDescription
        }
    }

    public string RepPlusDescription
    {
        get 
        {   
            string result;
            Place place = Repository.Places.FirstOrDefault(o => o.Id == placeId);
            if (place != null)
            {
                string placeName = place.Name;
                if (!String.IsNullOrWhiteSpace(placeName))
                {
                    result = placeName + ", ";
                }
            }
            return result += location.Description; 
        }
     }
}

Upvotes: 0

user2880486
user2880486

Reputation: 1108

You problem is here

                    <TextBlock x:Name="NameText" 
                        Text="{Binding Converter={StaticResource LocationToTextConverter}, UpdateSourceTrigger=PropertyChanged}" />

In your binding you didn't specify a path. When you did this, the item that gets binded will be a location object. Since you are changing the Descrition property of the Location object and not the Location object itself, TextBlock is not getting the property changed notification.

You only need to change a few lines of code, use a MultiBinding instead.

<TextBlock.Text>
    <MultiBinding>
        <Binding />
        <Binding Path=Description />
        //dont forget to specify the converter
....
class LocationToTextConverter : IMultiValueConverter
{
    //in the Convert method set, ignore value[1] and change location to values[0]
    Location location = values[0] as Location;

now your converter should get called anytime you change the description property.

Upvotes: 3

wake-0
wake-0

Reputation: 3968

The problem is that the Location is not changed so the NotifyPropertyChanged is not invoked. The easiest solution is to create property on the Location. So there are two option how you can do it:

  • Call the OnPropertyChanged("Location") from Location so that it is triggered
  • Add an own property on the Location and bind to this property instead of the Location

Upvotes: 0

Related Questions