Вы зашли как: Гость
14.07.2017 16:46 | dronov_va

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

3.4. Реализация оповещений

Если мы сейчас попробуем запустить приложение на выполнение, введём какое-либо число в поле Сантиметры и перенесём фокус ввода, то с удивлением обнаружим, что в поле ввода Дюймы ничего не появилось. И это притом, что, выполнив трассировку кода класса CIData, то заметим, что значение, занесённое в поле ввода Сантиметры, успешно переносится в поле __centimetres этого класса (посредством функции-сеттера свойства Centimetres).

По идее, поле ввода Дюймы должно обращаться к свойству Inches каждый раз, когда значение поля __centimetres изменится. Но не делает этого.

Почему? Потому что наш класс CIData не поддерживает оповещений.

Оповещение в терминологии UWP - это особое событие, которое должно поддерживаться классом-источником. Это событие должно возникать всякий раз, когда состояние источника изменяется (например, когда в свойство источника заносится новое значение, как в нашем случае). Приёмник привязывает к этому событию обработчик и, таким образом, всегда остаётся "в курсе", что состояние источника изменилось, и ему следует повторно обратиться к связанному свойству, дабы извлечь новое значение.

Событие, о котором только что шла речь, должно иметь имя PropertyChanged и относиться к типу PropertyChangedEventHandler. Его обработчики принимают в качестве второго аргумента объект класса PropertyChangedEventArgs. (Первым аргументом, как обычно, указывается объект, в котором возникло данное событие.) В конструкторе этого класса единственным аргументом ставится строковое имя свойства, значение которого изменилось; также можно указать пустую строку (String.Empty) - в этом случае подразумевается, что изменились значения всех свойств.

И, наконец, класс-источник должен реализовывать интерфейс INotifyPropertyChanged. Он-то и содержит вышеупомянутое событие PropertyChanged (других элементов в нём нет).

Все эти типы объявлены в пространстве имён System.ComponentModel. Поэтому в начало функционального кода следует добавить выражение, выполняющее присоединение этого пространства имён.

Давайте перепишем класс CIData таким образом, чтобы он получил поддержку оповещений. Обновлённый код этого класса приведён ниже.

using System.ComponentModel;

. . .

public class CIData : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private double __centimetres = 0;

    public string Centimetres
    {
        get
        {
            return this.__centimetres.ToString();
        }
        set
        {
            double fl = 0;
            if (double.TryParse(value, out fl))
            {
                if (this.__centimetres != fl)
                {
                    this.__centimetres = fl;
                    this.RaisePropertyChanged("Inches");
                }
            }
        }
    }

    public string Inches
    {
        get {
            return (this.__centimetres / 2.54).ToString();
        }
    }

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



Здесь всё довольно просто. Мы объявляем защищённый метод RaisePropertyChanged, который в качестве единственного параметра принимает строковое имя свойства, значение которого изменилось, проверяет, был ли к событию PropertyChanged привязан хотя бы один обработчик, и, если это так, запускает выполнение обработчиков данного события. Таковым мы передаём два аргумента: текущий объект (ведь именно в нём возникло событие) и объект класса PropertyChangedEventArgs, хранящий имя изменившегося свойства. Наконец, при занесении нового значения в поле __centimetres, в коде сеттера свойства Centimetres, мы вызываем метод RaisePropertyChanged, передав ему имя свойства Inches.

Также не забудем вставить в начало кода выражение, выполняющее присоединение пространства имён System.ComponentModel, - иначе получим ошибку компиляции.

Теперь мы можем, наконец, проверить наше полностью готовое приложение в действии. Сохраним проект, запустим его на выполнение, введём в поле ввода Сантиметры какое-то число и переведём фокус ввода на поле Дюймы. Мы сразу увидим в последнем ту же величину, но выраженную в дюймах.


3.5. Управление режимом переноса значения

Ранее говорилось, что, если в приёмнике связанным свойством является Text, перенос значения в "обратном" направлении выполняется только после потери элементом управления фокуса ввода. Мы уже заметили это: в нашем приложении вычисление и вывод величины в дюймах производится только тогда, когда поле ввода Сантиметры теряет фокус ввода. А это довольно неудобно, так как вынуждает пользователя выполнять лишний щелчок мышью или лишнее нажатие клавиши <Tab>.

Однако мы можем сделать так, чтобы "обратный" перенос значения выполнялся в определённый нами самими момент времени. Например, сразу после изменения значения в поле ввода.

Режимом переноса значения управляет свойство UpdateSourceTrigger класса Binding. В качестве значения оно принимает один из элементов перечисления UpdateSourceTrigger:

  • Default - режим по умолчанию (перенос выполняется сразу после изменения значения свойства; в случае свойства Text - после потери элементом управления фокуса ввода);
  • PropertyChanged - то же самое, что и Default;
  • Explicit - перенос выполняется явно и инициируется в функциональном C#-коде.


Отметим сразу, что режим переноса значения имеет смысл выставлять только в случае двунаправленной привязки. У привязок прочих разновидностей этот параметр не имеет смысла.

Итак, первое, что мы сделаем, - зададим свойству UpdateSourceTrigger привязки, заданной для поля ввода txtCentimetres, значение Explicit.

Если мы создавали привязку первым способом, то сможем указать нужный режим в раскрывающемся списке UpdateSourceTrigger окна Создание привязки данных (см. рис. 11). Если же привязка создавалась вторым способом, нам придётся ввести соответствующий XAML-код вручную:

<TextBox x:Name="txtCentimetres" . . . Text="{Binding . . . UpdateSourceTrigger=Explicit}"/>



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

  • вызвать у приёмника метод GetBindingExpression. Его единственным аргументом должно быть обозначение свойства зависимости приёмника, к которому была выполнена привязка; в качестве результата метод вернёт объект класса BindingExpression, который представит описание соответствующей привязки;
  • у полученного ранее объекта класса BindingExpression вызвать не принимающий аргументов и не возвращающий результата метод UpdateSource, который запустит перенос значения.


Нам нужно сделать так, чтобы результат вычисления обновлялся сразу после изменения значения, занесённого в поле ввода Сантиметры. Следовательно, инициирование переноса мы выполним в коде обработчика события TextChanged класса TextBox.

Выберем поле ввода txtCentimetres. Обратимся к панели Свойства, найдём расположенную правее поля ввода Имя кнопку со значком молнии и щёлкнем на ней. В панели вместо свойств появится список событий, поддерживаемых полем ввода. Найдём событие TextChanged и дважды щёлкнем на поле ввода, что находится правее имени этого события. Visual Studio сгенерирует объявление метода страницы - обработчика вышеупомянутого события.

Вот полный код этого метода-обработчика:

private void txtCentimetres_TextChanged(object sender, TextChangedEventArgs e)
{
    (sender as TextBox).GetBindingExpression(TextBox.TextProperty).UpdateSource();
}



Каждый метод - обработчик события в качестве первого параметра получает объект, в котором возникло это событие (источник события). В нашем случае значением этого параметра будет поле ввода txtCentimetres; этим мы и воспользуемся, чтобы получить к нему доступ (только предварительно приведём его к типу TextBox). Остальной код не требует пояснений.

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

На этом всё. Мы можем сохранить проект и запустить приложение на выполнение. Теперь преобразование введённой величины в дюймы будет выполняться непосредственно в процессе её ввода. Что очень удобно!


3.6. Необъектная привязка

Ещё в начале этой главы мы узнали, что каждая привязка, что мы создали в приложении, является объектом класса Binding, предоставляемого самой платформой. Такой объект создаётся при запуске приложения, отслеживает изменение значения в свойстве источника (и приёмника, если была создана двунаправленная привязка) и выполняет перенос значения. Поэтому такая привязка носит название объектной.

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

Однако платформа UWP поддерживает ещё и так называемую необъектную привязку. Во многих случаях её использование может оказаться выгоднее.


3.6.1. Необъектная привязка: описание, преимущества и недостатки

Необъектная привязка, как ясно из названия, не является объектом класса Binding. Для каждой такой привязки компилятор Visual Studio создаёт в исполняемом файле программный код, который и выполняет все задачи по отслеживанию изменения значения и его переносу. Другими словами, такая привязка полностью создаётся во время компиляции приложения.

В необъектной привязке в качестве источника всегда выступает объект текущей страницы. Это нужно иметь в виду.

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

Для создания необъектной привязки применяется объектное синтаксическое расширение x:Bind. Оно имеет практически такой же синтаксис, что и знакомое нам расширение Binding.

Необъектная привязка имеет ряд преимуществ при объектной:

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


Однако имеются и недостатки:

  • невозможно задать произвольный источник привязки (таковым всегда является страница);
  • невозможно управлять режимом переноса значения - поскольку такая привязка не является объектом, мы не можем программно инициировать перенос вызовом методов этого объекта;
  • если выполняется привязка к какому-либо объекту, являющемуся значением свойства источника, и точный класс этого объекта становится известным только на этапе выполнения, необъектная привязка вообще не может быть создана, - поскольку компилятор просто "не знает" класс объекта (с такой ситуацией мы столкнёмся позже, когда будет заниматься привязкой списков);
  • Visual Studio не предоставляет никаких визуальных инструментов для создания необъектных привязок, и необходимый XAML-код приходится вводить вручную.


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


3.6.2. Создание необъектной привязки

Создадим ещё один проект под именем Bindings3. У начальной страницы нового приложения сделаем тот же интерфейс, что и у предыдущих двух приложений, опять же, пока что без всяких привязок.

Добавим в проект новый файл программного кода с именем CIData.cs. Напишем в нём объявление класса CIData - то же, что записали при разработке предыдущей редакции приложения.

Поскольку у необъектной привязки источником всегда выступает объект страницы, нам следует сделать так, чтобы объект класса CIData был доступен через свойство класса страницы. И, разумеется, написать код, который создаст объект класса CIData и сохранит его в этом свойстве.

Откроем файл функционального кода страницы MainPage.xaml.cs и дополним код объявления её класса, чтобы он выглядел следующим образом:

public sealed partial class MainPage : Page
{
    public CIData CI { get; set; }

    public MainPage()
    {
        this.InitializeComponent();
        this.CI = new CIData();
    }
}



Здесь всё понятно без комментариев.

Переключимся на интерфейсный код страницы. Создадим у поля ввода txtCentimetres необъектную привязку свойства Text к свойству Centimetres объекта класса CIData, что хранится в свойстве CI страницы. Для этого следует дополнить XAML-код, создающий это поле ввода, таким фрагментом:

<TextBox x:Name="txtCentimetres" . . . Text="{x:Bind CI.Centimetres, Mode=TwoWay}"/>



Как видим, этот код практически аналогичен тому, что формирует объектную привязку.

Точно таким же образом создадим привязку свойства Text второго поля ввода к свойству Inches объекта класса CIData, хранящегося в свойстве CI страницы. Это будет однонаправленная привязка.

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

Кстати, автор этой статьи заметил, что даже будучи запущенным в режиме отладки, из среды Visual Studio, приложение Bindings3 стартует несколько быстрее, чем Bindings2.

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



Продолжение следует...

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

Комментарии

compik +95
Не в сети

  • Default - режим по умолчанию (перенос выполняется сразу после изменения значения свойства; в случае свойства Text - после потери элементом управления фокуса ввода);
  • PropertyChanged - то же самое, что и Default;


Если мне не изменяет память, Default меняет значение свойства при смене фокуса, а PropertyChanged при вводе каждого отдельного символа. 

18.07.17 00:55
0
Не в сети

compik писал:
Если мне не изменяет память, Default меняет значение свойства при смене фокуса, а PropertyChanged при вводе каждого отдельного символа.


Нет, эти значения дают одинаковый эффект. Так написано здесь.

18.07.17 09:36
0
compik +95
Не в сети

dronov_va писал:
Нет, эти значения дают одинаковый эффект. Так написано здесь.

Нет, я был прав, соберите этот простой пример и убедитесь сами

public sealed partial class MainPage : Page, INotifyPropertyChanged
    {
        public MainPage()
        {
            this.InitializeComponent();
            DataContext = this;
        }
        string text;
        public string Text { get { return text; } set { text = value; PropertyChanged(this, new PropertyChangedEventArgs(nameof(Text))); }   }
        public event PropertyChangedEventHandler PropertyChanged;
    }

<StackPanel>
        <TextBox Text="{Binding Text, Mode=TwoWay}" />
        <TextBox Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    </StackPanel>

18.07.17 17:19
0
Не в сети

compik, для чистоты эксперимента я взял приложение Bindings1 (из первых частей статьи), в котором не было никаких наворотов в плане управления переносом значения связанного свойства, и попробовал изменять значения свойства UpdateSourceTrigger привязки. При указании значений Default и PropertyChanged приложение работало одинаковым образом - значение переносилось сразу после его изменения, то есть, по Вашим словам, при вводе каждого символа.

Похоже, Вы используете другую платформу, не UWP, - скорее всего, полноценную .NET. В ней наблюдается такое поведение, как Вы описали (подробности [url=https://msdn.microsoft.com/en-us/library/system.windows.data.updatesourcetrigger(v=vs.110).aspx]здесь[/url]).

20.07.17 10:19
0
compik +95
Не в сети

dronov_va писал:
Похоже, Вы используете другую платформу, не UWP, - скорее всего, полноценную .NET. В ней наблюдается такое поведение, как Вы описали (подробности [url=https://msdn.microsoft.com/en-us/library/system.windows.data.updatesourcetrigger(v=vs.110).aspx]здесь[/url]).

Нет, создал новый UWP проект для Creators Update, триггер работает так же как и в WPF. Собсвтенно то что это UWP а не WPF вы можете определить по заголовку класса
public sealed partial class MainPage : Page, INotifyPropertyChanged
для WPF базовым был бы класс Window а не Page.
Могу прислать совой проект, если нужно.

21.07.17 23:20
0
Не в сети

compik, как бы то ни было, при указании у свойства UpdateSourceTrigger значений Default и PropertyChanged приложение работает одинаково. Следовательно эти значения дают одинаковый эффект.

Попробуйте создать новый проект UWP, без всяких наворотов наподобие реализации интерфейса INotifyPropertyChanged, и проверьте сами.

22.07.17 13:43
0
compik +95
Не в сети

dronov_va писал:
Попробуйте создать новый проект UWP, без всяких наворотов наподобие реализации интерфейса INotifyPropertyChanged, и проверьте сами.

Вы, простите, собственную статью читали. Пункт 3.4

И, наконец, класс-источник должен реализовывать интерфейс INotifyPropertyChanged. Он-то и содержит вышеупомянутое событие PropertyChanged (других элементов в нём нет).

dronov_va писал:
при указании у свойства UpdateSourceTrigger значений Default и PropertyChanged приложение работает одинаково.

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

22.07.17 14:37
0
Не в сети

Хм, действительно, значения Default и PropertyChanged свойства UpdateSourceTrigger привязки дают разный эффект: в первом случае перенос значения выполняется после потери фокуса элементом управления, а во втором - сразу после изменения. Выходит, в статью MSDN вкралась ошибка?

compik, Вы были правы.

24.07.17 10:19
0
compik +95
Не в сети

dronov_va писал:
Выходит, в статью MSDN вкралась ошибка?

Не думаю, что ошибка намеренная. Статья на которую вы мне предоставили ссылку описывает 
Windows.Foundation.UniversalApiContract, version 1.0 (т.е. для оригинальной версии Windows 10, 10240)
Текущая актуальная версия для Creators Update 4.0. Возможно, два года назад привязка и работала так как вы описали в статье. Думаю PropertyChanged на тот момент бы просто временной затычкой для портирования разметки из WPF. А вот почему МС забила на обновление документации - большой-большой секрет. 

24.07.17 11:25
0
Не в сети

compik писал:
Не думаю, что ошибка намеренная. Статья на которую вы мне предоставили ссылку описывает
Windows.Foundation.UniversalApiContract, version 1.0 (т.е. для оригинальной версии Windows 10, 10240)
Текущая актуальная версия для Creators Update 4.0. Возможно, два года назад привязка и работала так как вы описали в статье.


Однако в этой статье всё написано правильно. Правда, этому моменту посвящён всего один небольшой абзац (наверно, поэтому я и проглядел его, когда писал статью).

25.07.17 09:17
0
Для возможности комментировать войдите в 1 клик через

По теме

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