Разработка с помощью кода в WinRT

195

Создавать экземпляры элементов или элементов управления в XAML не обязательно - вы также можете создать их полностью в программном коде. Собственно, многое из того, что делается в XAML, можно сделать в коде. Данная возможность особенно полезна для создания множества однотипных объектов, потому что в XAML не существует цикла for.

Давайте модифицируем проект, который мы создали ранее. Измените компоновку MainPage.xaml, в которой мы будем использовать только элемент Grid без TextBlock:

<Grid Name="layoutGrid" 
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
</Grid>

Задание атрибута Name позволяет обращаться к Grid из файла отделенного кода. Также можно использовать синтаксис x:Name:

<Grid x:Name="layoutGrid" 
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
</Grid>

В большинстве случаев между Name и x:Name нет никаких практических различий. Префикс «x» означает, что атрибут x:Name относится к XAML и может использоваться для идентификации любых объектов в файле XAML. Для атрибута Name устанавливаются более жесткие ограничения: Name определяется классом FrameworkElement, поэтому он может использоваться только с классами, производными от FrameworkElement. Для классов, не являющихся производными от FrameworkElement, необходимо использовать запись x:Name. Иногда для соблюдения единства стиля везде используют запись x:Name. Я предпочитаю использовать x:Name.

Какой бы синтаксис вы ни выбрали, Name или x:Name, имена выбираются по тем же правилам, что и имена переменных. В частности, они не могут содержать пробелов или начинаться с цифры. Все имена в конкретном файле XAML должны быть уникальными. В файл MainPage.xaml.cs следует включить две дополнительные директивы using:

using Windows.UI;
using Windows.UI.Text;

Первая директива нужна для доступа к классу Colors, а вторая к перечислению FontStyle. Вставлять эти директивы вручную не обязательно. Если вы используете класс Colors или перечисление FontStyle, Visual Studio обозначает красной волнистой линией идентификатор, который не удалось разрешить; в этот момент вы можете щелкнуть на нем правой кнопкой мыши и выбрать в контекстном меню команду Resolve. Новая директива using добавляется к другим директивам в алфавитном порядке (при условии, что существующие директивы using упорядочены по алфавиту). Когда работа с файлом кода будет завершена, щелкните правой кнопкой мыши в любом месте файла и выберите команду Organize Usings --> Remove Unused Usings для чистки списка. (Я сделал это с файлом MainPage.xaml.cs.)

Конструктор MainPage хорошо подходит для создания элементов TextBlock, назначения свойств и их добавления в Grid:

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();

        TextBlock txb = new TextBlock();
        
        txb.Text = "Привет, Windows 8!";
        txb.FontSize = 80;
        txb.FontStyle = FontStyle.Italic;
        txb.FontFamily = new FontFamily("Arial");
        txb.Foreground = new SolidColorBrush(Colors.LimeGreen);
        txb.HorizontalAlignment = HorizontalAlignment.Center;
        txb.VerticalAlignment = VerticalAlignment.Center;
        
        layoutGrid.Children.Add(txb);
    }
}

Обратите внимание: последняя строка кода ссылается на элемент Grid с именем layoutGrid в файле XAML так, словно это обычный объект - вероятно, хранящийся в виде поля. (И как вы вскоре увидите, это действительно обычный объект, и он хранится как поле!) Хотя из XAML это не очевидно, элемент Grid содержит свойство Children, унаследованное от Panel. Свойство Children относится к типу UIElementCollection - коллекции, реализующей интерфейсы IList<UIElement> и IEnumerable<UIElement>. Именно по этой причине Grid поддерживает множественные дочерние элементы.

Разметка XAML обычно получается более компактной, чем программный код, потому что парсер XAML незаметно создает дополнительные объекты и выполняет преобразования. Из кода видно, что для свойства FontFamily необходимо создать объект FontFamily, что свойство Foreground относится к типу Brush и для него необходим экземпляр типа, производного от Brush (например, SolidColorBrush). Класс Colors содержит 141 статическое свойство типа Color. Также возможно создать значение Color из байтов ARGB с использованием статического метода Color.FromArgb.

Каждое из свойств FontStyle, HorizontalAlignment и VerticalAlignment относится к перечисляемому типу, имя которого совпадает с именем свойства. Свойства Text и FontSize выделяются на общем фоне, поскольку их значения относятся к примитивным типам: строка и вещественное число двойной точности.

Чтобы сделать код чуть более компактным, можно воспользоваться стилем инициализации свойств, введенным в C# 3.0:

TextBlock txb = new TextBlock
{
    Text = "Привет, Windows 8!",
    FontSize = 80,
    FontStyle = FontStyle.Italic,
    FontFamily = new FontFamily("Arial"),
    Foreground = new SolidColorBrush(Colors.LimeGreen),
    HorizontalAlignment = HorizontalAlignment.Center,
    VerticalAlignment = VerticalAlignment.Center
};

Я довольно часто применяю этот стиль. (При этом я стараюсь обходиться без другой популярной возможности, появившейся в C# 3.0, - неявной типизации с ключевым словом var, потому что она скорее запутывает код, нежели делает его более понятным и служит для других целей.) Как бы то ни было, вы можете откомпилировать и запустить проект, и результат будет выглядеть так же, как и XAML-версия. Он выглядит так же, потому что по сути ничем не отличается от нее.

Запуск приложения с настройками в коде

Также можно создать элемент TextBlock и включить его в коллекцию Children элемента Grid в переопределении метода OnNavigatedTo(). А можно создать элемент TextBlock в конструкторе, сохранить его в поле и добавить в элемент Grid в OnNavigatedTo().

Обратите внимание: я разместил код после вызова InitializeComponent() в конструкторе MainPage. Экземпляр TextBlock может быть создан до создания InitializeComponent(), но его включение в Grid должно быть выполнено после вызова InitializeComponent(), потому что Grid до этого вызова не существует. Метод InitializeComponent() разбирает XAML во время выполнения, создает экземпляры всех объектов XAML и объединяет их в дерево. Разумеется, метод InitializeComponent() играет важную роль; вероятно, его отсутствие в документации кого-то озадачит.

Дело вот в чем: в процессе компиляции приложения Visual Studio создает промежуточные файлы. Вы можете найти эти файлы в проводнике Windows; перейдите в решение для проекта WinRTTestApp, затем в проект, и наконец, в каталог obj/Debug. В списке файлов находятся файлы MainPage.g.cs и MainPage.g.i.cs (буква «g» от слова «generated», то есть «сгенерированный»). В обоих файлах определяются классы MainPage, производные от Page, с ключевым словом partial. Таким образом, составной класс MainPage состоит из файла MainPage.xaml.cs, находящегося под вашим контролем, и двух сгенерированных файлов, которые трогать не стоит.

Хотя эти файлы нельзя редактировать, знать о них необходимо, потому что они могут открыться в Visual Studio при возникновении ошибки времени выполнения из-за файла XAML.

Из этих двух файлов больший интерес представляет MainPage.g.i.cs. В нем находится определение метода InitializeComponent(), вызывающее статический метод с именем Application.LoadComponent() для загрузки файла MainPage.xaml. Также обратите внимание на то, что определение этого частичного класса содержит закрытое поле с именем layoutGrid - именем, присвоенным Grid в файле XAML. Метод InitializeComponent() завершается присваиванием этому полю объекта Grid, созданного Application.LoadComponent().

Таким образом, поле layoutGrid доступно для класса MainPage, но его значение остается равным null до вызова InitializeComponent().

Итак, разбор XAML состоит из двух фаз. Во время компиляции из XAML извлекаются все имена элементов (среди прочего) и генерируются промежуточные файлы C# в каталоге obj. Сгенерированные файлы C# компилируются вместе с файлами C#, находящимися под вашим контролем. Во время выполнения файл XAML снова разбирается с созданием экземпляров всех элементов, объединением их в визуальное дерево и получением ссылок на них.

Где же находится стандартный метод Main(), который служит точкой входа в любую программу C#? В файле App.g.i.cs - одном из двух файлов, сгенерированных Visual Studio на базе App.xaml.

Как упоминалось ранее, у многих свойств, с которыми мы имели дело - FontFamily, FontSize, FontStyle, Foreground, Text, HorizontalAlignment и VerticalAlignment - существуют соответствующие статические свойства зависимостей с именами FontFamilyProperty, FontSizeProperty и т.д. Для развлечения можете заменить обычную команду вида:

txb.FontStyle = FontStyle.Italic;

альтернативной формой довольно странного вида:

txb.SetValue(TextBlock.FontStyleProperty, FontStyle.Italic);

Что здесь происходит? Мы вызываем метод с именем SetValue(), определенный классом DependencyObject и унаследованный TextBlock. Метод вызывается для объекта TextBlock, но ему передается статический объект FontStyleProperty типа DependencyProperty, определенный TextBlock, и нужное значение свойства. Реальных различий между двумя способами задания свойства FontStyle нет. Скорее всего, определение свойства FontStyle в TextBlock выглядит примерно так:

public FontStyle FontStyle 
{ 
    set {
        SetValue(TextBlock.FontStyleProperty, value);
    }
    
    get {
        return (FontStyle)GetValue(TextBlock.FontStyleProperty);
    }
}

Я говорю «скорее всего», потому что не видел исходного кода Windows Runtime. Вероятно, этот код написан на C++, а не на C#. Но если свойство FontStyle определяется так же, как другие свойства со свойствами зависимостей, методы доступа set и get просто вызывают SetValue() и GetValue() со свойством зависимости TextBlock.FontStyleProperty. Это в высшей степени стандартное решение - паттерн, к которому вы скоро привыкнете.

Ранее я показывал, как задавать Foreground и шрифтовые свойства в теге Page разметки XAML (вместо TextBlock), и как эти свойства наследуются TextBlock. Конечно, то же самое можно сделать и в программном коде:

public MainPage()
{
    this.InitializeComponent();

    this.FontSize = 80;
    this.FontStyle = FontStyle.Italic;
    this.FontFamily = new FontFamily("Arial");
    this.Foreground = new SolidColorBrush(Colors.LimeGreen);

    TextBlock txb = new TextBlock();
    txb.Text = "Привет, Windows 8!";
    txb.HorizontalAlignment = HorizontalAlignment.Center;
    txb.VerticalAlignment = VerticalAlignment.Center;

    layoutGrid.Children.Add(txb);
}

В C# не нужно использовать префикс this при обращении к свойствам и методам класса, но во время редактирования файлов в Visual Studio при вводе префикса this IntelliSense выдает список доступных методов, свойств и событий.

Изображения в программном коде

Ранее мы использовали для отображения графики элемент Image. В XAML свойству Source задается URI с местонахождением изображения. Если судить исключительно по файлу XAML, можно предположить, что свойство Source определяется со строковым типом или с типом Uri. Но все сложнее, свойство Source относится к типу ImageSource, в котором инкапсулируются данные изображения, за вывод которого отвечает элемент Image. Тип ImageSource сам ничего не определяет, вы не сможете создать экземпляр этого типа, но ImageSource является предком ряда важных классов, как видно из следующего фрагмента иерархии классов:

Object
    DependencyObject
        ImageSource
            BitmapSource
                BitmapImage
                    WriteableBitmap

Класс ImageSource определяется в пространстве имен Windows.UI.Xaml.Media, но его производные классы находятся в пространстве Windows.UI.Xaml.Media.Imaging. Класс BitmapSource тоже не поддерживает создание экземпляров, но он определяет открытые свойства PixelWidth и PixelHeight, а также метод SetSource(), который позволяет прочитать графические данные из файла или сетевого потока. Класс BitmapImage наследует эти члены, а также определяет свойство UriSource.

Класс BitmapImage может использоваться для вывода изображения из программного кода. Помимо определения свойства UriSource, BitmapImage также определяет конструктор, получающий объект Uri. Ниже показан пример использования этого класса для отображения картинки из кода:

public MainPage()
{
    this.InitializeComponent();

    Uri uri = new Uri("http://professorweb.ru/my/windows8/rt/level1/files/win8logo.png");
    BitmapImage bitmap = new BitmapImage(uri);
    Image image = new Image();
    image.Source = bitmap;
    image.Stretch = Stretch.Fill;

    layoutGrid.Children.Add(image);
}

Для обращения к Grid из программного кода не обязательно использовать имя layoutGrid. Grid задается свойству Content объекта Page, так что обращение к Grid:

layoutGrid.Children.Add(image);

можно заменить следующим:

Grid grid = this.Content as Grid;
grid.Children.Add(image);

Более того, в такой простой программе можно обойтись вообще без Grid. Достаточно задать Image непосредственно свойству Content объекта MainPage, фактически исключая Grid из визуального дерева:

this.Content = image;

Свойство Content, унаследованное MainPage от UserControl, относится к типу UIElement, поэтому оно может поддерживать только одного потомка. Как правило, потомком MainPage является элемент, производный от Panel и поддерживающий множественных потомков, но так как нам нужен всего один потомок, можно использовать свойство Content объекта MainPage напрямую.

Также можно объединить эти два решения, XAML и программное: создайте элемент Image в XAML, a BitmapImage - в коде, или создать экземпляры Image и BitmapImage в XAML, а затем задать свойство UriSource экземпляра BitmapImage в коде. Я использовал первый вариант в примере ниже, где файл XAML уже содержит элемент Image, но без ссылки на конкретное изображение:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Image x:Name="image" Stretch="Fill" />
</Grid>

В файле отделенного кода свойство Source элемента Image задается в одной строке:

public MainPage()
{
    this.InitializeComponent();
    image.Source = 
        new BitmapImage(new Uri("ms-appx:///Images/win8logo.png"));
}

Обратите внимание на специальный формат URL-адреса графического файла в коде. В XAML префикс "x" не обязателен.

Существуют ли общие правила, определяющие, когда следует использовать XAML, а когда - программный код? Таких правил нет. Я обычно использую XAML везде, где это возможно, кроме случаев, когда повторения начинают выглядеть абсурдно. Мое стандартное правило для выбора решения с кодом - «при трех или более, используем for», но я часто допускаю и больше повторений в XAML, прежде чем переносить решение в код. Многое зависит от того, насколько компактной и элегантной у вас получится разметка XAML и сколько усилий потребуется для внесения изменений.

Создание приложения без страниц

Информацию о том, как проходит запуск программ Windows Runtime, можно получить из переопределения OnLaunched() в стандартном файле App.xaml.cs. Вы увидите, что переопределение создает объект Frame, использует его для перехода к экземпляру MainPage (так создается экземпляр MainPage), после чего задает этому объекту Frame заранее созданный объект Window, полученный через статическое свойство Window.Current. Упрощенный код выглядит так:

Frame rootFrame = Window.Current.Content as Frame;
rootFrame.Navigate(typeof(MainPage));
Window.Current.Content = rootFrame;
Window.Current.Activate();

Приложению Windows 8 не нужен потомок Page или Frame - не нужны даже файлы XAML. В завершение этой статьи мы создадим новый проект с именем StrippedDown и для начала удалим файлы App.xaml, App.xaml.cs, MainPage.xaml и MainPage.xaml.cs. Да, удалите их все! Теперь в проекте не осталось ни файлов программного кода, ни файлов XAML. Остался лишь манифест приложения, информация сборки и несколько файлов PNG.

Щелкните правой кнопкой мыши на имени проекта, выберите команды Add --> New Item. Выберите новый файл класса или кода, присвойте ему имя App.cs. Файл должен выглядеть так:

using Windows.ApplicationModel.Activation;
using Windows.UI;
using Windows.UI.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Controls;


namespace StrippedDown
{
    public class App : Application
    {
        static void Main()
        {
            Application.Start(app => new App());
        }

        protected override void OnLaunched(LaunchActivatedEventArgs args)
        {
            TextBlock txb = new TextBlock
            {
                Text = "Привет, Windows 8!",
                FontSize = 80,
                FontStyle = FontStyle.Italic,
                FontFamily = new FontFamily("Arial"),
                Foreground = new SolidColorBrush(Colors.LimeGreen),
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center
            };

            Window.Current.Content = txb;
            Window.Current.Activate();
        }
    }
}

Вот и все, что необходимо (а если вам достаточно свойств TextBlock по умолчанию - и того меньше). Статический метод Main() - точка входа, которая создает новый объект App и запускает его, а переопределение метода OnLaunched() создает элемент TextBlock и назначает его содержимым окна приложения по умолчанию. Я не буду применять этот способ создания приложений Windows 8 позже, но он безусловно работает.

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