元ネタ
めとべや東京 #7で登壇してきましたその1 ストアアプリでListViewに左右のスワイプ処理を追加する - shinji-japanのブログ
面白い内容です。読んでて記事内容で疑問点がいくつか出てきました。
- なぜItemTemplate内のBorderの横幅と縦幅を明示してるのか
- スワイプされた要素はどうやって特定するのか
- MVVMちっくに書きたい
なぜItemTemplate内のBorderの横幅と縦幅を明示してるのか
これはおそらくイベントを拾うため。ItemContainerStyleでHorizontalContentAlignmentとVerticalContentAlignmentをStretchにしてやることで、ItemTemplateの中身がいっぱいにひろがって明示的にサイズを指定しなくても、イベントが拾えるようになりました。
具体的にはItemContainerStyleプロパティのコピーを以下のように編集します。
<Style x:Key="ListViewItemStyle1" TargetType="ListViewItem"> <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/> <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/> <Setter Property="Background" Value="Transparent"/> <Setter Property="TabNavigation" Value="Local"/> <Setter Property="IsHoldingEnabled" Value="True"/> <Setter Property="Margin" Value="0,0,18,2"/> <Setter Property="HorizontalContentAlignment" Value="Left"/> <Setter Property="VerticalContentAlignment" Value="Top"/> <!-- この2個を追加 --> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="VerticalContentAlignment" Value="Stretch" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ListViewItem"> <-- 省略 --> </ControlTemplate> </Setter.Value> </Setter> </Style>
スワイプされた要素はどうやって特定するのか
イベント引数のOriginalSourceのDataContextからかっさらってやればよかったです。
MVVMチックに書きたい
ReactivePropertyのEventToReactiveCommandを使ってみました。
こんなConverterを作って、スワイプされた要素とスワイプの方向のTupleを返すようにしました。
public class ManipulationDeltaRoutedEventArgsToStringConverter : ReactiveConverter<ManipulationDeltaRoutedEventArgs, Tuple<string, string>> { protected override IObservable<Tuple<string, string>> OnConvert(IObservable<ManipulationDeltaRoutedEventArgs> source) { var ox = source .Select(x => Tuple.Create(x.OriginalSource as FrameworkElement, x.Delta.Translation.X)); return Observable.Merge( ox .Where(x => x.Item2 > 10) .Throttle(TimeSpan.FromSeconds(0.5)) .ObserveOnUIDispatcher() .Select(x => Tuple.Create(x.Item1.DataContext as string, "Right")), ox .Where(x => x.Item2 < -10) .Throttle(TimeSpan.FromSeconds(0.5)) .ObserveOnUIDispatcher() .Select(x => Tuple.Create(x.Item1.DataContext as string, "Left"))); } }
Throttleを入れてるのはログを吐くようにしてみたら10以上の移動が連続して発生していたので、フィルタリングするためです。これで無駄なイベントの発生を排除しています。
これをViewModelのCommandにBindingします。
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App3" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" xmlns:Core="using:Microsoft.Xaml.Interactions.Core" xmlns:Interactivity1="using:Reactive.Bindings.Interactivity" x:Class="App3.MainPage" mc:Ignorable="d"> <Page.Resources> <!-- ITemTemplate内からViewModelに触るためにリソースにVMを登録しておく --> <local:MainPageViewModel x:Key="viewModel"/> <DataTemplate x:Key="DataTemplate1"> <Grid ManipulationMode="TranslateX,System" Background="Pink"> <Interactivity:Interaction.Behaviors> <!-- ManipulationDeltaのイベント引数を変換してコマンドに渡す --> <Core:EventTriggerBehavior EventName="ManipulationDelta"> <Interactivity1:EventToReactiveCommand Command="{Binding SwipeCommand, Mode=OneWay, Source={StaticResource viewModel}}"> <local:ManipulationDeltaRoutedEventArgsToStringConverter /> </Interactivity1:EventToReactiveCommand> </Core:EventTriggerBehavior> </Interactivity:Interaction.Behaviors> <TextBlock Text="{Binding}" Style="{ThemeResource BodyTextBlockStyle}" /> </Grid> </DataTemplate> <Style x:Key="ListViewItemStyle1" TargetType="ListViewItem"> <!-- さっきの内容なので省略 --> </Style> </Page.Resources> <Page.DataContext> <StaticResource ResourceKey="viewModel"/> </Page.DataContext> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock TextWrapping="Wrap" Text="{Binding SwipeMessage.Value}" Style="{StaticResource BodyTextBlockStyle}" HorizontalAlignment="Center"/> <ListView Grid.Row="1" ItemsSource="{Binding Items}" ItemTemplate="{StaticResource DataTemplate1}" ItemContainerStyle="{StaticResource ListViewItemStyle1}" /> </Grid> </Page>
ViewModelはこんな感じ。
using Reactive.Bindings; using System; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; namespace App3 { public class MainPageViewModel { public ReadOnlyReactiveCollection<string> Items { get; private set; } public ReactiveCommand<Tuple<string, string>> SwipeCommand { get; private set; } public ReactiveProperty<string> SwipeMessage { get; private set; } public MainPageViewModel() { var source = new ObservableCollection<string>(Enumerable.Range(1, 1000).Select(x => "item " + x)); this.Items = source.ToReadOnlyReactiveCollection(x => x); this.SwipeCommand = new ReactiveCommand<Tuple<string, string>>(); this.SwipeMessage = this.SwipeCommand .Select(x => x + " swiped.") .ToReactiveProperty(); } } }
実行してスワイプしてみた。ちゃんととれてる。