понедельник, 28 января 2013 г.

О "всплывании" события изменения свойств

Итак, сегодня статья, опять навеяна форумами MSDN, а конкретно вот этим вопросом. Кто лениться ходить по ссылкам. Есть два класса, и один включается во второй. Если оба реализуют INotifyPropertyChanged, как сделать так, чтобы при измененнии свойств включенного класса, вызывалось событие изменения свойств класса включающего.
Поехали. Вот так у нас будет выглядеть класс, который будет включаться:

public class Person : INotifyPropertyChanged
{
    private string _lastName;

    public string LastName
    {
        get
        {
            return _lastName;
        }
        set
        {
            if (_lastName != value)
            {
                _lastName = value;
                OnPropertyChanged("LastName");
            }
        }
    }

    private string _firstName;

    public string FirstName
    {
        get
        {
            return _firstName;
        }
        set
        {
            if (_firstName != value)
            {
                _firstName = value;
                OnPropertyChanged("FirstName");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string p_propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(p_propertyName));
        }
    }
}
Ну а вот, класс, который включает экземпляр Person:

public class Student : INotifyPropertyChanged
{
    private Person _person;

    public Person Person
    {
        get
        {
            return _person;
        }
        set
        {
            if (_person != value)
            {
                _person = value;
                OnPropertyChanged("Person");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string p_propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(p_propertyName));
        }
    }
}

Я его специально не усложнал, поэтому пусть будет вот такой простой. Давайте сначала разберем пример, который работает. Для демонстрации, воспользуюсь вот такой разметкой на главном окне:

<StackPanel>
    <TextBlock Text="{Binding Person.LastName}" />
    <Button Click="Button_Click">Изменить фамилию</Button>
</StackPanel>

Ну и код главного окна:

public MainWindow()
{
    InitializeComponent();
    Student stud = new Student()
    {
        Person = new Person()
        {
            LastName = "Иванов",
            FirstName = "Иван"
        }
    };
    this.DataContext = stud;
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    Student stud = this.DataContext as Student;
    if (stud != null && stud.Person != null)
    {
        stud.Person.LastName = "Петров";
    }
}
Запускаем, нажимаем кнопку:
Пока все логично. Но, допустим, нам надо показывать не просто "Иванов", а "Иван Иванов". Понятно, что можно добавить еще одно поле для показа имени, но если нам надо будет делать что то вроде "И. Иванов", то дополнительным TextBox не обойдешься и придеться воспользоваться конвертором. Хорошо, пишем конвертор:

class PersonConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string result = "";
        Person person = value as Person;
        if (person != null)
        {
            string initial = string.IsNullOrWhiteSpace(person.FirstName) ? "" : person.FirstName[0] + ". ";
            result = string.Format("{0}{1}", initial, person.LastName);
        }
        return result;
    }

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

Подключаем пространство имен нашего проекта и, соответственно, конвертор, правим привязку текста в TextBox-е:

<Window x:Class="WpfApplication30.MainWindow"
        xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
        xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
        Title="MainWindow" Height="80" Width="200"
        xmlns:local="clr-namespace:WpfApplication30">
    <Window.Resources>
        <local:PersonConverter x:Key="PersonConverter" />
    </Window.Resources>
    <StackPanel>
        <TextBlock Text="{Binding Person,Converter={StaticResource PersonConverter}}" />
        <Button Click="Button_Click">Изменить фамилию</Button>
    </StackPanel>
</Window>

Запускаем, чтобы удостовериться, что при запуске у нас все показывается правильно, а вот на кнопку можем обнажиматься, все равно ничего не зимениться:
Почему так происходит? Да потому, что в поле Path биндинга у нас указано свойство Person, а оно то, как раз и не меняется. Как с этим бороться? Вариантов два. Первый, на мой взгляд не правильный, так как нарушает идею которую закладывали в интерфейс INotifyPropertyChanged. Давайте добавим вспомогателдьный метод и перепишем сеттер свойства Person у класса Student:

private void PersonPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    OnPropertyChanged("Person");
}

public Person Person
{
    get
    {
        return _person;
    }
    set
    {
        if (_person != value)
        {
            if (_person != null)
            {
                _person.PropertyChanged -= PersonPropertyChanged;
            }
            _person = value;
            if (_person != null)
            {
                _person.PropertyChanged += PersonPropertyChanged;
            }
            OnPropertyChanged("Person");
        }
    }
}
Теперь, если мы запустим приложение, все будет работать так, как нам нужно:
Но, как я уже сказал, у этого подхода есть недостатки. Во-первых, INotifyPropertyChanged должен вызываться только на изменение свойств объекта, а следовательно, если этот подход применить в большой системе, которую разрабатывают несколько программистов, то у тех из них, кто не будет знать о таком ходе конем, могут возникнуть проблемы с модификацией нашего класса Student. Во-вторых, у нас теперь появилась ссылка из Person на Student (подписанный на событие метод), что вызовет дополнительную головную боль по отписыванию этого события, по окончании работы с классом Student.
Ок, возвращаем класс Student к первоначальному виду и смотрим второй способ решить эту проблему.
В рамках этого подхода, мы воспользуемся так называемым MultiBinding-ом. Т.к. у нас свойство Text у TextBox-а, должно отслеживать изменение не одного (Person), а двух свойств(LastName и FirstName), то перепишем его:

<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource PersonConverter}">
            <Binding Path="Person.FirstName"/>
            <Binding Path="Person.LastName"/>
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

Запустить пока не получиться, т.к. у нас конвертор на одиночное значение, а у нас их теперь коллекция. Переписываем конвертор:

class PersonConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string result = "";
        if (values.Length >= 2)
        {
            string firstName = values[0].ToString();
            string initial = string.IsNullOrWhiteSpace(firstName) ? "" : firstName[0] + ". ";
            result = string.Format("{0}{1}", initial, values[1]);           
        }
        return result;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
Запускаем и удостоверяемся, что все работает так же, как и в перовм подходе.

Комментариев нет:

Отправить комментарий