Передача данных между страницами в WinRT

129

Очень часто страницам приходится обмениваться данными. Например, страницы часто совместно используют модель представления. Хорошим местом для хранения данных, передаваемых между страницами, является класс App. Не бойтесь добавлять методы и свойства в этот класс. Например, вы можете добавить открытое свойство с именем ViewModel, которое имеет открытый get-метод доступа с закрытым set-методом, чтобы это свойство могло инициализироваться в конструкторе App.

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

Проект DataPassingAndReturning содержит две простые страницы, демонстрирующие эти способы. Первая страница, как обычно, называется MainPage, а вторая называется DialogPage, потому что по своей функциональности она близка к диалоговому окну. Страница MainPage может переходить только к DialogPage, a DialogPage может только возвращаться к MainPage.

Из-за ограниченности навигации страницам не нужно сохранять свое состояние. Чтобы программа стала еще проще, она не сохраняет состояние навигации или состояние страниц в случае приостановки и не реализует команды перехода с клавиатуры или мыши. Несмотря на свою простоту, программа хорошо демонстрирует базовые приемы передачи данных. Файл XAML страницы DialogPage содержит три элемента управления RadioButton и кнопку Button с текстом «Закончить»:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <StackPanel>
            <TextBlock Text="Выберите цвет"
                       FontSize="60"
                       Margin="40"
                       HorizontalAlignment="Center" />

            <StackPanel Name="radios"
                        HorizontalAlignment="Center" 
                        Margin="40">
                <RadioButton Content="Красный" Margin="16">
                    <RadioButton.Tag>
                        <Color>Red</Color>
                    </RadioButton.Tag>
                </RadioButton>

                <RadioButton Content="Зеленый" Margin="16">
                    <RadioButton.Tag>
                        <Color>Green</Color>
                    </RadioButton.Tag>
                </RadioButton>

                <RadioButton Content="Синий" Margin="16">
                    <RadioButton.Tag>
                        <Color>Blue</Color>
                    </RadioButton.Tag>
                </RadioButton>
            </StackPanel>

            <Button Content="Закончить"
                    HorizontalAlignment="Center"
                    Margin="40"
                    Click="Return_BtnClick" />
        </StackPanel>
    </Grid>
</Page>

Обратите внимание: у каждого элемента RadioButton свойству Tag задано значение Color, соответствующее кнопке.

Файл фонового кода DialogPage отвечает за получение цвета, выбранного при помощи переключателей, и его возвращение MainPage. Интересно, что файл MainPage.xaml очень похож на DialogPage.xaml - только у панели Grid есть имя, средний элемент управления RadioButton установлен, а кнопка Button содержит текст «Загрузить цвет»:

<Page ...>

    <Grid Background="#FF1D1D1D" x:Name="layoutGrid">
        <StackPanel>
            <TextBlock Text="Выберите цвет"
                       FontSize="60"
                       Margin="40"
                       HorizontalAlignment="Center" />

            <StackPanel Name="radios"
                        HorizontalAlignment="Center" 
                        Margin="40">
                <RadioButton Content="Красный" Margin="16">
                    <RadioButton.Tag>
                        <Color>Red</Color>
                    </RadioButton.Tag>
                </RadioButton>

                <RadioButton Content="Зеленый" Margin="16" IsChecked="True">
                    <RadioButton.Tag>
                        <Color>Green</Color>
                    </RadioButton.Tag>
                </RadioButton>

                <RadioButton Content="Синий" Margin="16">
                    <RadioButton.Tag>
                        <Color>Blue</Color>
                    </RadioButton.Tag>
                </RadioButton>
            </StackPanel>

            <Button Content="Загрузить цвет"
                    HorizontalAlignment="Center"
                    Margin="40"
                    Click="Goto_BtnClick" />
        </StackPanel>
    </Grid>
</Page>

Элементы управления RadioButton из MainPage используются для выбора исходного состояния RadioButton в DialogPage; это означает, что страница MainPage должна передавать данные DialogPage.

Данные, передаваемые между MainPage и DialogPage, состоят из единственного значения Color, но в реальных приложениях их может быть намного больше. Чтобы отразить эту возможность в своем приложении, мы определим классы, предназначенные специально для передачи данных между страницами. Класс для передачи данных от MainPage к DialogPage выглядит так:

using Windows.UI;

namespace WinRTTestApp
{
    public class PassData
    {
        public Color InitializeColor { set; get; }
    }
}

В этом простом примере данные, возвращаемые от DialogPage к MainPage, выглядят практически так же:

using Windows.UI;

namespace WinRTTestApp
{
    public class ReturnData
    {
        public Color ReturnColor { set; get; }
    }
}

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

Будьте внимательны! Далее я буду часто переключаться между файлами фонового кода MainPage и DialogPage в соответствии с направлением логических переходов и передачи данных.

Передача данных от MainPage к DialogPage - простой случай. Когда пользователь щелкает на кнопке «Загрузить цвет» в MainPage, файл фонового кода создает объект типа PassData и просматривает коллекцию элементов управления RadioButton, чтобы определить, какой из них установлен. Полученное значение Color задается свойству InitializeColor объекта PassData, который становится вторым аргументом Navigate:

private void Goto_BtnClick(object sender, RoutedEventArgs e)
{
    // Создать объект PassData
    PassData passData = new PassData();

    // Задание свойства InitializeColor по состоянию элементов RadioButton
    foreach (UIElement child in radios.Children)
        if ((child as RadioButton).IsChecked.Value)
            passData.InitializeColor = (Color)(child as RadioButton).Tag;

    // Передача объекта Navigate
    this.Frame.Navigate(typeof(DialogPage), passData);
}

При вызове метода OnNavigatedTo класса DialogPage свойство Parameter аргументов события содержит объект, переданный во втором аргументе Navigate. Класс DialogPage использует его для инициализации своего набора элементов управления RadioButton:

protected override void OnNavigatedTo(NavigationEventArgs args)
{
    // Получение объекта, переданного во втором аргументе Navigate
    PassData passData = args.Parameter as PassData;

    // Использование его для инициализации элементы RadioButton
    foreach (UIElement child in radios.Children)
        if ((Color)(child as RadioButton).Tag == passData.InitializeColor)
            (child as RadioButton).IsChecked = true;

    base.OnNavigatedTo(args);
}

Было бы удобно, если бы у метода GoBack был дополнительный параметр, в котором можно было бы вернуть данные целевой странице. Но такого параметра нет. Механизма не существует, поэтому приходится применять другие средства.

Одна из возможностей: после того как DialogPage вызовет GoBack, вызывается переопределение OnNavigatedFrom класса DialogPage. Свойство Content аргумента событий содержит экземпляр MainPage, к которому будет осуществлен переход. Это означает, что MainPage может определить открытое свойство или метод, предназначенный специально для получения информации от DialogPage, и DialogPage сможет задать это свойство или вызвать этот метод в своем переопределении OnNavigatedFrom.

private void Return_BtnClick(object sender, RoutedEventArgs e)
{
    this.Frame.GoBack();
}

С архитектурной точки зрения такой прием выглядит сомнительно, потому что класс DialogPage должен располагать информацией о типах страниц, с которых осуществляется переход. В общем случае данное решение нельзя признать хорошим. Гораздо лучше определить в DialogPage событие Completed с типом данных, которые класс должен вернуть:

public sealed partial class DialogPage : Page
{
    public event EventHandler<ReturnData> Completed;

    // ...
}

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

protected override void OnNavigatedFrom(NavigationEventArgs args)
{
    if (args.SourcePageType.Equals(typeof(DialogPage)))
        (args.Content as DialogPage).Completed += OnDialogPageCompleted;

    base.OnNavigatedFrom(args);
}

MainPage знает о DialogPage, потому что переход осуществляется к DialogPage. Но приложение также может переходить и к другим страницам, поэтому оно проверяет свойство SourcePageType аргументов события, чтобы быть уверенным в том, что тип страницы для данного конкретного события OnNavigatedFrom был определен правильно.

В этой схеме классу DialogPage не нужно знать о MainPage; собственно, так и должно быть. Изоляция потребителя информации от поставщика информации - одна из главных целей применения событий в контексте объектно-ориентированного программирования.

DialogPage может инициировать событие Completed в обработчике Click кнопки Button, но я решил реализовать эту логику в OnNavigatedFrom:

protected override void OnNavigatedFrom(NavigationEventArgs args)
{
    if (Completed != null)
    {
        // Создание объекта ReturnData
        ReturnData returnData = new ReturnData();

        // Задание свойства ReturnColor по состоянию элементов управления RadioButton
        foreach (UIElement child in radios.Children)
            if ((child as RadioButton).IsChecked.Value)
                returnData.ReturnColor = (Color)(child as RadioButton).Tag;

        // Инициирование события Completed
        Completed(this, returnData);
    }

    base.OnNavigatedFrom(args);
}

Если для события Completed имеется обработчик, DialogPage создает экземпляр ReturnData и задает свойство ReturnColor по состоянию коллекции элементов управления RadioButton. В обработчике Completed класс MainPage использует данные, полученные от DialogPage, для задания свойства Background своего объекта Grid и проверки RadioButton:

private void OnDialogPageCompleted(object sender, ReturnData args)
{
    // Назначение фона по возвращенному цвету
    layoutGrid.Background = new SolidColorBrush(args.ReturnColor);

    // Задание состояния RadioButton для возврата цвета
    foreach (UIElement child in radios.Children)
        if ((Color)(child as RadioButton).Tag == args.ReturnColor)
            (child as RadioButton).IsChecked = true;

    (sender as DialogPage).Completed -= OnDialogPageCompleted;
}

В конце выполнения обработчик отсоединяет себя от отправителя. Но в представленном коде имеется недостаток: по умолчанию экземпляр MainPage, который назначает обработчик события Completed в DialogPage, не является экземпляром MainPage, к которому возвращается DialogPage! Для решения этой проблемы необходимо задать NavigationCacheMode другое значение вместо Disabled.

public sealed partial class MainPage : Page
{
    public MainPage()
    {
    this.InitializeComponent();
    this.NavigationCacheMode = NavigationCacheMode.Enabled;
    }

     // ...
}

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

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