WP7: BusyIndicator своими силами

ru-RU | создано: 09.04.2011 | опубликовано: 09.04.2011 | обновлено: 01.01.2018 | просмотров за всё время: 5524

Когда я начал писать программу "Вопросы и Ответы" для Windows Phone 7, я еще тогда не предполагал, что для этой платформы на данный момент нет достаточного количества контролов и компонентов. В этой статье я покажу как мне удалось реализовать функционал BusyIndicator для Windows Phone 7.

Постановка задачи

Когда я начал писать программу "Вопросы и Ответы" для Windows Phone 7, я еще тогда не предполагал, что для этой платформы на данный момент нет достаточного количества контролов и компонентов. Поэтому написание программы-игры "Вопросы и Ответы" для WP7 затянулось, потому что пришлось реализовывать еще и некоторые контролы, к которым привыкаешь из-за удобства их использования. Таким удобным контролом для Silverlight-приложений является BusyIndicator. Найти его можно в библиотеке контролов Silverlight 4 Toolkit. Существуют так же и другие библиотеки, в которых присутствует контрол с таким же функционалом, например, от компании Telerik.

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

Создадим проект

Запустим студию, выберим тип проекта "Windows Phone Class Library", название дадим не очень оригинальное, но понятное "BusyIndicator". Удалим Class1.cs, который студия содает автоматически.

создаем новый проект
рис.1 Создание проекта

Теперь создадим новый класс с именем BusyIndicator.cs. А так же еще и папку с именем Themes в корне проекта. А в этой папке создадим файл generic.xaml, причем, прообразом для этого файла я взял простой TextFile.txt просто поменял на требуемое мне название.


рис.2 Создание файла для шаблона

Проект теперь выглядит таким образом:


рис. 2-1. Файлы в проекте

Самое основное на данный момент подготовлено. Приступим к кодированию.

BusyIndicator класс

Наш "индикатор занятости" по сути простой компонент, во внутрь которого можно поместить какой-либо контент, который должен блокироваться при обновлении (получение данных). Ну, и раз таковым он является, не вижу другого варианта, кроме как унаследовать наш контрол от ContentControl. Так и поступим. А теперь в конструкторе приложения назначим нашему контролу DefaultStyleKey.

public BusyIndicator()
{
  this.DefaultStyleKey = typeof(BusyIndicator);
}

Далее надо добавить несколько DependencyProperty свойств. Одно из таких свойств - IsWaiting. Почему я его так назвал? Мой контрол - как хочу, так и называю. :) Вот это свойство:

#region IsWaiting  (DependencyProperty)

/// <summary>
/// IsWaiting Dependency Property 
/// </summary>
public static readonly DependencyProperty IsWaitingProperty = DependencyProperty.Register(
  "IsWaiting", 
  typeof(bool), 
  typeof(BusyIndicator), 
  new PropertyMetadata((bool)false, new PropertyChangedCallback(OnIsWaitingChanged)));

/// <summary>
/// Gets or sets the IsWaiting property.  This dependency property 
/// indicates is control value total waiting. 
/// </summary>
public bool IsWaiting { get { return (bool)GetValue(IsWaitingProperty); } set { SetValue(IsWaitingProperty, value); } } /// 


/// <summary>
/// Handles changes to the IsWaiting property.
/// </summary>
private static void OnIsWaitingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  BusyIndicator source = (BusyIndicator)d; 
  if (source != null)
  {
    source.UpdateUI(); 
  }
} 
#endregion

А теперь про шаблон (представление) контрола по умолчанию. В силу того, что контрол должен иметь визуальное представление (как же без него? а как показать пользователю, что идет обмен данных с сервером). Вот как раз для того, чтобы мы могли задать визуальное представление контрола по умолчанию и был создан файл generic.xaml в папке themes (именно такие названия (!) и никак по-другому, хотя названия не чувствительны к регистру).

Шаблон контрола по умолчанию (generic.xaml)

Очень часто в интернете на разных сайтах, особенно на тех, которые используют AJAX (или Web 2.0) можно увидеть что типа:

или .

Вот примерно как-то так, мне бы хотелось уведомлять пользователя о занятости программы. Откроем файл generic.xaml, который является абсолютно пустым, и добавим первоначальную разметку:

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:BusyIndicator"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:System="clr-namespace:System;assembly=mscorlib"
  mc:Ignorable="d">
ResourceDictionary>

Обратите внимание на добавленный namespace, который выделен жирным. А теперь создадим стиль нашего контрола:


рис. 3 Подключение шаблона

Обратите внимание на указанный TargetType, который как раз и "привязывает" наш шаблон к нашему классу. А вот если развернуть тег Grid, который выделен на картинке выше, то можно видеть следующее:


рис 4. Структура шаблона

Обратите внимание на именование объектов шаблона. Такое правило рекомендует использовать никто иной как сам "мистер Microsoft", вернее сказать разработчики из этой компании. Особенно часто и подробно они рассказывают в видео материалах на сайте silverlight.net. Применение префикса дает большие удобства при дальнейшей разработке в коде, да и путаница менее вероятна. Глядя на рис. 4 не могу не отметить того, что в ресурсах Grid.Resources, так же лежит и Storyboard, который "крутит" набор из Ellipse по кругу. Вернемся к кодированию.

Добавим кода

В следующей части этой статьи мы добавим некоторую логику нашему контролу. Для начала зададим внутренние константы для контрола. Значения констант соответствуют названию контролов в шаблоне:

#region elements names
private const string ElementRoot = "PATH_RootLoader";
private const string ElementBusyIndicator = "PATH_BusyIndicatorLoader";
private const string ElementFramework = "PATH_Framework";
private const string ElementCanvas1 = "PATH_CanvasLoader1";
#endregion elements names

эти константы нам очень пригодятся когда мы переопределим метод OnApplyTemplate, который наполнит наши внутренние переменные "экземплярами". Определим переменные:

#region private member fields
private Grid busyIndicator;
private Grid root;
private FrameworkElement FrameworkContent;
private Storyboard WaitAnimation;
private Canvas IndicatorAnimation;
#endregion private member fields

Хочется надеяться, что не требуется пояснять, что значит каждое из полей, хотя бы исходя из их названия. Добавим еще одно DependencyProperty, которое будет являть собой Brush - цвет всех Ellipse, которые будут крутиться:

#region ForegroundAnimation (DependencyProperty)

/// <summary>
/// Calabonga: Свойство  
/// </summary>
public Brush ForegroundAnimation {
  get { return (Brush)GetValue(ForegroundAnimationProperty); }
  set {SetValue(ForegroundAnimationProperty, value);}
}
public static readonly DependencyProperty ForegroundAnimationProperty = DependencyProperty.Register(
  "ForegroundAnimation",
  typeof(Brush),
  typeof(BusyIndicator),
  new PropertyMetadata(new SolidColorBrush(Colors.Orange)));
  
#endregion  ForegroundAnimation (DependencyProperty)

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

<Ellipse
  Fill="{TemplateBinding ForegroundAnimation}"
  Height="30"
  Canvas.Left="61"
  Canvas.Top="1"
  Width="30" />

Теперь, когда все свойства есть, и шаблон есть, надо бы добавить в проект какой-нибудь тестовый проект, на котором можно будет отладить контрол (и логику, и внешний вид). Вот так теперь выглядит проект:


рис 5. Решение (sln) после добавления тестового проекта

На странице MainPage.xaml тестового проекта добавим namespace соседнего проекта:

xmlns:clb="clr-namespace:BusyIndicator;assembly=WP7CustomBusyIndicator"

И, наконец, добавим вновь созданный контрол на страницу:

<clb:BusyIndicator
  ForegroundAnimation="Yellow"
  Grid.Row="1"
  Margin="12,0,12,0">
  <Grid  x:Name="ContentPanel">Grid>
clb:BusyIndicator>

А вот и изображение нашего контрола, отображаемого в режиме дизайнера:

красиво, не правда ли? Завершим работу над контролом, добавив его поведению некоторую логику.

А где же логика?

Наверное, вы уже обратили внимание на то, в DependencyProperty IsWaiting, которое было добавлено одним из первых, в обработчике метода OnIsWaitingChanged есть вызов внутреннего метода UpdateUI():

BusyIndicator source = (BusyIndicator)d;
if (source != null)
{
   source.UpdateUI();
}

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

public override void OnApplyTemplate()
{
  base.OnApplyTemplate();
  root = this.GetTemplateChild(ElementRoot) as Grid;

  busyIndicator = GetTemplateChild(ElementBusyIndicator) as Grid;
  FrameworkContent = GetTemplateChild(ElementFramework) as FrameworkElement;
  if (root != null)
  {
    WaitAnimation = root.Resources["PATH_WaitStoryboard"] as Storyboard;
  }
  IndicatorAnimation = GetTemplateChild(ElementCanvas1) as Canvas;
  if (busyIndicator != null)
  {
    busyIndicator.Visibility = System.Windows.Visibility.Visible;
  }
}

Этот метод как раз и "связывает" шаблон нашего контрола с кодом. Теперь обращаясь к переменным можно управлять поведением контрола. Вот как раз метод UpdateUI() обновляет (показывает и скрывает крутящиеся шарики, и при этом запускает и останавливает анимацию) внешний вид контрола всоответствие с настройками:

private void UpdateUI()
{
  if (!this.IsWaiting)
  {
    FrameworkContent.Opacity = 1;
    busyIndicator.Visibility = System.Windows.Visibility.Collapsed;
    WaitAnimation.Stop();
  }
  else
  {
    FrameworkContent.Opacity = .1;
    WaitAnimation.Stop();
    WaitAnimation.Begin();
    busyIndicator.Visibility = System.Windows.Visibility.Visible;
  }
} 

Кажется всё, да не всё. Контрол работает некорректно. Во-первых, не корректно отображается при первой отрисовке. Во-вторых, показывает странную ошибку подчеркивая ее синей волнистой линией. Исправим недочеты... Для начала введем еще одну переменную isInitialized, которая будет свидетельствовать о том, что инициализация прошла успешно:

private bool isInitialized;

теперь обновим метод обновления контрола (извините за каламбур):

private void UpdateUI()
{
  if (isInitialized)
  {
    if (!this.IsWaiting)
    {
      FrameworkContent.Opacity = 1;
      busyIndicator.Visibility = System.Windows.Visibility.Collapsed;
      WaitAnimation.Stop();
    }
    else
    {
      FrameworkContent.Opacity = .1;
      WaitAnimation.Stop();
      WaitAnimation.Begin();
      busyIndicator.Visibility = System.Windows.Visibility.Visible;
    }  }
}

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

public BusyIndicator()
{
  this.DefaultStyleKey = typeof(BusyIndicator);
  this.isInitialized = false;
}

установим первоначально, что контрол не готов к работе (значение false). Теперь далее в методе OnApplyTemplate после проверки корректности инициализации установим обратное значение и обновим внешний вид контрола:

public override void OnApplyTemplate()
{
  base.OnApplyTemplate();
  root = this.GetTemplateChild(ElementRoot) as Grid;

  busyIndicator = GetTemplateChild(ElementBusyIndicator) as Grid;
  FrameworkContent = GetTemplateChild(ElementFramework) as FrameworkElement;
  if (root != null)
  {
    WaitAnimation = root.Resources["PATH_WaitStoryboard"] as Storyboard;
  }
  IndicatorAnimation = GetTemplateChild(ElementCanvas1) as Canvas;
  if (busyIndicator != null)
  {
    busyIndicator.Visibility = System.Windows.Visibility.Visible;
  }
  if (root != null && busyIndicator != null &&
    FrameworkContent != null && WaitAnimation != null
    && IndicatorAnimation != null)
  {
    this.isInitialized = true;
    UpdateUI();
  }
}

Вот теперь, всё работает правильно и корректно. Собственно говоря, контрол готов. Я еще немного поколдовал над контролом и сделал на выбор два типа анимации. Будем это считать Вашим домашним заданием.

Комментарии к статье (4)

22.03.2013 13:10:31 starostin13

Ссылка на скачивание архива с исходниками не работает, можно поправить?

22.03.2013 14:24:54 Calabonga

Поправил ссылку

22.03.2013 21:00:31 starostin13

Клёво, очень полезно и хорошо написано.

Можно как-то ограничить CompositeTransform какими то значениями? Конкретика: я хочу сделать что бы моим busy индикатором были три Rectangle, которые из точки вырастают в полоску по очереди. И если можно как то ограничить, то наверно мне понадобиться событие достижения этой границы, или конца анимирования. Даже не приходит в голову как такое гуглить.

23.03.2013 1:40:53 Сalabonga

starostin13, на сколько я помню, "управлять" CompositeTransform не получится, этот вид трансформации появился позже в Silverlight и был призван объединить (упростить) комбинации других видов трансформаций (ScaleTransform и другие). Попробуйте использовать комбинации "простых" трансформеров, "управление" (подписка на события, в том числе) которыми более приемлемый вариант.