WPF Data Binding and IValueConverter
-
05-07-2019 - |
Question
Why is it that when I use a converter in my binding expression in WPF, the value is not updated when the data is updated.
I have a simple Person data model:
class Person : INotifyPropertyChanged
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
My binding expression looks like this:
<TextBlock Text="{Binding Converter={StaticResource personNameConverter}" />
My converter looks like this:
class PersonNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Person p = value as Person;
return p.FirstName + " " + p.LastName;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
If I bind the data without a converter it works great:
<TextBlock Text="{Binding Path=FirstName}" />
<TextBlock Text="{Binding Path=LastName}" />
What am I missing?
EDIT: Just to clarify a few things, both Joel and Alan are correct regarding the INotifyPropertyChanged interface that needs to be implemented. In reality I do actually implement it but it still doesn't work.
I can't use multiple TextBlock elements because I'm trying to bind the Window Title to the full name, and the Window Title does not take a template.
Finally, it is an option to add a compound property "FullName" and bind to it, but I'm still wondering why updating does not happen when the binding uses a converter. Even when I put a break point in the converter code, the debugger just doesn't get there when an update is done to the underlying data :-(
Thanks, Uri
Solution
(see edits below; latest: #2)
It isn't updating because your Person
object is not capable of notifying anything that the value of FirstName
or LastName
has changed. See this Question.
And here's how you implement INotifyPropertyChanged
. (Updated, see Edit 2)
using System.ComponentModel;
class Person : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
string _firstname;
public string FirstName {
get {
return _firstname;
}
set {
_firstname = value;
onPropertyChanged( "FirstName", "FullName" );
}
}
string _lastname;
public string LastName {
get {
return _lastname;
}
set {
_lastname = value;
onPropertyChanged( "LastName", "FullName" );
}
}
public string FullName {
get {
return _firstname + " " + _lastname;
}
}
void onPropertyChanged( params string[] propertyNames ) {
PropertyChangedEventHandler handler = PropertyChanged;
if ( handler != null ) {
foreach ( var pn in propertyNames ) {
handler( this, new PropertyChangedEventArgs( pn ) );
}
}
}
}
Edit 1
Actually, since you're after the first name and last name updating, and Path=FirstName
and such works just fine, I don't think you'll need the converter at all. Multiple TextBlocks
are just as valid, and can actually work better when you're localizing to a right-to-left language.
Edit 2
I've figured it out. It's not being notified that the properties have updated because it is binding to the object itself, not one of those properties. Even when I made Person
a DependencyObject
and made FirstName
and LastName
DependencyProperties
, it wouldn't update.
You will have to use a FullName
property, and I've update the code of the Person
class above to reflect that. Then you can bind the Title
. (Note: I've set the Person
object as the Window
's DataContext
.)
Title="{Binding Path=FullName, Mode=OneWay}"
If you're editing the names in a TextBox
and want the name changed reflected immediately instead of when the TextBox
loses focus, you can do this:
<TextBox Name="FirstNameEdit"
Text="{Binding Path=FirstName, UpdateSourceTrigger=PropertyChanged}" />
I know you didn't want to use a FullName
property, but anything that would accomplish what you want would probably be a bit of a Rube Goldberg device. Such as implementing INotifyPropertyChanged
and a Person
property on the Window
class itself, having the Window
listen on the PropertyChanged
event in order to fire the Window
's PropertyChanged
event, and using a relative binding like the following. You'd also have set the Person
property before InitializeComponent()
or fire PropertyChanged
after setting the Person
property so that it shows up, of course. (Otherwise it will be null
during InitializeComponent()
and needs to know when it's a Person
.)
<Window.Resources>
<loc:PersonNameConverter
x:Key="conv" />
</Window.Resources>
<Window.Title>
<Binding
RelativeSource="{RelativeSource Self}"
Converter="{StaticResource conv}"
Path="Person"
Mode="OneWay" />
</Window.Title>
OTHER TIPS
You can also use a MultiBinding.. Bind to the Person object, the FirstName and LastName. That way, the value gets updated as soon as FirstName or LastName throws the property changed event.
<MultiBinding Converter="{IMultiValueConverter goes here..}">
<Binding />
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
Or if you only use the FirstName and LastName, strip the Person object from the binding to something like this:
<MultiBinding Converter="{IMultiValueConverter goes here..}">
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
And the MultiValueConverter looks like this:
class PersonNameConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0].ToString() + " " + values[1].ToString();
}
public object ConvertBack(object[] values, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
But of course, the selected answer works as well, but a MultiBinding works more elegantly...
In Order for the binding to be updated, your person class needs to implement INotifyPropertyChanged to let the binding know that the object's properties have been udpated. You can also save yourself from the extra converter by providing a fullName property.
using System.ComponentModel;
namespace INotifyPropertyChangeSample
{
public class Person : INotifyPropertyChanged
{
private string firstName;
public string FirstName
{
get { return firstName; }
set
{
if (firstName != value)
{
firstName = value;
OnPropertyChanged("FirstName");
OnPropertyChanged("FullName");
}
}
}
private string lastName;
public string LastName
{
get { return lastName; }
set
{
if (lastName != value)
{
lastName = value;
OnPropertyChanged("LastName");
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get { return firstName + " " + lastName; }
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
#endregion
}
}
Your Binding will now look like this:
<TextBlock Text="{Binding Person.FullName}" />
I haven't check it but can you also try the following
<TextBlock Text="{Binding Path=/, Converter={StaticResource personNameConverter}}" />