読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

ストアアプリでListViewに左右のスワイプ処理を追加するという記事が面白かったのでReactivePropertyを使って書いてみた

元ネタ

めとべや東京 #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();
        }
    }
}

実行してスワイプしてみた。ちゃんととれてる。

f:id:okazuki:20150222234807p:plain