Tuesday, February 1, 2011

Bing Maps на Windows Phone 7. Часть 2. Масштабирование карты

Первая часть серии была посвящена созданию простейшего Bing Maps приложения для Windows Phone 7. Во второй части мы рассмотрим улучшение контролов изменения масштаба карты (zoom) и коснемся интерфейса INotifyPropertyChanged.

Изменение ViewModel

Может быть, вы заметили, что в прошлой части мы установили свойству ZoomBarVisibility значение Visible. Стандартный Zoom Bar выглядит достаточно грубо, кроме того, отображается в нижней части карты, что не очень удобно. Поэтому мы его заменим на наш собственный.

Для начала устанавливаем свойству ZoomBarVisibility значение Collapsed (или просто удаляем его из списка свойств).

Далее нам нужно расширить наш MapViewModel, добавив туда свойство Zoom, которое будет источником данных для свойства ZoomLevel у контрола Map. Добавляем в MapViewModel свойство и некоторые константы для удобства:

private const double DefaultZoomLevel = 4.0;
private const double MaxZoomLevel = 21.0;
private const double MinZoomLevel = 3.0;
public double Zoom
{
    get { return zoom; }
    set
    {
        var coercedZoom = Math.Max(MinZoomLevel, Math.Min(MaxZoomLevel, value));
        if (zoom != coercedZoom)
        {
            zoom = value;
            NotifyPropertyChanged("Zoom");
        }
    }
}

Здесь вроде бы все понятно за исключением NotifyPropertyChanged. NotifyPropertyChanged – это метод-реализация интерфейса INotifyPropertyChanged. Этот интерфейс используется для оповещения классов, которые привязываются (binding) к источнику данных, об изменениях в этом источнике. В нашем случае контрол Map нашей View будет таким образом оповещаться об изменениях в свойстве Zoom.

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

Добавляем в папку ViewModel проекта новый класс BaseViewModel, который будет выглядеть следующим образом:

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null && !String.IsNullOrEmpty(propertyName))
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

И наследуем класс MapViewModel от BaseViewModel:

public class MapViewModel : BaseViewModel

Работа с представлением

Теперь нам нужно перейти во View и привязать свойство ZoomLevel контрола Map к свойству Zoom модели представления:

ZoomLevel="{Binding Zoom, Mode=TwoWay}"

Добавляем кнопки для масштабирования карты внутрь <canvas>:

<Button x:Name="ButtonZoomIn" 
    BorderThickness="0" Margin="8,200,0,0" Padding="0"
    HorizontalAlignment="Left" VerticalAlignment="Top" 
    Width="72" Height="72"
    Click="ButtonZoomIn_Click">
        <Image Source="Images/Menu/ZoomIn_White.png" />
</Button>
<Button x:Name="ButtonZoomOut" 
    BorderThickness="0" Margin="8,280,0,0" Padding="0"
    HorizontalAlignment="Left" VerticalAlignment="Top" 
    Height="72" Width="72"
    Click="ButtonZoomOut_Click">
        <Image Source="Images/Menu/ZoomOut_White.png" />
</Button>

Соединяем все вместе

Теперь нжуно реализовать обработку нажатий на кнопки. Для этого добавляем обработчики событий для кнопок, которые будут изменять свойство Zoom модели представления, в code-behind файл MainPage.xaml.cs:

private void ButtonZoomIn_Click(object sender, RoutedEventArgs e)
{
    Model.Zoom++;
}

private void ButtonZoomOut_Click(object sender, RoutedEventArgs e)
{
    Model.Zoom--;
}

Добавляем свойство Model в тот же code-behind файл:

protected MapViewModel Model
{
    get { return (MapViewModel)Resources["ViewModel"]; }
}

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

Если говорить строго, то написание логики представления (а в нашем случае это обработка событий и устновка свойства Zoom) – это уже отход от правильной реализации паттерна MVVM. Но в то же время, code-behind страницы – это все еще View, поэтому мы можем сами решать, как оповещать наш ViewModel о событиях UI. Поэтому для начальной реализации приложения мы будем использовать code-behind страницы как прокси к ViewModel для упрощения реализации. В реальном же приложении нам нужно было бы использовать т.н. команды для привязки событий ко View, а не обычные обработчики. Но поскольку Silverlight до сих пор не обзавелся официальной поддержкой команд (за исключением интерфейса ICommand), а писать свои с нуля мы еще не готовы, то пока что отложим этот вопрос на будущее. Если кому-то интересно узнать больше уже сейчас, вот несколько ссылок:

http://weblogs.asp.net/nmarun/archive/2009/12/02/using-icommand-silverlight-4.aspx
http://johnpapa.net/silverlight/5-simple-steps-to-commanding-in-silverlight/
http://blogs.southworks.net/jdominguez/2008/08/icommand-for-silverlight-with-attached-behaviors/
http://houseofbilz.com/archives/2009/05/22/adventures-in-mvvm-commands-in-silverlight/

Добавляем картинки

Итак, логика готова. Нам осталось лишь добавить картинки для кнопок. Для этого создаем папку Images\Menu в проекте и добавляем туда файлы ZoomIn_White.png и ZoomOut_White.png. Скачать все файлы картинок, включая эти две, можно отсюда. Напоследок нам нужно открыть окно Properties для каждой из этих картинок и установить там свойство Build Action в Content вместо Resource. Этим мы убиваем двух зайцев: уменьшаем размер сборки, тем самым сокращая время загрузки приложения, и упрощаем доступ к файлам из приложения.

Запускаем приложение и видим, что у нас появились кнопки масштабирования. Их можно покликать – они работают :)

В следующей части мы реализуем получение нашего положения в пространстве при помощи GeoLocation API и научимся центрировать карту в это положение.

Вся серия:

Часть 1. Введение
Часть 2. Масштабирование карты
Часть 3. Полные исходники

4 comments:

  1. Хорошая статья, Саш. Спасибо! Есть одно предолжение по реализации INotifyPropertyChanged. Не стоит делать проверку на пустое значение имени свойства (!String.IsNullOrEmpty(propertyName) ) - это вполне законное значение, при котором View будет считать, что обновились все свойства view model'a, и перезапросит их значения. Иногда это очень удобно.

    ReplyDelete
  2. Спасибо, очень полезное уточнение. Не знал об этом соглашении.

    ReplyDelete
  3. Александр подскажите пожалуйста в конце второй части было сказано что будет и третья часть в которой вы собирались рассказать о реализации получения нашего положения в пространстве. Но так и не нашел эту статью или вы просто не стали ее писать?

    ReplyDelete
  4. Да, собирался. Но фидбека было очень мало, людям не интересно, поэтому решил не писать дальше. Вот здесь я выложил полные исходники: http://merle-amber.blogspot.com/2011/03/bing-maps-windows-phone-7.html

    ReplyDelete