Привязка к коллекции объектов

110

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

Привязка к единственному объекту довольно проста. Но все становится намного интереснее, когда нужно привязаться к некоторой коллекции объектов, например, ко всем машинам в таблице.

Хотя каждое свойство зависимости поддерживает привязку к одному значению, которая применялась до сих пор, привязка к коллекции требует несколько более интеллектуального элемента. В 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);
        }

Теперь, если программно удалить или добавить элемент в коллекцию, список будет соответствующим образом обновлен. Естественно, по-прежнему придется создавать код доступа к данным, который происходит перед модификацией коллекции — например, код, удаляющий запись о товаре из лежащей в основе базы данных.

Пройди тесты
Лучший чат для C# программистов