Практикум прикладного программирования на C# в среде VS.NET 2008

Основы привязки данных в WPF

Разбить на страницы
Показывать лекцию целиком

Часть I

Общие положения

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

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

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

Синтаксис привязки данных, как и в случае с ресурсами, также имеет два варианта: расширения разметки и элементов свойств, но отличается деталями. Ключевым элементом привязки для любого варианта является определение объекта Binding из пространства имен System.Windows.Data. Этот элемент всегда устанавливается на стороне приемника привязки, кроме режима Mode=OneWayToSource. Приемник должен быть производным от класса DependencyObject и привязываемое свойство (целевое свойство) должно быть свойством зависимости. В свойства зависимостей встроена способность посылать или принимать уведемления об изменениях.

К источнику привязки предъявляется гораздо меньше требований. Связываемое свойство источника не обязано быть зависимым свойством. Главное, чтобы источник имел оповещающее событие, указывающее на изменение связываемого свойства. Источником привязки может быть любое открытое свойство, в том числе свойства других элементов управления, объекты среды CLR, элементы XAML, наборы данных ADO.NET, фрагменты XML и т.д. Для правильного применения привязки к сложным объектам данных технология WPF предоставляет два специализированных класса - XmlDataProvider и ObjectDataProvider.

Свойства зависимостей еще называют присоединенными. В версии XAML, предназначенной для WPF, присоединенные свойства работают только в том случае, если и тип, в котором свойство определено, и тип, к которому оно присоединяется, оба наследуют классу DependencyObject. С помощью свойств зависимостей язык XAML имеет возможность расширять типы за счет свойств, предоставляемых другими типами. При использовании в разметке по синтаксису элементов свойств присоединенному свойству всегда предшествует имя предоставляющего его типа, даже если такое свойство употребляется в качестве атрибута.

Направления привязки

Тип привязки элемента Binding определяется его свойством Mode, которое может принимать одно из значений перечисления BindingMode из пространства имен System.Windows.Data:

  • Default - установлен по умолчанию и зависит от типа привязываемого свойства на стороне приемника (целевого свойства). Действует как режим двухсторонней привязки TwoWay для свойств, доступных для редактирования в пользовательском интерфейсе, таких как TextBox.Text или CheckBox.Checked, либо - как односторонняя привязка OneWay для иных свойств. Чтобы не полагаться на настройки по умолчанию, следует взять себе за правило - всегда явно устанавливать параметр направления привязки.
  • OneTime - односторонняя начальная привязка, когда значение целевого свойства устанавливается по значению источника только один раз: при инициализации, программной замены привязанного объекта-источника на новый, при изменении свойства DataContext или в результате программного вызова метода BindingExpression.UpdateTarget (). Иные поступающие уведомления об изменениях на стороне источника приемником учитываться не будут
  • OneWay - односторонняя привязка, когда целевое свойство обновляется при изменении свойства источника. Каждый раз при изменении на стороне источника поток данных направлен от источника к целевому объекту.
  • OneWayToSource - организует однонаправленную привязку, как и OneWay, только выражение привязки помещается в источник. Этот трюк может понадобиться в том случае, когда привязываемое свойство-приемник не является свойством зависимости, а свойство источника все-таки наследует классу DependencyObject.
  • TwoWay - двухсторонняя привязка, когда целевое свойство обновляется при изменении свойства источника и свойство-источник обновляется при изменении целевого свойства. Иными словами, режим привязки TwoWay отправляет данные от источника к целевому объекту, а в случае изменения значения свойства целевого объекта данные отправляются обратно от целевого объекта к источнику
  • Упражнение 1. Привязка элемента к элементу

    В данном упражнении на ряде примеров рассмотрим вопросы создания канала связи между свойствами зависимости визуальных элементов WPF пользовательского интерфейса.

  • Командой
  • Откройте файл .App.xaml и добавьте в коллекцию ресурсов приложения объект SolidColorBrush со следующими настройками
  • <Application x:Class="UseResource.App"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        StartupUri="Window1.xaml">
        <!-- Определение ресурсов в коллекции приложения -->
        <Application.Resources>
            <SolidColorBrush x:Key="ControlColorBrush" 
                             Color="{x:Static SystemColors.ControlColor}" />
        </Application.Resources>
    </Application>>

    Вкладка Page1. Обновление значений свойств элементов без использования привязки

    Вначале построим пример передачи информации между элементами с использованием событий, как это традиционно делается в обычных приложениях

  • Поместите в разметку файла Window1.xaml следующий дескрипторный код с вкладкой Page1
  • <Window x:Class="ElementWithElement.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Привязка элемента к элементу" 
        MinHeight="300" 
        MinWidth="300"
        Height="300" 
        Width="300"
        WindowStartupLocation="CenterScreen"
        Background="{StaticResource ControlColorBrush}"
        Loaded="Window_Loaded">
        <Grid>
            <TabControl>
                <!-- Page1. Использование обработчика для обновления свойств -->
                <TabItem Header="Page1">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15">
                            <TextBlock.Foreground>
                                <SolidColorBrush Color="Red" />
                            </TextBlock.Foreground>
                            Соединение свойств без привязки
                        </TextBlock>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Name="label1" Width="40" />
                            <Slider Name="slider1" Width="230" 
                                Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft"
                                ValueChanged="slider1_ValueChanged" />
                        </StackPanel>
                        <TextBlock Name="textBlock1" Margin="0,10" 
                            Foreground="Blue">
                            Simple Text
                        </TextBlock>
                    </StackPanel>
                </TabItem>
            </TabControl>
        </Grid>
    </Window>
  • Заполните файл процедурного кода Window1.xaml.cs следующим образом
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    namespace ElementWithElement
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        
            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                // Инициализация для Page1
                int value = Convert.ToInt32(textBlock1.FontSize);// Явное приведение типов
                slider1.Minimum = value;    // Тип int приводится к double по умолчанию
                label1.Content = value.ToString();// Content имеет тип Object и проглотит все
            }
        
            // Обработчик для Page1
            private void slider1_ValueChanged(object sender, 
                RoutedPropertyChangedEventArgs<double> e)
            {
                if (!this.IsLoaded)
                    return;// Событие возбуждается, а элементы еще не созданы
        
                Slider slider = (Slider)sender; // Повышаем полномочия ссылки
                int value = (int)slider.Value;  // Явное приведение типов
                label1.Content = value;         // Content имеет тип Object и проглотит все
                textBlock1.FontSize = value;    // Тип int приводится к double неявно
            }
        }
    }
  • Запустите приложение и испытайте работу элементов вкладки Page1. Разберитесь с кодом разметки и процедурным кодом
  • Результат будет выглядеть так

    Вкладка Page2. Однонаправленная привязка на основе разметки по синтаксису элементов свойств

    Привязка на основе разметки более предпочтительна, потому что она проще и требует меньше работы. Хотя, создание привязки в коде тоже имеет место быть. В данном примере мы решим ту же самую задачу, что и в предыдущем примере. Только будем использовать уже привязку свойства-источника Value бегунка к элементам отображения Label и TextBlock.

  • Добавьте в контейнер TabControl после элемента вкладки Page1 файла Window1.xaml еще один элемент вкладки с именем Page2 со следующим кодом привязки по синтаксису элементов свойств
  • <!-- Page2. Использование привязки по синтаксису элементов свойств -->
                <TabItem Header="Page2">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15">
                            <TextBlock.Foreground>
                                <SolidColorBrush Color="Red" />
                            </TextBlock.Foreground>
                            Использование привязки по
                            <LineBreak />
                            синтаксису элементов свойств
                        </TextBlock>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40" ContentStringFormat="F0">
                                <Label.Content>
                                    <Binding ElementName="slider2" Path="Value" Mode="OneWay" />
                                </Label.Content>
                            </Label>
                            <Slider Name="slider2" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="Both" />
                        </StackPanel>
                        <TextBlock Margin="0,10" Foreground="Blue">
                            <TextBlock.FontSize>
                                <Binding ElementName="slider2" Path="Value" Mode="OneWay" />
                            </TextBlock.FontSize>
                            Simple Text
                        </TextBlock>
                    </StackPanel>
                </TabItem>

    В разметке вкладки Page2 мы оставили имя только источника, поскольку на него ссылаемся в привязке. А имена приемников нам просто не нужны, так как обработчики в процедурном коде мы не создаем. Соответственно, убрана регистрация обработчика для события Slider.ValueChanged, в результате чего код получился намного проще, чем в примере вкладки Page2. Добавлен параметр Minimum="11", определяющий нижнее значение ползунка. Обратите внимание, что при инициализации вкладки это значение сразу отображается элементами Label и TextBlock - механизм привязки знает свое дело.

  • Попробуйте установить другое значение Minimum, например, Minimum="36" и подвигать ползунком
  • Элементы привязки мы поместили в приемники и в свойстве Path указали, какое именно свойство зависимости мы выбираем из источника для привязки. Свойство ContentStringFormat="F0" (здесь после F стоит ноль) в элементе Label запрещает выводить дробную часть значения ползунка. Свойство Mode="OneWay" устанавливает однонаправленный поток данных от источника ( Slider ) к приемникам ( Label и TextBlock ). Содержимое 'Simple Text' надписи последнего элемента TextBlock можно поменять местами с секцией привязки

  • Испытайте пример - функциональность вкладки Page2 получилась абсолютно одинаковой с вкладкой Page1. Разберитесь с разметкой и подивитесь простоте и мощности механизма привязки
  • Представление окна будет таким

    Вкладка Page3. Однонаправленная привязка по синтаксису расширения разметки

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page3, представленную следующей разметкой
  • <!-- Page3. Использование привязки по синтаксису расширения разметки -->
                <TabItem Header="Page3">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Привязка расширением разметки" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider3, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider3" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="BottomRight"
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider3, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>

    Привязка по синтаксису расширения разметки похожа на присоединение ресурсов по тому же синтаксису, только список параметров привязки разделяется запятыми. Примененный синтаксис привязки сильно сокращает код разметки. Этот синтаксис еще называют синтаксисом атрибута, поскольку он концентрирует код привязки данных внутри атрибута привязываемого элемента. В сущности, тег Binding помещается в фигурные скобки наравне с атрибутами элемента.

    Обратите внимание, что по сравнению с примером Page2 мы убрали из атрибутов элемента Label параметр форматирования ContentStringFormat="F0", а вместо этого добавили к элементу Slider свойство IsSnapToTickEnabled="True". Такой прием заставляет свойство Value элемента Slider меняться с установленным шагом дискретности TickFrequency="2" и притягивает головку бегунка к делениям шкалы. Это побуждает элемент Label отображать значение свойства Slider.Value типа double с нулевой дробной частью как целое число.

  • Запустите приложение - вкладка Page3 представляет тот же самый результат
  • И опять, для вкладки Page3 мы не написали ни строчки процедурного кода!

    Вкладка Page4. Множественная привязка

    В элементе-приемнике мы можем привязывать к различным источникам более чем одно свойство. Вот пример

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page4, представленную следующей разметкой
  • <!-- Page4. Множественная привязка -->
                <TabItem Header="Page4">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Множественная привязка" 
                            />
                        <TextBox Name="textBox4" Margin="5,0,5,15" Text="Simple Text" />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider4, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider4" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="BottomRight" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <ListBox Name="listBox4" Margin="5">
                            <ListBox.Resources>
                                <SolidColorBrush x:Key="redBrush" Color="Red" />
                                <SolidColorBrush x:Key="greenBrush" Color="Green" />
                                <SolidColorBrush x:Key="blueBrush" Color="Blue" />
                            </ListBox.Resources>
                            <ListBoxItem Tag="{StaticResource redBrush}" Content="Red" />
                            <ListBoxItem Tag="{StaticResource greenBrush}" Content="Green" />
                            <ListBoxItem Tag="{StaticResource blueBrush}" Content="Blue"
                                Selector.IsSelected="True" />
                        </ListBox> 
                        <TextBlock Margin="0,10" 
                            FontSize="{Binding ElementName=slider4, Path=Value, Mode=OneWay}"
                            Text="{Binding ElementName=textBox4, Path=Text, Mode=OneWay}"
                            Foreground="{Binding ElementName=listBox4, Path=SelectedItem.Tag, Mode=OneWay}"
                            />
                    </StackPanel>
                </TabItem>

    В данном примере мы ввели текстовое поле <TextBox> для изменения отображаемого текста, и элемент списка <ListBox>, который применили для выбора пользователем цвета. Элемент <TextBlock> мы привязали сразу к трем элементам-источникам, в которых определяется: содержимое текста, размер и цвет шрифта. Передача информации в связанных элементах осуществляется в однонаправленном режиме. Для привязки по цвету используется свойство Tag элементов списка, которое относится к типу Object.

  • Запустите приложение и испытайте работу вкладки Page4, реализующей множественную привязку. Разберитесь с кодом разметки
  • Вкладка будет выглядеть так

    При однонаправленной привязке на стороне приемника данные источника непрерывно отражаются в приемнике. Это видно на примере поведения свойства Text элемента TextBlock, которое получает однонаправленные данные из текстового поля объекта textBox4. Далее мы увидим, что при двунаправленной привязке, когда приемник привязан к источнику в режиме Mode=TwoWay, приемник также будет получать данные источника непрерывно, но обратную отсылку при вводе данных на стороне приемника он сможет осуществить источнику только при потере фокуса. Это обусловлено значением параметра UpdateSourceTrigger=LostFocus, которое принято по умолчанию объектом Binding для обновления привязки.

    Вкладка Page5. Связывание элементов в цепочку

    Передачу информации можно осуществлять по цепочке, связывая элементы последовательно друг с другом. Например, источник <Slider> можно связать с приемником <Label>, а затем считать <Label> как источник и связать с ним приемник <TextBlock>. При манипулировании с источником <Slider> информация от него через элемент <Label> транзитом будет передаваться в <TextBlock>. Несмотря на то, что такой способ работает исправно, он может изрядно запутать код разметки. Лучше связывать источник и приемник напрямую. Но мы, как программисты, должны знать об этом, поэтому приведем простой пример.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page5, представленную следующей разметкой
  • <!-- Page5. Цепочки привязок -->
                <TabItem Header="Page5">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Цепочки привязок" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Name="label5" Width="40"
                                Content="{Binding ElementName=slider5, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider5" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=label5, Path=Content, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Запустите приложение и испытайте работу вкладки Page5, реализующей цепочку привязок
  • Вкладка будет выглядеть точно также, как вкладка Page3, где мы два разных приемника привязали к одному и тому же источнику.

    Этот способ может пригодиться в том случае, если источник находится где-то в труднодоступном месте (например, в другом окне или сборке) и код привязки достаточно объемен. Основательно потрудившись над привязкой к такому источнику первого приемника, другие приемники проще будет привязать к нему, чем к первоисточнику.

    Вкладка Page6. Двунаправленная привязка приемника

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page6, представленную следующей разметкой
  • <!-- Page6. Двунаправленная привязка -->
                <TabItem Header="Page6">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red">
                            Двунаправленная привязка 
                            <LineBreak />
                            элемента TextBox к Slider
                            <LineBreak />
                            без проверки достоверности
                        </TextBlock>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider6, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider6" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                Value="35"
                                />
                        </StackPanel>
                        <TextBox Margin="5" 
                            Text="{Binding ElementName=slider6, Path=Value, Mode=TwoWay}" 
                            />
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider6, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>

    В разметку вкладки мы добавили текстовое поле с двунаправленной привязкой. У всех привязанных элементов отсутствуют имена кроме источника, поскольку его имя используется при привязке этими элементами. Мы установили начальное положение бегунка Value="35", которое при инициализации получают все привязанные к нему элементы. Теперь, за счет двунаправленности привязки, значение бегунка можно менять как перемещением головки, так и через текстовое поле. Значения не просто выталкиваются из источника к целевому элементу, но и наоборот - от цели к источнику.

  • Запустите приложение и испытайте работу вкладки Page6. Попробуйте вводить в текстовое поле некорректное содержимое, которое нельзя трактовать как число, например, буквы
  • На данном примере мы наблюдаем несколько особенностей поведения WPF:

  • Одной из особенностей механизма привязки WPF является то, что он не возбуждает никаких исключений, чтобы известить нас о проблемах привязки данных. Если мы привязываем несуществующий элемент или свойство, или если типы соединенных привязкой свойств отличаются так, что данные источника не могут быть корректно приведены к типу данных приемника, WPF будет молчать и данные просто не появятся в приемнике. Однако, возникновение такой ситуации отслеживает оболочка, которая выводит в панель Output трассировочную информацию о работе средств привязки. Другой способ - применить механизм, выполняющий проверку достоверности (validation) данных (об этом речь пойдет ниже), или реализовать такую проверку в процедурном коде
  • Мы уже говорили, что при однонаправленной привязке на стороне приемника данные источника непрерывно отображаются в приемнике. При двунаправленной привязке на стороне приемника TextBox, введенные в текстовое поле значения приемника не применяются к источнику и к другим связанным с ним элементам до тех пор, пока TextBox не потеряет фокус ввода (по нажатию клавиши Tab или щелчку мыши на другом интерфейсном элементе, при переходе на другую вкладку или активации другого окна). Такое поведение можно изменить, установив непрерывное обновление, если использовать свойство UpdateSourceTrigger объекта Binding, которое принимает одно из значений одноименного перечисления System.Windows.Data.UpdateSourceTrigger (об этом речь пойдет ниже)
  • Вкладка Page7. Двунаправленная привязка источника-приемника

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page7, представленную следующей разметкой
  • <!-- Page7. Двунаправленная привязка источника-приемника -->
                <TabItem Header="Page7">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red">
                            Двунаправленная привязка 
                            <LineBreak />
                            источника - приемника
                        </TextBlock>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider7, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider7" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                Value="{Binding ElementName=textBox7, Path=Text, Mode=TwoWay}" 
                                />
                        </StackPanel>
                        <TextBox Name="textBox7" Margin="5" Text="21" />
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider7, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Запустите приложение и испытайте работу вкладки Page7 - она продолжает работать точно также, как и для вкладки Page6
  • Элемент <TextBox> у нас оказался несвязанным, зато пришлось ему присвоить имя, чтобы можно было его привязать на стороне элемента <Slider> в качестве источника. Теперь ползунок находится под контролем элемента TextBox и одновременно является источником для элементов <Label> и <TextBlock>. Когда окно отображается впервые, оно извлекает значение свойства Text объекта TextBox и копирует его в свойство Value объекта Slider. Оттуда, в свою очередь, это значение тиражируется в связанные объекты Label и TextBlock.

    Когда пользователь перетаскивает ползунок в новую позицию, действует канал обратной отсылки от ползунка-приемника к текстовому полю-источнику. Когда пользователь меняет значение текстового поля-источника, действует прямой канал передачи информации от источника TextBox к приемнику Slider.

  • Уберите инициализацию Text="21" из элемента TextBox - при запуске приложения ползунок не получает данные от источника и принимает состояние по умолчанию, устанавливая вслед за собой и другие его приемники
  • Обратите внимание, за счет прямой передачи данных от источника TextBox к приемнику Slider последний не ждет потери фокуса ввода источником, а непрерывно меняет свое значение. В обратной отсылке от приемника к источнику требуется потеря приемником фокуса ввода. В нашем случае при движении ползунка-приемника источник TextBox его и не получает, поэтому тоже изменяется непрерывно.

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

    Вкладка Page8. Создание привязки элементов в процедурном коде

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

  • Создать другую привязку в зависимости от возникших обстоятельств в работающем приложении
  • Удалить существующую связь и перейти к программному изменению свойств элементов обычным способом на основе событий
  • При создании пользовательских элементов управления вынести возможности привязки в программный интерфейс элемента для последующей его настройки сторонним программистом
  • Вначале приведем код разметки для вкладки Page8 с настройкой одним из уже рассмотренных способов, затем изменим вкладку так, чтобы реализовать для ее элементов привязку в процедурном коде.

  • Поместите в контейнер TabControl файла Window1.xaml новую вкладку Page8, стартовый код разметки которой имеет вид
  • <!-- Page8. Привязка элементов в коде -->
                <TabItem Header="Page8">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Привязка элементов в коде" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider8, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider8" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider8, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Удалите код привязки из разметки элементов Label и TextBlock и добавьте на их место имена с атрибутами Name="label8" и Name="textBlock8" соответственно, - для возможности доступа из процедурного кода. После чего код разметки должен стать таким
  • <!-- Page8. Привязка элементов в коде -->
                <TabItem Header="Page8">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Привязка элементов в коде" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Name="label8" 
                                />
                            <Slider Name="slider8" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            Name="textBlock8"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Перейдите к редактированию файла Window1.xaml.cs и добавьте в конструктор класса Window1 следующий процедурный код
  • public Window1()
            {
                InitializeComponent();
        
                // Создаем и настраиваем объект привязки
                Binding binding = new Binding();
                binding.Source = slider8;// Источник 
                binding.Path = new PropertyPath("Value"); 
                binding.Mode = BindingMode.OneWay;
        
                // Привязываем элементы-приемники
                label8.SetBinding(Label.ContentProperty, binding);          // Label
                textBlock8.SetBinding(TextBlock.FontSizeProperty, binding); // TextBlock
            }

    В начале мы создаем сам узел (объект) привязки с необходимой информацией об источнике. Затем с помощью экземплярного метода SetBinding(), который каждый интерфейсный элемент наследует от класса FrameworkElement, присоединяем узел привязки к соответствующему свойству зависимости элемента-приемника. Свойства зависимости элементов определены как статические только для чтения с отложенной одноразовой инициализацией и имеют тип DependencyProperty. Например, в библиотечном классе System.Windows.Controls.TextBlock свойство зависимости FontSizeProperty определено так

    static readonly System.Windows.DependencyProperty FontSizeProperty

    Иными словами, в отличие от кода разметки, где для читабельности привязка осуществляется с использованием имени экземплярного свойства, например FontSize, в процедурном коде привязка к объекту-приемнику выполняется к применением имени типа элемента и статического свойства зависимости с окончанием Property, например FontSizeProperty. Такое правило справедливо для всех типов интерфейсных элементов при создании привязки в процедурном коде.

  • Запустите приложение и испытайте работу вкладки Page8 - она работает как и положено
  • Представление окна приложения с открытой вкладкой Page8 будет таким

    Прежде, чем двигаться дальше в демонстрации примеров данного упражнения, нужно оговорить следующее (чтобы не посчитали дураками).

  • Запустите приложение и откройте вкладку Page4, где у нас больше всего интерфейсных элементов
  • Если максимально увеличить размер шрифта текста, то он выходит за границы окна за счет того, что мы после разработки этой вкладки добавили еще несколько таких же. Полезная область окна уменьшилась по вине появления дополнительных закладок. А мы еще в данном упражнении собираемся добавлять все новые вкладки, чтобы продолжить рассмотрение темы привязки элемента к элементу (извилины к извилине, денежки к денежке).

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

    Вкладка Page9. Изменение привязки элементов в процедурном коде

    Если мы уже создали привязку в разметке или процедурном коде, то чтобы установить новую привязку для тех же элементов, нужно вначале удалить прежнюю. Для этого существуют статические методы BindingOperations. ClearBinding() и BindingOperations. ClearAllBindings() пространства имен System.Windows.Data. Метод ClearBinding() ожидает ссылку на объект, содержащий узел привязки, и ссылку на его свойство зависимостей, которое имеет привязку. Он удаляет привязку указанного свойства зависимостей в указанном объекте. Метод ClearAllBindings() ожидает ссылку на объект, содержащий привязки, и удаляет привязки для всех свойств этого объекта.

    Для демонстрации сказанного используем стартовый код разметки из предыдущего примера с уже готовыми декларативными привязками элементов.

  • Поместите в контейнер TabControl файла Window1.xaml новую вкладку Page9 со стартовым кодом разметки из предыдущего примера и присвойте элементам имена, как показано в следующем листинге
  • <!-- Page9. Изменение привязки в коде -->
                <TabItem Header="Page9">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Изменение привязки в коде" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Name="label9" Width="40"
                                Content="{Binding ElementName=slider9, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider9" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Name="textBlock9" Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider9, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Запустите приложение на выполнение и убедитесь, что элементы вкладки Page9 являются связанными с бегунком и реагируют на его изменения должным образом
  • Удалите программно заданные декларативно привязки в элементах label9 и textBlock9, дополнив конструктор класса Window1 следующим кодом
  • public Window1()
            {
                InitializeComponent();
        
                // Создаем и настраиваем объект привязки
                Binding binding = new Binding();
                binding.Source = slider8;// Источник 
                binding.Path = new PropertyPath("Value"); 
                binding.Mode = BindingMode.OneWay;
        
                // Привязываем элементы-приемники
                label8.SetBinding(Label.ContentProperty, binding);          // Label
                textBlock8.SetBinding(TextBlock.FontSizeProperty, binding); // TextBlock
        
                // Удаление декларативных привязок во вкладке Page9
                BindingOperations.ClearBinding(label9, Label.ContentProperty);
                BindingOperations.ClearAllBindings(textBlock9);
            }
  • Запустите приложение на выполнение и убедитесь, что элементы вкладки Page9 теперь не реагируют на изменения бегунка
  • Добавьте в конструкторе класса Window1 код создания привязок, реализующий новую схему привязок как цепочку, рассмотренную ранее на вкладке Page5
  • public Window1()
            {
                InitializeComponent();
        
                // Создаем и настраиваем объект привязки
                Binding binding = new Binding();
                binding.Source = slider8;// Источник 
                binding.Path = new PropertyPath("Value"); 
                binding.Mode = BindingMode.OneWay;
        
                // Привязываем элементы-приемники
                label8.SetBinding(Label.ContentProperty, binding);          // Label
                textBlock8.SetBinding(TextBlock.FontSizeProperty, binding); // TextBlock
        
                // Удаление декларативных привязок во вкладке Page9
                BindingOperations.ClearBinding(label9, Label.ContentProperty);
                BindingOperations.ClearAllBindings(textBlock9);
        
                //******************************************************
                // Привязываем элементы во вкладке Page9 по новой схеме
                //******************************************************
                // Создаем и настраиваем узел привязки
                binding = new Binding();
                binding.Source = slider9;// Источник 
                binding.Path = new PropertyPath("Value");
                binding.Mode = BindingMode.OneWay;
        
                // Привязываем label9 к бегунку slider9
                label9.SetBinding(Label.ContentProperty, binding);
        
                // Привязываем textBlock9 не к slider9, а к label9,
                // реализуя схему цепочки привязок (для разнообразия).
                // Для этого надо создать новый узел привязки!!!
                binding = new Binding();
                binding.Source = label9;// Источник 
                binding.Path = new PropertyPath("Content");
                binding.Mode = BindingMode.OneWay;
        
                textBlock9.SetBinding(TextBlock.FontSizeProperty, binding);
                slider9.Value = 21;// Чуть отступим вправо
                //******************************************************
            }
  • Запустите приложение на выполнение и убедитесь, что элементы вкладки Page9 теперь работают как надо (в соответствии с возрастом)
  • Окно с вкладкой Page9 будет таким

    Вкладка Page10. Размещение узла привязки на стороне источника

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

    Но иногда свойство приемника, к которому мы хотим привязать передачу информации, не является свойством зависимости, а свойство источника - является таковым, и узел привязки приходится размещать в источнике. Тогда атрибуту ElementName нужно присвоить имя целевого элемента - приемника, а режим привязки установить как Mode=OneWayToSource. Рассмотрим пример.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page10 со следующим пробным кодом разметки
  • <!-- Page10. Размещение привязки на стороне источника -->
                <TabItem Header="Page10" Selector.IsSelected="True">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="5" Foreground="Red"
                            Text="Размещение привязки на источнике" 
                            />
                        <FlowDocumentScrollViewer Height="110" Margin="5" BorderThickness="2">
                            <FlowDocument Background="White">
                                <Paragraph>
                                    Этот приемник текста!
                                </Paragraph>
                                <Paragraph>
                                    <Run Name="runParagraph10" 
                                        Text="{Binding ElementName=textBox10, Path=Text}"
                                        /> 
                                </Paragraph>
                            </FlowDocument>
                        </FlowDocumentScrollViewer>
                        <TextBox Name="textBox10"
                            Margin="5" MinLines="3" MaxLines="3"
                            TextWrapping="Wrap"
                            AcceptsReturn="True"
                            AcceptsTab="True"
                            VerticalScrollBarVisibility="Visible"
                            />
                    </StackPanel>
                </TabItem>

    Элемент <Run> с именем runParagraph10 в этом примере является приемником, а элемент <TextBox> с именем textBox10 - источником. По-старинке, мы пытаемся привязать свойство Text приемника к свойству Text источника. Но уже на этапе проектирования редактор кода (а за ним и компилятор - при попытке компиляции) начинает ругаться, что свойство Text приемника не является свойством зависимостей, порожденным классом DependencyObject.

  • Зная наверняка, что свойство Text элемента <TextBox> является свойством зависимости, переместите (удалите в приемнике - добавьте к источнику) узел привязки из элемента <Run> в элемент <TextBox> с именем textBox10, чтобы последний стал таким (разметка вкладки Page10 приводится полностью)
  • <!-- Page10. Размещение привязки на стороне источника -->
                <TabItem Header="Page10" Selector.IsSelected="True">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="5" Foreground="Red"
                            Text="Размещение привязки на источнике" 
                            />
                        <FlowDocumentScrollViewer Height="110" Margin="5" BorderThickness="2">
                            <FlowDocument Background="White">
                                <Paragraph>
                                    Этот приемник текста!
                                </Paragraph>
                                <Paragraph>
                                    <Run Name="runParagraph10" /> 
                                </Paragraph>
                            </FlowDocument>
                        </FlowDocumentScrollViewer>
                        <TextBox
                            Margin="5" MinLines="3" MaxLines="3"
                            TextWrapping="Wrap"
                            AcceptsReturn="True"
                            AcceptsTab="True"
                            VerticalScrollBarVisibility="Visible"
                            Text="{Binding ElementName=runParagraph10, 
                                Path=Text,
                                Mode=OneWayToSource, 
                                UpdateSourceTrigger=PropertyChanged}"
                                />
                    </StackPanel>
                </TabItem>

    Мы удалили имя элемента TextBox как ненужное и добавили свойство Text, привязанное к именованному приемнику по синтаксису расширения разметки. Атрибут UpdateSourceTrigger=PropertyChanged синхронизирует передачу информации в непрерывном режиме. Если его убрать, то приемник будет обновляться только после потери источником фокуса ввода в соответствии с установкой по умолчанию UpdateSourceTrigger=LostFocus.

  • Запустите приложение с вкладкой Page10, испытайте его работу и восхититесь чудесами, на которые способна привязка в WPF. Особенно, если учесть, что мы в этом примере не написали ни одной строчки процедурного кода
  • Вкладка Page11. Режимы обновления привязки

    Обновления привязки устанавливают режим, как быстро изменения на одном связанном элементе отобразятся на другом. Обычно такая задача имеет место тогда, когда целевое свойство берет на себя еще и роль источника в привязках типа TwoWay или OneWayToSource. Этот режим определяется в выражении привязки элемента Binding и зависит от его свойства UpdateSourceTrigger. Свойство UpdateSourceTrigger может принимать следующие значения из одноименного перечисления System.Windows.Data.UpdateSourceTrigger

    Значения из перечисления UpdateSourceTrigger
    Значение Описание
    Default Поведение обновления целевого свойства определяется установками по умолчанию. Обычно это PropertyChanged, хотя для TextBox.Text значение по умолчанию равно LostFocus
    Explicit Источник не обновляется, пока не будет вызван один из экземплярных методов UpdateSource() или UpdateTarget() класса BindingExpression пространства имен System.Windows.Data сборки PresentationFramework.dll. Но перед этим из элемента нужно извлечь выражение привязки с помощью метода GetBindingExpression()
    LostFocus Источник обновляется, когда целевой элемент теряет фокус
    PropertyChanged Источник обновляется немедленно после изменения целевого свойства

    Конечно, самым удобным режимом обновления являются непрерывные изменения PropertyChanged. Но это и самый затратный режим, который может создать впечатление замедленного выполнения приложения и нарушить логику обновления источника до завершения процесса редактирования. Режим UpdateSourceTrigger.Explicit требует программной поддержки, зато самый предсказуемый. Можно, например, добавить кнопку Apple (Применить), по щелчку на которой будет вызвано обновление источника экземплярным методом UpdateSource(), когда процесс подготовки данных уже полностью завершен. В этом случае пользователь сам полностью контролирует процесс обновлений.

    Поясним сказанное на разметке предыдущего примера.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page11 со следующим кодом разметки
  • <!-- Page11. Обновление элемента привязки -->
                <TabItem Header="Page11" Selector.IsSelected="True">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="5" Foreground="Red"
                            Text="Обновление элемента привязки" 
                            />
                        <FlowDocumentScrollViewer Height="110" Margin="5" BorderThickness="2">
                            <FlowDocument Background="White">
                                <Paragraph>
                                    <Run Name="runParagraph11" />
                                </Paragraph>
                            </FlowDocument>
                        </FlowDocumentScrollViewer>
                        <DockPanel LastChildFill="True">
                            <Button DockPanel.Dock="Left" 
                                Content="Apple" 
                                Width="75" Height="25" 
                                VerticalAlignment="Center" 
                                Margin="5,0"
                                Click="Button_Click" 
                                />
                            <TextBox Name="textBox11"
                                Margin="5" MinLines="3" MaxLines="3"
                                TextWrapping="Wrap"
                                AcceptsReturn="True"
                                AcceptsTab="True"
                                VerticalScrollBarVisibility="Visible"
                                Text="{Binding ElementName=runParagraph11, 
                                    Path=Text,
                                    Mode=OneWayToSource, 
                                    UpdateSourceTrigger=Explicit}"
                                    />
                        </DockPanel>
                    </StackPanel>
                </TabItem>
  • Щелкните на записи Click="Button_Click" правой кнопкой мыши и командой Navigate to Event Handler создайте обработчик Button_Click()
  • Запустите приложение с вкладкой Page11 на выполнение и убедитесь, что сам интерфейс функционирует, но при выбранном режиме обновления Explicit данные в элемент <FlowDocument> не передаются, потому что обработчик не заполнен соответствующим кодом
  • Перейдите в файл Window1.xaml.cs и заполните обработчик кнопки Apple следующим процедурным кодом
  • private void Button_Click(object sender, RoutedEventArgs e)
            {
                // Получить выражение привязки к свойству зависимости Text, 
                // которая размещена в текстовом элементе TextBox с именем textBox11
                BindingExpression binding = 
                    textBox11.GetBindingExpression(TextBox.TextProperty);
                // Обновить связанный элемент FlowDocument
                binding.UpdateSource();
            }
  • Запустите приложение и испытайте работу вкладки Page11, которая будет выглядеть примерно так
  • В принятой схеме обновления применяются в целевом элементе только по нажатию кнопки Apple.

    Часть II

    Упражнение 2. Привязка интерфейсных элементов к невизуальным объектам

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

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

    Основные свойства класса Binding
    Свойство Тип Описание
    ElementName String -класс Задает или извлекает имя элемента, который будет использоваться как источник информации. При связывании с элементом WPF применяется вместо свойства Source
    FallbackValue Object -класс Задает или извлекает значение для использования, если привязка не может вернуть значение. Значение по умолчанию UnsetValue
    Mode BindingMode -перечисление Получает или устанавливает значение, которое указывает направление потока данных в привязке
    NotifyOnSourceUpdated Boolean -структура Получает или устанавливает флаг, который указывает, возбуждается ли событие SourceUpdated при передаче данных от целевого элемента к источнику
    Path PropertyPath -класс Задает или извлекает путь к свойству источника
    RelativeSource RelativeSource -класс Задает или извлекает источник путем указания его местоположения относительно целевого элемента в визуальном дереве
    Source Оbject -класс Используется для указания на объект-источник, если он не является элементом WPF и поэтому нельзя применить ElementName

    Способы привязки рассмотрим на примерах.

  • Добавьте к решению DataBinding командой File/Add/New Project новый проект WPF Application с именем ElementWithObject и назначьте его стартовым
  • Настройте открывающий дескриптор окна Window1 нового проекта так
  • <Window x:Class="ElementWithObject.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="300" 
        Width="300"
        Title="Привязка элемента к объекту" 
        MinHeight="300" 
        MinWidth="300"
        WindowStartupLocation="CenterScreen"
        >  
        <Grid>
        </Grid>
    </Window>
  • Добавьте к разметке файла Window1.xaml логический ресурс для обеспечения классического цвета фона окна и присоедините его к свойству Background сетки Grid
  • <Window x:Class="ElementWithObject.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="300" 
        Width="300"
        Title="Привязка элемента к объекту" 
        MinHeight="300" 
        MinWidth="300"
        WindowStartupLocation="CenterScreen"
        >   
        <Window.Resources>
            <SolidColorBrush x:Key="ControlColorBrush" 
                Color="{x:Static SystemColors.ControlColor}" />
        </Window.Resources>
        <Grid Background="{StaticResource ResourceKey=ControlColorBrush}">
        </Grid>
    </Window>

    Вкладка Page1. Использование логического ресурса без привязки

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

  • Добавьте в разметку контейнер TabControl с вкладкой Page1 следующего содержания
  • <Grid Background="{StaticResource ResourceKey=ControlColorBrush}">
            <TabControl>
                <!-- Page1. Использование логического ресурса без привязки -->
                <TabItem Header="Page1">
                    <TabItem.Resources>
                        <ImageBrush x:Key="flower1"
                            ImageSource="Images/flower1.jpg"
                            Stretch="UniformToFill"
                            Opacity="1"
                            />
                    </TabItem.Resources>
                    <Rectangle 
                        Stroke="Red"
                        StrokeThickness="5.0" 
                        Opacity="1"
                        Fill="{StaticResource flower1}" />
                </TabItem>
            </TabControl>
        </Grid>
  • В панели Solution Explorer добавьте к корню проекта ElementWithObject командой контекстного меню Add/New Folder папку с именем Images
  • В панели Solution Explorer для папки Images вызовите контекстное меню и скопируйте в нее командой Add/Existing Item из прилагаемого каталога Source файл flower1.jpg
  • Выделите файл flower1.jpg и в панели Properties проверьте его настройки в оболочке. Они должны быть такими
  • Build Action=Resource
  • Copy to Output Directory=Do not copy
  • Запустите приложение - представление вкладки будет таким
  • Здесь мы пока не применяли механизм привязки, но вспомнили использование ресурсов.

    Вкладка Page2. Связывание целевого элемента с исходным объектом через логический ресурс

    Когда встает вопрос привязки визуального элемента к объекту, в отличии от привязки элемента к элементу, то в выражении привязки для обозначения источника нужно использовать взамен атрибута ElementName другой атрибут - Source (или RelativeSource ). В следующем примере используем в качестве посредника привязки логический ресурс.

  • В панели Solution Explorer для папки Images вызовите контекстное меню и скопируйте в нее командой Add/Existing Item из прилагаемого каталога Source файл flower2.jpg
  • Выделите файл flower2.jpg и в панели Properties проверьте его настройки в оболочке. Они должны быть такими
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • В пространство имен файла Window1.xaml.cs добавьте новый класс Pictures со следующим процедурным кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    namespace ElementWithObject
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        }
        
        class Pictures
        {
            // Поле
            public static ImageBrush picture = new ImageBrush();
        
            static Pictures()
            {
                picture.ImageSource = new BitmapImage(
                    new Uri(@"Images\flower2.jpg", UriKind.Relative));
                picture.Stretch = Stretch.UniformToFill;
                picture.Opacity = 1.0D;
            }
        
            // Свойство
            public static ImageBrush Picture { get { return picture; } }
        }
    }
  • Добавьте в заголовок дескриптора Window запись импортирования пространства имен приложения для доступа к свойству класса Pictures из разметки
  • <Window x:Class="ElementWithObject.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="300" 
        Width="300"
        Title="Привязка элемента к объекту" 
        MinHeight="300" 
        MinWidth="300"
        WindowStartupLocation="CenterScreen"
            
        xmlns:local="clr-namespace:ElementWithObject"
        >   
        ..............................................................
            
    </Window>
  • Добавьте в контейнер TabControl новую вкладку с именем Page2 с привязкой логического ресурса к фону кнопки
  • <!-- Page2. Связывание целевого элемента с исходным
                            объектом через логический ресурс -->
                <TabItem Header="Page2">
                    <TabItem.Resources>
                        <TextBlock x:Key="title2"
                            FontSize="16"
                            TextAlignment="Center"
                            FontWeight="Bold">
                            <TextBlock.Foreground>
                                <SolidColorBrush Color="Yellow" />
                            </TextBlock.Foreground>
                            Связывание целевого элемента
                            <LineBreak />
                            с исходным объектом
                            <LineBreak />
                            через логический ресурс
                        </TextBlock>
                        <local:Pictures x:Key="flower2" />
                    </TabItem.Resources>
                    <Button 
                        Content="{StaticResource ResourceKey=title2}"
                        Background="{Binding Source={StaticResource flower2},
                                             Path=Picture, Mode=OneWay}" />
                </TabItem>
  • Запустите проект с вкладкой Page2 - область вывода будет такой
  • Использование свойства DataContext элементов для назначения источника привязки

    Иногда несколько приемников нужно привязать к одному и тому же источнику. Чтобы не дублировать в каждом выражении привязки имя источника и как-то автоматизировать эту задачу, в WPF очень часто используют свойство DataContext. Это свойство введено классом FrameworkElement и наследуется каждым элементом, производным от этого класса. Его использование является альтернативным способом обозначения объекта-источника, задействованного в привязке.

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

    Выполняя поиск по дереву, WPF пытается найти первый контекст данных DataContext, не равный null. Если контекст данных найден, он используется для привязки, если нет, то выражение привязки не применяет никакого значения к целевому элементу и он ничего отображать не будет.

    Предположим, что имеется несколько элементов управления TextBlock, явным образом связанных с одним и тем же источником привязки:

    <StackPanel>
            <TextBlock 
                Text="{Binding Source={StaticResource personView}, 
                               Path=FullName}" />
            <TextBlock 
                Text="{Binding Source={StaticResource personView}, 
                               Path=Title}" />
            <TextBlock 
                Text="{Binding Source={StaticResource personView}, 
                               Path=City}" />
        </StackPanel>

    А вот те же самые три элемента управления TextBlock, связанные с контекстом данных DataContext контейнера StackPanel:

    <StackPanel DataContext="{Binding Source={StaticResource personView}}">
            <TextBlock Text="{Binding Path=FullName}" />
            <TextBlock Text="{Binding Path=Title}" />
            <TextBlock Text="{Binding Path=City}" />
        </StackPanel>

    Если в очередном ближнем контейнере атрибут DataContext не определен, поиск будет продолжен в следующем внешнем контейнере, пока не будет найден ненулевой контекст данных DataContext или поиск закончится неудачей.

    Вкладка Page3. Использование свойства DataContext в целевом элементе

    В следующем примере свойство DataContext помещено в приемник привязки.

  • Добавьте в контейнер TabControl новую вкладку Page3 со следующей разметкой
  • <!-- Page3. Использование свойства DataContext -->
                <TabItem Header="Page3">
                    <StackPanel>
                        <!-- Источник привязки -->
                        <ScrollBar Name="scroll3"
                            Orientation="Horizontal" Margin="24"
                            Maximum="100" LargeChange="10" SmallChange="1" 
                            Value="50"
                        />
        
                        <!-- Приемник привязки -->
                        <Label HorizontalAlignment="Center"
                            ContentStringFormat="F0"
                            DataContext="{Binding ElementName=scroll3}"
                            Content="{Binding Path=Value}"
                        />
                    </StackPanel>
                </TabItem>

    В элементе ScrollBar атрибут Maximum="100" означает верхнюю границу диапазона значений, LargeChange="10" - шаг изменения положения бегунка по щелчкам на областях справа и слева от него, SmallChange="1" - шаг изменения значения по щелчкам на концевых элементах линейки. В приведенном примере свойство DataContext никаких преимуществ не дает, а просто делит выражение привязки на две части.

  • Запустите приложение - вкладка Page3 обеспечит следующую область вывода
  • Вкладка Page4. Использование свойства DataContext в родительском элементе

  • Добавьте в контейнер TabControl новую вкладку Page4
  • Переместите назначение источника привязки свойства DataContext из приемника в родительский элемент <StackPanel> следующим образом
  • <!-- Page4. Использование свойства DataContext -->
                <TabItem Header="Page4">
                    <StackPanel DataContext="{Binding ElementName=scroll4}">
                        <!-- Источник привязки -->
                        <ScrollBar Name="scroll4"
                            Orientation="Horizontal" Margin="24"
                            Maximum="100" LargeChange="10" SmallChange="1"
                            Value="50"
                        />
        
                        <!-- Приемник привязки -->
                        <Label HorizontalAlignment="Center"
                            ContentStringFormat="F0"
                            Content="{Binding Path=Value}"
                        />
                    </StackPanel>
                </TabItem>

    Элемент <StackPanel> тоже наследует классу FrameworkElement и содержит свойство DataContext.

  • Испытайте работу вкладки Page4 - она ничем не будет отличаться от вкладки Page3
  • Вкладка Page5. Привязка нескольких приемников к одному источнику без свойства DataContext

    Рассмотрим пример, когда отказ от использования свойства DataContext влечет за собой относительно громоздкий код разметки.

  • Добавьте в контейнер TabControl вкладку Page5 со следующей разметкой
  • <!-- Page5. Разметка без свойства DataContext --> 
                 <TabItem Header="Page5"> 
                     <StackPanel> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=Source}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=LineSpacing}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=FamilyTypefaces[0].Style}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=FamilyTypefaces[0].Weight}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=FamilyNames.Count}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=FamilyMaps.Count}"
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=Baseline}"  
                        /> 
                     </StackPanel> 
                 </TabItem>
  • Запустите приложение - результат будет таким
  • Вкладка Page6. Привязка нескольких приемников к одному источнику с использованием свойства DataContext

  • Добавьте новую вкладку Page6 и перепишите разметку предудущего примера так
  • <!-- Page6. Разметка с использованием свойства DataContext -->
                <TabItem Header="Page6">
                    <StackPanel
                        DataContext="{Binding Source={x:Static SystemFonts.IconFontFamily}}"
                        >
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=Source}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=LineSpacing}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=FamilyTypefaces[0].Style}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=FamilyTypefaces[0].Weight}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=FamilyNames.Count}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=FamilyMaps.Count}"
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=Baseline}"  
                        />
                    </StackPanel>
                </TabItem>

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

  • Запустите приложение - представление вкладки Page6 полностью совпадает с областью вывода вкладки Page5
  • Сделаем несколько замечаний:

  • 1.Если в родительском элементе определен источник через свойство DataContext, но одновременно в каком-то целевом элементе (или нескольких) в выражении привязки явно определен свой источник через свойство Source, то приемник использует свой источник и не будет далее искать контекст данных.
  • 2.В разметке свойству DataContext может быть присвоена ссылка на логический ресурс объекта
  • 3.Когда свойство DataContext задается в процедурном коде, то связанные элементы управления не будут отображать связанные данные до тех пор, пока этому свойству не будет присвоена ссылка на объект-источник.
  • Вкладка Page7. Присвоение свойству DataContext логической ссылки на ресурс

    Вернемся к примеру вкладки Page2 и немного изменим его.

  • В панели Solution Explorer для папки Images вызовите контекстное меню и скопируйте в нее командой Add/Existing Item из прилагаемого каталога Source файл wood.jpg
  • Выделите файл wood.jpg и в панели Properties проверьте его настройки в оболочке. Они должны быть такими
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • В файле Window1.xaml.cs в класс Pictures добавьте новый код
  • class Pictures
        {
            // Поле
            public static ImageBrush picture = new ImageBrush();
            public static ImageBrush picture1 = new ImageBrush();
        
            static Pictures()
            {
                picture.ImageSource = new BitmapImage(
                    new Uri(@"Images\flower2.jpg", UriKind.Relative));
                picture.Stretch = Stretch.UniformToFill;
                picture.Opacity = 1.0D;
        
                picture1.ImageSource = new BitmapImage(
                    new Uri(@"Images\wood.jpg", UriKind.Relative));
                picture1.Stretch = Stretch.Uniform;
                picture1.Opacity = 1.0D;
            }
        
            // Свойство
            public static ImageBrush Picture { get { return picture; } }
            public static ImageBrush Picture1 { get { return picture1; } }
        }
  • В коллекцию ресурсов разметки окна Window1 добавьте новый ресурс
  • <Window.Resources> 
            <SolidColorBrush x:Key="ControlColorBrush" 
                Color="{x:Static SystemColors.ControlColor}" /> 
            <local:Pictures x:Key="wood" /> 
        </Window.Resources>

    Вспомним, что при разработке вкладки Page2 мы уже добавили в заголовок дескриптора Window запись

    xmlns:local="clr-namespace:ElementWithObject"

    импортирования пространства имен приложения для доступа к свойству класса Pictures из разметки и теперь просто используем готовый псевдоним local.

  • В контейнер TabControl добавьте новую вкладку Page7 со следующей разметкой

  • <!-- Page7. Присвоение свойству DataContext 
                            логической ссылки на ресурс -->
                <TabItem Header="Page7"
                    DataContext=" {StaticResource ResourceKey=wood}">
                    <Button Background="{Binding Path=Picture1, Mode=OneWay}" />
                </TabItem>

    Синтаксический анализатор ищет ресурс вначале в элементе его использования со свойством DataContext, а затем вверх по логическому дереву, поэтому мы расположили определение ресурса в словаре окна. То же самое присходит и с DataContext. Когда в выражении привязки атрибут Source пропущен, то WPF ищет определение источника в атрибуте DataContext вначале в элементе с выражением привязки и далее в родительских элементах, поэтому мы расположили DataContext в элементе <TabItem>.

  • Запустите приложение - результат вкладки Page7 выглядет так

  • Вкладка Page8. Управление свойством DataContext в процедурном коде

    Обычно DataContext задается в коде. В частности, эта методика применяется к объектам ADO.NET, которые необходимо инициировать в коде и нельзя заполнять декларативно. Пока что без объектов ADO.NET рассмотрим этот способ. Естественно, что элемент, в котором будет задаваться свойство DataContext, должен иметь имя для возможности обращения из процедурного кода.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page8

  • <!-- Page8. Управление свойством DataContext в процедурном коде -->
                <TabItem Header="Page8">
                    <StackPanel Name="stackPanel8">
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Управление DataContext в коде" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider8" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="BottomRight"
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <Button 
                            Margin="5"
                            Content="Подключить источник в DataContext"
                            Click="button8_Click">
                        </Button>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Расширьте класс Window1 следующим процедурным кодом

  • public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        
            bool bindingFlag = false;
            private void button8_Click(object sender, RoutedEventArgs e)
            {
                Button button = (Button)sender;
                if (bindingFlag == false)
                {
                    stackPanel8.DataContext = slider8;
                    button.Content = "Отключить источник в DataContext";
                }
                else
                {
                    stackPanel8.DataContext = null;
                    button.Content = "Подключить источник в DataContext";
                }
        
                bindingFlag = !bindingFlag;
            }
        }
  • Запустите приложение и испытайте работу вкладки Page8, внешний вид которой будет таким

  • Вкладка Page9. Создание и привязка пользовательского объекта

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

  • Добавьте в пространство имен ElementWithObject текущего проекта в файле Window1.xaml.cs новый класс SimpleElement

  • // Простой пользовательский элемент управления
        class SimpleElement : FrameworkElement
        {
            // Статическое поле зависимости, базовое для свойства
            public static DependencyProperty NumberProperty;
        
            // Инициализация поля в статическом конструкторе
            static SimpleElement()
            {
                NumberProperty = DependencyProperty.Register(
                    "Number",
                    typeof(double),
                    typeof(SimpleElement),
                    new FrameworkPropertyMetadata(0.0,
                        FrameworkPropertyMetadataOptions.AffectsRender));
            }
        
            // Свойство зависимости для доступа к полю
            public double Number
            {
                get { return (double)this.GetValue(NumberProperty); }
                set { this.SetValue(NumberProperty, value); }
            }
        
            // Жесткое кодирование размера области вывода
            protected override Size MeasureOverride(Size availableSize)
            {
                return new Size(200, 20);
            } 
        
            // Вывод значения свойства Number
            protected override void OnRender(DrawingContext drawingContext)
            {
                drawingContext.DrawRectangle(Brushes.Yellow, 
                    new Pen(Brushes.Red, 1.0), 
                    new Rect(new Size(200, 20)));
                drawingContext.DrawText(
                    new FormattedText(
                        Convert.ToInt32(Number).ToString(),
                        System.Globalization.CultureInfo.CurrentCulture,
                        FlowDirection.LeftToRight,
                        new Typeface("Arial"),
                        16.0D,
                        SystemColors.WindowTextBrush),
                    new Point(0, 0));
            }
        }

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

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page9, которую заполните так

  • <!-- Привязка пользовательского элемента управления -->
                <TabItem Header="Page9">
                    <StackPanel>
                        <ScrollBar Orientation="Horizontal"
                            Margin="24"
                            Maximum="100"
                            LargeChange="10"
                            SmallChange="1"
                            Value="{Binding ElementName=simple9, Path=Number,
                                            Mode=OneWayToSource}" />
                       <local:SimpleElement x:Name="simple9"
                            HorizontalAlignment="Center" />
                       <ScrollBar Name="scroll9"
                            Orientation="Horizontal"
                            Margin="24"
                            Maximum="100"
                            LargeChange="10"
                            SmallChange="1"
                            Value="{Binding ElementName=simple9, Path=Number,
                                            Mode=TwoWay}" />
                       <local:SimpleElement
                            HorizontalAlignment="Center"
                            Number="{Binding ElementName=scroll9, Path=Value,
                                             Mode=OneWay}" />
                    </StackPanel>
                </TabItem>

    Здесь мы тоже просто используем ранее определенный псевдоним local импортирования пространства имен приложения для доступа к классу SimpleElement из разметки.

    Разметка вводит в контейнер <StackPanel> две пары элементов: "библиотечный элемент прокрутки" - "пользовательский элемент". В секции определения первого пользовательского элемента вводится его имя в пространство имен XAML: x:Name="simple9", которое в дальнейшем используется в выражениях привязки другими элементами как имя объекта-источника. Применение обычного атрибута Name="simple9" будет считаться ошибкой.

    Первый объект ScrollBar определяет одностороннюю привязку типа OneWayToSource со свойством Number пользовательского элемента и успешно обновляет это свойство при перемещении бегунка. Второй элемент ScrollBar определяет двухстороннюю привязку к свойству Number и успешно обновляет его в представлении двух пользовательских элементов, а также обновляется сам при изменении этого свойства. Однако перемещение второго бегунка хоть и изменяет свойство Number пользовательского элемента, но не влияет на состояние первого бегунка, поскольку определенная в первом ScrollBar привязка OneWayToSource обеспечивает передачу информации только в одну сторону.

  • Запустите приложение и убедитесь в сказанном (слава Петцольду!)

  • Привязка к источнику с помощью свойства RelativeSource

    Класс System.Windows.Data. RelativeSource в выражении привязки целевого элемента указывает источник, которым является родительский объект, находящийся на расстоянии неопределенного (или определенного) числа шагов вверх по дереву элементов в направлении к корню. Этот объект также можно использовать и для привязки элемента к самому себе.

    ип такой привязки задается свойством Mode из перечисления RelativeSourceMode, возможные значения которого следующие (Ancestor - предок)

    Возможные значения перечисления RelativeSourceMode
    Значение Описание
    FindAncestor Ссылается на источник с данными в визуальном дереве, который находится на неопределенное число шагов выше целевого элемента, содержащего выражение привязки. При использовании этого режима необходимо задавать дополнительные уточнения в свойствах класса RelativeSource, таких как AncestorType и AncestorLevel. Свойство AncestorType указывает тип источника привязки. Свойство AncestorLevel определяет, с какого вышестоящего уровня от приемника начинать поиск источника. Значение AncestorLevel=1 указывает ближайший к приемнику уровень начального поиска исходного объекта привязки
    PreviousData Когда ссылки на источники данных перечислены в списке, этот режим позволяет сменить текущий источник на предыдущий в списке
    Self Действует внутри элемента, содержащего выражение привязки, и позволяет сменить одно привязанное свойство на другое в том же элементе, т.е. привязать объект к самому себе
    TemplatedParent Если целевой элемент с выражением привязки находится внутри шаблона, такой режим привязывает его к данным шаблона

    На первый взгляд привязка со свойством RelativeSource кажется излишним усложнением. Ведь к источнику можно привязаться обычными средствами с помощью ElementName или Source. Однако такое возможно не всегда, в частности, при использовании шаблонов может выручить только привязка со свойством RelativeSource. Механизм шаблонов в этой теме не рассматривается, а вот применение данной привязки в традиционных задачах мы продемонстрируем на нескольких примерах.

    Исполнение привязки со свойством RelativeSource, как и обычной, тоже возможно в двух синтаксисах: по синтаксису элементов свойств и по синтаксису расширения разметки.

    Вкладка Page10. Привязка RelativeSource к свойствам родительского элемента

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page10, которую заполните так

  • <!-- Относительная привязка RelativeSource к источнику -->
                <TabItem Header="Page10">
                    <StackPanel>
                        <Grid Tag="Привязка к Grid по синтаксису элементов свойств">
                            <Button Margin="5" FontSize="10">
                               <Button.Content>
                                    <Binding Path="Tag" Mode="OneWay">
                                       <Binding.RelativeSource>
                                           <RelativeSource Mode="FindAncestor" 
                                                            AncestorType="Grid"
                                                            AncestorLevel="1" />
                                        </Binding.RelativeSource>
                                    </Binding>
                                </Button.Content>
                            </Button>
                        </Grid>
                        <Grid Tag="Привязка к Grid по синтаксису расширения разметки">
                            <Button Margin="5" FontSize="10"
                                Content="{Binding Path=Tag,
                                                  RelativeSource={RelativeSource 
                                                      Mode=FindAncestor,
                                                      AncestorType=Grid, 
                                                      AncestorLevel=1},
                                                  Mode=OneWay}" />
                        </Grid>
                        <!-- Привязка к свойству заголовка окна 
                             по синтаксису элементов свойств -->
                        <TextBlock TextAlignment="Center">
                            <TextBlock.Text>
                               <Binding Path="Title" Mode="OneWay">
                                    <Binding.RelativeSource>
                                        <RelativeSource Mode="FindAncestor"
                                                        AncestorType="Window" />
                                    </Binding.RelativeSource>
                                </Binding>
                            </TextBlock.Text>
                        </TextBlock>
                        <Label HorizontalAlignment="Center">
                            <Label.Content>
                                <Binding Path="Title" Mode="OneWay">
                                   <Binding.RelativeSource>
                                        <RelativeSource Mode="FindAncestor"
                                                        AncestorType="{x:Type Window}">
                                        </RelativeSource>
                                   </Binding.RelativeSource>
                                </Binding>
                            </Label.Content>
                        </Label>
                       <!-- Привязка к свойству заголовка окна 
                             по синтаксису расширения разметки -->
                        <TextBlock TextAlignment="Center"
                                   Text="{Binding Path=Title, 
                                                  RelativeSource=
                                                      {RelativeSource FindAncestor, 
                                                          AncestorType={x:Type Window}},
                                                  Mode=OneWay}" />
                        <Label HorizontalAlignment="Center"
                               Content="{Binding Path=Title, 
                                                 RelativeSource= 
                                                      {RelativeSource Mode=FindAncestor, 
                                                          AncestorType=Window},
                                                 Mode=OneWay}" />
                    </StackPanel>
                <TabItem>

    Свойство Content первых двух кнопок мы привязали к свойству Tag родительского элемента Grid. Остальные 4 элемента привязаны к свойству Title окна. Свойство AncestorLevel по умолчанию равно 1, поэтому там, где не требуется явно задать иное значение, его можно опустить. В синтаксисе расширения разметки атрибут Mode=FindAncestor можно писать без параметра Mode.

  • Запустите приложение - вкладка Page10 будет выглядеть так

  • Вкладка Page11. Привязка RelativeSource к вложенным свойствам

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

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page11, которую заполните так

  • <!-- Относительная привязка RelativeSource -->
                <TabItem Header="Page11" Selector.IsSelected="True">
                   <DockPanel LastChildFill="True">
                        <StackPanel DockPanel.Dock="Top">
                           <TextBlock FontSize="16"
                                TextAlignment="Center" Margin="0,5,0,5">
                                <TextBlock.Foreground>
                                    <SolidColorBrush Color="Purple" />
                                </TextBlock.Foreground>
                                Привязка с помощью RelativeSource
                           </TextBlock>
                            <ListBox Foreground="Red" SelectionMode="Multiple" Height="60">
                                <CheckBox Selector.IsSelected="False"
                                    IsChecked="{Binding RelativeSource={RelativeSource Mode=Self}, 
                                                        Path=(Selector.IsSelected), Mode=TwoWay}"
                                    Foreground="{Binding Path=Foreground,
                                                         RelativeSource=
                                                             {RelativeSource Mode=FindAncestor,
                                                                 AncestorType=ListBox},
                                                         Mode=OneWay}">
                                    1) Это еще флажок, элемент списка правее
                                </CheckBox>
                                <CheckBox ListBox.IsSelected="False" 
                                    IsChecked="{Binding RelativeSource={RelativeSource 
                                                            Mode=FindAncestor, 
                                                            AncestorLevel=1,
                                                            AncestorType={x:Type ListBoxItem}}, 
                                                        Path=IsSelected, Mode=TwoWay}"
                                    Foreground="{Binding Path=Foreground,
                                                        RelativeSource=
                                                            {RelativeSource Mode=FindAncestor,
                                                                AncestorType=ListBox},
                                                        Mode=OneWay}">
                                    2) Это еще флажок, элемент списка правее
                                </CheckBox>
                                <CheckBox 
                                    Foreground="{Binding Path=Foreground,
                                                         RelativeSource=
                                                             {RelativeSource FindAncestor,
                                                                 AncestorType=ListBox},
                                                         Mode=OneWay}">
                                    3) Это еще флажок, элемент списка правее
                                </CheckBox>
                                <ListBoxItem IsSelected="False">
                                    <CheckBox 
                                        IsChecked="{Binding RelativeSource={RelativeSource 
                                                                AncestorType={x:Type ListBoxItem}}, 
                                                            Path=IsSelected, Mode=TwoWay}"
                                        Foreground="{Binding Path=Foreground,
                                                             RelativeSource=
                                                                 {RelativeSource FindAncestor,
                                                                     AncestorType=ListBox},
                                                             Mode=OneWay}">
                                        4) Это еще флажок, элемент списка правее
                                    </CheckBox>
                                </ListBoxItem>
                            </ListBox>
                        </StackPanel>
                        <RichTextBox 
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                        </RichTextBox>
                    </DockPanel>
                </TabItem>

    Поскольку вкладка <TabItem> может содержать только один элемент, мы упаковали ее содержимое в компоновочную панель <DockPanel>. В качестве содержимого контейнера <DockPanel> определили два элемента: компоновочную панель <StackPanel> и контейнер <RichTextBox>, где чуть позже разместим краткую инструкцию пользователя (для тренировки). Настройка <StackPanel DockPanel.Dock="Top"> приклеивает эту панель к верхней части родителя <DockPanel>, а настройка <DockPanel LastChildFill="True"> заставляет элемент <RichTextBox> занять всю остальную оставшуся свободной часть.

    На первом месте в контейнере <StackPanel> размещен элемент <TextBlock> для вывода заголовка. Далее следует элемент списка <ListBox>, который в качестве своих дочерних объектов содержит элементы флажков <CheckBox>. Интересно то, что первые три элемента списка не указаны явно и только последний элемент указан.

    Если не создавать контейнер элемента ListBoxItem явно, то WPF сама создает его неявно и присваивает его свойству IsSelected значение Selector.IsSelected. Только один раз в момент создания свойство Selector.IsSelected считывается привязанными элементами. Но далее при изменении выбора элемента это свойство не обновляется. Чтобы показать, что Selector.IsSelected действительно не обновляется, свойство IsChecked первого флажка связывается с Selector.IsSelected первого элемента ListBoxItem в соответствии с режимом RelativeSource Mode=Self.

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

    Итак, когда контейнер элемента не создается явно, следует использовать вложенное свойство зависимостей Selector.IsSelected для начального выделения элемента в Selector. Объекты TabItem, ListBox и ComboBox являются контейнерами для списковых элементов управления и наследуют от Selector. Поэтому, когда их дочерние элементы заданы явно, то каждый имеет свое свойство IsSelected. К этому свойству мы и привязываемся во втором и четвертом элементах списка, что дает взаимную синхронизацию выделения флажка и элемента списка.

  • Запустите приложение - пока вкладка Page11 может выглядеть так

  • Замечание. Если из заголовка вкладки

    <TabItem Header="Page11" Selector.IsSelected="True">

    удалить атрибут Selector.IsSelected="True", то синхронная работа второго элемента нарушается (не знаю, почему...).

    Свойство Foreground каждого элемента списка, заданного явно или неявно, мы связали с одноименным свойством контейнера <ListBox>, в котором задали ему значение Red для обеспечения контраста надписи на выделенном элементе (и для тренировки). Эту привязку мы тоже выполнили по рассматриваемой технологии RelativeSource.

    Последним в компоновочном контейнере <DockPanel> мы забронировали элемент <RichTextBox>, который обеспечивает поддержку документов нефиксированного формата <FlowDocument>. Наша задача - создать краткую инструкцию 'чего-то... чего-то', разместить ее во внешнем файле как элемент <FlowDocument> и загрузить в контейнер <RichTextBox> во время выполнения. К теме привязки этот кусочек задачи отношения не имеет, но для тренировки, чтобы заполнить оставшееся на вкладке (и в голове) вакантное место, решим ее.

    Там шпионки с крепким телом,

    Ты их в дверь - они в окно!

    Говори, что с 'этим делом'

    Мы покончили давно!

    (В.С. Высоцкий)

  • Добавьте к текущему проекту командой Project/Add New Item новый XAML -файл с именем Instruct.xaml (не забудьте изменить расширение файла c xml на xaml )

  • Выделите в Solution Explorer новый файл Instruct.xaml и измените через панель Properties его свойства на

  • Build Action = None
  • Copy to Output Directory=Copy if newer
  • Удалите в файле Instruct.xaml сгенерированное мастером содержимое и заполните его следующей разметкой (чем бы то нибыло, абы-кабы, аты-баты и т.д.)

  • <FlowDocument FontSize="11" Background="White"
        >
        <List MarkerStyle="Decimal">
            <ListItem>
                <Paragraph>
                    Первый флажок двунаправленно привязан булевым свойством 
                    IsChecked к вложенному логическому свойству Selector.IsSelected, 
                    которое инициализируется один раз и больше не меняется. 
                    Поэтому флажок и элемент списка меняются независимо.
                    Режим поиска родителя определен значением Mode=Self 
                    - искать источник в том же элементе.
                </Paragraph>
            </ListItem>
            <ListItem>
                <Paragraph>
                    Второй флажок своим свойством IsChecked привязан двунаправленной 
                    привязкой к свойству IsSelected своего ближайшего контейнера,
                    которым является элемент списка ListBoxItem, 
                    поэтому и меняется синхронно с ним, 
                    обеспечивая нормальную логику работы списка.
                    Режим поиска родителя определен значением Mode=FindAncestor - 
                    искать источник в ближайшем родителе.
                </Paragraph>
            </ListItem>
            <ListItem>
                <Paragraph>
                    В третьем элементе списка флажок и элемент списка ListBoxItem 
                    никак не связаны, поэтому изменяются автономно как в первом элементе, 
                    где связь хоть и установлена, но источник Selector.IsSelected 
                    остается постоянным
                </Paragraph>
            </ListItem>
            <ListItem>
                <Paragraph>
                    Четвертый элемент списка задается явно и флажок своим свойством 
                    IsChecked привязан двунаправленной привязкой к свойству 
                    IsSelected этого контейнера. Поэтому флажок и элемент списка 
                    работают синхронно, как и в случае со вторым списком.
                </Paragraph>
            </ListItem>
        </List>
    </FlowDocument>
  • Запустите приложение - компилятор (синтаксический анализатор) начинает ругаться, что не понимает конструкцию файла и не может преобразовать ее в объектный код. Текстовый редактор оболочки тоже зашелся в истерике, потому что не видит библиотеку WPF

  • Добавьте в открывающий дескриптор <FlowDocument> подключение пространства имен WPF -библиотеки

  • <FlowDocument FontSize="11" Background="White"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        >
        .............................................
    </FlowDocument>;

    Откомпилируйте решение командой Build/Build Solution - вроде бы все успокоилось

    Если бы мы поместили содержимое файла Instruct.xaml сразу внутрь контейнера <RichTextBox>...</RichTextBox>, то в дескрипторе окна <Window> уже есть такое подключение и ничего специального предпринимать бы не пришлось. И все бы сразу заработало, можете так и попробовать. Но чтобы не захломлять разметку вкладки Page11, мы пошли окружным путем и решили вынести документ в отдельный файл Instruct.xaml. В наказание за инициативу нам еще нужно будет этот файл подключить к контейнеру <RichTextBox>. Сделать это можно в процедурном коде или в разметке - через словарь ресурсов. Применим поочередно оба способа.

    Подключение документа через процедурный код

  • Дополните в разметке вкладки Page11 контейнер <RichTextBox> именем и зарегистрируйте обработчик события Loaded так

  • <RichTextBox Name="documentReader" Loaded="documentReader_Loaded"
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                        </RichTextBox>
  • В разметке на записи регистрации обработчика события Loaded контейнера <RichTextBox> вызовите контекстное меню и командой Navigate to Event Handler создайте в файле Window1.xaml.cs заготовку этого обработчика, которую заполните следующим кодом

  • private void documentReader_Loaded(object sender, RoutedEventArgs e)
            {
                String fileName = "Instruct.xaml";
                FileStream xamlFile = new FileStream(fileName, FileMode.Open, FileAccess.Read);
                FlowDocument content = XamlReader.Load(xamlFile) as FlowDocument;
                documentReader.Document = content;
        
                xamlFile.Close();
            }
  • В начало файла Window1.xaml.cs добавьте подключение пространств имен для используемых в обработчике классов и перечислений

  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using System.Windows.Markup;// Для класса XamlReader
    using System.IO;            // Для класса FileStream, 
                                // перечислений FileMode и FileAccess
        
    namespace ElementWithObject
    {
        public partial class Window1 : Window
        {
            ....................................................
        }
    }
  • Запустите приложение с вкладкой Page11 - все работает как надо и снимок экрана может быть таким

  • Подключение документа через ресурс

    Теперь ту же задачу подключения документа к контейнеру <RichTextBox> быстренько повторим с использованием механизма ресурсов, чтобы вспомнить молодость. При этом нам не придется писать ни строчки процедурного кода, все выполним в разметке. Чтобы не вмешиваться в процедурный код, просто удалим регистрацию обработчика события Loaded из разметки элемента RichTextBox. Пусть обработчик существует как обычный незадействованный метод класса Window1 и никуда не встревает со своими услугами. А компилятор тоже будет молчать.

    Было
    <RichTextBox Name="documentReader" Loaded="documentReader_Loaded"
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                        </RichTextBox>
    Стало
    <RichTextBox Name="documentReader" 
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                        </RichTextBox>

    Имя удалять не будем, потому что на него ссылается код обработчика. Если хотим убрать и имя, тогда нужно очистить или вообще удалить обработчик documentReader_Loaded() из класса Window1 (кому как, а лично мне это делать жалко, код-то вымучен бессонными ночами и недопитыми литрами. Хотя с допитыми может быть получилось бы лучше).

  • В панели Solution Explorer выделите корневой узел текущего проекта и через меню оболочки командой Project/Add Resource Dictionary добавьте новый файл с именем по умолчанию Dictionary1.xaml

  • Выделите в Solution Explore новый файл Dictionary1.xaml и проверьте через панель Properties его свойства, чтобы они были такими

  • Build Action = Page
  • Copy to Output Directory=Do not copy
  • Скопируйте из файла Instruct.xaml в новый файл Dictionary1.xaml все содержимое с разметкой документа <FlowDocument> и обязательно добавьте в его открывающем дескрипторе ключ ресурса x:Key="instruct". При этом можете удалить, а можете и оставить, индивидуальное подключение пространств имен XAML, которое в дескрипторе ресурса уже присутствует (можете удалить любое из них или ничего не удалять)

  • Файл Dictionary1.xaml должен стать таким (текст неважен, важна разметка)

    <ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        >
        <FlowDocument x:Key="instruct" FontSize="11" Background="White"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            >
            <List MarkerStyle="Decimal">
                <ListItem>
                    <Paragraph>
                        Первый флажок двунаправленно привязан булевым свойством 
                        IsChecked к вложенному логическому свойству Selector.IsSelected, 
                        которое инициализируется один раз и больше не меняется. 
                        Поэтому флажок и элемент списка меняются независимо.
                        Режим поиска родителя определен значением Mode=Self - 
                        искать источник в том же элементе.
                    </Paragraph>
                </ListItem>
                <ListItem>
                    <Paragraph>
                        Второй флажок своим свойством IsChecked привязан двунаправленной 
                        привязкой к свойству IsSelected своего ближайшего контейнера,
                        которым является элемент списка ListBoxItem, 
                        поэтому и меняется синхронно с ним, 
                        обеспечивая нормальную логику работы списка.
                        Режим поиска родителя определен значением Mode=FindAncestor - 
                        искать источник в ближайшем родителе.
                    </Paragraph>
                </ListItem>
                <ListItem>
                    <Paragraph>
                        В третьем элементе списка флажок и элемент списка ListBoxItem 
                        никак не связаны, поэтому изменяются автономно как в первом элементе, 
                        где связь хоть и установлена, но источник Selector.IsSelected 
                        остается постоянным
                    </Paragraph>
                </ListItem>
                <ListItem>
                    <Paragraph>
                        Четвертый элемент списка задается явно и флажок своим свойством 
                        IsChecked привязан двунаправленной привязкой к свойству 
                        IsSelected этого контейнера. Поэтому флажок и элемент списка 
                        работают синхронно, как и в случае со вторым списком.
                    </Paragraph>
                </ListItem>
            </List>
        </FlowDocument>
    </ResourceDictionary>
  • Дополните разметку элемента RichTextBox во вкладке Page11 следующей конструкцией подключения ресурса по синтаксису элемента свойства

  • <RichTextBox Name="documentReader"
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                            <RichTextBox.Resources>
                                <ResourceDictionary>
                                    <ResourceDictionary.MergedDictionaries>
                                        <ResourceDictionary Source="Dictionary1.xaml" />
                                    </ResourceDictionary.MergedDictionaries>
                                </ResourceDictionary>
                            </RichTextBox.Resources>
    	
                            <RichTextBox.Document>
                                <StaticResource ResourceKey="instruct" />                        
                            </RichTextBox.Document>
                       </RichTextBox>
  • Запустите приложение - вкладка Page11 должна выглядеть точно так, как и в предыдущем случае подключения документа через процедурный код

  • Обращает на себя внимание применение элементов ListBox (ListBoxItem и List (ListItem). Оба называются списками. Но первый из них относится к элементам управления, поскольку служит для отображения информации и обеспечивает взаимодействие с пользователем. А второй - просто к элементам (хотя это и более общая категория), поскольку незримо участвует только в верстке документа. Да ладно, в конце-концов, - это касается только терминологии. Главное - не как называть, а правильно понимать и применять.

    Вкладка Page12. Привязка RelativeSource в процедурном коде

    Возьмем за основу привязку RelativeSource в разметке, а потом перенесем ее в процедурный код.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page12, которую заполните следующей начальной разметкой, использующей привязку RelativeSource

  • <!-- Привязка RelativeSource в коде --<
                <TabItem Header="Page12"<
                    <!-- Привязка Button.Content к StackPanel уровней 1 и 2 --<
                    <StackPanel Tag="Эта информация находится в StackPanel уровня 2"<
                        <StackPanel Tag="Эта информация находится в StackPanel уровня 1"<
                            <Button Margin="5"
                                Content="{Binding Path=Tag,
                                                  RelativeSource={RelativeSource 
                                                      Mode=FindAncestor,
                                                      AncestorType=StackPanel, 
                                                      AncestorLevel=1},
                                                  Mode=OneWay}" /<
                           <Button Margin="5"
                                Content="{Binding Path=Tag,
                                                  RelativeSource={RelativeSource 
                                                      Mode=FindAncestor,
                                                      AncestorType=StackPanel,
                                                      AncestorLevel=2},
                                                  Mode=OneWay}" /<
                        </StackPanel<
                        <!-- Привязка TextBlock.Text к Window.Title --<
                        <Label<
                            Следующая информация находится в Window:
                        </Label<
                        <TextBlock Margin="5,0"<
                            <TextBlock.Text<
                                <Binding Path="Title" Mode="OneWay"<
                                    <Binding.RelativeSource<
                                        <RelativeSource Mode="FindAncestor"
                                                        AncestorType="Window" /<
                                    </Binding.RelativeSource<
                                </Binding<
                            </TextBlock.Text<
                        </TextBlock<
                    </StackPanel<
                </TabItem<
  • Запустите приложение - вывод вкладки Page12 будет таким

  • Удалите привязку RelativeSource из элементов вкладки Page12 и присвойте привязываемым источникам имена для возможности адресации из процедурного кода

  • Окончательный вид вкладки Page12 будет таким

    <!-- Привязка RelativeSource в коде -->
                <TabItem Header="Page12" Selector.IsSelected="True">
                    <!-- Привязка Button.Content к StackPanel уровней 1 и 2 -->
                    <StackPanel Tag="Эта информация находится в StackPanel уровня 2">
                        <StackPanel Tag="Эта информация находится в StackPanel уровня 1">
                            <Button Margin="5" Name="button12level1" />
                            <Button Margin="5" Name="button12level2" />
                        </StackPanel>
                        <!-- Привязка TextBlock.Text к Window.Title -->
                        <Label>
                            Следующая информация находится в Window:
                        </Label>
                        <TextBlock Margin="5,0" Name="textBlock12" />
                    </StackPanel>
                </TabItem>
  • Добавьте в конструктор класса Window1 файла Window1.xaml.cs следующий процедурный код привязки RelativeSource

  • public Window1()
            {
                InitializeComponent();
        
                //********************************************************
                // Привязка RelativeSource для элементов вкладки Page12
                //********************************************************
                //
                // button12level1
                //
                Binding binding = new Binding();
                binding.Path = new PropertyPath("Tag");
                binding.Mode = BindingMode.OneWay;
                binding.RelativeSource = new RelativeSource(
                    RelativeSourceMode.FindAncestor, typeof(StackPanel), 1);
                BindingOperations.SetBinding(button12level1, Button.ContentProperty, binding);
                //
                // button12level2
                //
                binding = new Binding("Tag");
                binding.Mode = BindingMode.OneWay;
                binding.RelativeSource = new RelativeSource(
                    RelativeSourceMode.FindAncestor, typeof(StackPanel), 2);
                BindingOperations.SetBinding(button12level2, Button.ContentProperty, binding);
                //
                // textBlock12
                //
                binding = new Binding();
                binding.Path = new PropertyPath("Title");
                binding.Mode = BindingMode.OneWay;
                binding.RelativeSource = new RelativeSource(
                    RelativeSourceMode.FindAncestor, typeof(Window), 1);
                BindingOperations.SetBinding(textBlock12, TextBlock.TextProperty, binding);
            }
  • Запустите приложение - вывод вкладки Page12 остался в точности таким, как и с привязкой в разметк

  • Пока об основах привязки поговорили достаточно. Но это еще далеко не все. В следующих разделах мы рассмотрим вопросы более серьезного применения привязки: привязки WPF к объектам ADO.NET, привязки в шаблонах и т.д.

    Страницы:

    Часть I

    Общие положения

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

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

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

    Синтаксис привязки данных, как и в случае с ресурсами, также имеет два варианта: расширения разметки и элементов свойств, но отличается деталями. Ключевым элементом привязки для любого варианта является определение объекта Binding из пространства имен System.Windows.Data. Этот элемент всегда устанавливается на стороне приемника привязки, кроме режима Mode=OneWayToSource. Приемник должен быть производным от класса DependencyObject и привязываемое свойство (целевое свойство) должно быть свойством зависимости. В свойства зависимостей встроена способность посылать или принимать уведемления об изменениях.

    К источнику привязки предъявляется гораздо меньше требований. Связываемое свойство источника не обязано быть зависимым свойством. Главное, чтобы источник имел оповещающее событие, указывающее на изменение связываемого свойства. Источником привязки может быть любое открытое свойство, в том числе свойства других элементов управления, объекты среды CLR, элементы XAML, наборы данных ADO.NET, фрагменты XML и т.д. Для правильного применения привязки к сложным объектам данных технология WPF предоставляет два специализированных класса - XmlDataProvider и ObjectDataProvider.

    Свойства зависимостей еще называют присоединенными. В версии XAML, предназначенной для WPF, присоединенные свойства работают только в том случае, если и тип, в котором свойство определено, и тип, к которому оно присоединяется, оба наследуют классу DependencyObject. С помощью свойств зависимостей язык XAML имеет возможность расширять типы за счет свойств, предоставляемых другими типами. При использовании в разметке по синтаксису элементов свойств присоединенному свойству всегда предшествует имя предоставляющего его типа, даже если такое свойство употребляется в качестве атрибута.

    Направления привязки

    Тип привязки элемента Binding определяется его свойством Mode, которое может принимать одно из значений перечисления BindingMode из пространства имен System.Windows.Data:

  • Default - установлен по умолчанию и зависит от типа привязываемого свойства на стороне приемника (целевого свойства). Действует как режим двухсторонней привязки TwoWay для свойств, доступных для редактирования в пользовательском интерфейсе, таких как TextBox.Text или CheckBox.Checked, либо - как односторонняя привязка OneWay для иных свойств. Чтобы не полагаться на настройки по умолчанию, следует взять себе за правило - всегда явно устанавливать параметр направления привязки.
  • OneTime - односторонняя начальная привязка, когда значение целевого свойства устанавливается по значению источника только один раз: при инициализации, программной замены привязанного объекта-источника на новый, при изменении свойства DataContext или в результате программного вызова метода BindingExpression.UpdateTarget (). Иные поступающие уведомления об изменениях на стороне источника приемником учитываться не будут
  • OneWay - односторонняя привязка, когда целевое свойство обновляется при изменении свойства источника. Каждый раз при изменении на стороне источника поток данных направлен от источника к целевому объекту.
  • OneWayToSource - организует однонаправленную привязку, как и OneWay, только выражение привязки помещается в источник. Этот трюк может понадобиться в том случае, когда привязываемое свойство-приемник не является свойством зависимости, а свойство источника все-таки наследует классу DependencyObject.
  • TwoWay - двухсторонняя привязка, когда целевое свойство обновляется при изменении свойства источника и свойство-источник обновляется при изменении целевого свойства. Иными словами, режим привязки TwoWay отправляет данные от источника к целевому объекту, а в случае изменения значения свойства целевого объекта данные отправляются обратно от целевого объекта к источнику
  • Упражнение 1. Привязка элемента к элементу

    В данном упражнении на ряде примеров рассмотрим вопросы создания канала связи между свойствами зависимости визуальных элементов WPF пользовательского интерфейса.

  • Командой
  • Откройте файл .App.xaml и добавьте в коллекцию ресурсов приложения объект SolidColorBrush со следующими настройками
  • <Application x:Class="UseResource.App"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        StartupUri="Window1.xaml">
        <!-- Определение ресурсов в коллекции приложения -->
        <Application.Resources>
            <SolidColorBrush x:Key="ControlColorBrush" 
                             Color="{x:Static SystemColors.ControlColor}" />
        </Application.Resources>
    </Application>>

    Вкладка Page1. Обновление значений свойств элементов без использования привязки

    Вначале построим пример передачи информации между элементами с использованием событий, как это традиционно делается в обычных приложениях

  • Поместите в разметку файла Window1.xaml следующий дескрипторный код с вкладкой Page1
  • <Window x:Class="ElementWithElement.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Привязка элемента к элементу" 
        MinHeight="300" 
        MinWidth="300"
        Height="300" 
        Width="300"
        WindowStartupLocation="CenterScreen"
        Background="{StaticResource ControlColorBrush}"
        Loaded="Window_Loaded">
        <Grid>
            <TabControl>
                <!-- Page1. Использование обработчика для обновления свойств -->
                <TabItem Header="Page1">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15">
                            <TextBlock.Foreground>
                                <SolidColorBrush Color="Red" />
                            </TextBlock.Foreground>
                            Соединение свойств без привязки
                        </TextBlock>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Name="label1" Width="40" />
                            <Slider Name="slider1" Width="230" 
                                Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft"
                                ValueChanged="slider1_ValueChanged" />
                        </StackPanel>
                        <TextBlock Name="textBlock1" Margin="0,10" 
                            Foreground="Blue">
                            Simple Text
                        </TextBlock>
                    </StackPanel>
                </TabItem>
            </TabControl>
        </Grid>
    </Window>
  • Заполните файл процедурного кода Window1.xaml.cs следующим образом
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    namespace ElementWithElement
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        
            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                // Инициализация для Page1
                int value = Convert.ToInt32(textBlock1.FontSize);// Явное приведение типов
                slider1.Minimum = value;    // Тип int приводится к double по умолчанию
                label1.Content = value.ToString();// Content имеет тип Object и проглотит все
            }
        
            // Обработчик для Page1
            private void slider1_ValueChanged(object sender, 
                RoutedPropertyChangedEventArgs<double> e)
            {
                if (!this.IsLoaded)
                    return;// Событие возбуждается, а элементы еще не созданы
        
                Slider slider = (Slider)sender; // Повышаем полномочия ссылки
                int value = (int)slider.Value;  // Явное приведение типов
                label1.Content = value;         // Content имеет тип Object и проглотит все
                textBlock1.FontSize = value;    // Тип int приводится к double неявно
            }
        }
    }
  • Запустите приложение и испытайте работу элементов вкладки Page1. Разберитесь с кодом разметки и процедурным кодом
  • Результат будет выглядеть так

    Вкладка Page2. Однонаправленная привязка на основе разметки по синтаксису элементов свойств

    Привязка на основе разметки более предпочтительна, потому что она проще и требует меньше работы. Хотя, создание привязки в коде тоже имеет место быть. В данном примере мы решим ту же самую задачу, что и в предыдущем примере. Только будем использовать уже привязку свойства-источника Value бегунка к элементам отображения Label и TextBlock.

  • Добавьте в контейнер TabControl после элемента вкладки Page1 файла Window1.xaml еще один элемент вкладки с именем Page2 со следующим кодом привязки по синтаксису элементов свойств
  • <!-- Page2. Использование привязки по синтаксису элементов свойств -->
                <TabItem Header="Page2">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15">
                            <TextBlock.Foreground>
                                <SolidColorBrush Color="Red" />
                            </TextBlock.Foreground>
                            Использование привязки по
                            <LineBreak />
                            синтаксису элементов свойств
                        </TextBlock>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40" ContentStringFormat="F0">
                                <Label.Content>
                                    <Binding ElementName="slider2" Path="Value" Mode="OneWay" />
                                </Label.Content>
                            </Label>
                            <Slider Name="slider2" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="Both" />
                        </StackPanel>
                        <TextBlock Margin="0,10" Foreground="Blue">
                            <TextBlock.FontSize>
                                <Binding ElementName="slider2" Path="Value" Mode="OneWay" />
                            </TextBlock.FontSize>
                            Simple Text
                        </TextBlock>
                    </StackPanel>
                </TabItem>

    В разметке вкладки Page2 мы оставили имя только источника, поскольку на него ссылаемся в привязке. А имена приемников нам просто не нужны, так как обработчики в процедурном коде мы не создаем. Соответственно, убрана регистрация обработчика для события Slider.ValueChanged, в результате чего код получился намного проще, чем в примере вкладки Page2. Добавлен параметр Minimum="11", определяющий нижнее значение ползунка. Обратите внимание, что при инициализации вкладки это значение сразу отображается элементами Label и TextBlock - механизм привязки знает свое дело.

  • Попробуйте установить другое значение Minimum, например, Minimum="36" и подвигать ползунком
  • Элементы привязки мы поместили в приемники и в свойстве Path указали, какое именно свойство зависимости мы выбираем из источника для привязки. Свойство ContentStringFormat="F0" (здесь после F стоит ноль) в элементе Label запрещает выводить дробную часть значения ползунка. Свойство Mode="OneWay" устанавливает однонаправленный поток данных от источника ( Slider ) к приемникам ( Label и TextBlock ). Содержимое 'Simple Text' надписи последнего элемента TextBlock можно поменять местами с секцией привязки

  • Испытайте пример - функциональность вкладки Page2 получилась абсолютно одинаковой с вкладкой Page1. Разберитесь с разметкой и подивитесь простоте и мощности механизма привязки
  • Представление окна будет таким

    Вкладка Page3. Однонаправленная привязка по синтаксису расширения разметки

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page3, представленную следующей разметкой
  • <!-- Page3. Использование привязки по синтаксису расширения разметки -->
                <TabItem Header="Page3">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Привязка расширением разметки" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider3, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider3" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="BottomRight"
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider3, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>

    Привязка по синтаксису расширения разметки похожа на присоединение ресурсов по тому же синтаксису, только список параметров привязки разделяется запятыми. Примененный синтаксис привязки сильно сокращает код разметки. Этот синтаксис еще называют синтаксисом атрибута, поскольку он концентрирует код привязки данных внутри атрибута привязываемого элемента. В сущности, тег Binding помещается в фигурные скобки наравне с атрибутами элемента.

    Обратите внимание, что по сравнению с примером Page2 мы убрали из атрибутов элемента Label параметр форматирования ContentStringFormat="F0", а вместо этого добавили к элементу Slider свойство IsSnapToTickEnabled="True". Такой прием заставляет свойство Value элемента Slider меняться с установленным шагом дискретности TickFrequency="2" и притягивает головку бегунка к делениям шкалы. Это побуждает элемент Label отображать значение свойства Slider.Value типа double с нулевой дробной частью как целое число.

  • Запустите приложение - вкладка Page3 представляет тот же самый результат
  • И опять, для вкладки Page3 мы не написали ни строчки процедурного кода!

    Вкладка Page4. Множественная привязка

    В элементе-приемнике мы можем привязывать к различным источникам более чем одно свойство. Вот пример

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page4, представленную следующей разметкой
  • <!-- Page4. Множественная привязка -->
                <TabItem Header="Page4">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Множественная привязка" 
                            />
                        <TextBox Name="textBox4" Margin="5,0,5,15" Text="Simple Text" />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider4, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider4" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="BottomRight" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <ListBox Name="listBox4" Margin="5">
                            <ListBox.Resources>
                                <SolidColorBrush x:Key="redBrush" Color="Red" />
                                <SolidColorBrush x:Key="greenBrush" Color="Green" />
                                <SolidColorBrush x:Key="blueBrush" Color="Blue" />
                            </ListBox.Resources>
                            <ListBoxItem Tag="{StaticResource redBrush}" Content="Red" />
                            <ListBoxItem Tag="{StaticResource greenBrush}" Content="Green" />
                            <ListBoxItem Tag="{StaticResource blueBrush}" Content="Blue"
                                Selector.IsSelected="True" />
                        </ListBox> 
                        <TextBlock Margin="0,10" 
                            FontSize="{Binding ElementName=slider4, Path=Value, Mode=OneWay}"
                            Text="{Binding ElementName=textBox4, Path=Text, Mode=OneWay}"
                            Foreground="{Binding ElementName=listBox4, Path=SelectedItem.Tag, Mode=OneWay}"
                            />
                    </StackPanel>
                </TabItem>

    В данном примере мы ввели текстовое поле <TextBox> для изменения отображаемого текста, и элемент списка <ListBox>, который применили для выбора пользователем цвета. Элемент <TextBlock> мы привязали сразу к трем элементам-источникам, в которых определяется: содержимое текста, размер и цвет шрифта. Передача информации в связанных элементах осуществляется в однонаправленном режиме. Для привязки по цвету используется свойство Tag элементов списка, которое относится к типу Object.

  • Запустите приложение и испытайте работу вкладки Page4, реализующей множественную привязку. Разберитесь с кодом разметки
  • Вкладка будет выглядеть так

    При однонаправленной привязке на стороне приемника данные источника непрерывно отражаются в приемнике. Это видно на примере поведения свойства Text элемента TextBlock, которое получает однонаправленные данные из текстового поля объекта textBox4. Далее мы увидим, что при двунаправленной привязке, когда приемник привязан к источнику в режиме Mode=TwoWay, приемник также будет получать данные источника непрерывно, но обратную отсылку при вводе данных на стороне приемника он сможет осуществить источнику только при потере фокуса. Это обусловлено значением параметра UpdateSourceTrigger=LostFocus, которое принято по умолчанию объектом Binding для обновления привязки.

    Вкладка Page5. Связывание элементов в цепочку

    Передачу информации можно осуществлять по цепочке, связывая элементы последовательно друг с другом. Например, источник <Slider> можно связать с приемником <Label>, а затем считать <Label> как источник и связать с ним приемник <TextBlock>. При манипулировании с источником <Slider> информация от него через элемент <Label> транзитом будет передаваться в <TextBlock>. Несмотря на то, что такой способ работает исправно, он может изрядно запутать код разметки. Лучше связывать источник и приемник напрямую. Но мы, как программисты, должны знать об этом, поэтому приведем простой пример.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page5, представленную следующей разметкой
  • <!-- Page5. Цепочки привязок -->
                <TabItem Header="Page5">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Цепочки привязок" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Name="label5" Width="40"
                                Content="{Binding ElementName=slider5, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider5" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=label5, Path=Content, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Запустите приложение и испытайте работу вкладки Page5, реализующей цепочку привязок
  • Вкладка будет выглядеть точно также, как вкладка Page3, где мы два разных приемника привязали к одному и тому же источнику.

    Этот способ может пригодиться в том случае, если источник находится где-то в труднодоступном месте (например, в другом окне или сборке) и код привязки достаточно объемен. Основательно потрудившись над привязкой к такому источнику первого приемника, другие приемники проще будет привязать к нему, чем к первоисточнику.

    Вкладка Page6. Двунаправленная привязка приемника

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page6, представленную следующей разметкой
  • <!-- Page6. Двунаправленная привязка -->
                <TabItem Header="Page6">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red">
                            Двунаправленная привязка 
                            <LineBreak />
                            элемента TextBox к Slider
                            <LineBreak />
                            без проверки достоверности
                        </TextBlock>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider6, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider6" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                Value="35"
                                />
                        </StackPanel>
                        <TextBox Margin="5" 
                            Text="{Binding ElementName=slider6, Path=Value, Mode=TwoWay}" 
                            />
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider6, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>

    В разметку вкладки мы добавили текстовое поле с двунаправленной привязкой. У всех привязанных элементов отсутствуют имена кроме источника, поскольку его имя используется при привязке этими элементами. Мы установили начальное положение бегунка Value="35", которое при инициализации получают все привязанные к нему элементы. Теперь, за счет двунаправленности привязки, значение бегунка можно менять как перемещением головки, так и через текстовое поле. Значения не просто выталкиваются из источника к целевому элементу, но и наоборот - от цели к источнику.

  • Запустите приложение и испытайте работу вкладки Page6. Попробуйте вводить в текстовое поле некорректное содержимое, которое нельзя трактовать как число, например, буквы
  • На данном примере мы наблюдаем несколько особенностей поведения WPF:

  • Одной из особенностей механизма привязки WPF является то, что он не возбуждает никаких исключений, чтобы известить нас о проблемах привязки данных. Если мы привязываем несуществующий элемент или свойство, или если типы соединенных привязкой свойств отличаются так, что данные источника не могут быть корректно приведены к типу данных приемника, WPF будет молчать и данные просто не появятся в приемнике. Однако, возникновение такой ситуации отслеживает оболочка, которая выводит в панель Output трассировочную информацию о работе средств привязки. Другой способ - применить механизм, выполняющий проверку достоверности (validation) данных (об этом речь пойдет ниже), или реализовать такую проверку в процедурном коде
  • Мы уже говорили, что при однонаправленной привязке на стороне приемника данные источника непрерывно отображаются в приемнике. При двунаправленной привязке на стороне приемника TextBox, введенные в текстовое поле значения приемника не применяются к источнику и к другим связанным с ним элементам до тех пор, пока TextBox не потеряет фокус ввода (по нажатию клавиши Tab или щелчку мыши на другом интерфейсном элементе, при переходе на другую вкладку или активации другого окна). Такое поведение можно изменить, установив непрерывное обновление, если использовать свойство UpdateSourceTrigger объекта Binding, которое принимает одно из значений одноименного перечисления System.Windows.Data.UpdateSourceTrigger (об этом речь пойдет ниже)
  • Вкладка Page7. Двунаправленная привязка источника-приемника

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page7, представленную следующей разметкой
  • <!-- Page7. Двунаправленная привязка источника-приемника -->
                <TabItem Header="Page7">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red">
                            Двунаправленная привязка 
                            <LineBreak />
                            источника - приемника
                        </TextBlock>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider7, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider7" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                Value="{Binding ElementName=textBox7, Path=Text, Mode=TwoWay}" 
                                />
                        </StackPanel>
                        <TextBox Name="textBox7" Margin="5" Text="21" />
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider7, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Запустите приложение и испытайте работу вкладки Page7 - она продолжает работать точно также, как и для вкладки Page6
  • Элемент <TextBox> у нас оказался несвязанным, зато пришлось ему присвоить имя, чтобы можно было его привязать на стороне элемента <Slider> в качестве источника. Теперь ползунок находится под контролем элемента TextBox и одновременно является источником для элементов <Label> и <TextBlock>. Когда окно отображается впервые, оно извлекает значение свойства Text объекта TextBox и копирует его в свойство Value объекта Slider. Оттуда, в свою очередь, это значение тиражируется в связанные объекты Label и TextBlock.

    Когда пользователь перетаскивает ползунок в новую позицию, действует канал обратной отсылки от ползунка-приемника к текстовому полю-источнику. Когда пользователь меняет значение текстового поля-источника, действует прямой канал передачи информации от источника TextBox к приемнику Slider.

  • Уберите инициализацию Text="21" из элемента TextBox - при запуске приложения ползунок не получает данные от источника и принимает состояние по умолчанию, устанавливая вслед за собой и другие его приемники
  • Обратите внимание, за счет прямой передачи данных от источника TextBox к приемнику Slider последний не ждет потери фокуса ввода источником, а непрерывно меняет свое значение. В обратной отсылке от приемника к источнику требуется потеря приемником фокуса ввода. В нашем случае при движении ползунка-приемника источник TextBox его и не получает, поэтому тоже изменяется непрерывно.

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

    Вкладка Page8. Создание привязки элементов в процедурном коде

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

  • Создать другую привязку в зависимости от возникших обстоятельств в работающем приложении
  • Удалить существующую связь и перейти к программному изменению свойств элементов обычным способом на основе событий
  • При создании пользовательских элементов управления вынести возможности привязки в программный интерфейс элемента для последующей его настройки сторонним программистом
  • Вначале приведем код разметки для вкладки Page8 с настройкой одним из уже рассмотренных способов, затем изменим вкладку так, чтобы реализовать для ее элементов привязку в процедурном коде.

  • Поместите в контейнер TabControl файла Window1.xaml новую вкладку Page8, стартовый код разметки которой имеет вид
  • <!-- Page8. Привязка элементов в коде -->
                <TabItem Header="Page8">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Привязка элементов в коде" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding ElementName=slider8, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider8" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider8, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Удалите код привязки из разметки элементов Label и TextBlock и добавьте на их место имена с атрибутами Name="label8" и Name="textBlock8" соответственно, - для возможности доступа из процедурного кода. После чего код разметки должен стать таким
  • <!-- Page8. Привязка элементов в коде -->
                <TabItem Header="Page8">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Привязка элементов в коде" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Name="label8" 
                                />
                            <Slider Name="slider8" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            Name="textBlock8"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Перейдите к редактированию файла Window1.xaml.cs и добавьте в конструктор класса Window1 следующий процедурный код
  • public Window1()
            {
                InitializeComponent();
        
                // Создаем и настраиваем объект привязки
                Binding binding = new Binding();
                binding.Source = slider8;// Источник 
                binding.Path = new PropertyPath("Value"); 
                binding.Mode = BindingMode.OneWay;
        
                // Привязываем элементы-приемники
                label8.SetBinding(Label.ContentProperty, binding);          // Label
                textBlock8.SetBinding(TextBlock.FontSizeProperty, binding); // TextBlock
            }

    В начале мы создаем сам узел (объект) привязки с необходимой информацией об источнике. Затем с помощью экземплярного метода SetBinding(), который каждый интерфейсный элемент наследует от класса FrameworkElement, присоединяем узел привязки к соответствующему свойству зависимости элемента-приемника. Свойства зависимости элементов определены как статические только для чтения с отложенной одноразовой инициализацией и имеют тип DependencyProperty. Например, в библиотечном классе System.Windows.Controls.TextBlock свойство зависимости FontSizeProperty определено так

    static readonly System.Windows.DependencyProperty FontSizeProperty

    Иными словами, в отличие от кода разметки, где для читабельности привязка осуществляется с использованием имени экземплярного свойства, например FontSize, в процедурном коде привязка к объекту-приемнику выполняется к применением имени типа элемента и статического свойства зависимости с окончанием Property, например FontSizeProperty. Такое правило справедливо для всех типов интерфейсных элементов при создании привязки в процедурном коде.

  • Запустите приложение и испытайте работу вкладки Page8 - она работает как и положено
  • Представление окна приложения с открытой вкладкой Page8 будет таким

    Прежде, чем двигаться дальше в демонстрации примеров данного упражнения, нужно оговорить следующее (чтобы не посчитали дураками).

  • Запустите приложение и откройте вкладку Page4, где у нас больше всего интерфейсных элементов
  • Если максимально увеличить размер шрифта текста, то он выходит за границы окна за счет того, что мы после разработки этой вкладки добавили еще несколько таких же. Полезная область окна уменьшилась по вине появления дополнительных закладок. А мы еще в данном упражнении собираемся добавлять все новые вкладки, чтобы продолжить рассмотрение темы привязки элемента к элементу (извилины к извилине, денежки к денежке).

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

    Вкладка Page9. Изменение привязки элементов в процедурном коде

    Если мы уже создали привязку в разметке или процедурном коде, то чтобы установить новую привязку для тех же элементов, нужно вначале удалить прежнюю. Для этого существуют статические методы BindingOperations. ClearBinding() и BindingOperations. ClearAllBindings() пространства имен System.Windows.Data. Метод ClearBinding() ожидает ссылку на объект, содержащий узел привязки, и ссылку на его свойство зависимостей, которое имеет привязку. Он удаляет привязку указанного свойства зависимостей в указанном объекте. Метод ClearAllBindings() ожидает ссылку на объект, содержащий привязки, и удаляет привязки для всех свойств этого объекта.

    Для демонстрации сказанного используем стартовый код разметки из предыдущего примера с уже готовыми декларативными привязками элементов.

  • Поместите в контейнер TabControl файла Window1.xaml новую вкладку Page9 со стартовым кодом разметки из предыдущего примера и присвойте элементам имена, как показано в следующем листинге
  • <!-- Page9. Изменение привязки в коде -->
                <TabItem Header="Page9">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Изменение привязки в коде" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Name="label9" Width="40"
                                Content="{Binding ElementName=slider9, Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider9" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="TopLeft" 
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <TextBlock Name="textBlock9" Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding ElementName=slider9, Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Запустите приложение на выполнение и убедитесь, что элементы вкладки Page9 являются связанными с бегунком и реагируют на его изменения должным образом
  • Удалите программно заданные декларативно привязки в элементах label9 и textBlock9, дополнив конструктор класса Window1 следующим кодом
  • public Window1()
            {
                InitializeComponent();
        
                // Создаем и настраиваем объект привязки
                Binding binding = new Binding();
                binding.Source = slider8;// Источник 
                binding.Path = new PropertyPath("Value"); 
                binding.Mode = BindingMode.OneWay;
        
                // Привязываем элементы-приемники
                label8.SetBinding(Label.ContentProperty, binding);          // Label
                textBlock8.SetBinding(TextBlock.FontSizeProperty, binding); // TextBlock
        
                // Удаление декларативных привязок во вкладке Page9
                BindingOperations.ClearBinding(label9, Label.ContentProperty);
                BindingOperations.ClearAllBindings(textBlock9);
            }
  • Запустите приложение на выполнение и убедитесь, что элементы вкладки Page9 теперь не реагируют на изменения бегунка
  • Добавьте в конструкторе класса Window1 код создания привязок, реализующий новую схему привязок как цепочку, рассмотренную ранее на вкладке Page5
  • public Window1()
            {
                InitializeComponent();
        
                // Создаем и настраиваем объект привязки
                Binding binding = new Binding();
                binding.Source = slider8;// Источник 
                binding.Path = new PropertyPath("Value"); 
                binding.Mode = BindingMode.OneWay;
        
                // Привязываем элементы-приемники
                label8.SetBinding(Label.ContentProperty, binding);          // Label
                textBlock8.SetBinding(TextBlock.FontSizeProperty, binding); // TextBlock
        
                // Удаление декларативных привязок во вкладке Page9
                BindingOperations.ClearBinding(label9, Label.ContentProperty);
                BindingOperations.ClearAllBindings(textBlock9);
        
                //******************************************************
                // Привязываем элементы во вкладке Page9 по новой схеме
                //******************************************************
                // Создаем и настраиваем узел привязки
                binding = new Binding();
                binding.Source = slider9;// Источник 
                binding.Path = new PropertyPath("Value");
                binding.Mode = BindingMode.OneWay;
        
                // Привязываем label9 к бегунку slider9
                label9.SetBinding(Label.ContentProperty, binding);
        
                // Привязываем textBlock9 не к slider9, а к label9,
                // реализуя схему цепочки привязок (для разнообразия).
                // Для этого надо создать новый узел привязки!!!
                binding = new Binding();
                binding.Source = label9;// Источник 
                binding.Path = new PropertyPath("Content");
                binding.Mode = BindingMode.OneWay;
        
                textBlock9.SetBinding(TextBlock.FontSizeProperty, binding);
                slider9.Value = 21;// Чуть отступим вправо
                //******************************************************
            }
  • Запустите приложение на выполнение и убедитесь, что элементы вкладки Page9 теперь работают как надо (в соответствии с возрастом)
  • Окно с вкладкой Page9 будет таким

    Вкладка Page10. Размещение узла привязки на стороне источника

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

    Но иногда свойство приемника, к которому мы хотим привязать передачу информации, не является свойством зависимости, а свойство источника - является таковым, и узел привязки приходится размещать в источнике. Тогда атрибуту ElementName нужно присвоить имя целевого элемента - приемника, а режим привязки установить как Mode=OneWayToSource. Рассмотрим пример.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page10 со следующим пробным кодом разметки
  • <!-- Page10. Размещение привязки на стороне источника -->
                <TabItem Header="Page10" Selector.IsSelected="True">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="5" Foreground="Red"
                            Text="Размещение привязки на источнике" 
                            />
                        <FlowDocumentScrollViewer Height="110" Margin="5" BorderThickness="2">
                            <FlowDocument Background="White">
                                <Paragraph>
                                    Этот приемник текста!
                                </Paragraph>
                                <Paragraph>
                                    <Run Name="runParagraph10" 
                                        Text="{Binding ElementName=textBox10, Path=Text}"
                                        /> 
                                </Paragraph>
                            </FlowDocument>
                        </FlowDocumentScrollViewer>
                        <TextBox Name="textBox10"
                            Margin="5" MinLines="3" MaxLines="3"
                            TextWrapping="Wrap"
                            AcceptsReturn="True"
                            AcceptsTab="True"
                            VerticalScrollBarVisibility="Visible"
                            />
                    </StackPanel>
                </TabItem>

    Элемент <Run> с именем runParagraph10 в этом примере является приемником, а элемент <TextBox> с именем textBox10 - источником. По-старинке, мы пытаемся привязать свойство Text приемника к свойству Text источника. Но уже на этапе проектирования редактор кода (а за ним и компилятор - при попытке компиляции) начинает ругаться, что свойство Text приемника не является свойством зависимостей, порожденным классом DependencyObject.

  • Зная наверняка, что свойство Text элемента <TextBox> является свойством зависимости, переместите (удалите в приемнике - добавьте к источнику) узел привязки из элемента <Run> в элемент <TextBox> с именем textBox10, чтобы последний стал таким (разметка вкладки Page10 приводится полностью)
  • <!-- Page10. Размещение привязки на стороне источника -->
                <TabItem Header="Page10" Selector.IsSelected="True">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="5" Foreground="Red"
                            Text="Размещение привязки на источнике" 
                            />
                        <FlowDocumentScrollViewer Height="110" Margin="5" BorderThickness="2">
                            <FlowDocument Background="White">
                                <Paragraph>
                                    Этот приемник текста!
                                </Paragraph>
                                <Paragraph>
                                    <Run Name="runParagraph10" /> 
                                </Paragraph>
                            </FlowDocument>
                        </FlowDocumentScrollViewer>
                        <TextBox
                            Margin="5" MinLines="3" MaxLines="3"
                            TextWrapping="Wrap"
                            AcceptsReturn="True"
                            AcceptsTab="True"
                            VerticalScrollBarVisibility="Visible"
                            Text="{Binding ElementName=runParagraph10, 
                                Path=Text,
                                Mode=OneWayToSource, 
                                UpdateSourceTrigger=PropertyChanged}"
                                />
                    </StackPanel>
                </TabItem>

    Мы удалили имя элемента TextBox как ненужное и добавили свойство Text, привязанное к именованному приемнику по синтаксису расширения разметки. Атрибут UpdateSourceTrigger=PropertyChanged синхронизирует передачу информации в непрерывном режиме. Если его убрать, то приемник будет обновляться только после потери источником фокуса ввода в соответствии с установкой по умолчанию UpdateSourceTrigger=LostFocus.

  • Запустите приложение с вкладкой Page10, испытайте его работу и восхититесь чудесами, на которые способна привязка в WPF. Особенно, если учесть, что мы в этом примере не написали ни одной строчки процедурного кода
  • Вкладка Page11. Режимы обновления привязки

    Обновления привязки устанавливают режим, как быстро изменения на одном связанном элементе отобразятся на другом. Обычно такая задача имеет место тогда, когда целевое свойство берет на себя еще и роль источника в привязках типа TwoWay или OneWayToSource. Этот режим определяется в выражении привязки элемента Binding и зависит от его свойства UpdateSourceTrigger. Свойство UpdateSourceTrigger может принимать следующие значения из одноименного перечисления System.Windows.Data.UpdateSourceTrigger

    Значения из перечисления UpdateSourceTrigger
    Значение Описание
    Default Поведение обновления целевого свойства определяется установками по умолчанию. Обычно это PropertyChanged, хотя для TextBox.Text значение по умолчанию равно LostFocus
    Explicit Источник не обновляется, пока не будет вызван один из экземплярных методов UpdateSource() или UpdateTarget() класса BindingExpression пространства имен System.Windows.Data сборки PresentationFramework.dll. Но перед этим из элемента нужно извлечь выражение привязки с помощью метода GetBindingExpression()
    LostFocus Источник обновляется, когда целевой элемент теряет фокус
    PropertyChanged Источник обновляется немедленно после изменения целевого свойства

    Конечно, самым удобным режимом обновления являются непрерывные изменения PropertyChanged. Но это и самый затратный режим, который может создать впечатление замедленного выполнения приложения и нарушить логику обновления источника до завершения процесса редактирования. Режим UpdateSourceTrigger.Explicit требует программной поддержки, зато самый предсказуемый. Можно, например, добавить кнопку Apple (Применить), по щелчку на которой будет вызвано обновление источника экземплярным методом UpdateSource(), когда процесс подготовки данных уже полностью завершен. В этом случае пользователь сам полностью контролирует процесс обновлений.

    Поясним сказанное на разметке предыдущего примера.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку Page11 со следующим кодом разметки
  • <!-- Page11. Обновление элемента привязки -->
                <TabItem Header="Page11" Selector.IsSelected="True">
                    <StackPanel>
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="5" Foreground="Red"
                            Text="Обновление элемента привязки" 
                            />
                        <FlowDocumentScrollViewer Height="110" Margin="5" BorderThickness="2">
                            <FlowDocument Background="White">
                                <Paragraph>
                                    <Run Name="runParagraph11" />
                                </Paragraph>
                            </FlowDocument>
                        </FlowDocumentScrollViewer>
                        <DockPanel LastChildFill="True">
                            <Button DockPanel.Dock="Left" 
                                Content="Apple" 
                                Width="75" Height="25" 
                                VerticalAlignment="Center" 
                                Margin="5,0"
                                Click="Button_Click" 
                                />
                            <TextBox Name="textBox11"
                                Margin="5" MinLines="3" MaxLines="3"
                                TextWrapping="Wrap"
                                AcceptsReturn="True"
                                AcceptsTab="True"
                                VerticalScrollBarVisibility="Visible"
                                Text="{Binding ElementName=runParagraph11, 
                                    Path=Text,
                                    Mode=OneWayToSource, 
                                    UpdateSourceTrigger=Explicit}"
                                    />
                        </DockPanel>
                    </StackPanel>
                </TabItem>
  • Щелкните на записи Click="Button_Click" правой кнопкой мыши и командой Navigate to Event Handler создайте обработчик Button_Click()
  • Запустите приложение с вкладкой Page11 на выполнение и убедитесь, что сам интерфейс функционирует, но при выбранном режиме обновления Explicit данные в элемент <FlowDocument> не передаются, потому что обработчик не заполнен соответствующим кодом
  • Перейдите в файл Window1.xaml.cs и заполните обработчик кнопки Apple следующим процедурным кодом
  • private void Button_Click(object sender, RoutedEventArgs e)
            {
                // Получить выражение привязки к свойству зависимости Text, 
                // которая размещена в текстовом элементе TextBox с именем textBox11
                BindingExpression binding = 
                    textBox11.GetBindingExpression(TextBox.TextProperty);
                // Обновить связанный элемент FlowDocument
                binding.UpdateSource();
            }
  • Запустите приложение и испытайте работу вкладки Page11, которая будет выглядеть примерно так
  • В принятой схеме обновления применяются в целевом элементе только по нажатию кнопки Apple.

    Часть II

    Упражнение 2. Привязка интерфейсных элементов к невизуальным объектам

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

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

    Основные свойства класса Binding
    Свойство Тип Описание
    ElementName String -класс Задает или извлекает имя элемента, который будет использоваться как источник информации. При связывании с элементом WPF применяется вместо свойства Source
    FallbackValue Object -класс Задает или извлекает значение для использования, если привязка не может вернуть значение. Значение по умолчанию UnsetValue
    Mode BindingMode -перечисление Получает или устанавливает значение, которое указывает направление потока данных в привязке
    NotifyOnSourceUpdated Boolean -структура Получает или устанавливает флаг, который указывает, возбуждается ли событие SourceUpdated при передаче данных от целевого элемента к источнику
    Path PropertyPath -класс Задает или извлекает путь к свойству источника
    RelativeSource RelativeSource -класс Задает или извлекает источник путем указания его местоположения относительно целевого элемента в визуальном дереве
    Source Оbject -класс Используется для указания на объект-источник, если он не является элементом WPF и поэтому нельзя применить ElementName

    Способы привязки рассмотрим на примерах.

  • Добавьте к решению DataBinding командой File/Add/New Project новый проект WPF Application с именем ElementWithObject и назначьте его стартовым
  • Настройте открывающий дескриптор окна Window1 нового проекта так
  • <Window x:Class="ElementWithObject.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="300" 
        Width="300"
        Title="Привязка элемента к объекту" 
        MinHeight="300" 
        MinWidth="300"
        WindowStartupLocation="CenterScreen"
        >  
        <Grid>
        </Grid>
    </Window>
  • Добавьте к разметке файла Window1.xaml логический ресурс для обеспечения классического цвета фона окна и присоедините его к свойству Background сетки Grid
  • <Window x:Class="ElementWithObject.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="300" 
        Width="300"
        Title="Привязка элемента к объекту" 
        MinHeight="300" 
        MinWidth="300"
        WindowStartupLocation="CenterScreen"
        >   
        <Window.Resources>
            <SolidColorBrush x:Key="ControlColorBrush" 
                Color="{x:Static SystemColors.ControlColor}" />
        </Window.Resources>
        <Grid Background="{StaticResource ResourceKey=ControlColorBrush}">
        </Grid>
    </Window>

    Вкладка Page1. Использование логического ресурса без привязки

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

  • Добавьте в разметку контейнер TabControl с вкладкой Page1 следующего содержания
  • <Grid Background="{StaticResource ResourceKey=ControlColorBrush}">
            <TabControl>
                <!-- Page1. Использование логического ресурса без привязки -->
                <TabItem Header="Page1">
                    <TabItem.Resources>
                        <ImageBrush x:Key="flower1"
                            ImageSource="Images/flower1.jpg"
                            Stretch="UniformToFill"
                            Opacity="1"
                            />
                    </TabItem.Resources>
                    <Rectangle 
                        Stroke="Red"
                        StrokeThickness="5.0" 
                        Opacity="1"
                        Fill="{StaticResource flower1}" />
                </TabItem>
            </TabControl>
        </Grid>
  • В панели Solution Explorer добавьте к корню проекта ElementWithObject командой контекстного меню Add/New Folder папку с именем Images
  • В панели Solution Explorer для папки Images вызовите контекстное меню и скопируйте в нее командой Add/Existing Item из прилагаемого каталога Source файл flower1.jpg
  • Выделите файл flower1.jpg и в панели Properties проверьте его настройки в оболочке. Они должны быть такими
  • Build Action=Resource
  • Copy to Output Directory=Do not copy
  • Запустите приложение - представление вкладки будет таким
  • Здесь мы пока не применяли механизм привязки, но вспомнили использование ресурсов.

    Вкладка Page2. Связывание целевого элемента с исходным объектом через логический ресурс

    Когда встает вопрос привязки визуального элемента к объекту, в отличии от привязки элемента к элементу, то в выражении привязки для обозначения источника нужно использовать взамен атрибута ElementName другой атрибут - Source (или RelativeSource ). В следующем примере используем в качестве посредника привязки логический ресурс.

  • В панели Solution Explorer для папки Images вызовите контекстное меню и скопируйте в нее командой Add/Existing Item из прилагаемого каталога Source файл flower2.jpg
  • Выделите файл flower2.jpg и в панели Properties проверьте его настройки в оболочке. Они должны быть такими
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • В пространство имен файла Window1.xaml.cs добавьте новый класс Pictures со следующим процедурным кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    namespace ElementWithObject
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        }
        
        class Pictures
        {
            // Поле
            public static ImageBrush picture = new ImageBrush();
        
            static Pictures()
            {
                picture.ImageSource = new BitmapImage(
                    new Uri(@"Images\flower2.jpg", UriKind.Relative));
                picture.Stretch = Stretch.UniformToFill;
                picture.Opacity = 1.0D;
            }
        
            // Свойство
            public static ImageBrush Picture { get { return picture; } }
        }
    }
  • Добавьте в заголовок дескриптора Window запись импортирования пространства имен приложения для доступа к свойству класса Pictures из разметки
  • <Window x:Class="ElementWithObject.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="300" 
        Width="300"
        Title="Привязка элемента к объекту" 
        MinHeight="300" 
        MinWidth="300"
        WindowStartupLocation="CenterScreen"
            
        xmlns:local="clr-namespace:ElementWithObject"
        >   
        ..............................................................
            
    </Window>
  • Добавьте в контейнер TabControl новую вкладку с именем Page2 с привязкой логического ресурса к фону кнопки
  • <!-- Page2. Связывание целевого элемента с исходным
                            объектом через логический ресурс -->
                <TabItem Header="Page2">
                    <TabItem.Resources>
                        <TextBlock x:Key="title2"
                            FontSize="16"
                            TextAlignment="Center"
                            FontWeight="Bold">
                            <TextBlock.Foreground>
                                <SolidColorBrush Color="Yellow" />
                            </TextBlock.Foreground>
                            Связывание целевого элемента
                            <LineBreak />
                            с исходным объектом
                            <LineBreak />
                            через логический ресурс
                        </TextBlock>
                        <local:Pictures x:Key="flower2" />
                    </TabItem.Resources>
                    <Button 
                        Content="{StaticResource ResourceKey=title2}"
                        Background="{Binding Source={StaticResource flower2},
                                             Path=Picture, Mode=OneWay}" />
                </TabItem>
  • Запустите проект с вкладкой Page2 - область вывода будет такой
  • Использование свойства DataContext элементов для назначения источника привязки

    Иногда несколько приемников нужно привязать к одному и тому же источнику. Чтобы не дублировать в каждом выражении привязки имя источника и как-то автоматизировать эту задачу, в WPF очень часто используют свойство DataContext. Это свойство введено классом FrameworkElement и наследуется каждым элементом, производным от этого класса. Его использование является альтернативным способом обозначения объекта-источника, задействованного в привязке.

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

    Выполняя поиск по дереву, WPF пытается найти первый контекст данных DataContext, не равный null. Если контекст данных найден, он используется для привязки, если нет, то выражение привязки не применяет никакого значения к целевому элементу и он ничего отображать не будет.

    Предположим, что имеется несколько элементов управления TextBlock, явным образом связанных с одним и тем же источником привязки:

    <StackPanel>
            <TextBlock 
                Text="{Binding Source={StaticResource personView}, 
                               Path=FullName}" />
            <TextBlock 
                Text="{Binding Source={StaticResource personView}, 
                               Path=Title}" />
            <TextBlock 
                Text="{Binding Source={StaticResource personView}, 
                               Path=City}" />
        </StackPanel>

    А вот те же самые три элемента управления TextBlock, связанные с контекстом данных DataContext контейнера StackPanel:

    <StackPanel DataContext="{Binding Source={StaticResource personView}}">
            <TextBlock Text="{Binding Path=FullName}" />
            <TextBlock Text="{Binding Path=Title}" />
            <TextBlock Text="{Binding Path=City}" />
        </StackPanel>

    Если в очередном ближнем контейнере атрибут DataContext не определен, поиск будет продолжен в следующем внешнем контейнере, пока не будет найден ненулевой контекст данных DataContext или поиск закончится неудачей.

    Вкладка Page3. Использование свойства DataContext в целевом элементе

    В следующем примере свойство DataContext помещено в приемник привязки.

  • Добавьте в контейнер TabControl новую вкладку Page3 со следующей разметкой
  • <!-- Page3. Использование свойства DataContext -->
                <TabItem Header="Page3">
                    <StackPanel>
                        <!-- Источник привязки -->
                        <ScrollBar Name="scroll3"
                            Orientation="Horizontal" Margin="24"
                            Maximum="100" LargeChange="10" SmallChange="1" 
                            Value="50"
                        />
        
                        <!-- Приемник привязки -->
                        <Label HorizontalAlignment="Center"
                            ContentStringFormat="F0"
                            DataContext="{Binding ElementName=scroll3}"
                            Content="{Binding Path=Value}"
                        />
                    </StackPanel>
                </TabItem>

    В элементе ScrollBar атрибут Maximum="100" означает верхнюю границу диапазона значений, LargeChange="10" - шаг изменения положения бегунка по щелчкам на областях справа и слева от него, SmallChange="1" - шаг изменения значения по щелчкам на концевых элементах линейки. В приведенном примере свойство DataContext никаких преимуществ не дает, а просто делит выражение привязки на две части.

  • Запустите приложение - вкладка Page3 обеспечит следующую область вывода
  • Вкладка Page4. Использование свойства DataContext в родительском элементе

  • Добавьте в контейнер TabControl новую вкладку Page4
  • Переместите назначение источника привязки свойства DataContext из приемника в родительский элемент <StackPanel> следующим образом
  • <!-- Page4. Использование свойства DataContext -->
                <TabItem Header="Page4">
                    <StackPanel DataContext="{Binding ElementName=scroll4}">
                        <!-- Источник привязки -->
                        <ScrollBar Name="scroll4"
                            Orientation="Horizontal" Margin="24"
                            Maximum="100" LargeChange="10" SmallChange="1"
                            Value="50"
                        />
        
                        <!-- Приемник привязки -->
                        <Label HorizontalAlignment="Center"
                            ContentStringFormat="F0"
                            Content="{Binding Path=Value}"
                        />
                    </StackPanel>
                </TabItem>

    Элемент <StackPanel> тоже наследует классу FrameworkElement и содержит свойство DataContext.

  • Испытайте работу вкладки Page4 - она ничем не будет отличаться от вкладки Page3
  • Вкладка Page5. Привязка нескольких приемников к одному источнику без свойства DataContext

    Рассмотрим пример, когда отказ от использования свойства DataContext влечет за собой относительно громоздкий код разметки.

  • Добавьте в контейнер TabControl вкладку Page5 со следующей разметкой
  • <!-- Page5. Разметка без свойства DataContext --> 
                 <TabItem Header="Page5"> 
                     <StackPanel> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=Source}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=LineSpacing}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=FamilyTypefaces[0].Style}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=FamilyTypefaces[0].Weight}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=FamilyNames.Count}" 
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=FamilyMaps.Count}"
                        /> 
                         <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
                            Path=Baseline}"  
                        /> 
                     </StackPanel> 
                 </TabItem>
  • Запустите приложение - результат будет таким
  • Вкладка Page6. Привязка нескольких приемников к одному источнику с использованием свойства DataContext

  • Добавьте новую вкладку Page6 и перепишите разметку предудущего примера так
  • <!-- Page6. Разметка с использованием свойства DataContext -->
                <TabItem Header="Page6">
                    <StackPanel
                        DataContext="{Binding Source={x:Static SystemFonts.IconFontFamily}}"
                        >
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=Source}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=LineSpacing}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=FamilyTypefaces[0].Style}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=FamilyTypefaces[0].Weight}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=FamilyNames.Count}" 
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=FamilyMaps.Count}"
                        />
                        <TextBlock 
                            FontSize="18" FontWeight="Bold"
                            Text="{Binding Path=Baseline}"  
                        />
                    </StackPanel>
                </TabItem>

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

  • Запустите приложение - представление вкладки Page6 полностью совпадает с областью вывода вкладки Page5
  • Сделаем несколько замечаний:

  • 1.Если в родительском элементе определен источник через свойство DataContext, но одновременно в каком-то целевом элементе (или нескольких) в выражении привязки явно определен свой источник через свойство Source, то приемник использует свой источник и не будет далее искать контекст данных.
  • 2.В разметке свойству DataContext может быть присвоена ссылка на логический ресурс объекта
  • 3.Когда свойство DataContext задается в процедурном коде, то связанные элементы управления не будут отображать связанные данные до тех пор, пока этому свойству не будет присвоена ссылка на объект-источник.
  • Вкладка Page7. Присвоение свойству DataContext логической ссылки на ресурс

    Вернемся к примеру вкладки Page2 и немного изменим его.

  • В панели Solution Explorer для папки Images вызовите контекстное меню и скопируйте в нее командой Add/Existing Item из прилагаемого каталога Source файл wood.jpg
  • Выделите файл wood.jpg и в панели Properties проверьте его настройки в оболочке. Они должны быть такими
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • В файле Window1.xaml.cs в класс Pictures добавьте новый код
  • class Pictures
        {
            // Поле
            public static ImageBrush picture = new ImageBrush();
            public static ImageBrush picture1 = new ImageBrush();
        
            static Pictures()
            {
                picture.ImageSource = new BitmapImage(
                    new Uri(@"Images\flower2.jpg", UriKind.Relative));
                picture.Stretch = Stretch.UniformToFill;
                picture.Opacity = 1.0D;
        
                picture1.ImageSource = new BitmapImage(
                    new Uri(@"Images\wood.jpg", UriKind.Relative));
                picture1.Stretch = Stretch.Uniform;
                picture1.Opacity = 1.0D;
            }
        
            // Свойство
            public static ImageBrush Picture { get { return picture; } }
            public static ImageBrush Picture1 { get { return picture1; } }
        }
  • В коллекцию ресурсов разметки окна Window1 добавьте новый ресурс
  • <Window.Resources> 
            <SolidColorBrush x:Key="ControlColorBrush" 
                Color="{x:Static SystemColors.ControlColor}" /> 
            <local:Pictures x:Key="wood" /> 
        </Window.Resources>

    Вспомним, что при разработке вкладки Page2 мы уже добавили в заголовок дескриптора Window запись

    xmlns:local="clr-namespace:ElementWithObject"

    импортирования пространства имен приложения для доступа к свойству класса Pictures из разметки и теперь просто используем готовый псевдоним local.

  • В контейнер TabControl добавьте новую вкладку Page7 со следующей разметкой

  • <!-- Page7. Присвоение свойству DataContext 
                            логической ссылки на ресурс -->
                <TabItem Header="Page7"
                    DataContext=" {StaticResource ResourceKey=wood}">
                    <Button Background="{Binding Path=Picture1, Mode=OneWay}" />
                </TabItem>

    Синтаксический анализатор ищет ресурс вначале в элементе его использования со свойством DataContext, а затем вверх по логическому дереву, поэтому мы расположили определение ресурса в словаре окна. То же самое присходит и с DataContext. Когда в выражении привязки атрибут Source пропущен, то WPF ищет определение источника в атрибуте DataContext вначале в элементе с выражением привязки и далее в родительских элементах, поэтому мы расположили DataContext в элементе <TabItem>.

  • Запустите приложение - результат вкладки Page7 выглядет так

  • Вкладка Page8. Управление свойством DataContext в процедурном коде

    Обычно DataContext задается в коде. В частности, эта методика применяется к объектам ADO.NET, которые необходимо инициировать в коде и нельзя заполнять декларативно. Пока что без объектов ADO.NET рассмотрим этот способ. Естественно, что элемент, в котором будет задаваться свойство DataContext, должен иметь имя для возможности обращения из процедурного кода.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page8

  • <!-- Page8. Управление свойством DataContext в процедурном коде -->
                <TabItem Header="Page8">
                    <StackPanel Name="stackPanel8">
                        <TextBlock FontSize="16" TextDecorations="Underline"
                            TextAlignment="Center" Margin="0,5,0,15" Foreground="Red"
                            Text="Управление DataContext в коде" 
                            />
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <Label Width="40"
                                Content="{Binding Path=Value, Mode=OneWay}" 
                                />
                            <Slider Name="slider8" Width="230" 
                                Minimum="11" Maximum="54"
                                TickFrequency="2" TickPlacement="BottomRight"
                                IsSnapToTickEnabled="True"
                                />
                        </StackPanel>
                        <Button 
                            Margin="5"
                            Content="Подключить источник в DataContext"
                            Click="button8_Click">
                        </Button>
                        <TextBlock Margin="0,10" 
                            Foreground="Blue"
                            FontSize="{Binding Path=Value, Mode=OneWay}"
                            Text="Simple Text"
                            />
                    </StackPanel>
                </TabItem>
  • Расширьте класс Window1 следующим процедурным кодом

  • public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }
        
            bool bindingFlag = false;
            private void button8_Click(object sender, RoutedEventArgs e)
            {
                Button button = (Button)sender;
                if (bindingFlag == false)
                {
                    stackPanel8.DataContext = slider8;
                    button.Content = "Отключить источник в DataContext";
                }
                else
                {
                    stackPanel8.DataContext = null;
                    button.Content = "Подключить источник в DataContext";
                }
        
                bindingFlag = !bindingFlag;
            }
        }
  • Запустите приложение и испытайте работу вкладки Page8, внешний вид которой будет таким

  • Вкладка Page9. Создание и привязка пользовательского объекта

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

  • Добавьте в пространство имен ElementWithObject текущего проекта в файле Window1.xaml.cs новый класс SimpleElement

  • // Простой пользовательский элемент управления
        class SimpleElement : FrameworkElement
        {
            // Статическое поле зависимости, базовое для свойства
            public static DependencyProperty NumberProperty;
        
            // Инициализация поля в статическом конструкторе
            static SimpleElement()
            {
                NumberProperty = DependencyProperty.Register(
                    "Number",
                    typeof(double),
                    typeof(SimpleElement),
                    new FrameworkPropertyMetadata(0.0,
                        FrameworkPropertyMetadataOptions.AffectsRender));
            }
        
            // Свойство зависимости для доступа к полю
            public double Number
            {
                get { return (double)this.GetValue(NumberProperty); }
                set { this.SetValue(NumberProperty, value); }
            }
        
            // Жесткое кодирование размера области вывода
            protected override Size MeasureOverride(Size availableSize)
            {
                return new Size(200, 20);
            } 
        
            // Вывод значения свойства Number
            protected override void OnRender(DrawingContext drawingContext)
            {
                drawingContext.DrawRectangle(Brushes.Yellow, 
                    new Pen(Brushes.Red, 1.0), 
                    new Rect(new Size(200, 20)));
                drawingContext.DrawText(
                    new FormattedText(
                        Convert.ToInt32(Number).ToString(),
                        System.Globalization.CultureInfo.CurrentCulture,
                        FlowDirection.LeftToRight,
                        new Typeface("Arial"),
                        16.0D,
                        SystemColors.WindowTextBrush),
                    new Point(0, 0));
            }
        }

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

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page9, которую заполните так

  • <!-- Привязка пользовательского элемента управления -->
                <TabItem Header="Page9">
                    <StackPanel>
                        <ScrollBar Orientation="Horizontal"
                            Margin="24"
                            Maximum="100"
                            LargeChange="10"
                            SmallChange="1"
                            Value="{Binding ElementName=simple9, Path=Number,
                                            Mode=OneWayToSource}" />
                       <local:SimpleElement x:Name="simple9"
                            HorizontalAlignment="Center" />
                       <ScrollBar Name="scroll9"
                            Orientation="Horizontal"
                            Margin="24"
                            Maximum="100"
                            LargeChange="10"
                            SmallChange="1"
                            Value="{Binding ElementName=simple9, Path=Number,
                                            Mode=TwoWay}" />
                       <local:SimpleElement
                            HorizontalAlignment="Center"
                            Number="{Binding ElementName=scroll9, Path=Value,
                                             Mode=OneWay}" />
                    </StackPanel>
                </TabItem>

    Здесь мы тоже просто используем ранее определенный псевдоним local импортирования пространства имен приложения для доступа к классу SimpleElement из разметки.

    Разметка вводит в контейнер <StackPanel> две пары элементов: "библиотечный элемент прокрутки" - "пользовательский элемент". В секции определения первого пользовательского элемента вводится его имя в пространство имен XAML: x:Name="simple9", которое в дальнейшем используется в выражениях привязки другими элементами как имя объекта-источника. Применение обычного атрибута Name="simple9" будет считаться ошибкой.

    Первый объект ScrollBar определяет одностороннюю привязку типа OneWayToSource со свойством Number пользовательского элемента и успешно обновляет это свойство при перемещении бегунка. Второй элемент ScrollBar определяет двухстороннюю привязку к свойству Number и успешно обновляет его в представлении двух пользовательских элементов, а также обновляется сам при изменении этого свойства. Однако перемещение второго бегунка хоть и изменяет свойство Number пользовательского элемента, но не влияет на состояние первого бегунка, поскольку определенная в первом ScrollBar привязка OneWayToSource обеспечивает передачу информации только в одну сторону.

  • Запустите приложение и убедитесь в сказанном (слава Петцольду!)

  • Привязка к источнику с помощью свойства RelativeSource

    Класс System.Windows.Data. RelativeSource в выражении привязки целевого элемента указывает источник, которым является родительский объект, находящийся на расстоянии неопределенного (или определенного) числа шагов вверх по дереву элементов в направлении к корню. Этот объект также можно использовать и для привязки элемента к самому себе.

    ип такой привязки задается свойством Mode из перечисления RelativeSourceMode, возможные значения которого следующие (Ancestor - предок)

    Возможные значения перечисления RelativeSourceMode
    Значение Описание
    FindAncestor Ссылается на источник с данными в визуальном дереве, который находится на неопределенное число шагов выше целевого элемента, содержащего выражение привязки. При использовании этого режима необходимо задавать дополнительные уточнения в свойствах класса RelativeSource, таких как AncestorType и AncestorLevel. Свойство AncestorType указывает тип источника привязки. Свойство AncestorLevel определяет, с какого вышестоящего уровня от приемника начинать поиск источника. Значение AncestorLevel=1 указывает ближайший к приемнику уровень начального поиска исходного объекта привязки
    PreviousData Когда ссылки на источники данных перечислены в списке, этот режим позволяет сменить текущий источник на предыдущий в списке
    Self Действует внутри элемента, содержащего выражение привязки, и позволяет сменить одно привязанное свойство на другое в том же элементе, т.е. привязать объект к самому себе
    TemplatedParent Если целевой элемент с выражением привязки находится внутри шаблона, такой режим привязывает его к данным шаблона

    На первый взгляд привязка со свойством RelativeSource кажется излишним усложнением. Ведь к источнику можно привязаться обычными средствами с помощью ElementName или Source. Однако такое возможно не всегда, в частности, при использовании шаблонов может выручить только привязка со свойством RelativeSource. Механизм шаблонов в этой теме не рассматривается, а вот применение данной привязки в традиционных задачах мы продемонстрируем на нескольких примерах.

    Исполнение привязки со свойством RelativeSource, как и обычной, тоже возможно в двух синтаксисах: по синтаксису элементов свойств и по синтаксису расширения разметки.

    Вкладка Page10. Привязка RelativeSource к свойствам родительского элемента

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page10, которую заполните так

  • <!-- Относительная привязка RelativeSource к источнику -->
                <TabItem Header="Page10">
                    <StackPanel>
                        <Grid Tag="Привязка к Grid по синтаксису элементов свойств">
                            <Button Margin="5" FontSize="10">
                               <Button.Content>
                                    <Binding Path="Tag" Mode="OneWay">
                                       <Binding.RelativeSource>
                                           <RelativeSource Mode="FindAncestor" 
                                                            AncestorType="Grid"
                                                            AncestorLevel="1" />
                                        </Binding.RelativeSource>
                                    </Binding>
                                </Button.Content>
                            </Button>
                        </Grid>
                        <Grid Tag="Привязка к Grid по синтаксису расширения разметки">
                            <Button Margin="5" FontSize="10"
                                Content="{Binding Path=Tag,
                                                  RelativeSource={RelativeSource 
                                                      Mode=FindAncestor,
                                                      AncestorType=Grid, 
                                                      AncestorLevel=1},
                                                  Mode=OneWay}" />
                        </Grid>
                        <!-- Привязка к свойству заголовка окна 
                             по синтаксису элементов свойств -->
                        <TextBlock TextAlignment="Center">
                            <TextBlock.Text>
                               <Binding Path="Title" Mode="OneWay">
                                    <Binding.RelativeSource>
                                        <RelativeSource Mode="FindAncestor"
                                                        AncestorType="Window" />
                                    </Binding.RelativeSource>
                                </Binding>
                            </TextBlock.Text>
                        </TextBlock>
                        <Label HorizontalAlignment="Center">
                            <Label.Content>
                                <Binding Path="Title" Mode="OneWay">
                                   <Binding.RelativeSource>
                                        <RelativeSource Mode="FindAncestor"
                                                        AncestorType="{x:Type Window}">
                                        </RelativeSource>
                                   </Binding.RelativeSource>
                                </Binding>
                            </Label.Content>
                        </Label>
                       <!-- Привязка к свойству заголовка окна 
                             по синтаксису расширения разметки -->
                        <TextBlock TextAlignment="Center"
                                   Text="{Binding Path=Title, 
                                                  RelativeSource=
                                                      {RelativeSource FindAncestor, 
                                                          AncestorType={x:Type Window}},
                                                  Mode=OneWay}" />
                        <Label HorizontalAlignment="Center"
                               Content="{Binding Path=Title, 
                                                 RelativeSource= 
                                                      {RelativeSource Mode=FindAncestor, 
                                                          AncestorType=Window},
                                                 Mode=OneWay}" />
                    </StackPanel>
                <TabItem>

    Свойство Content первых двух кнопок мы привязали к свойству Tag родительского элемента Grid. Остальные 4 элемента привязаны к свойству Title окна. Свойство AncestorLevel по умолчанию равно 1, поэтому там, где не требуется явно задать иное значение, его можно опустить. В синтаксисе расширения разметки атрибут Mode=FindAncestor можно писать без параметра Mode.

  • Запустите приложение - вкладка Page10 будет выглядеть так

  • Вкладка Page11. Привязка RelativeSource к вложенным свойствам

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

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page11, которую заполните так

  • <!-- Относительная привязка RelativeSource -->
                <TabItem Header="Page11" Selector.IsSelected="True">
                   <DockPanel LastChildFill="True">
                        <StackPanel DockPanel.Dock="Top">
                           <TextBlock FontSize="16"
                                TextAlignment="Center" Margin="0,5,0,5">
                                <TextBlock.Foreground>
                                    <SolidColorBrush Color="Purple" />
                                </TextBlock.Foreground>
                                Привязка с помощью RelativeSource
                           </TextBlock>
                            <ListBox Foreground="Red" SelectionMode="Multiple" Height="60">
                                <CheckBox Selector.IsSelected="False"
                                    IsChecked="{Binding RelativeSource={RelativeSource Mode=Self}, 
                                                        Path=(Selector.IsSelected), Mode=TwoWay}"
                                    Foreground="{Binding Path=Foreground,
                                                         RelativeSource=
                                                             {RelativeSource Mode=FindAncestor,
                                                                 AncestorType=ListBox},
                                                         Mode=OneWay}">
                                    1) Это еще флажок, элемент списка правее
                                </CheckBox>
                                <CheckBox ListBox.IsSelected="False" 
                                    IsChecked="{Binding RelativeSource={RelativeSource 
                                                            Mode=FindAncestor, 
                                                            AncestorLevel=1,
                                                            AncestorType={x:Type ListBoxItem}}, 
                                                        Path=IsSelected, Mode=TwoWay}"
                                    Foreground="{Binding Path=Foreground,
                                                        RelativeSource=
                                                            {RelativeSource Mode=FindAncestor,
                                                                AncestorType=ListBox},
                                                        Mode=OneWay}">
                                    2) Это еще флажок, элемент списка правее
                                </CheckBox>
                                <CheckBox 
                                    Foreground="{Binding Path=Foreground,
                                                         RelativeSource=
                                                             {RelativeSource FindAncestor,
                                                                 AncestorType=ListBox},
                                                         Mode=OneWay}">
                                    3) Это еще флажок, элемент списка правее
                                </CheckBox>
                                <ListBoxItem IsSelected="False">
                                    <CheckBox 
                                        IsChecked="{Binding RelativeSource={RelativeSource 
                                                                AncestorType={x:Type ListBoxItem}}, 
                                                            Path=IsSelected, Mode=TwoWay}"
                                        Foreground="{Binding Path=Foreground,
                                                             RelativeSource=
                                                                 {RelativeSource FindAncestor,
                                                                     AncestorType=ListBox},
                                                             Mode=OneWay}">
                                        4) Это еще флажок, элемент списка правее
                                    </CheckBox>
                                </ListBoxItem>
                            </ListBox>
                        </StackPanel>
                        <RichTextBox 
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                        </RichTextBox>
                    </DockPanel>
                </TabItem>

    Поскольку вкладка <TabItem> может содержать только один элемент, мы упаковали ее содержимое в компоновочную панель <DockPanel>. В качестве содержимого контейнера <DockPanel> определили два элемента: компоновочную панель <StackPanel> и контейнер <RichTextBox>, где чуть позже разместим краткую инструкцию пользователя (для тренировки). Настройка <StackPanel DockPanel.Dock="Top"> приклеивает эту панель к верхней части родителя <DockPanel>, а настройка <DockPanel LastChildFill="True"> заставляет элемент <RichTextBox> занять всю остальную оставшуся свободной часть.

    На первом месте в контейнере <StackPanel> размещен элемент <TextBlock> для вывода заголовка. Далее следует элемент списка <ListBox>, который в качестве своих дочерних объектов содержит элементы флажков <CheckBox>. Интересно то, что первые три элемента списка не указаны явно и только последний элемент указан.

    Если не создавать контейнер элемента ListBoxItem явно, то WPF сама создает его неявно и присваивает его свойству IsSelected значение Selector.IsSelected. Только один раз в момент создания свойство Selector.IsSelected считывается привязанными элементами. Но далее при изменении выбора элемента это свойство не обновляется. Чтобы показать, что Selector.IsSelected действительно не обновляется, свойство IsChecked первого флажка связывается с Selector.IsSelected первого элемента ListBoxItem в соответствии с режимом RelativeSource Mode=Self.

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

    Итак, когда контейнер элемента не создается явно, следует использовать вложенное свойство зависимостей Selector.IsSelected для начального выделения элемента в Selector. Объекты TabItem, ListBox и ComboBox являются контейнерами для списковых элементов управления и наследуют от Selector. Поэтому, когда их дочерние элементы заданы явно, то каждый имеет свое свойство IsSelected. К этому свойству мы и привязываемся во втором и четвертом элементах списка, что дает взаимную синхронизацию выделения флажка и элемента списка.

  • Запустите приложение - пока вкладка Page11 может выглядеть так

  • Замечание. Если из заголовка вкладки

    <TabItem Header="Page11" Selector.IsSelected="True">

    удалить атрибут Selector.IsSelected="True", то синхронная работа второго элемента нарушается (не знаю, почему...).

    Свойство Foreground каждого элемента списка, заданного явно или неявно, мы связали с одноименным свойством контейнера <ListBox>, в котором задали ему значение Red для обеспечения контраста надписи на выделенном элементе (и для тренировки). Эту привязку мы тоже выполнили по рассматриваемой технологии RelativeSource.

    Последним в компоновочном контейнере <DockPanel> мы забронировали элемент <RichTextBox>, который обеспечивает поддержку документов нефиксированного формата <FlowDocument>. Наша задача - создать краткую инструкцию 'чего-то... чего-то', разместить ее во внешнем файле как элемент <FlowDocument> и загрузить в контейнер <RichTextBox> во время выполнения. К теме привязки этот кусочек задачи отношения не имеет, но для тренировки, чтобы заполнить оставшееся на вкладке (и в голове) вакантное место, решим ее.

    Там шпионки с крепким телом,

    Ты их в дверь - они в окно!

    Говори, что с 'этим делом'

    Мы покончили давно!

    (В.С. Высоцкий)

  • Добавьте к текущему проекту командой Project/Add New Item новый XAML -файл с именем Instruct.xaml (не забудьте изменить расширение файла c xml на xaml )

  • Выделите в Solution Explorer новый файл Instruct.xaml и измените через панель Properties его свойства на

  • Build Action = None
  • Copy to Output Directory=Copy if newer
  • Удалите в файле Instruct.xaml сгенерированное мастером содержимое и заполните его следующей разметкой (чем бы то нибыло, абы-кабы, аты-баты и т.д.)

  • <FlowDocument FontSize="11" Background="White"
        >
        <List MarkerStyle="Decimal">
            <ListItem>
                <Paragraph>
                    Первый флажок двунаправленно привязан булевым свойством 
                    IsChecked к вложенному логическому свойству Selector.IsSelected, 
                    которое инициализируется один раз и больше не меняется. 
                    Поэтому флажок и элемент списка меняются независимо.
                    Режим поиска родителя определен значением Mode=Self 
                    - искать источник в том же элементе.
                </Paragraph>
            </ListItem>
            <ListItem>
                <Paragraph>
                    Второй флажок своим свойством IsChecked привязан двунаправленной 
                    привязкой к свойству IsSelected своего ближайшего контейнера,
                    которым является элемент списка ListBoxItem, 
                    поэтому и меняется синхронно с ним, 
                    обеспечивая нормальную логику работы списка.
                    Режим поиска родителя определен значением Mode=FindAncestor - 
                    искать источник в ближайшем родителе.
                </Paragraph>
            </ListItem>
            <ListItem>
                <Paragraph>
                    В третьем элементе списка флажок и элемент списка ListBoxItem 
                    никак не связаны, поэтому изменяются автономно как в первом элементе, 
                    где связь хоть и установлена, но источник Selector.IsSelected 
                    остается постоянным
                </Paragraph>
            </ListItem>
            <ListItem>
                <Paragraph>
                    Четвертый элемент списка задается явно и флажок своим свойством 
                    IsChecked привязан двунаправленной привязкой к свойству 
                    IsSelected этого контейнера. Поэтому флажок и элемент списка 
                    работают синхронно, как и в случае со вторым списком.
                </Paragraph>
            </ListItem>
        </List>
    </FlowDocument>
  • Запустите приложение - компилятор (синтаксический анализатор) начинает ругаться, что не понимает конструкцию файла и не может преобразовать ее в объектный код. Текстовый редактор оболочки тоже зашелся в истерике, потому что не видит библиотеку WPF

  • Добавьте в открывающий дескриптор <FlowDocument> подключение пространства имен WPF -библиотеки

  • <FlowDocument FontSize="11" Background="White"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        >
        .............................................
    </FlowDocument>;

    Откомпилируйте решение командой Build/Build Solution - вроде бы все успокоилось

    Если бы мы поместили содержимое файла Instruct.xaml сразу внутрь контейнера <RichTextBox>...</RichTextBox>, то в дескрипторе окна <Window> уже есть такое подключение и ничего специального предпринимать бы не пришлось. И все бы сразу заработало, можете так и попробовать. Но чтобы не захломлять разметку вкладки Page11, мы пошли окружным путем и решили вынести документ в отдельный файл Instruct.xaml. В наказание за инициативу нам еще нужно будет этот файл подключить к контейнеру <RichTextBox>. Сделать это можно в процедурном коде или в разметке - через словарь ресурсов. Применим поочередно оба способа.

    Подключение документа через процедурный код

  • Дополните в разметке вкладки Page11 контейнер <RichTextBox> именем и зарегистрируйте обработчик события Loaded так

  • <RichTextBox Name="documentReader" Loaded="documentReader_Loaded"
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                        </RichTextBox>
  • В разметке на записи регистрации обработчика события Loaded контейнера <RichTextBox> вызовите контекстное меню и командой Navigate to Event Handler создайте в файле Window1.xaml.cs заготовку этого обработчика, которую заполните следующим кодом

  • private void documentReader_Loaded(object sender, RoutedEventArgs e)
            {
                String fileName = "Instruct.xaml";
                FileStream xamlFile = new FileStream(fileName, FileMode.Open, FileAccess.Read);
                FlowDocument content = XamlReader.Load(xamlFile) as FlowDocument;
                documentReader.Document = content;
        
                xamlFile.Close();
            }
  • В начало файла Window1.xaml.cs добавьте подключение пространств имен для используемых в обработчике классов и перечислений

  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using System.Windows.Markup;// Для класса XamlReader
    using System.IO;            // Для класса FileStream, 
                                // перечислений FileMode и FileAccess
        
    namespace ElementWithObject
    {
        public partial class Window1 : Window
        {
            ....................................................
        }
    }
  • Запустите приложение с вкладкой Page11 - все работает как надо и снимок экрана может быть таким

  • Подключение документа через ресурс

    Теперь ту же задачу подключения документа к контейнеру <RichTextBox> быстренько повторим с использованием механизма ресурсов, чтобы вспомнить молодость. При этом нам не придется писать ни строчки процедурного кода, все выполним в разметке. Чтобы не вмешиваться в процедурный код, просто удалим регистрацию обработчика события Loaded из разметки элемента RichTextBox. Пусть обработчик существует как обычный незадействованный метод класса Window1 и никуда не встревает со своими услугами. А компилятор тоже будет молчать.

    Было
    <RichTextBox Name="documentReader" Loaded="documentReader_Loaded"
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                        </RichTextBox>
    Стало
    <RichTextBox Name="documentReader" 
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                        </RichTextBox>

    Имя удалять не будем, потому что на него ссылается код обработчика. Если хотим убрать и имя, тогда нужно очистить или вообще удалить обработчик documentReader_Loaded() из класса Window1 (кому как, а лично мне это делать жалко, код-то вымучен бессонными ночами и недопитыми литрами. Хотя с допитыми может быть получилось бы лучше).

  • В панели Solution Explorer выделите корневой узел текущего проекта и через меню оболочки командой Project/Add Resource Dictionary добавьте новый файл с именем по умолчанию Dictionary1.xaml

  • Выделите в Solution Explore новый файл Dictionary1.xaml и проверьте через панель Properties его свойства, чтобы они были такими

  • Build Action = Page
  • Copy to Output Directory=Do not copy
  • Скопируйте из файла Instruct.xaml в новый файл Dictionary1.xaml все содержимое с разметкой документа <FlowDocument> и обязательно добавьте в его открывающем дескрипторе ключ ресурса x:Key="instruct". При этом можете удалить, а можете и оставить, индивидуальное подключение пространств имен XAML, которое в дескрипторе ресурса уже присутствует (можете удалить любое из них или ничего не удалять)

  • Файл Dictionary1.xaml должен стать таким (текст неважен, важна разметка)

    <ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        >
        <FlowDocument x:Key="instruct" FontSize="11" Background="White"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            >
            <List MarkerStyle="Decimal">
                <ListItem>
                    <Paragraph>
                        Первый флажок двунаправленно привязан булевым свойством 
                        IsChecked к вложенному логическому свойству Selector.IsSelected, 
                        которое инициализируется один раз и больше не меняется. 
                        Поэтому флажок и элемент списка меняются независимо.
                        Режим поиска родителя определен значением Mode=Self - 
                        искать источник в том же элементе.
                    </Paragraph>
                </ListItem>
                <ListItem>
                    <Paragraph>
                        Второй флажок своим свойством IsChecked привязан двунаправленной 
                        привязкой к свойству IsSelected своего ближайшего контейнера,
                        которым является элемент списка ListBoxItem, 
                        поэтому и меняется синхронно с ним, 
                        обеспечивая нормальную логику работы списка.
                        Режим поиска родителя определен значением Mode=FindAncestor - 
                        искать источник в ближайшем родителе.
                    </Paragraph>
                </ListItem>
                <ListItem>
                    <Paragraph>
                        В третьем элементе списка флажок и элемент списка ListBoxItem 
                        никак не связаны, поэтому изменяются автономно как в первом элементе, 
                        где связь хоть и установлена, но источник Selector.IsSelected 
                        остается постоянным
                    </Paragraph>
                </ListItem>
                <ListItem>
                    <Paragraph>
                        Четвертый элемент списка задается явно и флажок своим свойством 
                        IsChecked привязан двунаправленной привязкой к свойству 
                        IsSelected этого контейнера. Поэтому флажок и элемент списка 
                        работают синхронно, как и в случае со вторым списком.
                    </Paragraph>
                </ListItem>
            </List>
        </FlowDocument>
    </ResourceDictionary>
  • Дополните разметку элемента RichTextBox во вкладке Page11 следующей конструкцией подключения ресурса по синтаксису элемента свойства

  • <RichTextBox Name="documentReader"
                                     Margin="0,5,0,0"
                                     ScrollViewer.VerticalScrollBarVisibility="Auto">
                            <RichTextBox.Resources>
                                <ResourceDictionary>
                                    <ResourceDictionary.MergedDictionaries>
                                        <ResourceDictionary Source="Dictionary1.xaml" />
                                    </ResourceDictionary.MergedDictionaries>
                                </ResourceDictionary>
                            </RichTextBox.Resources>
    	
                            <RichTextBox.Document>
                                <StaticResource ResourceKey="instruct" />                        
                            </RichTextBox.Document>
                       </RichTextBox>
  • Запустите приложение - вкладка Page11 должна выглядеть точно так, как и в предыдущем случае подключения документа через процедурный код

  • Обращает на себя внимание применение элементов ListBox (ListBoxItem и List (ListItem). Оба называются списками. Но первый из них относится к элементам управления, поскольку служит для отображения информации и обеспечивает взаимодействие с пользователем. А второй - просто к элементам (хотя это и более общая категория), поскольку незримо участвует только в верстке документа. Да ладно, в конце-концов, - это касается только терминологии. Главное - не как называть, а правильно понимать и применять.

    Вкладка Page12. Привязка RelativeSource в процедурном коде

    Возьмем за основу привязку RelativeSource в разметке, а потом перенесем ее в процедурный код.

  • Добавьте в контейнер TabControl файла Window1.xaml новую вкладку с именем Page12, которую заполните следующей начальной разметкой, использующей привязку RelativeSource

  • <!-- Привязка RelativeSource в коде --<
                <TabItem Header="Page12"<
                    <!-- Привязка Button.Content к StackPanel уровней 1 и 2 --<
                    <StackPanel Tag="Эта информация находится в StackPanel уровня 2"<
                        <StackPanel Tag="Эта информация находится в StackPanel уровня 1"<
                            <Button Margin="5"
                                Content="{Binding Path=Tag,
                                                  RelativeSource={RelativeSource 
                                                      Mode=FindAncestor,
                                                      AncestorType=StackPanel, 
                                                      AncestorLevel=1},
                                                  Mode=OneWay}" /<
                           <Button Margin="5"
                                Content="{Binding Path=Tag,
                                                  RelativeSource={RelativeSource 
                                                      Mode=FindAncestor,
                                                      AncestorType=StackPanel,
                                                      AncestorLevel=2},
                                                  Mode=OneWay}" /<
                        </StackPanel<
                        <!-- Привязка TextBlock.Text к Window.Title --<
                        <Label<
                            Следующая информация находится в Window:
                        </Label<
                        <TextBlock Margin="5,0"<
                            <TextBlock.Text<
                                <Binding Path="Title" Mode="OneWay"<
                                    <Binding.RelativeSource<
                                        <RelativeSource Mode="FindAncestor"
                                                        AncestorType="Window" /<
                                    </Binding.RelativeSource<
                                </Binding<
                            </TextBlock.Text<
                        </TextBlock<
                    </StackPanel<
                </TabItem<
  • Запустите приложение - вывод вкладки Page12 будет таким

  • Удалите привязку RelativeSource из элементов вкладки Page12 и присвойте привязываемым источникам имена для возможности адресации из процедурного кода

  • Окончательный вид вкладки Page12 будет таким

    <!-- Привязка RelativeSource в коде -->
                <TabItem Header="Page12" Selector.IsSelected="True">
                    <!-- Привязка Button.Content к StackPanel уровней 1 и 2 -->
                    <StackPanel Tag="Эта информация находится в StackPanel уровня 2">
                        <StackPanel Tag="Эта информация находится в StackPanel уровня 1">
                            <Button Margin="5" Name="button12level1" />
                            <Button Margin="5" Name="button12level2" />
                        </StackPanel>
                        <!-- Привязка TextBlock.Text к Window.Title -->
                        <Label>
                            Следующая информация находится в Window:
                        </Label>
                        <TextBlock Margin="5,0" Name="textBlock12" />
                    </StackPanel>
                </TabItem>
  • Добавьте в конструктор класса Window1 файла Window1.xaml.cs следующий процедурный код привязки RelativeSource

  • public Window1()
            {
                InitializeComponent();
        
                //********************************************************
                // Привязка RelativeSource для элементов вкладки Page12
                //********************************************************
                //
                // button12level1
                //
                Binding binding = new Binding();
                binding.Path = new PropertyPath("Tag");
                binding.Mode = BindingMode.OneWay;
                binding.RelativeSource = new RelativeSource(
                    RelativeSourceMode.FindAncestor, typeof(StackPanel), 1);
                BindingOperations.SetBinding(button12level1, Button.ContentProperty, binding);
                //
                // button12level2
                //
                binding = new Binding("Tag");
                binding.Mode = BindingMode.OneWay;
                binding.RelativeSource = new RelativeSource(
                    RelativeSourceMode.FindAncestor, typeof(StackPanel), 2);
                BindingOperations.SetBinding(button12level2, Button.ContentProperty, binding);
                //
                // textBlock12
                //
                binding = new Binding();
                binding.Path = new PropertyPath("Title");
                binding.Mode = BindingMode.OneWay;
                binding.RelativeSource = new RelativeSource(
                    RelativeSourceMode.FindAncestor, typeof(Window), 1);
                BindingOperations.SetBinding(textBlock12, TextBlock.TextProperty, binding);
            }
  • Запустите приложение - вывод вкладки Page12 остался в точности таким, как и с привязкой в разметк

  • Пока об основах привязки поговорили достаточно. Но это еще далеко не все. В следующих разделах мы рассмотрим вопросы более серьезного применения привязки: привязки WPF к объектам ADO.NET, привязки в шаблонах и т.д.

    Вернуться к учебному плану