Привязка к коллекции объектов
110WPF --- Привязка, команды и стили WPF --- Привязка к коллекции объектов
»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ
Привязка к единственному объекту довольно проста. Но все становится намного интереснее, когда нужно привязаться к некоторой коллекции объектов, например, ко всем машинам в таблице.
Хотя каждое свойство зависимости поддерживает привязку к одному значению, которая применялась до сих пор, привязка к коллекции требует несколько более интеллектуального элемента. В WPF все классы, унаследованные от ItemsControl, способны отображать целый список элементов. Возможностью привязки данных обладают ListBox, ComboBox, ListView и DataGrid (а также Menu и TreeView — для иерархических данных).
Хотя может показаться, что WPF предлагает относительно небольшой набор списочных элементов управления, тем не менее, эти элементы предоставляют почти неограниченные возможности по отображению данных. Это связано с тем, что списочные элементы управления поддерживают шаблоны данных, которые позволяют непосредственно управлять отображением элементов списка.
Для поддержки привязки коллекций в классе ItemsControl определены три ключевых свойства:
- ItemsSource
Указывает на коллекцию, содержащую все объекты, которые будут показаны в списке
- DisplayMemberPath
Идентифицирует свойство, которое будет применяться для создания отображаемого текста каждого элемента коллекции
- ItemTemplate
Принимает шаблон данных, который будет использован для создания визуального представления каждого элемента. Это свойство намного мощнее, чем DisplayMemberPath
Здесь, возможно, возникает вопрос, какой именно тип коллекции можно поместить в свойство ItemSource? K счастью, практически любой. Все, что понадобится — это поддержка интерфейса IEnumerable, которую обеспечивают массивы, все типы коллекций и многие другие специализированные объекты, служащие оболочками для групп элементов.
Однако поддержка, получаемая от базового интерфейса IEnumerable, ограничена привязкой только для чтения. Для редактирования коллекции (например, вставки и удаления), как вскоре будет показано, понадобится немного более сложная инфраструктура.
Отображение и редактирование элементов коллекции
Рассмотрим окно, показанное на рисунке ниже, которое отображает список машин. Когда модель машины выбрана, информация о ней отображается в нижней части окна, где ее можно редактировать. (В данном примере GridSplitter позволяет подкорректировать место, выделенное для верхней и нижней части окна.)
Чтобы создать этот пример, необходимо начать с построения логики доступа к данным. В данном случае метод MainWindow.GetCars извлекает список всех машин из базы данных, используя LINQ to Entities. Для каждой записи создается объект CarTable, который добавляется в обобщенную коллекцию List. (Здесь можно использовать любую коллекцию — например, массив или слабо типизированный ArrayList будут работать одинаково.)
Ниже показана видоизмененная разметка окна и необходимый код для получения списка элементов:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ListBox Name="lstCars" Margin="5" />
<StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal" Margin="5,2,5,10">
<Button Margin="2,0,0,0" Padding="2" Content="Получить список машин" Click="cmdGetCar_Click"/>
<Button Margin="2,0,0,0" Padding="2" Content="Удалить" Click="cmdDeleteCar_Click"/>
<Button Margin="2,0,0,0" Padding="2" Content="Добавить" Click="cmdAddCar_Click"/>
</StackPanel>
</Grid>
<GridSplitter Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" ResizeBehavior="PreviousAndNext" Height="5"/>
<Border Grid.Row="2" Padding="7" Margin="7" Background="LightSteelBlue">
<Grid Name="gridCarDetails" DataContext="{Binding ElementName=lstCars, Path=SelectedItem}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Margin="7">Марка:</TextBlock>
<TextBox Margin="5" Grid.Column="1" Text="{Binding Path=ModelName}"></TextBox>
<TextBlock Margin="7" Grid.Row="1">Модель:</TextBlock>
<TextBox Margin="5" Grid.Row="1" Grid.Column="1" Text="{Binding Path=ModelNumber}"></TextBox>
<TextBlock Margin="7" Grid.Row="2">Цена (руб):</TextBlock>
<TextBox Margin="5" Grid.Row="2" Grid.Column="1" Text="{Binding Path=Cost}"></TextBox>
<TextBlock Margin="7,7,7,0" Grid.Row="3">Описание:</TextBlock>
<TextBox Margin="7" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2"
VerticalScrollBarVisibility="Visible" TextWrapping="Wrap"
Text="{Binding Path=Description, TargetNullValue=[Описание не предоставлено]}"></TextBox>
</Grid>
</Border>
</Grid>
public static List<CarTable> GetCars()
{
AutoShopEntities context = new AutoShopEntities();
return context.CarTables.Select(p => p).ToList<CarTable>();
}
private List<CarTable> cars;
private void cmdGetCar_Click(object sender, RoutedEventArgs e)
{
cars = MainWindow.GetCars();
lstCars.ItemsSource = cars;
}
// Обработчики удаления и добавления будут показаны ниже
private void cmdDeleteCar_Click(object sender, RoutedEventArgs e)
{
}
private void cmdAddCar_Click(object sender, RoutedEventArgs e)
{
}
Для корректного вывода названия модели придется переопределить метод ToString() сущностного класса CarTable находящегося в файле AutoShopDataModel.Designer.cs (если вы следовали моим рекомендациям при создании сущностной модели базы данных):
[EdmEntityTypeAttribute(NamespaceName="AutoShopModel", Name="CarTable")]
[Serializable()]
[DataContractAttribute(IsReference=true)]
public partial class CarTable : EntityObject
{
public override string ToString()
{
return ModelName + " " + ModelNumber;
}
...
Приняв решение, как будет отображаться информация в списке, можно заняться следующей проблемой — отображением подробностей текущего выбранного элемента списка в сетке, которая появляется под этим списком. С этой задачей можно справиться, реагируя на событие SelectionChanged и вручную изменяя контекст данных сетки, но есть более быстрый способ, который вообще не требует никакого кода. Нужно просто установить выражение привязки для свойства Grid.DataContext, которое извлечет выбранный в списке объект Product, как показано ниже:
<Grid Name="gridCarDetails" DataContext="{Binding ElementName=lstCars, Path=SelectedItem}">
...
Когда окно отображается в первый раз, в списке ничего не выбрано. Свойство ListBox.SelectedItem равно null, а потому Grid.DataContext также равно null, и никакой информации не появляется. Как только выбран элемент в списке, контекст данных устанавливается в соответствующий объект, и вся информация тут же отображается.
На удивление этот пример уже полностью функционален. Имеется возможность редактировать товары, перемещаться по списку и затем, вернувшись к предыдущей записи, удостовериться, что изменения были успешно зафиксированы.
Фактически, можно даже изменить значение, которое затрагивает отображаемый в списке текст. Если модифицировать наименование модели и перейти с помощью клавиши <Tab> на другой элемент управления, то соответствующая позиция в списке автоматически обновится. (Опытные разработчики оценят это преимущество, которого так не хватает приложениям Windows Forms.)
Чтобы предотвратить возможность редактирования поля, установите свойство IsLocked текстового поля в true или, что лучше — воспользуйтесь элементом управления, который разрешает только чтение (вроде TextBlock).
Разумеется, чтобы завершить этот пример с точки зрения приложения, нужно еще добавить некоторый код. Например, может понадобиться метод UpdateProducts(), принимающий коллекцию товаров и выполняющий соответствующие операторы. Реализация обновления базы данных была показана в предыдущей статье.
Вставка и удаление элементов коллекций
Одно из ограничений предыдущего примера заключается в том, что он не может отобразить изменения, внесенные в коллекцию. Он замечает измененные объекты CarTable, но не может обновить список в случае добавления нового элемента или удаления элемента в коде.
Например, предположим, что добавлена кнопка Удалить, которая выполняет следующий код:
private void cmdDeleteCar_Click(object sender, RoutedEventArgs e)
{
cars.Remove((CarTable)lstCars.SelectedItem);
}
Удаленный элемент исключается из коллекции, но упорно остается видимым в привязанном списке.
Чтобы включить отслеживание изменений коллекции, необходимо использовать коллекцию, реализующую интерфейс INotifyCollectionChanged. Большинство обобщенных коллекций этого не делают, в том числе и List<T>, которая применяется в приведенном примере. Фактически WPF включает единственную коллекцию, реализующую INotifyCollectionChanged — это класс ObservableCollection.
Можно унаследовать собственную коллекцию от ObservableCollection, чтобы настроить по своему усмотрению ее работу, хотя это не обязательно. В данном примере достаточно заменить объект List<CarTable> на ObservableCollection<CarTable>, как показано ниже:
public static ObservableCollection<CarTable> GetCars()
{
AutoShopEntities context = new AutoShopEntities();
return new ObservableCollection<CarTable>(
context.CarTables.Select(p => p).ToList<CarTable>());
}
private ObservableCollection<CarTable> cars;
private void cmdGetCar_Click(object sender, RoutedEventArgs e)
{
cars = MainWindow.GetCars();
lstCars.ItemsSource = cars;
}
private void cmdDeleteCar_Click(object sender, RoutedEventArgs e)
{
cars.Remove((CarTable)lstCars.SelectedItem);
}
Теперь, если программно удалить или добавить элемент в коллекцию, список будет соответствующим образом обновлен. Естественно, по-прежнему придется создавать код доступа к данным, который происходит перед модификацией коллекции — например, код, удаляющий запись о товаре из лежащей в основе базы данных.