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 として設定済みです。調べればもっと面白い機能があると思うのですが、現状はこれで。
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>
実行するとこうなる・・・
使ってみるとなかなか便利です。また安定性も前のバージョンから相当増してるようで、そろそろプロジェクトに本格導入してもよさそうですね。