WPFでズンドコキヨシ&テッテーテレッテーしてみた

皆様ご存じズンドコキヨシをWPFでやってみた
ついでに最近ずっと聞き続けているGYARIさんの[ﾃｯﾃｰﾃﾚｯﾃｰ](https://www.youtube.com/watch?v=-iFEKeULaIo&index=3&list=PL5F08pgDWVRYc04X-t270fB9u3QTXvj-0)も同じようにやってみた('ω')

**18/07/17    MVVMらしく改修**

#作ったもの

![ZundokoSample.gif](https://qiita-image-store.s3.amazonaws.com/0/145898/5864d1fd-93a6-5530-1751-d109f10aeb1e.gif)

入力パターン(カンマ区切り)、完成パターン(カンマ区切り)と出力文字を指定してStartを押すと入力パターンからランダムに文字を出力して、完成パターンと一致したら出力文字を末尾につけてダイアログを出して終了するよ(=ﾟωﾟ)ﾉ
また、文字出力中にStopを押すと停止できる(ΦωΦ)

#作り方
ライブラリは
[Prism](https://github.com/PrismLibrary/Prism)
[ReactiveProperty](https://blog.okazuki.jp/entry/2014/05/07/014133)
[MahApps.Metro](https://mahapps.com/)
を使用

##Model
Modelといっても、今回は入力パターンと完成パターンを指定して完成パターンに一致するまでランダムに文字列を出力するメソッドを作るだけ

```csharp:StreamCreater.cs
public static class StreamCreater
{
    static readonly private Queue<string> queue = new Queue<string>();
    static private Random random = new Random();
    /// <summary>
    /// 入力したパターンからランダムに出力し、完成パターンと一致すれば出力を終了する
    /// </summary>
    /// <param name="inputPattern">入力パターン</param>
    /// <param name="finishedPattern">完成パターン</param>
    /// <returns></returns>
    public static IEnumerable<string> Create(string[] inputPattern, string[] finishedPattern)
    {
        queue.Clear();
        while (!queue.SequenceEqual(finishedPattern))
        {
            queue.Enqueue(inputPattern[random.Next(inputPattern.Count())]);
            if (queue.Count == finishedPattern.Count() + 1)
            {
                queue.Dequeue();
            }
            yield return queue.Last();
        }
    }
}
```

戻り値の型をIEnumerable<T>にしてyield returnすることでforeachで一個づつ値が取れるようにしている

##ViewModel

```csharp:ShellViewModel.cs
public class ShellViewModel : BindableBase
{
    public ReactiveProperty<string> LeftInputPattern { get; private set; } = new ReactiveProperty<string>("");
    public ReactiveProperty<string> LeftFinishedPattern { get; private set; } = new ReactiveProperty<string>("");
    public ReactiveProperty<string> LeftOutput { get; private set; } = new ReactiveProperty<string>("");
    public ReactiveProperty<bool> IsLeftChecked { get; private set; } = new ReactiveProperty<bool>(true);

    public ReactiveProperty<string> RightInputPattern { get; private set; } = new ReactiveProperty<string>("");
    public ReactiveProperty<string> RightFinishedPattern { get; private set; } = new ReactiveProperty<string>("");
    public ReactiveProperty<string> RightOutput { get; private set; } = new ReactiveProperty<string>("");
    public ReactiveProperty<bool> IsRightChecked { get; private set; } = new ReactiveProperty<bool>(false);

    public ReactiveProperty<bool> IsStarted { get; private set; } = new ReactiveProperty<bool>(false);
    public ReactiveProperty<int> StreamCount { get; private set; } = new ReactiveProperty<int>();
    public ReactiveProperty<string> ResultText { get; private set; } = new ReactiveProperty<string>();
    public ReactiveProperty<string> StartStopButtonText { get; private set; } = new ReactiveProperty<string>("Start");

    public InteractionRequest<Notification> Notification { get; } = new InteractionRequest<Notification>();

    public ReactiveCommand StreamStartCommand { get; private set; } = new ReactiveCommand();

    private CancellationTokenSource cancellationTokenSource;

    public ShellViewModel()
    {
        StreamStartCommand.Subscribe(async () =>
        {
            if (!this.IsStarted.Value)
            {
                if (this.IsLeftChecked.Value) await this.StreamStart(this.LeftInputPattern.Value, this.LeftFinishedPattern.Value, this.LeftOutput.Value);
                else if (this.IsRightChecked.Value) await this.StreamStart(this.RightInputPattern.Value, this.RightFinishedPattern.Value, this.RightOutput.Value);
            }
            else this.StreamStop();
        });
    }

    /// <summary>
    /// Streamを開始する
    /// </summary>
    /// <param name="input"></param>
    /// <param name="finished"></param>
    /// <param name="output"></param>
    private async Task StreamStart(string input, string finished, string output)
    {
        if (this.IsStarted.Value) return;
        if (string.IsNullOrWhiteSpace(input) || string.IsNullOrWhiteSpace(finished)) return;

        var inputPattern = input.Split(',');
        var finishedPattern = finished.Split(',');
        //パターンが一致可能か確認
        if (finishedPattern.Except(inputPattern).Any())
        {
            this.Notification.RaiseEx("エラー", "完成パターン内に、入力パターンに存在しないものがあります。");
            return;
        }
        //初期化
        this.StartStopButtonText.Value = "Stop";
        this.IsStarted.Value = true;
        this.StreamCount.Value = 0;
        this.ResultText.Value = "";
        this.cancellationTokenSource = new CancellationTokenSource();

        //Streamを非同期で生成
        var res = await Task.Run(async () =>
        {
            foreach (var item in StreamCreater.Create(inputPattern, finishedPattern))
            {
                if (cancellationTokenSource.IsCancellationRequested) return false;
                this.ResultText.Value += item + ',';
                this.StreamCount.Value++;
                await Task.Delay(1);
            }
            this.ResultText.Value += output;
            this.IsStarted.Value = false;
            return true;
        }, this.cancellationTokenSource.Token);
        //キャンセルされなければダイアログ表示
        if (res)
        {
            this.Notification.RaiseEx("パターン一致", $"{finished},{output}\r\n{this.StreamCount.Value}回で完成パターンと一致しました。");
            this.StartStopButtonText.Value = "Start";
        }
    }
    /// <summary>
    /// 動作中のStreamを停止する
    /// </summary>
    private void StreamStop()
    {
        if (!this.IsStarted.Value) return;
        this.StartStopButtonText.Value = "Start";
        this.IsStarted.Value = false;
        this.cancellationTokenSource.Cancel();
    }
}
```

ここではViewから入力パターンなどを受け取ったり、結果を表示したりするプロパティやコマンド、ダイアログ表示の為のInteractionRequest等を定義している
ここのコマンドで上記のStreamCreater.Create()している
また、ViewModelは[INotifyPropertyChangedを実装していないとメモリリークする](http://aridai.net/article/?p=15)ようなのでINotifyPropertyChangedを実装したPrismのBindableBaseを継承している
コードが少し汚いけどお許しを…
今回はViewModelに実行処理も書いているが、この処理もModel層に記述して呼び出しだけ行ったほうがよりMVVMらしいと思う

##View

```xml：Shell.xaml
<metro:MetroWindow x:Class="ZunDokoKIYOSHI.Views.Shell"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:prism="http://prismlibrary.com/"
        xmlns:prismEx="clr-namespace:Prism.InteractivityExtension;assembly=Prism.InteractivityExtension"
        prism:ViewModelLocator.AutoWireViewModel="True"
        xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls"
        xmlns:vm="clr-namespace:ZunDokoKIYOSHI.ViewModels"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        d:DataContext="{d:DesignInstance {x:Type vm:ShellViewModel}}"
        xmlns:local="clr-namespace:ZunDokoKIYOSHI.Views"
        mc:Ignorable="d"
        Title="Shell" Height="450" Width="800">
    <i:Interaction.Triggers>
        <prism:InteractionRequestTrigger SourceObject="{Binding Notification}">
            <prismEx:PopupMetroWindowAction IsModal="True"/>
        </prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <StackPanel
                Orientation="Vertical">
                <RadioButton
                    HorizontalAlignment="Center"
                    GroupName="StreamSelect"
                    IsChecked="{Binding IsLeftChecked.Value}"/>
                <TextBox
                    Text="{Binding LeftInputPattern.Value}"
                    metro:TextBoxHelper.Watermark="入力パターン"/>
                <TextBox
                    Text="{Binding LeftFinishedPattern.Value}"
                    metro:TextBoxHelper.Watermark="完成パターン"/>
                <TextBox
                    Text="{Binding LeftOutput.Value}"
                    metro:TextBoxHelper.Watermark="出力文字"/>
            </StackPanel>
            <StackPanel
                Orientation="Vertical"
                Grid.Column="1">
                <RadioButton
                    HorizontalAlignment="Center"
                    GroupName="StreamSelect"
                    IsChecked="{Binding IsRightChecked.Value}"/>
                <TextBox
                    Text="{Binding RightInputPattern.Value}"
                    metro:TextBoxHelper.Watermark="入力パターン"/>
                <TextBox
                    Text="{Binding RightFinishedPattern.Value}"
                    metro:TextBoxHelper.Watermark="完成パターン"/>
                <TextBox
                    Text="{Binding RightOutput.Value}"
                    metro:TextBoxHelper.Watermark="出力文字"/>
            </StackPanel>
        </Grid>
        <Button
            Grid.Row="1"
            Content="{Binding StartStopButtonText.Value}"
            Command="{Binding StreamStartCommand}"/>
        <Grid Grid.Row="2">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <StackPanel
                Orientation="Horizontal">
                <TextBlock Text="Count:"/>
                <TextBlock Text="{Binding StreamCount.Value}"/>
            </StackPanel>
            <TextBox
                Grid.Row="1"
                IsReadOnly="True"
                TextWrapping="Wrap"
                Text="{Binding ResultText.Value}"
                ScrollViewer.VerticalScrollBarVisibility="Auto"/>
        </Grid>
    </Grid>
</metro:MetroWindow>

```

MahApps.Metroを使用している為、WindowはMetroWindowにしている

```xml
    <i:Interaction.Triggers>
        <prism:InteractionRequestTrigger SourceObject="{Binding Notification}">
            <prismEx:PopupMetroWindowAction IsModal="True"/>
        </prism:InteractionRequestTrigger>
    </i:Interaction.Triggers>
```

ここではダイアログを表示する為のInteractionRequestTriggerを定義している
Prismの標準ではMetroWindowのダイアログを出すことができない為、[以前作った](https://qiita.com/kuro4/items/1207323fde8fb233d212)[Prism.InteractivityExtension](https://github.com/Kuro4/Prism.InteractivityExtension)を使用している

#まとめ
遊び半分で作った(ﾟ∀ﾟ)
無駄に[DL先](https://www.dropbox.com/s/aub36mb6tl9rj00/ZunDokoKIYOSHI.zip?dl=0)を置いておく
コードの全容は[ココ](https://github.com/Kuro4/ZunDokoKI-YO-SHI-)