This article has been localized into Russian by the community.
Отвечая на изменения
До сих пор в этом учебнике мы обычно создавали привязки данных между элементами UI (от англ. User Interface, пользовательский интерфейс) и существующими классами, но в реальных приложениях, вы будете, очевидно, связывать ваши собственные объекты данных. Это так же просто, но когда вы начнете делать это, вы с разочарованием заметите, что изменения не отображаются автоматически, как это было в предыдущих примерах. Как вы узнаете из этой статьи, благодаря WPF эта задача требует совсем немного дополнительных усилий.
Реагирование на изменения источника данных
При работе с изменяющимися данными есть два основных сценария: изменение самого списка элементов и изменения в свойствах объектов, связанных с представлением. Подходы к обработке этих сценариев зависят от того, какую задачу решает разработчик, и какой результат он хочет получить. WPF предоставляет 2 достаточно простых решения для обоих случаев: класс ObservableCollection и интерфейс INotifyPropertyChanged.
Следующий пример показывает в чем необходимость использования этих двух способов:
<Window x:Class="WpfTutorialSamples.DataBinding.ChangeNotificationSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ChangeNotificationSample" Height="150" Width="300">
<DockPanel Margin="10">
<StackPanel DockPanel.Dock="Right" Margin="10,0,0,0">
<Button Name="btnAddUser" Click="btnAddUser_Click">Add user</Button>
<Button Name="btnChangeUser" Click="btnChangeUser_Click" Margin="0,5">Change user</Button>
<Button Name="btnDeleteUser" Click="btnDeleteUser_Click">Delete user</Button>
</StackPanel>
<ListBox Name="lbUsers" DisplayMemberPath="Name"></ListBox>
</DockPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Windows;
namespace WpfTutorialSamples.DataBinding
{
public partial class ChangeNotificationSample : Window
{
private List<User> users = new List<User>();
public ChangeNotificationSample()
{
InitializeComponent();
users.Add(new User() { Name = "John Doe" });
users.Add(new User() { Name = "Jane Doe" });
lbUsers.ItemsSource = users;
}
private void btnAddUser_Click(object sender, RoutedEventArgs e)
{
users.Add(new User() { Name = "New user" });
}
private void btnChangeUser_Click(object sender, RoutedEventArgs e)
{
if(lbUsers.SelectedItem != null)
(lbUsers.SelectedItem as User).Name = "Random Name";
}
private void btnDeleteUser_Click(object sender, RoutedEventArgs e)
{
if(lbUsers.SelectedItem != null)
users.Remove(lbUsers.SelectedItem as User);
}
}
public class User
{
public string Name { get; set; }
}
}
Попробуйте запустить эту программу и Вы увидите, что при добавлении в список новых данных или изменении одного из пользователей ничего в пользовательском интерфейсе не меняется. Это очень простой пример, с использованием класса User, в котором хранится имя пользователя, ListBox для отображения и несколько кнопок, которые позволят изменить как сам список, так и его содержимое. Источник данных (англ. ItemsSource) связан с небольшим списком пользователей, который мы создали в конструкторе окна. Проблема заключается в том, что ни одна из кнопок не работает. Давайте в двух простых действиях исправим это.
Отображение изменений в источнике данных списка
Первым делом надо заставить пользовательский интерфейс отвечать на изменения источника данных списка - например, когда мы добавляем нового или удаляем одного из пользователей. Что нам надо, так это список, который уведомляет о любых изменениях своего содержимого, и, к счастью, в WPF есть тип такого списка, который делает как раз то, что надо. Он называется ObservableCollection и используется точно так же, как и обычный List<T>, за исключением небольших различий.
В последнем примере, который вы найдете ниже, мы просто заменили List<User> на ObservableCollection<User> - и это всё что требовалось! Это делает рабочими кнопки "Add" и "Delete", но ничего не произойдет с кнопкой "Change name", потому что изменение будет происходить с привязанным объектом данных, а не source list - этот сценарий мы рассмотрим на следующем шаге.
Отражение изменений в объектах данных
Вторым шагом является реализация интерфейса INotifyPropertyChanged нашим пользовательским классом User. Реализовав интерфейс, объекты класса User смогут оповещать слой UI об изменениях своих свойств. Конечно, данный способ более громоздкий по сравнению с простым изменением типа списка, как было проделано выше, но он все еще остается одним из самым простых способов реализации автоматического обновления данных.
Окончательный рабочий пример
С двумя изменениями, описанными выше у нас есть пример, который БУДЕТ отображать изменения в источнике данных. Код выглядит следующим образом:
<Window x:Class="WpfTutorialSamples.DataBinding.ChangeNotificationSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ChangeNotificationSample" Height="135" Width="300">
<DockPanel Margin="10">
<StackPanel DockPanel.Dock="Right" Margin="10,0,0,0">
<Button Name="btnAddUser" Click="btnAddUser_Click">Add user</Button>
<Button Name="btnChangeUser" Click="btnChangeUser_Click" Margin="0,5">Change user</Button>
<Button Name="btnDeleteUser" Click="btnDeleteUser_Click">Delete user</Button>
</StackPanel>
<ListBox Name="lbUsers" DisplayMemberPath="Name"></ListBox>
</DockPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;
namespace WpfTutorialSamples.DataBinding
{
public partial class ChangeNotificationSample : Window
{
private ObservableCollection<User> users = new ObservableCollection<User>();
public ChangeNotificationSample()
{
InitializeComponent();
users.Add(new User() { Name = "John Doe" });
users.Add(new User() { Name = "Jane Doe" });
lbUsers.ItemsSource = users;
}
private void btnAddUser_Click(object sender, RoutedEventArgs e)
{
users.Add(new User() { Name = "New user" });
}
private void btnChangeUser_Click(object sender, RoutedEventArgs e)
{
if(lbUsers.SelectedItem != null)
(lbUsers.SelectedItem as User).Name = "Random Name";
}
private void btnDeleteUser_Click(object sender, RoutedEventArgs e)
{
if(lbUsers.SelectedItem != null)
users.Remove(lbUsers.SelectedItem as User);
}
}
public class User : INotifyPropertyChanged
{
private string name;
public string Name {
get { return this.name; }
set
{
if(this.name != value)
{
this.name = value;
this.NotifyPropertyChanged("Name");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if(this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
Заключение
Как вы могли заметить, интерфейс INotifyPropertyChanged достаточно прост в реализации, но он нагружает ваши классы дополнительным кодом и добавляет немного дополнительной логики в ваши свойства. Это цена, которую вы должны заплатить, если хотите осуществить привязку ваших собственных классов и мгновенно отображать изменения в UI. Разумеется, что вызывать метод NotifyPropertyChanged вам придется только в setter'ах свойств, для которых вы осуществляете привязку - остальные свойства останутся неизменными.
С другой стороны, пользоваться ObservableCollection очень просто - от вас лишь требуется использовать данный тип списка в случаях, когда вы хотите, чтобы изменения в исходном списке отражались привязанными элементами управления.