Многостраничные приложения в Silverlight

ОГЛАВЛЕНИЕ

Основной темой данной статьи будет создание приложения, которое позволит вам, переключаясь с одной страницы на другую, при этом передавать  также информацию с одной страницы на другую. Чтобы продемонстрировать пригодность данного приложения, мы создадим список слов на первой странице и передадим  его одной из двух других страниц: Search (страница поиска) или Count (страница подсчета).

Страница Search будет использовать список в качестве источника для поля типа AutoComplete, а страница Count будет использовать этот список для графика:

Рисунок 8-1. Первый обзор страницы

Для того, чтобы заполнить список, надо создать диалоговое окно открытия файла (Open File),

Рисунок 8-2. Диалоговое окно открытия файла (Open File)

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

Рисунок 8-3. Прогресс

Обратите внимание на то, что данная статья рассматривает способ осуществления соединения поля типа AutoCompleteBox и элемента Chart, которые принадлежат набору элементов управления Silverlight (Control Toolkit), но их описание выходит за рамки данной статьи. О них вам стоит узнать немного  больше, если вы с ними не знакомы.

Архитектура многостраничных приложений

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

Мы начнем с того, что каждая “страница” в приложении Silverlight на самом деле является пользовательским элементом управления (UserControl), который может иметь в себе другие элементы в качестве содержимого.   В этом вся краса Silverlight.


Создание первого проекта

Давайте начнем с создания нового приложения Silverlight в Visual Studio (позволив ему создать проект веб-приложения ASP.NET) и назовем его PageSwitchSimple.

Обратите внимание на то, что по умолчанию Visual Studio создает одну страницу для вас - Page.xaml, и эта страница является пользовательским элементом управления (это можно заметить в режиме просмотра Xaml данной страницы Page.xaml).

Ключевые файлы

Наша архитектура для переключения страниц требует наличия четырех файлов, которые должны быть добавлены к любому проекту, который хочет учавствовать в данном подходе:

  1. Пользовательский элемент управления, названный PageSwitcher.xaml и его файл с фоновым кодом PageSwitcher.xaml.cs
  2. Статический класс, названный Swticher
  3. Интерфейс: ISwitchable

В дополнение ко всему нам надо добавить четыре строки к App.xaml.cs, но это мы обсудим позже.

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

PageSwitcher.xaml

Вся работа будет выполнена в основном проекте (тот, что содержит Page.xaml и App.xaml).

Добавьте PageSwitcher.xaml точно так же, как вы добавили бы любой другой пользовательский элемент управления, но как только вы создадите ее, удалите табличную сетку, которую Visual Studio создает за вас и измените ширину и высоту UserControl на 800 x 600 (данные значения могут быть произвольными)

<UserControl x:Class="PageSwitchSimple.PageSwitcher"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="800" Height="600">
</UserControl>

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

Основой PageSwitcher.xaml.cs является перегруженный метод Navigate, который получает UserControl либо UserControl и объект.

using System;
using System.Windows.Controls;
 
namespace PageSwitchSimple
{
   public partial class PageSwitcher : UserControl
   {
      public PageSwitcher()
      {
         InitializeComponent();
      }
 
      // 1-ая перегрузка
      public void Navigate( UserControl nextPage )
      {
         this.Content = nextPage;
      }
 
       // 2-ая перегрузка
      public void Navigate( UserControl nextPage, object state )
      {
         this.Content = nextPage;
         ISwitchable s = nextPage as ISwitchable;
         if ( s != null )
         {
            s.UtilizeState( state );
         }
         else
         {
            throw new ArgumentException( "nextPage is not ISwitchable! "
              + nextPage.Name.ToString() );
         }
      }
   }
}

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

Проверка предположений

Далее мы создадим некоторый временный код. Для начала, закомментируйте вторую перегрузку метода.

Во-вторых, добавьте в конструктор следующую строку:

this.Content = new Page2();

Наконец откройте App.xaml.cs и найдите метод Application_Startup. Измените установку RootVisual с Page() в PageSwitcher() и запустите приложение. Вы должны увидеть Page2.

Запустив приложение в отладчике и установив точку остановки на методе Application_Startup вы можете увидеть, что на самом деле создается PageSwitcher и в его конструкторе его же содержимое заполняется новым экземпляром Page2.

Это демонстрирует то, что наши предположения реальны - когда PageSwitcher заполняет свое содержимое пользовательским элементом управления, то это выглядит так, как будто все, что отображается,  это этот самый пользовательский интерфейс.


Интерфейс ISwitchable

Отлично! Теперь вы можете удалить строку, которую вы добавили в конструктор PageSwitcher. До того, как вы раскомментируете вторую перегрузку метода Navigate, вам нужно создать интерфейс ISwitchable, потому что он используется в данной перегрузке,

public void Navigate( UserControl nextPage, object state )
 {
    this.Content = nextPage;
    ISwitchable s = nextPage as ISwitchable;

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

public interface ISwitchable
{
   void UtilizeState( object state );
}

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

Вы создаете интерфейс, указав Visual Studio, что вы хотите создать новый класс, названный ISwitchable.cs, и затем заменить класс, который он создает для вас, указанным кодом интерфейса.

Как только вы объявили ISwitchable, вы можете раскомментировать второй метод Navigate в SearchPage.xaml.cs.


Как страница получает экземпляр PageSwitcher

Для того, чтобы одна страница переходила к другой, она должна вызвать метод Navigate для PageSwitcher. Существует два подхода. Тот, который я буду использовать заключается в том, что страница (Page) будет запрашивать о своем родителе, который будет возвращать экземпляр PageSwitcher (поскольку страница на самом деле является содержимым PageSwitcher). Это не так просто и подразумевает, что страница знает больше об архитектуре, чем должна, и при этом повторяет запросы и код конвертирования на всех страницах.

Более элегантным решением будет использование посредника: статический класс, названный Switcher (переключатель), который будет инициализирован с экземпляром PageSwitcher , при этом каждой странице понадобится использовать одну строку для навигации. Это не только выглядит более элегантно, но и позволяет легче управлять и масштабировать, а также предоставляет хорошую инкапсуляцию.

Класс Switcher имеет две перегрузки своего статического метода Switch. Как вы уже наверно догадываетесь, один получает UserControl, а другой - UserControl и объект. Без проверок на исключительные ситуации это выглядит следующим образом:

public static class Switcher
 {
    public static PageSwitcher pageSwitcher;
 
    public static void Switch( UserControl newPage )
    {
         pageSwitcher.Navigate( newPage );
    }
 
    public static void Switch( UserControl newPage, object state )
    {
       pageSwitcher.Navigate( newPage, state );
    }
 }

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

Switcher.Switch ( new Search(), words );

Все механизмы манипуляции содержимым пользовательского элемента управления PageSwitcher, а также передачи объекта, являются должным образом сокрыты от вызывающего класса, и поскольку Switch является статическим методом, то нет необходимости в создании экземпляра Switcher, что очень удобно.


Контрольная точка синхронизации

На данном этапе PageSwitchSimple должен иметь в себе два проекта, один из которых  назван PageSwitchSimple, а второй - PageSwitchSimple.web. PageSwitchSimple должен выглядеть следующим образом:

Рисунок 8-4. PageSwitchSimple

До того, как мы сможем продолжить, нам нужно обновить App.xaml.cs. Замените содержимое Application_Startup следующими строками,

PageSwitcher pageSwitcher = new PageSwitcher();
this.RootVisual = pageSwitcher;
Switcher.pageSwitcher = pageSwitcher;
Switcher.Switch( new Page() );

Это создаст экземпляр PageSwitcher и установит  значение для RootVisual (которое может быть установлено только единажды при запуске и не может быть сброшено во время работы), а также устанавливает статическое свойство pageSwitcher в то же самое значение. Наконец, четвертая строка вызывает статический метод Switch, передавая новый экземпляр страницы (Page) и заставляя механизм переключения отображать первую страницу.

Давайте создадим очень простые страницы Page и Page2 для того, чтобы протестировать созданный механизм.

В Page добавьте следующий код Xaml в табличную сетку,

<TextBlock Text="Your Name: " FontSize="18" />
<TextBox x:Name="Name" FontSize="18" Width="150" Height="35"
VerticalAlignment="Top" Margin="5"/>
<Button x:Name="ChangePage" Content="Change" FontSize="18"
Width="100" Height="50" />

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

В коде Page.xaml.cs вам всего лишь необходимо зарегистрировать и затем реализовать обработчик события кнопки, где вы получите текст из текстового поля и передадите его новому экземпляру Page2. Вы перейдете к этому новому экземпляру Page2 при помощи статического метода Switch класса Switcher,

public Page()
{
  InitializeComponent();
  ChangePage.Click += new RoutedEventHandler( ChangePage_Click );
}
 
void ChangePage_Click( object sender, RoutedEventArgs e )
{
  Switcher.Switch( new Page2(), Name.Text );
}

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

<TextBlock x:Name="Message" Text="Page2" FontSize="18" />
<Button x:Name="ChangePage" Content="Change" FontSize="18"
Width="100" Height="50" />

Для того, чтобы различать две страницы, вы должны модифицировать Grid в Page2, к примеру сделать фоновый цвет типа Bisque,

<Grid x:Name="LayoutRoot" Background="Bisque">

Код Page2 должен реализовывать интерфейс ISwitchable, который требует два шага: объявление того, что интерфейс поддерживается,

public partial class Page2 : UserControl, ISwitchable

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

public void UtilizeState( object state )
{
  Message.Text = state.ToString();
}

Все что осталось сделать в данном файле, так объявить обработчик события для кнопки,

public Page2()
{
   InitializeComponent();
   ChangePage.Click += new RoutedEventHandler( ChangePage_Click );
}

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

void ChangePage_Click( object sender, RoutedEventArgs e )
{
  Switcher.Switch( new Page() );
}

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

Рисунок 8-5. Второй тест по переключению страниц

Обзор деталей

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


Запуск приложения

При запуске приложение автоматически вызывает Application_Startup. Как уже обсуждалось ранее, создается новый экземпляр PageSwitcher, и он упрятан в Switcher (так же, как RootVisual). Cоздается новый экземпляр Page и вызывается Switch, при этом статическому методу передается элемент управления.

Метод Switch перегружается, и Application_Startup вызывает  первую версию метода, который получает только один аргумент. Он ищет и находит только что созданный pageSwitcher и вызывает Navigate, передавая при этом только что полученный UserControl.

Теперь  хорошо было бы пересмотреть Switcher и добавить проверку  ошибок, которую мы опустили ранее. Здесь я продемонстрирую вам полную версию Switcher.cs,

using System;
using System.Windows.Controls;
 
namespace PageSwitchSimple
{
   public static class Switcher
   {
      public static PageSwitcher pageSwitcher;
 
      public static void Switch( UserControl newPage )
      {
 
         if ( pageSwitcher != null )
         {
            pageSwitcher.Navigate( newPage );
         }
         else
         {
 
            throw new Exception( "Switcher.pageSwitcher is null" );
         }
      }
 
      public static void Switch( UserControl newPage, object state )
      {
         if ( pageSwitcher != null )
         {
            pageSwitcher.Navigate( newPage, state );
         }
         else
         {
           throw new Exception( "Switcher.pageSwitcher is null" );
         }
      }
   }
}

Элемент управления переключается к перегруженному методу Navigate экземпляра PageSwitcher, где устанавливается содержимое,

public void Navigate( UserControl nextPage )
{
   this.Content = nextPage;
}

На данном этапе на экране отображается первая страница и система находится в покое.


Нажатие на кнопку изменения (Change)

При нажатии на кнопку вызывается обработчик события,

Switcher.Switch( new Page2(), Name.Text );

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

Обратите внимание на то, что создается новый экземпляр Page2(). Вы заметите, что это заменяет содержимое объекта PageSwitcher (Page), и поэтому старое содержимое будет удалено. Мы очень рассчетливы, когда дело касается пользовательской памяти.

Switch предстоит выполнить некоторую работу. Он устанвливает свое содержимое, но затем ему необходимо вызвать UtilizeState для новой страницы, передавая состояние, которое он получил. К сожалению, он не может вызвать данный метод для UserControl, потому он должен осуществлять преобразование, тестируя для того, чтобы обеспечить то, что UserControl на самом деле реализует ISwitchable.

Оператор as возвратит экземпляр класса, который реализует интерфейс, если данный класс на самом деле реализует интерфейс, либо же он возвратит null в противном случае.

ISwitchable s = nextPage as ISwitchable;
if ( s != null )
{
  s.UtilizeState( state );
}

На объект ссылаются как на экземпляр типа ISwitchable, но конечно же, это всего лишь условное обозначение для выражения “s является экземпляром класса, который реализует ISwitchable”. В любом случае, s теперь может вызывать метод UtilizeState содержащийся в ISwitchable, что оно и делает, передавая полученный объект.

Обратите внимание на то, что нет возможности в C# создавать объект типа Interface. Поэтому вы можете написать

ISwitchable s = new ISwitchable();

Тут мы просто создаем ссылку, которая означает “класс, который реализует данный интерфейс” и указывает данную ссылку на объект, который уже существует .

Данный вызов UtilizeState статическим методом Switcher избавляет нас от необходимости осуществления явного запроса от UserControl к UserState, тем самым UserControl не должен даже знать о существовании PageSwitcher.

Также обратите внимание на то, что PageSwitcher - всего лишь курьер, и он никогда не исследует содержимое объекта, не знает какого он типа, и не хранит его; PageSwitcher просто принимает объект от вызывающей страницы и передает его в метод UtilizeState вызванной страницы.

UtilizeState в данном случае присваивает объект, посланный свойству Text элемента textBlock Messsage. Поскольку мы знаем о том, что объект состояния является строкой, то нам не нужно осуществлять данное присвоение, но нам все равно необходимо осуществить преобразование либо вызвать ToString,

public void UtilizeState( object state )
{
  Message.Text = state.ToString();
}

Очень важно соблюдать порядок операций, но он полностью автоматический. Page2 создается на вызове Switch, тем самым вызывается его конструктор, и его компоненты инициализируются до того, как PageSwitcher попытается вызвать метод UtilizeState (очень хорошая вещь, поскольку в противном случае метод не сработал бы), тем не менее, UtilizeState вызывается до того, как PageSwitcher отобразит новое содержимое, поэтому состояние было использовано вовремя для того, чтобы отобразить его пользователю.


Более практичное использование

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

Начнем с создания нового решения PageSwitching. Как только это будет открыто в Visual Studio, скопируйте файлы App.xaml.cs, ISwitchable.cs, PageSwitcher.xaml, PageSwitcher.xaml.cs и Switcher.cs из PageSwitch в новую директорию.

App.xaml.cs перепишет тот файл, что был создан Visual Studio (убедитесь в том, что вы настроили пространство имен файла), все остальные файлы будут новыми, и потому вам надо будет добавить их в ваш проект щелкнув правой кнопкой мыши по нему в Solution Explorer и выбрав пункт добавления нового элемента (Add Existing Item),

Рисунок 8-6. Добавление существующих элементов

Удалите Page.xaml и Page.xaml.cs и добавьте три новых UserControls: Find, Search и Count. Вот что у вас должно получиться после всего этого :

Рисунок 8-7. Проект PageSwitching

Давайте добавим внешний вид для страницы Find - первая страница, используемая для создания списка слов.

Вот Xaml, который я использовал для создания внешнего вида файла, но вы можете создать свой,

<UserControl x:Class="PageSwitching.Find"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="500" Height="600">
  <Grid
    x:Name="LayoutRoot"
    Background="White">
    <Grid.RowDefinitions>
      <RowDefinition
        Height="0.078*" />
      <RowDefinition Height="0.072*"/>
      <RowDefinition
        Height="0.85*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition
        Width="0.5*" />
      <ColumnDefinition
        Width="0.5*" />
    </Grid.ColumnDefinitions>
    <TextBlock
      x:Name="Message"
      Text="Ready..."
      TextWrapping="Wrap"
      FontFamily="Georgia"
      FontSize="18"
      VerticalAlignment="Bottom"
      HorizontalAlignment="Left"
      Visibility="Visible" Grid.Row="1" />
    <ScrollViewer
      x:Name="WordDisplayViewer"
      BorderBrush="Black"
      BorderThickness="1"
      Grid.Row="2"
      Grid.Column="0"
      Margin="5,10,0,2"
      Background="Bisque"
      VerticalScrollBarVisibility="Auto"
      HorizontalScrollBarVisibility="Hidden"
      VerticalAlignment="Stretch"
      Width="240"
      HorizontalAlignment="Left"
      Visibility="Visible">
      <TextBlock
        x:Name="WordDisplay"
        TextWrapping="Wrap"
        Text="WordDisplay"
        Width="160" />
    </ScrollViewer>
    <ScrollViewer
      x:Name="SortDisplayViewer"
      BorderBrush="Black"
      BorderThickness="1"
      Grid.Row="2"
      Grid.Column="1"
      Margin="0,10,8,0"
      Width="240"
      Background="Wheat"
      VerticalScrollBarVisibility="Auto"
      HorizontalScrollBarVisibility="Hidden"
      VerticalAlignment="Stretch"
      HorizontalAlignment="Right"
      Visibility="Visible">
      <TextBlock
        x:Name="SortDisplay"
        TextWrapping="Wrap"
        Width="160"
        Text="SortDisplay" />
    </ScrollViewer>
    <StackPanel
      Height="Auto"
      VerticalAlignment="Stretch"
      Grid.Column="1"
      Orientation="Horizontal" Grid.RowSpan="2">
      <Button
        x:Name="FilePicker"
        Content="Pick a file"
        Width="100"
        Background="#FF00FF00"
        FontFamily="Georgia"
        FontSize="18"
        Height="35" HorizontalAlignment="Center"
VerticalAlignment="Center" />
      <Grid Height="Auto" x:Name="ButtonGrid" Width="153">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="0.5*"/>
          <ColumnDefinition Width="0.5*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="0.5*"/>
          <RowDefinition Height="0.5*"/>
        </Grid.RowDefinitions>
        <Button
    x:Name="SearchPage"
    Content="Search"
    Background="#FFFF0000"
    FontFamily="Georgia"
    FontSize="18"
    Margin="5,0,5,0"
    Visibility="Visible"
          HorizontalAlignment="Center"
          VerticalAlignment="Center"
          Grid.ColumnSpan="1"
          Grid.RowSpan="2" />
        <Button
    x:Name="CountPage"
    Content="Count"
    Background="#FFFF0000"
    FontFamily="Georgia"
    FontSize="18"
    Visibility="Visible"
          VerticalAlignment="Center"
          Grid.Row="0"
          Grid.Column="1"
          HorizontalAlignment="Center"
          Grid.RowSpan="2" />
      </Grid>
    </StackPanel>
  </Grid>
</UserControl>

Вот как это выглядит

Рисунок 8-8. Поиск страницы

Find.xaml.cs должен реализовывать обработчики событий трех кнопок,

public Find()
{
  InitializeComponent();
  FilePicker.Click += new RoutedEventHandler( FilePicker_Click );
  SearchPage.Click += new RoutedEventHandler( ChangePage );
  CountPage.Click +=new RoutedEventHandler( ChangePage );

Обратите внимание на то, что кнопки SearchPage и CountPage имеют общий обработчик события потому, что их реализация очень схожа,

void ChangePage( object sender, RoutedEventArgs e )
{
  Button b = e.OriginalSource as Button;
  string btnName = b.Content.ToString().ToUpper();
  if ( btnName == "SEARCH" )
    Switcher.Switch( new SearchPage(), SortedWords );
  else
    Switcher.Switch( new CountPage(), SortedWords );
}

Логика здесь заключается в преобразовании свойства OriginalSource, принадлежащего Event Argument, в тип Button, и затем использовании для того, чтобы извлечь верхний регистр из содержимого кнопки. Мы затем сравниваем это со словом при поиске, и если они совпадут  - мы вызываем метод Switch для страницы SearchPage (либо для CountPage) и передаем SortedWords, который является набором слов, необходимых для новой страницы.

Мы собираем данные слова путем прохода по большому документу и выдергивая все уникальные слова из него. Вы можете взять любой текстовый файл и положить его в соответствующий каталог на диске. Когда пользователь нажмет на кнопку выбора файла (Pick A File), обработчик события откроет диалоговое окно и попросит выбрать файл,

void FilePicker_Click( object sender, RoutedEventArgs e )
 {
   FilePicker.IsEnabled = false;
   OpenFileDialog openFileDialog1 = new OpenFileDialog();
   openFileDialog1.Filter = "Text Files (.txt)|*.txt|All Files (*.*)|*.*";
   openFileDialog1.FilterIndex = 1;
   openFileDialog1.Multiselect = false;
   bool? userClickedOK = openFileDialog1.ShowDialog();

Если пользователь нажмет на файл и затем кнопку OK - значение логической переменной userClickedOK будет равно true. На данном этапе вам нужно открыть streamReader по отношению к файлу для того, чтобы считать его содержимое в StringBuilder, прочитав все до конца файла, и добавляя каждую строку в ваш StringBuilder,

if ( userClickedOK == true )
{
   // Ограничение количества считываемого
  const long MAXBYTES = 2000; // 200000
  System.IO.FileInfo file = openFileDialog1.File;
  StringBuilder sb = new StringBuilder();
 
  if ( file != null )
  {
    System.IO.Stream fileStream = file.OpenRead();
    using ( System.IO.StreamReader reader =
               new System.IO.StreamReader( fileStream ) )
    {
      string temp = string.Empty;
 
      // соединение со строкой, пока у вас есть что считать
      // и вы не дошли до лимита MAXBYTES
      try
      {
        do
        {
          temp = reader.ReadLine();
          sb.Append( temp );
        } while ( temp != null && sb.Length < MAXBYTES );
      }
      catch { }  // пока мы игнорируем исключительные ситуации
    }    
    fileStream.Close();  // прибираем за собой (здесь должен быть блок finally)
}


Получение массива слов из строки

Имея все это в StringBuilder (sb) вы готовы разбить строку на слова, которые определяются символами, расположенными между пробелами. Мы сделаем это при помощи метода Split класса RegularExpression, который возвратит массив слов,

string pattern = "\\b";
string[] allWords = System.Text.RegularExpressions.Regex.Split(
sb.ToString(), pattern );

(Если вы не знакомы с Regular Expressions, то вам стоит уделить некоторое время изучению данного аспекта в соответствующей литературе).

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

private List<string> words = new List<string>();

и мы добавим два свойства для того, чтобы получить слова либо в порядке, в котором они были получены, либо отсортированные по алфавиту,

public List<string> Words
{
  get { return this.words; }
}
 
public List<string> SortedWords
{
  get
  {
    List<string> temp = this.words;
    temp.Sort();
    return temp;
  }
}

Создание уникального списка слов

Теперь мы можем пройтись по массиву, возвращенному регулярным выражением, и проверить каждое слово на предмет его уникальности, что гарантируется  его отсутствием  в нашем списке, который  не должен быть пуст (этого допускать нельзя) и  не должен содержать «мусора» (где  мусором  являются пунктуация, цифры, символы и разделители).  Мы передадим функцию определения мусора вспомогательному методу, IsJunk:

foreach ( string word in allWords )
{
  if ( words.Contains( word ) == false &&
        word.Length > 0 &&
        !IsJunk( word ) )
  {
    words.Add( word );
  }
}       // конец для каждого слова во всех словах

Вспомогательный метод получает каждое слово, исследует каждый символ в слове и возвращает false в случае если слово не содержит мусора

private bool IsJunk( string theWord )
{
   foreach ( char c in theWord.ToCharArray() )
   {
     if ( char.IsPunctuation( c ) ||
        char.IsDigit( c ) ||
        char.IsSymbol( c ) ||
        char.IsSeparator( c ) )
       return true;
   }
   return false;
}

Наконец, мы завершим обработчик события кнопки вызовом другого вспомогательного метода Display, который отобразит слова в двух элементах ScrollViewers используя свойства Words и SortedWords,  для того, чтобы получить содержимое набора слов в желаемом порядке.

private void Display()
{
  Message.Text = words.Count + " unique words added. ";
  WordDisplay.Text = string.Empty;
  SortDisplay.Text = string.Empty;
  foreach ( string s in Words )
  {
    WordDisplay.Text += " " + s;
  }
  foreach ( string s2 in SortedWords )
  {
    SortDisplay.Text += " " + s2;
  }
}

Данный код является полноценным кодом страницы Find.  Если вы будете делать свое приложение, то вы можете деактивировать (либо сделать невидимыми) кнопки для страниц Search и Count до данного момента потому, что только сейчас у вас есть список слов, который вы можете передать страницам.


Страница поиска

Поскольку данная статья рассматривает переключение между страницами, то я немного поработал над остальными двумя страницами, как я уже упоминал ранее, поэтому в то время как я  буду демонстрировать вам использование элементов управления из Toolkit, я не буду вдаваться в их подробности. Далее вам предоставлен полный Xaml страницы Search,

<UserControl x:Class="PageSwitching.Search"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"            
    Width="500" Height="185">
<Grid
    x:Name="LayoutRoot"
    Background="#FF000000" Height="Auto">
    <Grid.RowDefinitions>
      <RowDefinition Height="0" />
      <RowDefinition Height="50*" />
      <RowDefinition Height="2*" />
      <RowDefinition Height="30*" />
      <RowDefinition Height="39*" />
      <RowDefinition Height="0" />
      <RowDefinition Height="80*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="1*" />
      <ColumnDefinition Width="4.6*" />
    </Grid.ColumnDefinitions>
    <TextBlock
      x:Name="wordPrompt" Text="The Word: "
      HorizontalAlignment="Right"
      Margin="0,12,5,0" Grid.Row="1"
      FontFamily="Verdana" FontSize="24"
      TextWrapping="Wrap" />
    <controls:AutoCompleteBox
      x:Name="myAutoComplete"
      Margin="5,9,0,9" Grid.Column="1"
      Grid.RowSpan="1" Grid.Row="1"
      HorizontalAlignment="Left"
      Height="30" Width="210"
      FontFamily="Verdana"
      FontSize="14"  />
    <TextBlock
      x:Name="minPrefix"
      Text="Minimum Prefix Length:"
      Padding="5" FontFamily="Verdana"
      Margin="0,0,25,0" Grid.Row="3"
      HorizontalAlignment="Right"
      VerticalAlignment="Bottom"
      FontSize="18" />
    <TextBlock
      x:Name="negOne"
      HorizontalAlignment="Left"
      VerticalAlignment="Bottom"
      Grid.Column="0" Grid.Row="4"
      FontFamily="Verdana"
      Text="-1"  Margin="5,0,0,0"
      FontSize="14" />
    <TextBlock x:Name="eight"
      Margin="0,0,5,0"
      HorizontalAlignment="Right"
      VerticalAlignment="Bottom"
      Grid.Column="0" Grid.Row="4"
      FontFamily="Verdana"
      Text="8" FontSize="14" />
    <TextBlock  x:Name="CurrentValue"
      Text="2" HorizontalAlignment="Right"
      VerticalAlignment="Bottom"
      Margin="0,0,3,5"  Width="20"
      Grid.Column="0"  Grid.Row="3"
      TextWrapping="Wrap"
      FontFamily="Verdana"
      Foreground="#FFF6300B"
      FontSize="18" />
    <Slider x:Name="SetPrefixLength"
      Minimum="-1" Value="2"
      Maximum="8" SmallChange="1"
      LargeChange="2" Grid.Row="4"
      Grid.Column="0"
      Margin="24,0,20,0" />
    <Border
      Height="Auto"
      x:Name="Border"
      HorizontalAlignment="Left"
      VerticalAlignment="Stretch"
      Width="500"
      Margin="0,0,0,0"
      Grid.Row="1" Grid.RowSpan="4"
      Canvas.ZIndex="-1"
      Background="#FF73B8F2" Grid.Column="0" Grid.ColumnSpan="2" />
    <TextBlock Margin="0,0,0,0" Grid.Row="6"
          Text="" TextWrapping="Wrap"  x:Name="TheWord"
          HorizontalAlignment="Center" VerticalAlignment="Center"
          FontFamily="Georgia" FontSize="48" Foreground="#FFFFFF00"/>
    <StackPanel HorizontalAlignment="Stretch" Margin="0,0,0,0"
          Width="Auto" Grid.Column="1" Grid.Row="6"  x:Name="ButtonSP"
          Orientation="Horizontal">
      <StackPanel.Background>
        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
          <GradientStop Color="#FF000000"/>
          <GradientStop Color="#FFF8527C" Offset="1"/>
        </LinearGradientBrush>
      </StackPanel.Background>
      <Button Height="40" x:Name="returnButton" Width="100"
          RenderTransformOrigin="0.5,0.5" Background="#FF00FF00"
          FontFamily="Georgia" FontSize="20" Content="Return"
          HorizontalAlignment="Left" VerticalAlignment="Bottom"
          Margin="5,0,0,0" Canvas.ZIndex="1"/>
    </StackPanel>
  </Grid>
</UserControl>

Ключевым моментом в Xaml является пространство имен (в самом верху файла), используемое, чтобы включить код инструментария для того, чтобы включить возможность автозаполнения (AutoCompleteBox):

xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"

Для этого вам понадобится добавить ссылку на сборку Microsoft.Windows.Controls, которая поставляется вместе с инструментарием Controls.

Search.xaml.cs

Хотя код страницы Search немного сложнее, чем тот, что мы видели в предыдущем примере, код передачи значения остался тем же. Мы предоставляем переменную экземпляра типа List<String> ,

private List<string> sortedWords = null;

И заполняем это в пределах реализации UtilizeState,

public void UtilizeState( object state )
{
   if ( state != null )
   {
     sortedWords = state as List<string>;
     myAutoComplete.ItemsSource = sortedWords;
   }
}

Обратите внимание на то, что мы также устанавливаем свойство ItemSource для AutoCompleteBox на данный список строк. Это “загружает” AutoCompleteBox словами, которые он затем предложит в качестве пользовательских типов.

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

returnButton.Click += new RoutedEventHandler( returnButton_Click );
SetPrefixLength.ValueChanged +=
  new RoutedPropertyChangedEventHandler<double>(
SetPrefixLength_ValueChanged );
myAutoComplete.MinimumPrefixLength = 2;
myAutoComplete.LostFocus += new RoutedEventHandler( myAutoComplete_LostFocus );

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

Рисунок 8-9. Потеря фокуса

Вот полный код Search.xaml.cs,

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
 
namespace PageSwitching
{
  public partial class Search : UserControl, ISwitchable
  {
 
    private List<string> sortedWords = null;
 
    public Search()
    {
      InitializeComponent();
      Loaded += new RoutedEventHandler( Search_Loaded );
    }
 
    public void UtilizeState( object state )
    {
      if ( state != null )
      {
        sortedWords = state as List<string>;
        myAutoComplete.ItemsSource = sortedWords;
      }
    }
 
    void Search_Loaded( object sender, RoutedEventArgs e )
    {
      returnButton.Click +=
new RoutedEventHandler( returnButton_Click );
      returnCleanButton.Click +=
           new RoutedEventHandler( returnCleanButton_Click );
      SetPrefixLength.ValueChanged +=
        new RoutedPropertyChangedEventHandler<double>
( SetPrefixLength_ValueChanged );
      myAutoComplete.MinimumPrefixLength = 2;
      myAutoComplete.LostFocus += new RoutedEventHandler
( myAutoComplete_LostFocus );
    }
 
    void returnButton_Click( object sender, RoutedEventArgs e )
    {
      Switcher.Switch( new Find());
    }
 
    void myAutoComplete_LostFocus( object sender, RoutedEventArgs e )
    {
      if ( myAutoComplete.Text != null && myAutoComplete.Text.Length > 1 )
      {
        TheWord.Text = myAutoComplete.Text;
      }
    }
 
    void SetPrefixLength_ValueChanged(
object sender,
      RoutedPropertyChangedEventArgs<double> e )
    {
      myAutoComplete.MinimumPrefixLength =
         (int) Math.Floor( SetPrefixLength.Value );
      CurrentValue.Text = myAutoComplete.MinimumPrefixLength.ToString();
    }
  }
}


Счетчик

Страница Сount схожа со страницей Search. Мы начнем с добавления двух пространств имен, которые необходимо расположить в верхней части страницы Xaml,

xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"
xmlns:charting="clr-namespace:Microsoft.Windows.Controls.DataVisualization.Charting;assembly=Microsoft.Windows.Controls.DataVisualization"           

Это требует добавления другого пространства имен, на этот раз Microsoft.Windows.ControlsDataVisualization.dll, который также прилагается к инструментарию.

Пользовательский интерфейс состоит из графика и кнопки возврата (Return), поэтому для этого я создал две строки в табличной сетке в соотношении 6 к 1,

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
      <RowDefinition Height="6*" />
      <RowDefinition Height="1*" />
    </Grid.RowDefinitions>

В пределах данной сетки расположен объект Chart. Я назвал его letterFreqChart и данное название может быть использовано программным путем. График имеет три критических привязки:

  • IndependentValueBinding
  • DependendentValueBinding
  • ItemSource

В нашем случае мы установим первые два в Xaml, а третью программным путем, получив ItemSoure посредством метода UtilizeState.

Измерение частоты

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

public class Freq
{
  public int Count { get; set; }
  public char Letter { get; set; }
 
  public static List<Freq> Tally( List<string> words )
  {
    string prevChar = "a";
    int counter = 0;
    List<Freq> freqs = new List<Freq>();
    foreach ( string w in words )
    {
      if ( w.ToLower().StartsWith( prevChar.ToLower() ) )
        counter++;
      else
      {
        freqs.Add( new Freq(){ Letter=prevChar.ToLower()[0] ,Count=counter });
        prevChar = w.Substring( 0, 1 );
        counter = 1;
      }
      // z
      freqs.Add( new Freq() { Letter = prevChar.ToLower()[0], Count = counter } );        
    }
    return freqs;
  }
}

Frequency предоставляет два свойства - Count и Letter, а также один статический метод Tally. Вы передаете список строки в Tally и он возвращает список объектов Freq , каждый из которых содержит букву и количество слов в оригинальном списке, которые начинаются с данной буквы. Поэтому, если вы передадите в Tally список данных слов: “also, author, away, both, bottom, change” , то вы получите список из трех элементов Freq, первый из них будет иметь свойство Count со значением 3 и Letter со значением ‘a’. Второй будет иметь свойство Count со значением 2 и Letter со значением ‘b’, а у третьего Count будет равно 1 и Letter со значением ‘c’.

Вот полный Xaml-код страницы Count,

<UserControl x:Class="PageSwitching.Count"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Microsoft.Windows.Controls;
   assembly=Microsoft.Windows.Controls"
xmlns:charting="clr-  
  namespace:Microsoft.Windows.Controls.DataVisualization.Charting;
    assembly=Microsoft.Windows.Controls.DataVisualization"            
Width="400" Height="300">
  <Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
      <RowDefinition Height="6*" />
      <RowDefinition Height="1*" />
    </Grid.RowDefinitions>
    <charting:Chart x:Name="letterFreqChart" >
      <charting:Chart.Series>
        <charting:ColumnSeries
          Title="Count"
          IndependentValueBinding="{Binding Letter}"
          DependentValueBinding="{Binding Count}" />
      </charting:Chart.Series>
    </charting:Chart>
    <Button x:Name="returnBtn" Content="Return"
    FontSize="18" Grid.Row="1" Height="30" Width="100"/>
  </Grid>
</UserControl>

Вы можете заметить, что Independent Value (независимое значение) привязывается к свойству Letter , а Dependent Value (зависимое значением) - к свойству Count, явно объекту типа Freq, хотя мы еще не предоставили такой объект - это произойдет в коде. А вот код для Count.xaml.cs

using System.Collections.Generic;
using System.Windows.Controls;
 
namespace PageSwitching
{
  public partial class Count : UserControl, ISwitchable
  {
    private List<Freq> freqs;
    private List<string> sortedWords;
 
    public Count()
    {
      InitializeComponent();
      Loaded += new System.Windows.RoutedEventHandler( Count_Loaded );
    }
 
    void Count_Loaded( object sender, System.Windows.RoutedEventArgs e )
    {
      returnBtn.Click += new System.Windows.RoutedEventHandler( returnBtn_Click );
      Microsoft.Windows.Controls.DataVisualization.Charting.ColumnSeries cs = letterFreqChart.Series[0] as
        Microsoft.Windows.Controls.DataVisualization.Charting.ColumnSeries;
      cs.ItemsSource = freqs;
    }
 
    void returnBtn_Click( object sender, System.Windows.RoutedEventArgs e )
    {
      Switcher.Switch( new PageSwitcher() );
    }
 
    public void UtilizeState( object state )
    {
      sortedWords = state as List<string>;
      freqs = Freq.Tally( sortedWords );
    }
  }
}

Мы начнем с объявления двух наборов - список объектов Freq и список строк. В важном методе UtilzieState sortedWords заполняется из состояния и затем вызывается Tally с sortedWords в качестве входного параметра, а список объектов Freq -- в качестве возвращаемого значения.

Вторая и третья строки обработчика события Loaded назначает List<Freqs> в качестве источника данных СolumnSeries ItemSource,

Microsoft.Windows.Controls.DataVisualization.Charting.ColumnSeries cs = letterFreqChart.Series[0] as
   Microsoft.Windows.Controls.DataVisualization.Charting.ColumnSeries;
 cs.ItemsSource = freqs;

Вторая строка не совсем понятна, потому давайте ее разберем,

letterFreqChart.Series[0]  // найдите первый элемент в списке для нашего графика 

// преобразуйте его в тип Column

as Microsoft.Windows.Controls.DataVisualization.Charting.ColumnSeries 

//назначение его ссылки на объект данного типа

Microsoft.Windows.Controls.DataVisualization.Charting.ColumnSeries cs =

Как только мы совершили данное присвоение, у нас будет ColumnSeries и теперь мы можем назначить ItemSource. А вот как будет выглядеть график,

Рисунок 8-10. Заполненный график

Скачать исходники примеров

Jesse Liberty
Источник