WPF で Google Map API を使ってルート検索してみる(Livet版)

昨日 Google Map API でルート検索するサンプルを公開しましたが、本日はさらに一歩進め、先日のサンプルの Livet 版を作ってみました。
Livet は純国産の MVVM インフラで、先日最新版 0.98RC が公開されました。前のバージョンから大きく変わったとのことなので、その検証も兼ねて試してみます。もっとも MVVM に精通してる訳じゃないので Livet の使い方がまだよく判ってません。問題点あれば遠慮なく言って頂けるとありがたいです、


関連記事Livet WPF4 MVVM インフラストラクチャ
関連記事Livet ダウンロード


Model

VB で Livet の新規プロジェクトを作ります。Livet のプロジェクトを新規作成すると、Model・View・ViewModel が自動生成されるので、まず Model から作成します。Customer クラスと Customer のコレクションを管理する Customers クラスを用意します。Customer クラスは単純に住所を保持するだけ。Customers は内部で ObservableCollection を使い、Customer のインスタンスを管理します。Livet で生成される Model のアクセスレベルは 既定値だと Friend ですが、このサンプルでは Public に変更してます。

Customer.vb

Public Class Customer
	Inherits NotificationObject
	Public Property Address As String
End Class


Customers.vb

Imports System.Collections.ObjectModel

Public Class Customers
	Inherits NotificationObject
	Private _customers As ObservableCollection(Of Customer)
	Default Public Property Items(index As Integer) As ObservableCollection(Of Customer)
		Get
			Return _customers
		End Get
		Set(ByVal value As ObservableCollection(Of Customer))
			_customers = value
		End Set
	End Property
End Class


Model の追加は「新しい項目の追加」で「Livet WPF4 モデル」を選択し追加します。すると NotificationObject を継承したクラスがプロジェクトに追加されます。NotificationObjectクラスのソースを見ると INotifyPropertyChanged・IWeakEventListenerHolder インターフェイスを実装したクラスだということが判ります。Model はステートフルであるべきとの考えを基にした設計ですね。以下 NotificationObject.cs からの抜粋。

/// <summary>
/// 変更通知オブジェクトの基底クラスです。
/// </summary>
public class NotificationObject : INotifyPropertyChanged,IWeakEventListenerHolder
{
	//・・・・中略・・・
}


でも自動生成される Model のアクセス既定値が Friend なのは何ぜなんですかね? Friend のままだと、以下のように ViewModel から Model をプロパティで公開した場合、コンパイルエラーが発生します。

Public Class MainWindowViewModel
	Inherits ViewModel
・・・・中略・・・・
#Region "Customer変更通知プロパティ"
	Private _Customer As Customer

	Public Property Customer() As Customer
		Get
			Return _Customer
		End Get
		Set(ByVal value As Customer)
			_Customer = value
			RaisePropertyChanged("Customer")
		End Set
	End Property
#End Region
・・・・中略・・・・
End Class

'Customer' は、型 'Customer' を class 'MainWindowViewModel' 経由でプロジェクトの外側に公開できません。


これはまずいので、このサンプルでは Model を Public に変更してます。
あとVB では通常プロジェクトのオプションが Option Explicit On/Option Strict Off になってますが、暗黙の型変換を許すのは非常に危険なので、私はいつもコーディングする際

Option Explicit On
Option Strict On

の二行を先頭に宣言しています。VB6 の移行ユーザーならまだしも、WPF 使うくらいのレベルなら Option Explicit On/Option Strict On の組み合わせで問題ないでしょう。(というかむしろ頼む)・・・というわけで、Livet のクラスジェネレーターでこのオプションが最初から明示的に宣言されてると嬉しいですね。



ViewModel

次は ViewModel です。View と通信するプロパティとコマンドを実装します。
ViewModel のコードはどうしても冗長になりますが、Livet のスニペットを使うとかなり楽に実装されます。また前日のサンプルでは ICommand を実装した RelayCommand クラスを用意しましたが、Livet では ICommand を実装した ViewModelCommand や ListenerCommand クラスが用意されてます。サンプルを見ると Livet 独自のクラス、ReadOnlyNotificationDispatcherCollection を使ってコレクションを管理してるようですが、この辺りの仕様はよく判らないので本サンプルでは使ってません。


MainWindowViewModel.vb

Imports System.Collections.ObjectModel

Public Class MainWindowViewModel
	Inherits ViewModel

#Region "コンストラクタ"
	Public Sub New()
		_Customers.Add(New Customer() With {.Address = "東京駅"})
	End Sub
#End Region

#Region "Address変更通知プロパティ"
	Private _Address As String

	Public Property Address() As String
		Get
			Return _Address
		End Get
		Set(ByVal value As String)
			If (_Address = value) Then Return
			_Address = value
			RaisePropertyChanged("Address")
			Me.AddItemCommand = New ViewModelCommand(AddressOf AddItem, AddressOf CanAddItem)
		End Set
	End Property
#End Region

#Region "Customer変更通知プロパティ"
	Private _Customer As Customer

	Public Property Customer() As Customer
		Get
			Return _Customer
		End Get
		Set(ByVal value As Customer)
			_Customer = value
			RaisePropertyChanged("Customer")
		End Set
	End Property
#End Region

#Region "Customers変更通知プロパティ"
	Private _Customers As New ObservableCollection(Of Customer)

	Public Property Customers() As ObservableCollection(Of Customer)
		Get
			Return _Customers
		End Get
		Set(ByVal value As ObservableCollection(Of Customer))
			_Customers = value
			RaisePropertyChanged("Customers")
		End Set
	End Property
#End Region

	Public ReadOnly Property NavigateString() As String
		Get
			Return GetNavigateString()
		End Get
	End Property

#Region "AddItemCommand"
	Private _AddItemCommand As ViewModelCommand

	Public Property AddItemCommand() As ViewModelCommand
		Get
			If _AddItemCommand Is Nothing Then
				_AddItemCommand = New ViewModelCommand(AddressOf AddItem, AddressOf CanAddItem)
			End If
			Return _AddItemCommand
		End Get
		Private Set(value As ViewModelCommand)
			_AddItemCommand = value
			RaisePropertyChanged("AddItemCommand")
		End Set
	End Property

	Private Function CanAddItem() As Boolean
		If (String.IsNullOrEmpty(Me.Address)) Then
			Return False
		End If
		Return _Customers.Count <= 25
	End Function

	Private Sub AddItem()
		_Customers.Add(New Customer() With {.Address = Me.Address})
		Me.Address = String.Empty
	End Sub
#End Region

#Region "DeleteCommand"
	Private _DeleteCommand As ViewModelCommand

	Public ReadOnly Property DeleteCommand() As ViewModelCommand
		Get
			If _DeleteCommand Is Nothing Then
				_DeleteCommand = New ViewModelCommand(AddressOf Delete, AddressOf CanDelete)
			End If
			Return _DeleteCommand
		End Get
	End Property

	Private Function CanDelete() As Boolean
		Return True
	End Function

	Private Sub Delete()
		_Customers.Remove(Me.Customer)
	End Sub
#End Region

#Region "メソッド"

	Public Function GetNavigateString() As String
		Dim html As New System.Text.StringBuilder()
		With html
			.AppendLine("<!DOCTYPE html '-//W3C//DTD XHTML 1.0 Strict//EN'  ")
			.AppendLine("  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'> ")
			.AppendLine("<!-- saved from url=(0017)http://localhost/ -->")
			.AppendLine("<html xmlns='http://www.w3.org/1999/xhtml'> ")
			.AppendLine("	<head> ")
			.AppendLine("	<meta http-equiv='content-type' content='text/html; charset=utf-8'/> ")
			.AppendLine("	<script type='text/javascript' src='http://maps.google.com/maps?file=api&key=(key)&sensor=false'></script> ")
			.AppendLine("	<script type='text/javascript'> ")
			.AppendLine("		var map; ")
			.AppendLine("		var directions; ")
			.AppendLine()
			.AppendLine("		function initialize() { ")
			.AppendLine("			if (GBrowserIsCompatible()) { ")
			.AppendLine("				map = new GMap2(document.getElementById('map_canvas')); ")
			.AppendLine("				map.enableScrollWheelZoom(); ")
			.AppendLine("				map.setCenter(new GLatLng(35.681379, 139.765577), 13); ")
			.AppendLine("				directions = new GDirections(map, document.getElementById('route')); ")
			.AppendFormat("				var pointArray = [{0}]; ", GetPointList())
			.AppendLine()
			.AppendLine("				directions.loadFromWaypoints(pointArray,{ locale: 'ja_JP' }); ")
			.AppendLine("			} ")
			.AppendLine("		} ")
			.AppendLine("	</script> ")
			.AppendLine("</head> ")
			.AppendLine("	<body onload='initialize()'> ")
			.AppendLine("		<div id='map_canvas' style='width: 100%; height: 400px'></div> ")
			.AppendLine("		<div id='route' style='width: 100%; height: Auto'></div> ")
			.AppendLine("	</body> ")
			.AppendLine("</html> ")
		End With
		Return html.ToString()
	End Function

	Private Function GetPointList() As String
		Dim ret As String = String.Empty
		For Each item In _customers
			ret += "'" + item.Address + "',"
		Next
		If (Not String.IsNullOrEmpty(ret)) Then
			ret = ret.Substring(0, ret.Length - 1)
		End If
		Return ret
	End Function

#End Region

End Class


プロパティとコマンドはスニペットを使って実装しましたが、実装はたいへん楽になりますね。
また Livet を使わない標準プロジェクトでも、Livet がインストールされてるとスニペットは有効になります。lvcom とか lprop とか入力して Tab を叩くとスニペットが挿入されるのでちょっと便利。でも「Imports Livet」が勝手に挿入されるのは玉に傷ですが(苦笑

あと AddItemCommand では、CanAddItem メソッドが走るタイミングが判らずコマンドが有効にならないので、ReadOnly 外してセッターを設け RaisePropertyChanged を実行させるようにし、Address プロパティ変更時に AddItemCommand を再生成するようにしてます。この辺りの CanExecute メソッドが走るタイミングといいますか・・・何か用意されてるとは思うんですが、現状まだ判らないので保留。


View

最後は View です。コードビハインドは昨日と変わりません。画面デザインも昨日と全く同じ。でも Livet が生成した Xaml は初期状態で ViewModel を DataContext として設定済みです。調べればもっと面白い機能があると思うのですが、現状はこれで。


MainWindow.xaml.vb

Public Class MainWindow
	Public Shared ReadOnly Navigate As New RoutedCommand

	Public Sub Navigate_Execute(sender As Object, e As RoutedEventArgs)
		If (Me.DataContext Is Nothing) Then Return
		If (Me.browser Is Nothing) Then Return

		Dim navigateString = DirectCast(Me.DataContext, ViewModel).NavigateString
		Me.browser.NavigateToString(navigateString)
	End Sub
End Class


MainWindow.xaml

<Window x:Class="MainWindow"
		xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
		xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
		xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
		xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
		xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
		xmlns:local="clr-namespace:LivetGoogleMapSample"
		Title="MainWindow" Height="630" Width="900"
		WindowStartupLocation="CenterScreen" >
	<Window.DataContext>
		<local:MainWindowViewModel />
	</Window.DataContext>
	<Window.CommandBindings>
		<CommandBinding Command="local:MainWindow.Navigate" Executed="Navigate_Execute" />
	</Window.CommandBindings>
	<i:Interaction.Triggers>
		<i:EventTrigger EventName="Loaded">
			<i:InvokeCommandAction Command="local:MainWindow.Navigate" />
		</i:EventTrigger>
	</i:Interaction.Triggers>
	<Grid>
		<Grid.RowDefinitions>
			<RowDefinition Height="8" />
			<RowDefinition Height="135" />
			<RowDefinition Height="8" />
			<RowDefinition />
			<RowDefinition Height="8" />
		</Grid.RowDefinitions>
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="8" />
			<ColumnDefinition />
			<ColumnDefinition Width="8" />
		</Grid.ColumnDefinitions>
		<Grid Grid.Column="1" Grid.Row="1" >
			<Grid.ColumnDefinitions>
				<ColumnDefinition />
				<ColumnDefinition Width="8" />
				<ColumnDefinition />
			</Grid.ColumnDefinitions>
			<ListBox Grid.Row="0" Grid.Column="0" DisplayMemberPath="Address"
					 ItemsSource="{Binding Customers}" SelectedValue="{Binding Customer}" >
				<ListBox.InputBindings>
					<KeyBinding Key="Delete" Command="{Binding DeleteCommand}" />
				</ListBox.InputBindings>
			</ListBox>
			<StackPanel Grid.Column="2">
				<Label Height="24" Content="ルート検索する住所を入力してください" />
				<TextBox Height="24" VerticalContentAlignment="Center"
						InputMethod.PreferredImeState="On"
						InputMethod.PreferredImeConversionMode="FullShape,Native" 
						Text="{Binding Address, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
				<Button Width="80" Height="24" Margin="0,12,0,0" 
						Content="追加"
						HorizontalAlignment="Right"
						Command="{Binding AddItemCommand}"/>
				<Button Width="80" Height="24" Margin="0,12,0,0" 
						Content="検索"
						HorizontalAlignment="Right"
						Command="local:MainWindow.Navigate" />
			</StackPanel>
		</Grid>
		<WebBrowser x:Name="browser" Grid.Row="3" Grid.Column="1" />
	</Grid>
</Window>


実行するとこうなる・・・


使ってみるとなかなか便利です。また安定性も前のバージョンから相当増してるようで、そろそろプロジェクトに本格導入してもよさそうですね。



WPF FAQ の目次に戻る