Итак, сегодня статья, опять навеяна форумами MSDN, а конкретно вот этим вопросом. Кто лениться ходить по ссылкам. Есть два класса, и один включается во второй. Если оба реализуют INotifyPropertyChanged, как сделать так, чтобы при измененнии свойств включенного класса, вызывалось событие изменения свойств класса включающего.
Поехали. Вот так у нас будет выглядеть класс, который будет включаться:
private string _lastName;
get
{
return _lastName;
}
set
{
if (_lastName != value)
{
_lastName = value;
OnPropertyChanged("LastName");
}
}
}
get
{
return _firstName;
}
set
{
if (_firstName != value)
{
_firstName = value;
OnPropertyChanged("FirstName");
}
}
}
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(p_propertyName));
}
}
}
Ну а вот, класс, который включает экземпляр Person:
private Person _person;
get
{
return _person;
}
set
{
if (_person != value)
{
_person = value;
OnPropertyChanged("Person");
}
}
}
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(p_propertyName));
}
}
}
Я его специально не усложнал, поэтому пусть будет вот такой простой. Давайте сначала разберем пример, который работает. Для демонстрации, воспользуюсь вот такой разметкой на главном окне:
<Button Click="Button_Click">Изменить фамилию</Button>
</StackPanel>
Ну и код главного окна:
InitializeComponent();
Student stud = new Student()
{
Person = new Person()
{
LastName = "Иванов",
FirstName = "Иван"
}
};
this.DataContext = stud;
}
Student stud = this.DataContext as Student;
if (stud != null && stud.Person != null)
{
stud.Person.LastName = "Петров";
}
}
Запускаем, нажимаем кнопку:
Пока все логично. Но, допустим, нам надо показывать не просто "Иванов", а "Иван Иванов". Понятно, что можно добавить еще одно поле для показа имени, но если нам надо будет делать что то вроде "И. Иванов", то дополнительным TextBox не обойдешься и придеться воспользоваться конвертором. Хорошо, пишем конвертор:
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;
}
throw new NotImplementedException();
}
}
Подключаем пространство имен нашего проекта и, соответственно, конвертор, правим привязку текста в TextBox-е:
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:
OnPropertyChanged("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), то перепишем его:
<MultiBinding Converter="{StaticResource PersonConverter}">
<Binding Path="Person.FirstName"/>
<Binding Path="Person.LastName"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
Запустить пока не получиться, т.к. у нас конвертор на одиночное значение, а у нас их теперь коллекция. Переписываем конвертор:
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;
}
throw new NotImplementedException();
}
}
Запускаем и удостоверяемся, что все работает так же, как и в перовм подходе.
Поехали. Вот так у нас будет выглядеть класс, который будет включаться:
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/presentationxmlns: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();
}
}
Запускаем и удостоверяемся, что все работает так же, как и в перовм подходе.
Комментариев нет:
Отправить комментарий