皆様ご存じズンドコキヨシをWPFでやってみた
ついでに最近ずっと聞き続けているGYARIさんのテッテーテレッテーも同じようにやってみた('ω')
18/07/17 MVVMらしく改修
作ったもの
入力パターン(カンマ区切り)、完成パターン(カンマ区切り)と出力文字を指定してStartを押すと入力パターンからランダムに文字を出力して、完成パターンと一致したら出力文字を末尾につけてダイアログを出して終了するよ(=゚ω゚)ノ
また、文字出力中にStopを押すと停止できる(ΦωΦ)
作り方
ライブラリは
Prism
ReactiveProperty
MahApps.Metro
を使用
Model
Modelといっても、今回は入力パターンと完成パターンを指定して完成パターンに一致するまでランダムに文字列を出力するメソッドを作るだけ
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にしてyield returnすることでforeachで一個づつ値が取れるようにしている
ViewModel
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を実装していないとメモリリークするようなのでINotifyPropertyChangedを実装したPrismのBindableBaseを継承している
コードが少し汚いけどお許しを…
今回はViewModelに実行処理も書いているが、この処理もModel層に記述して呼び出しだけ行ったほうがよりMVVMらしいと思う
View
<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にしている
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding Notification}">
<prismEx:PopupMetroWindowAction IsModal="True"/>
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
ここではダイアログを表示する為のInteractionRequestTriggerを定義している
Prismの標準ではMetroWindowのダイアログを出すことができない為、以前作ったPrism.InteractivityExtensionを使用している