Вы зашли как: Гость
24.08.2017 10:55 | dronov_va

Шестая, и последняя, часть статьи, посвящённой привязке данных и её использованию при программировании универсальных приложений Windows 10.

4.4. Сложный случай: привязка изменяемого списка



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

  • Сам список-источник должен поддерживать оповещения о добавлении, удалении элементов и изменении значений его свойств.

    Практически всегда такой список создаётся в виде объекта обобщённого класса ObservableCollection<T> - этот класс поддерживает все необходимые оповещения.
  • Если список-источник хранит элементы - объекты какого-либо класса, последний также должен поддерживать оповещения, реализуя интерфейс INotifyPropertyChanged (см. параграф 3.4).


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

Создадим новый проект Bindings6. Откроем файл с интерфейсным кодом начальной страницы и укажем у присутствующего на ней контейнера-сетки набор из четырёх строк и двух столбцов. Для трёх последних строк укажем такую высоту, чтобы только вместить их содержимое, а для первой - растягивание на всё оставшееся пространство контейнера (то есть высоту в одну долю). Для первого столбца укажем такую ширину, чтобы вместить его содержимое, для второго - опять же, растягивание на всё оставшееся пространство контейнера (также одна доля).

Поместим на страницу список ListBox с такими параметрами:

  • Имя - lstLangs;
  • Width - Auto;
  • Height - Auto;
  • Row - 0;
  • Column - 0;
  • RowSpan - 1;
  • ColumnSpan - 2;
  • HorizontalAlignment - Stretch;
  • VerticalAlignment - Stretch;
  • Margin - 0 пикселов со всех сторон.



Добавим две надписи. Для первой укажем параметры:

  • Text - "Название";
  • Width - Auto;
  • Height - Auto;
  • Row - 1;
  • Column - 0;
  • RowSpan - 1;
  • ColumnSpan - 1;
  • HorizontalAlignment - Stretch;
  • VerticalAlignment - Stretch;
  • Margin - 10 пикселов со всех сторон.



Вторая получит те же параметры, за исключением:

  • Text - "Описание";
  • Row - 2.



Далее добавим два поля ввода. Для первого укажем такие параметры:

  • Имя - txtName;
  • Text - "" (пустая строка);
  • Width - Auto;
  • Height - Auto;
  • Row - 1;
  • Column - 1;
  • RowSpan - 1;
  • ColumnSpan - 1;
  • HorizontalAlignment - Stretch;
  • VerticalAlignment - Stretch;
  • Margin - 10 пикселов со всех сторон.



Для второго - такие же, за исключением:

  • Имя - txtDescription;
  • Row - 2.



После этого поместим на форму контейнер-стопку (StackPanel) и зададим для неё параметры:

  • Width - Auto;
  • Height - Auto;
  • Row - 3;
  • Column - 0;
  • RowSpan - 1;
  • ColumnSpan - 2;
  • Orientation - Horizontal;
  • HorizontalAlignment - Right;
  • VerticalAlignment - Stretch.



В стопку вставим три кнопки (Button). Первая получит параметры:

  • Имя - cmdAdd;
  • Text - "Добавить";
  • Width - Auto;
  • Height - Auto;
  • HorizontalAlignment - Left;
  • VerticalAlignment - Center;
  • Margin - 10 пикселов со всех сторон.



Вторая - те же самые параметры, кроме следующих:

  • Имя - cmdSave;
  • Text - "Сохранить".



Третья - те же самые параметры, за исключением:

  • Имя - cmdDelete;
  • Text - "Удалить".



Добавим в проект новый файл программного кода PL.cs, в котором объявим класс PL, хранящий название и описание языка программирования и поддерживающий оповещения. Код, объявляющий этот класс, должен выглядеть так:

using System.ComponentModel;

. . .

public class PL : INotifyPropertyChanged
{
    private string __name;
    private string __description;
    public event PropertyChangedEventHandler PropertyChanged;

    public PL(string name, string description)
    {
        this.__name = name;
        this.__description = description;
    }

    public string Name
    {
        get
        {
            return this.__name;
        }
        set
        {
            if (this.__name != value)
            {
                this.__name = value;
                this.RaisePropertyChanged("Name");
            }
        }
    }

    public string Description
    {
        get
        {
            return this.__description;
        }
        set
        {
            if (this.__description != value)
            {
                this.__description = value;
                this.RaisePropertyChanged("Description");
            }
        }
    }

    protected void RaisePropertyChanged(string name)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}



Здесь всё нам уже знакомо.

Откроем файл с функциональным кодом начальной страницы, добавим в класс страницы поле data, в котором будет храниться список-источник, а в конструктор вставим код, создающий этот список. Вот так будет выглядеть объявление класса страницы:

using System.Collections.ObjectModel;

. . .

public sealed partial class MainPage : Page
{
    public ObservableCollection<PL> data;

    public MainPage()
    {
        this.InitializeComponent();

        this.data = new ObservableCollection<PL>();
    }
}



Переключимся на интерфейсный код начальной страницы и создадим привязку списка к полю data класса страницы:

<ListBox . . . ItemsSource="{x:Bind data, Mode=OneWay}"/>



Теперь нужно дать пользователю возможность пополнять список различными языками программирования и править его содержимое.

Выберем сам список, обратимся к панели Свойства, переключимся на перечень событий и создадим обработчик для события SelectionChanged - оно возникает при выборе какого-либо пункта в элементе-списке ListBox. В этом обработчике мы проверим, действительно ли в списке был выбран какой-либо пункт, получим индекс выбранного пункта и поместим в поля ввода txtName и txtDescription, соответственно, название и описание, извлечённые из соответствующего элемента списка-источника. После этого посетитель сможет исправить их.

Получить индекс выбранного в списке пункта можно обращением к свойству SelectedIndex. Если в списке не было выбрано ни одного пункта, это свойство хранит значение -1.

Вот код обработчика события SelectionChanged списка:

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    PL lang;
    int ind = lstLangs.SelectedIndex;
    if (ind != -1)
    {
        lang = this.data[ind];
        txtName.Text = lang.Name;
        txtDescription.Text = lang.Description;
    }
}



При щелчке на кнопке cmdAdd нам следует очистить оба поля ввода и сделать все пункты в списке lstLangs невыбранными; после этого пользователь сможет добавить в список новую позицию. Следовательно, нам следует привязать обработчик к событию Click вышеупомянутой кнопки (это событие возникает как раз при щелчке на элементе интерфейса).

Вот код обработчика события Click кнопки cmdAdd:

private void cmdAdd_Click(object sender, RoutedEventArgs e)
{
    lstLangs.SelectedIndex = -1;
    txtName.Text = String.Empty;
    txtDescription.Text = String.Empty;
}



Доступное только для чтения поле Empty класса String хранит пустую строку.

Кнопка cmdSave сохранит изменения, сделанные пользователем в позиции списка - либо вновь созданной, либо уже существующей. Код обработчика её события Click приведён ниже. Попробуйте сами разобраться, как он работает.

private void cmdSave_Click(object sender, RoutedEventArgs e)
{
    PL lang;
    int ind = lstLangs.SelectedIndex;
    if (ind == -1)
    {
        this.data.Add(new PL(txtName.Text, txtDescription.Text));
    }
    else
    {
        lang = this.data[ind];
        lang.Name = txtName.Text;
        lang.Description = txtDescription.Text;
    }
}



Метод Add класса ObservableCollection<T> делает то же самое, что одноимённый метод класса List<T>.

И, наконец, кнопка cmdDelete удалит выбранную в списке позицию. Код обработчика её события Click очень прост:

private void cmdDelete_Click(object sender, RoutedEventArgs e)
{
    int ind = lstLangs.SelectedIndex;
    if (ind != -1)
    {
        this.data.RemoveAt(ind);
    }
}



Метод RemoveAt класса ObservableCollection<T> удаляет элемент списка с индексом, переданным ему единственным аргументом, и не возвращает результата.

Осталось только задать для списка lstLangs шаблон. Переключимся на интерфейсный код начальной страницы и исправим тег <ListBox>, создающий этот список, следующим образом:

<ListBox . . .>
    <ListBox.ItemTemplate>
        <DataTemplate x:DataType="local:PL">
            <StackPanel>
                <TextBlock Text="{x:Bind Name, Mode=OneWay}" FontWeight="Bold"/>
                <TextBlock Text="{x:Bind Description, Mode=OneWay}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>



Здесь мы указали однонаправленную привязку для обеих надписей, что входит в состав шаблона. Это нужно для того, чтобы изменения, сделанные пользователям в выбранной позиции списка, после сохранения отображались в списке.

Внимание!
Для необъектной привязки значение её направления по умолчанию в различных ситуациях разное: в одних - OneWay, в других - OneTime. Поэтому у такой привязки всегда рекомендуется указывать направление явно.

Теперь можно сохранить проект, запустить приложение на выполнение и попробовать добавить несколько языков в список (рис. 17). После этого испытаем, как работает правка и удаление позиций.


Рис. 17. Приложение Bindings6




4.5. Самый сложный случай: привязка к выбранному пункту списка

Очень часто возникает необходимость вывести на экран какие-либо дополнительные данные, относящиеся к выбранному в списке пункту. Это могут быть как данные, слишком большие для того, чтобы отобразить их непосредственно в списке, списки подчинённых позиций и пр.

В приложении, которое мы сейчас создадим, в списке будут выводиться лишь названия языков программирования, а описание и, дополнительно, название какого-либо языка станет отображаться под списком в случае выбора представляющего его пункта. Для простоты список будет неизменяемым (как и в приложении Bindings5, описанном в параграфе 4.2).


4.5.1. Создание приложения

Создадим ещё один новый проект с именем Bindings7. Сразу же добавим в него файл программного кода PL.cs, в котором объявим класс PL. Вот код объявления этого класса:

public class PL
{
    public string Name { get; set; }
    public string Description { get; set; }

    public PL(string name, string description)
    {
        this.Name = name;
        this.Description = description;
    }
}



Если в предыдущих приложениях этот класс у нас имел два поля - name и description, - в которых хранились имя и описание языка программирования, то здесь мы превратили их в полноценные свойства. Зачем это сделано, мы узнаем чуть позже.

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

Откроем файл с интерфейсным кодом начальной страницы. У присутствующего там контейнера Grid создадим две строки; вторая получит такое значение высоты, чтобы лишь вместить своё содержимое, а первая займёт всё оставшееся пространство контейнера.

Добавим на страницу список с параметрами:

  • Имя - lstLangs;
  • Width - Auto;
  • Height - Auto;
  • Row - 0;
  • Column - 0;
  • RowSpan - 1;
  • ColumnSpan - 1;
  • HorizontalAlignment - Stretch;
  • VerticalAlignment - Stretch;
  • DisplayMemberPath - name.



Свойство DisplayMemberPath списка имеет смысл указывать только в том случае, если список-источник хранит объекты, а для элемента-списка не задан шаблон. Оно указывает имя свойства, из которого будет браться значение для формирования очередного пункта списка.

Отметим, что элемент-список с установленным свойством DisplayMemberPath может извлекать значения только из свойств. Если мы укажем в этом свойстве имя поля, список его не "найдёт". Поэтому в нашем новом классе PL для хранения данных мы использовали полноценные свойства.

Поместим на страницу контейнер-стопку с такими параметрами:

  • Width - Auto;
  • Height - Auto;
  • Row - 1;
  • Column - 0;
  • RowSpan - 1;
  • ColumnSpan - 1;
  • HorizontalAlignment - Stretch;
  • VerticalAlignment - Stretch.



В стопку вставим две надписи. Для обеих укажем одинаковые параметры:

  • Text - "" (пустая строка);
  • Width - Auto;
  • Height - Auto;
  • HorizontalAlignment - Stretch;
  • VerticalAlignment - Stretch;
  • Margin - 10 пикселов со всех сторон.



Сразу же создадим необъектную привязку списка к полю data страницы:

<ListBox . . . ItemsSource="{x:Bind data, Mode=OneTime}"/>



Сохраним проект и запустим приложение на выполнение. Если мы не допустили ошибок, список перечислит названия всех "знакомых" приложению Bindings7 языков программирования (рис. 18).


Рис. 18. Приложение Bindings7 в процессе разработки. Список показывает названия языков программирования



Теперь нам нужно сделать так, чтобы в надписях выводились название и описание языка программирования, выбранного в списке. Сделать это можно двумя способами - простым и сложным.


4.5.2. Простой способ: привязка к выбранному пункту списка

Проще всего привязать обе надписи к выбранному в списке пункту. А именно - к свойству SelectedItem списка, хранящему элемент списка-источника, соответствующий выбранному в элементе-списке пункту.

Поскольку в качестве источника обе надписи будут использовать значение вышеупомянутого свойства, мы укажем его в качестве источника непосредственно для родителя надписей - контейнера-стопки. Здесь будет иметь место знакомая нам привязка к элементу управления, а именно - свойства DataContext стопки к свойству SelectedItem списка lstLangs.

Воспользуемся для этого визуальными средствами Visual Studio - диалоговым окном Создание привязки данных, в котором зададим параметры, показанные на рис. 19.


Рис. 19. Параметры привязки



Теперь останется только создать привязки свойств Text обеих надписей к соответствующим свойствам элемента списка-источника - класса PL.

Странный факт: Visual Studio позволяет для такого случая создать необъектную привязку и даже успешно компилирует приложение, но привязка не работает - при выборе в списке любого пункта в надписях ничего не выводится. Поэтому нам придётся создать объектную привязку. Формирующий её код для первой надписи будет выглядеть так:

<TextBlock . . . Text="{Binding Name}"/>



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

Сохраним проект и запустим приложение на выполнение. Попробуем выбрать в списке какой-либо пункт и посмотрим, что выводится под списком (рис. 20).


Рис. 20. Полностью функциональное приложение Bindings7



Привязку к выбранному в списке пункту имеет смысл применять, если нам не требуется реализовывать в списке сортировку и группировку пунктов. В таком случае мы исключим необходимость создания дополнительного объекта представления, описываемого далее.


4.5.3. Сложный способ: привязка к представлению

Более сложный подход - привязка и списка, и надписей к так называемому представлению.

Представлением называется объект класса CollectionViewSource, который:

  • выступает посредником между источником и приёмниками привязки;
  • при привязке его к списочному элементу управления передаёт ему список-источник;
  • отслеживает выбранный в списке пункт;
  • при привязке к элементам интерфейса, не являющихся списочными, передаёт им элемент списка-источника, соответствующий выбранному в элементе-списке пункту;
  • поддерживает ряд дополнительных возможностей, в частности, сортировку и группировку элементов списка-источника, которые не затрагивают последний.



Для указания источника в представлении служит свойство Source. Здесь также применяется привязка.

Обычно представление помещается в состав ресурсов страницы или приложения, а элементы интерфейса связываются с ним. Так мы и поступим.

Переключимся на интерфейсный код страницы и добавим следующий код непосредстенно после открывающего тега <Page>:

<Page . . . >
   
    <Page.Resources>
        <CollectionViewSource x:Key="PLSource" Source="{x:Bind data}"/>
    </Page.Resources>
   
    . . .
</Page>



Здесь мы привязываем к представлению поле data страницы и помещаем представление в ресурсы страницы под ключом "PLSource".

Теперь привяжем к только что созданному представлению список, использовав для указания источника ещё нам не знакомое свойство Source класса Binding. (Это свойство применяется в особых случаях, подобных этому.) Вот XAML-код, создающий соответствующую привязку:

<ListBox . . . ItemsSource="{Binding Source={StaticResource PLSource}}"/>



Здесь, опять же, мы использовали объектную привязку, поскольку только она позволяет связать приёмник с источником-ресурсом.

Укажем представление в качестве источника для контейнера-стопки:

<StackPanel . . . DataContext="{Binding Source={StaticResource PLSource}}">



Как видим, здесь используется привязка с теми же самыми параметрами, что и предыдущая.

И, наконец, свяжем обе надписи с нужными свойствами класса PL. Делается это точно так же, как и в предыдущем, простом, случае.

Сохраним проект, запустим приложение и убедимся, что оно работает (см. рис. 20).

Привязку к представлению следует применять, если нам нужно реализовать в списке сортировку и группировку пунктов. Если же таковое не нужно, имеет смысл использовать первый, упрощённый, способ.


Дополнительные материалы




Владимир Дронов, MSInsider Team
Май 2017

Комментарии

Комментариев нет...
Для возможности комментировать войдите в 1 клик через

По теме

Акции MSFT
74.41 0.00
Акции торгуются с 17:30 до 00:00 по Москве
Мы на Facebook
Мы ВКонтакте
Все права принадлежат © MSInsider.ru (ex TheVista.ru), 2017
Сайт является источником уникальной информации о семействе операционных систем Windows и других продуктах Microsoft. Перепечатка материалов возможна только с разрешения редакции.
Работает на WMS 2.34 (Страница создана за 0.023 секунд (Общее время SQL: 0.005 секунд - SQL запросов: 31 - Среднее время SQL: 0.00016 секунд))