この記事は公開から3年以上経過しています。
プログラムの結合テストで
- テストケースXのデータファイルをフォルダ○○へコピーする。
- データファイルの処理完了まで待機する。
- 結果が○○であることを確認する。
のような非効率でヒューマンエラーが発生しやすい単調な反復作業を行う必要があったため、最近ご無沙汰していたWPFのリハビリも兼ねて即席ツールをオフタイムに作成してみましたので紹介します。
XAMLを駆使したゴリゴリのMVVM実装にはなっていませんが、これからWPFでMVVMプログラミングを始めたい方や、プレゼンテーションとドメインの分離を何となく理解したい方のサンプルとしてもお使い頂けます。
ちなみに今回はテスト進行や工数の都合から即席ツールで凌ぎましたが、テスト開始前や序盤ならFriendlyやPowerAutomateなどを使い、もっとガッツリ自動化しておきたいところです。
起動方法
後述のツールソースをビルドしたうえで
DataCopyBrowser.exe コピー元ディレクトリパス コピー先ディレクトリパス
のような形で起動パラメータにコピー元とコピー先フォルダを指定します。
パラメータのパスにスペースが含まれる場合は、パス全体をダブルクォーテーションで囲ったものを指定してください。
Windowsアプリケーションにはコマンドラインパラメータにパスを渡すときに闇雲にダブルクォーテーションで囲むとバックスラッシュがエスケープ扱いになり意図しないパスになるという困った仕様があるので注意が必要です。
(C:\Temp
とC:\
をダブルクォーテーションで囲ってコマンドラインパラメータに渡すと再現できます。)
使い方
バッチファイルやテストプログラムからパスを変更しながら本ツールを呼び出す
のような利用を想定しています。
画面中央の➠
ボタン、またはAlt
+→
キーを押下するとコピー確認ダイアログが表示されます。ここではい
を選択すると、左のフォルダ内の全ファイルを右のフォルダにコピーします。
コピーされるのは同一階層のファイルのみ、コピー先に同一ファイル名のファイルが存在する場合はコピーをスキップします。
エクスプローラのファイル表示部は処理対象ファイルやディレクトリを視覚的に表すためで、ここでファイル操作を行うことを目的としていません。シェルのデフォルト機能としてファイル操作が可能ですが、あくまでWebBrowserコントロールの既定の振る舞いによるものです。
バイナリとサンプルソースコード
ビルド済バイナリとサンプルソースコード一式(Visual Studio 2019/C#/WPF/.NET Framework 4.8)をこちらに用意しました。
ライセンスはMITライセンス、VirusTotalでウイルスチェック済です。
以下、Model/View/ViewModelだけ抜粋しておきます。
View(MainWindow.xaml)
<Window
x:Class="DataCopyBrowser.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:DataCopyBrowser.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DataCopyBrowser.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Width="800"
Height="450"
ResizeMode="CanResizeWithGrip"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.Title>
<MultiBinding StringFormat="データコピーツール ({0} ➡ {1})">
<Binding Mode="OneWay" Path="SrcDir" />
<Binding Mode="OneWay" Path="DstDir" />
</MultiBinding>
</Window.Title>
<Window.Resources>
<converters:StatusMessageConverter x:Key="stsConv" />
</Window.Resources>
<Window.InputBindings>
<KeyBinding Command="{Binding CopyCmd}" Gesture="Alt+Right" />
<KeyBinding Command="{Binding RefreshCmd}" Gesture="F5" />
</Window.InputBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBox
Grid.Row="0"
Grid.Column="0"
IsReadOnly="True"
Text="{Binding SrcDir, Mode=OneWay}" />
<TextBox
Grid.Row="0"
Grid.Column="2"
IsReadOnly="True"
Text="{Binding DstDir, Mode=OneWay}" />
<WebBrowser
Name="wb1"
Grid.Row="1"
Grid.Column="0" />
<Button
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
Command="{Binding CopyCmd}"
Content=" ➡ " />
<WebBrowser
Name="wb2"
Grid.Row="1"
Grid.Column="2" />
<StatusBar Grid.Row="2" Grid.ColumnSpan="3">
<TextBlock Text="{Binding Status, Mode=OneWay, Converter={StaticResource stsConv}}" />
</StatusBar>
</Grid>
</Window>
View(MainWindow.xaml.cs/コードビハインド)
using System;
using System.Windows;
using DataCopyBrowser.Models;
using DataCopyBrowser.ViewModels;
namespace DataCopyBrowser.Views
{
/// <summary>
/// View
/// </summary>
public sealed partial class MainWindow : Window
{
private readonly MainWindowViewModel _viewModel;
private readonly FileCopyJobModel _model;
public MainWindow(string[] args)
{
InitializeComponent();
wb1.Navigating += (o, e) => e.Cancel = wb1.Source != null && wb1.Source != e.Uri;
wb2.Navigating += (o, e) => e.Cancel = wb2.Source != null && wb2.Source != e.Uri;
_model = new FileCopyJobModel();
_viewModel = new MainWindowViewModel(_model);
DataContext = _viewModel;
_viewModel.PropertyChanged += (o, e) =>
{
switch (e.PropertyName)
{
case nameof(MainWindowViewModel.SrcDir):
if (_model.SrcDir.Length > 0)
wb1.Navigate(new Uri(_model.SrcDir));
break;
case nameof(MainWindowViewModel.DstDir):
if (_model.DstDir.Length > 0)
wb2.Navigate(new Uri(_model.DstDir));
break;
}
};
_viewModel.CopyConfirmation = () =>
{
return MessageBox.Show(this, "ファイルをコピーしますか?", "確認", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No) == MessageBoxResult.Yes;
};
_viewModel.RefreshView = () =>
{
wb1.Refresh();
wb2.Refresh();
};
Loaded += (o, e) =>
{
_model.SrcDir = args[0];
_model.DstDir = args[1];
};
}
}
}
ViewModel(MainWindowViewModel.cs)
using System;
using DataCopyBrowser.Classes;
using DataCopyBrowser.Constants;
using DataCopyBrowser.Models;
namespace DataCopyBrowser.ViewModels
{
/// <summary>
/// ViewModel
/// </summary>
public sealed class MainWindowViewModel : NotificationBase
{
private readonly FileCopyJobModel _model;
public MainWindowViewModel(FileCopyJobModel model)
{
_model = model;
_model.OnStatusChanged += (o, e) =>
{
NotifyPropChanged(nameof(SrcDir));
NotifyPropChanged(nameof(DstDir));
NotifyPropChanged(nameof(Status));
CopyCmd.NotifyChanged();
};
CopyCmd = new RelayCommand(
(_) => _model.Status.Code != StatusCode.NoSrcFiles,
(_) =>
{
if (CopyConfirmation())
_model.Copy();
});
RefreshCmd = new RelayCommand(
(_) => true,
(_) =>
{
RefreshView();
_model.StateChanged();
CopyCmd.NotifyChanged();
});
}
public RelayCommand CopyCmd { get; }
public RelayCommand RefreshCmd { get; }
public string SrcDir => _model.SrcDir;
public string DstDir => _model.DstDir;
public FileCopyJobStatus Status => _model.Status;
public Func<bool> CopyConfirmation { get; set; } = () => false;
public Action RefreshView { get; set; } = () => { };
}
}
Model(FileCopyModel.cs)
using System;
using System.IO;
using System.Linq;
using DataCopyBrowser.Classes;
using DataCopyBrowser.Constants;
namespace DataCopyBrowser.Models
{
/// <summary>
/// Model
/// </summary>
public sealed class FileCopyJobModel
{
private readonly FileCopyJobStatus _modelStatus = new FileCopyJobStatus();
private string _srcDir = string.Empty;
private string _dstDir = string.Empty;
public event EventHandler OnStatusChanged;
public string SrcDir
{
get => _srcDir;
set
{
_srcDir = value;
StateChanged();
}
}
public string DstDir
{
get => _dstDir;
set
{
_dstDir = value;
StateChanged();
}
}
public FileCopyJobStatus Status => _modelStatus;
public void Copy()
{
var srcFilePaths = Directory.GetFiles(SrcDir);
_modelStatus.NumOfFilesCopied = 0;
foreach (var srcFilePath in srcFilePaths)
{
var dstFileName = Path.GetFileName(srcFilePath);
var dstFilePath = Path.Combine(_dstDir, dstFileName);
if (!File.Exists(dstFilePath))
{
try
{
File.Copy(srcFilePath, dstFilePath);
++_modelStatus.NumOfFilesCopied;
}
catch { }
}
if (_modelStatus.NumOfFilesCopied > 0)
_modelStatus.Code = StatusCode.CopyCompleted;
else
_modelStatus.Code = StatusCode.FailedToCopy;
RaiseStatusChanged();
}
}
public void StateChanged()
{
if (string.IsNullOrEmpty(_srcDir) || !Directory.Exists(_srcDir))
_modelStatus.Code = StatusCode.SrcDirError;
else if (string.IsNullOrEmpty(_dstDir) || !Directory.Exists(_dstDir))
_modelStatus.Code = StatusCode.DstDirError;
else if (Directory.GetFiles(_srcDir).Length == 0)
_modelStatus.Code = StatusCode.NoSrcFiles;
else
_modelStatus.Code = StatusCode.Ready;
RaiseStatusChanged();
}
private void RaiseStatusChanged() => OnStatusChanged?.Invoke(this, EventArgs.Empty);
}
}
以上です。