Классы отделенного кода XAML

86

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

<Window x:Class="WpfApplication1.MainWindow"

Префикс пространства имен "x" помещает атрибут Class в пространство имен XAML, что означает более общую часть языка XAML. Фактически атрибут Class сообщает анализатору XAML, чтобы он сгенерировал новый класс с указанным именем. Этот класс наследуется от класса, именованного элементом XML. Другими словами, этот пример создает новый класс по имени MainWindow, который наследуется от базового класса Window.

Класс MainWindow генерируется автоматически во время компиляции. И здесь начинается самое интересное. Вы можете предоставить часть класса MainWindow, которая будет объединена с автоматически сгенерированной частью этого класса. Указанная вами часть — блестящий контейнер для кода обработки событий.

Эта "магия" возможна благодаря средству C#, известному под названием частичные классы (partial class). Частичные классы позволяют разделить класс на две или более отдельных части во время разработки, которые соединяются вместе в скомпилированной сборке. Частичные классы могут применяться во многих сценариях управления кодом, но более всего удобны, когда код должен объединяться с файлом, сгенерированным визуальным конструктором.

Среда Visual Studio оказывает помощь, автоматически создавая частичный класс, куда можно поместить код обработки событий. Например, при создании приложения по имени WpfApplication1, содержащего окно по имени MainWindow. Visual Studio начнет с создания следующего базового каркаса класса:

using System;
using System.Collections.Generic;
using System.Linq;
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 WpfApplication1
{
    /// <summary>
    /// Логика взаимодействия для MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

Во время компиляции приложения код XAML, определяющий пользовательский интерфейс (такой как MainWindow.xaml), транслируется в объявление типа CLR, объединенного с логикой файла класса отделенного кода (подобного MainWindow.xaml.сs), формируя один общий модуль.

Метод InitializeComponent()

В данный момент класс MainWindow не содержит реальной функциональности. Однако он включает одну важную деталь — конструктор по умолчанию, который вызывает метод InitializeComponent(), когда создается экземпляр класса.

Метод InitializeComponent() играет ключевую роль в приложениях WPF. По этой причине никогда не следует удалять вызов InitializeComponent() из конструктора окна. В случае добавления к классу окна другого конструктора обязательно предусмотрите в нем вызов InitializeComponent().

Метод InitializeComponent() не видим в исходном коде, потому что генерируется автоматически при компиляции приложения. По существу все, что делает InitializeComponent() — это вызов метода LoadComponent() класса System.Windows.Application. Метод LoadComponent() извлекает код BAML (скомпилированный XAML) из сборки и использует его для построения пользовательского интерфейса.

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

Именование элементов

Есть еще одна деталь, которая должна приниматься во внимание. В классе отделенного кода часто требуется программно манипулировать элементами управления. Например, необходимо читать либо изменять свойства, а также присоединять или отсоединять обработчики событий на лету. Чтобы обеспечить такую возможность, элемент управления должен включать XAML-атрибут Name. В предыдущем примере элемент Grid не содержит атрибут Name, поэтому манипулировать им в отделенном коде не получится.

Ниже показано, как назначить имя элементу Grid:

<Grid x:Name="MyGrid">
    </Grid>

Можно внести это изменение в документ XAML вручную или выбрать элемент в визуальном конструкторе Visual Studio и установить свойство Name в окне Properties (Свойства).

В обоих случаях атрибут Name сообщит анализатору XAML о необходимости добавить поле следующего вида к автоматически сгенерированной части класса MainWindow:

private System.Windows.Controls.Grid MyGrid;

Теперь с этим элементом можно взаимодействовать в коде класса MainWindow указывая имя MyGrid:

MessageBox.Show("Размер сетки: " + MyGrid.ActualWidth + "x" + MyGrid.ActualHeight);

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

Показанное ранее свойство Name является частью языка XAML и используется для того, чтобы помочь в интеграции класса отделенного кода. Из-за того, что многие классы определяют собственное свойство Name, происходит некоторая путаница. (Примером может служить базовый класс FrameworkElement, от которого наследуются все элементы WPF.) Анализаторы XAML элегантно справляются с этой проблемой. Можно либо установить XAML-свойство Name (используя префикс х:), либо свойство Name, относящееся к действительному элементу (опустив префикс).

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

Это значит, что следующая разметка эквивалентна тому, что вы уже видели:

<Grid Name="MyGrid">
    </Grid>

Такой трюк работает только в том случае, если включающий свойство Name класс оснащен атрибутом RuntimeNameProperty. Атрибут RuntimeNameProperty указывает на то, какое свойство должно трактоваться в качестве имени экземпляра этого типа. (Очевидно, обычно таким свойством является Name.) Класс FrameworkElement содержит атрибут RuntimeNameProperty, так что никаких проблем нет.

В традиционном приложении Windows Forms каждый элемент управления имеет имя. В приложении WPF такого требования нет. Однако при создании окна перетаскиванием элементов на поверхность визуального конструктора Visual Studio каждому элементу назначается автоматически сгенерированное имя. Таково соглашение. Если вы не собираетесь взаимодействовать с элементом в коде, то можете удалить атрибут Name из кода разметки.

Управление объявлениями классов и переменных-членов

Многие из этих ключевых полей вы увидите в действии там, где они понадобятся. Давайте в качестве простого примера рассмотрим следующее определение XAML <Window>, в котором используются ключевые слова ClassModifier и FieldModifier, а также x:Name и x:Class:

<!-- Класс MainWindow объявлен как internal в файле *.g.cs -->
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid x:Name="MyGrid">
        <!-- Кнопка имеет модификатор доступа public -->
        <Button Name="Btn1" x:FieldModifier="public" Content="Cancel"></Button>
    </Grid>
</Window>

По умолчанию все определения сгенерированных типов C#/XAML в WPF являются внутренними (internal), а члены - общедоступными (public). Однако на основе показанного определения XAML результирующий автоматически сгенерированный файл содержит тип класса internal с public-членом Button:

    [System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
    internal partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {        
        
        #line 6 "..\..\..\MainWindow.xaml"
        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
        internal System.Windows.Controls.Grid MyGrid;
        
        #line default
        #line hidden
        
        /// <summary>
        /// Btn1 Name Field
        /// </summary>
        
        #line 8 "..\..\..\MainWindow.xaml"
        [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
        public System.Windows.Controls.Button Btn1;
Лучший чат для C# программистов