Связывание данных WPF и IValueConverter

Почему это происходит, когда я использую конвертер в моем выражении привязки в WPF, значение не обновляется при обновлении данных.

У меня есть простая модель данных Person:

class Person : INotifyPropertyChanged
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Мое привязывающее выражение выглядит следующим образом:

<TextBlock Text="{Binding Converter={StaticResource personNameConverter}" />

Мой конвертер выглядит следующим образом:

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();
    }
}

Если я привяжу данные без конвертера, он отлично работает:

<TextBlock Text="{Binding Path=FirstName}" />
<TextBlock Text="{Binding Path=LastName}" />

Что мне не хватает?

EDIT: Чтобы просто прояснить некоторые вещи, Джоэл и Алан верны в отношении интерфейса INotifyPropertyChanged, который должен быть реализован. В действительности я действительно реализую его, но он все еще не работает.

Я не могу использовать несколько элементов TextBlock, потому что я пытаюсь связать заголовок окна с полным именем, а заголовок окна не принимает шаблон.

Наконец, это возможность добавить составное свойство "FullName" и привязать его, но мне все еще интересно, почему обновление не происходит, когда привязка использует конвертер. Даже когда я помещаю точку прерывания в код конвертера, отладчик просто не появляется там, когда обновление выполняется с базовыми данными: - (

Спасибо, Uri

Ответы

Ответ 1

(см. правки ниже: последние: # 2)

Он не обновляется, потому что ваш объект Person не способен уведомить ничего, что изменило значение FirstName или LastName. См. этот вопрос.

И вот как вы реализуете INotifyPropertyChanged. (Обновлено, см. Редактировать 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 ) );
            }
        }
    }
}

Изменить 1

На самом деле, так как вы после первого имени и последнего обновления, и Path=FirstName, и все работает отлично, я не думаю, что вам понадобится конвертер вообще. Множественные TextBlocks настолько же эффективны и могут работать лучше, когда вы локализуете язык справа налево.

Изменить 2

Я понял это. Он не уведомляется о том, что свойства обновлены, потому что он привязан к самому объекту, а не к одному из этих свойств. Даже когда я сделал Person a DependencyObject и сделал FirstName и LastName DependencyProperties, он не обновлялся.

Вам нужно будет использовать свойство FullName, и я обновил код класса Person выше, чтобы это отразить. Затем вы можете привязать Title. ( Примечание: Я установил объект Person как Window DataContext.)

Title="{Binding Path=FullName, Mode=OneWay}"

Если вы редактируете имена в TextBox и хотите, чтобы имя изменилось сразу же, а не когда тег TextBox потерял фокус, вы можете сделать это:

<TextBox Name="FirstNameEdit"
    Text="{Binding Path=FirstName, UpdateSourceTrigger=PropertyChanged}" />

Я знаю, что вы не хотели использовать свойство FullName, но все, что могло бы сделать то, что вы хотите, вероятно, было бы частью устройства Rube Goldberg. Например, реализовать свойство INotifyPropertyChanged и a Person в самом классе Window, имея Window прослушать событие PropertyChanged, чтобы запустить событие Window PropertyChanged и использовать относительную привязку как и следующее. Вы также установили свойство Person до InitializeComponent() или пожар PropertyChanged после установки свойства Person, чтобы оно отображалось, конечно. (В противном случае он будет null во время InitializeComponent() и должен знать, когда он 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>

Ответ 2

Вы также можете использовать MultiBinding.. Привязать к объекту Person, FirstName и LastName. Таким образом, значение будет обновляться, как только FirstName или LastName выбрасывает измененное свойство.

<MultiBinding Converter="{IMultiValueConverter goes here..}">
    <Binding />
    <Binding Path="FirstName" />
    <Binding Path="LastName" />
</MultiBinding>

Или, если вы используете только имя FirstName и LastName, отделите объект Person от привязки к чему-то вроде этого:

<MultiBinding Converter="{IMultiValueConverter goes here..}">
    <Binding Path="FirstName" />
    <Binding Path="LastName" />
</MultiBinding>

И MultiValueConverter выглядит так:

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();
    }
}

Но, конечно, выбранный ответ также работает, но MultiBinding работает более элегантно...

Ответ 3

В порядке обновления привязки ваш класс человека должен реализовать INotifyPropertyChanged, чтобы привязка знала, что свойства объекта были обнулены. Вы также можете сохранить себя из дополнительного конвертера, предоставив свойство fullName.

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
    }
}

Теперь ваше привязка будет выглядеть следующим образом:

<TextBlock Text="{Binding Person.FullName}" />

Ответ 4

Я не проверял его, но вы также можете попробовать следующее

<TextBlock Text="{Binding Path=/, Converter={StaticResource personNameConverter}}" />