[WPF]直線の片方の頂点を選択した時、SelectedItemsが直線の両方の頂点を示してしまう
Q&A
Closed
お世話になっております。
解決したいこと
C# + WPFでベクターグラフィックスドローイングツールを開発しています。
※下にソースコードへの案内を記載しております。よろしければそちらを参照ください。
この記事のタイトルはちょっとわかりにくいかもしれないので、順を追って説明していきます。
発生している問題・エラー
ユニットテストでテスト失敗になっています。
そのテストは「直線の頂点1つが選択されている」というテストメソッドで、コードは下記のようなものです。
using boilersGraphics.Models;
using boilersGraphics.ViewModels;
using NUnit.Framework;
using System.Linq;
using System.Windows;
namespace Question20210925.Test
{
[TestFixture]
public class SelectedItemsTest
{
:
[Test]
public void 直線の頂点1つが選択されている()
{
boilersGraphics.App.IsTest = true;
var mainWindowViewModel = new MainWindowViewModel(null);
var viewModel = new DiagramViewModel(mainWindowViewModel, 1000, 1000);
viewModel.Layers.Clear();
var layer1 = new Layer();
layer1.Name.Value = "レイヤー1";
viewModel.Layers.Add(layer1);
layer1.IsSelected.Value = true;
var item1 = new StraightConnectorViewModel(viewModel, new Point(10, 10), new Point(20, 20));
viewModel.AddItemCommand.Execute(item1);
layer1.Children[0].IsSelected.Value = true;
item1.SnapPoint0VM.Value.IsSelected.Value = true;
item1.SnapPoint1VM.Value.IsSelected.Value = false;
Assert.That(viewModel.SelectedItems.Value.ToList(), Has.Count.EqualTo(1)); //Expected: property Count equal to 1 But was: 2
Assert.That(viewModel.SelectedItems.Value.ElementAt(0).IsSelected.Value, Is.True);
}
}
}
この問題の背景を説明しますと、この製品boiler's Graphicsは描画されたアイテムをクリックして選択したり、なげなわツールで選択すると、内部的にDiagramViewModelクラスのSelectedItemsプロパティで選択されたアイテムをアイテム全体から抽出して返すような仕組みになっています。これはReactivePropertyにより実現しました。
しかし、先日、直線の頂点の片方を選択した時に、矢印キーで移動させると、その選択した直線の頂点の片方が動くようにプログラミングしたつもりだったのですが、本記事で挙げているテストコードで失敗になってしまいました。
つまり、直線の頂点の片方を選択した時に、矢印キーで移動させると、その選択した直線の頂点の片方が動くはずが、その選択した直線の頂点の両方が選択されてしまい、結果的に直線全体が動いてしまうという現象が起きています。
恐らく、DiagramViewModelクラスのSelectedItemsプロパティに問題が潜んでいるのだと思いますが、どのようにかけば当テストケースが通るようになるでしょうか。
public class DiagramViewModel : BindableBase, IDiagramViewModel, IDisposable
{
:
public ReadOnlyReactivePropertySlim<SelectableDesignerItemViewModelBase[]> AllItems { get; }
public ReadOnlyReactivePropertySlim<SelectableDesignerItemViewModelBase[]> SelectedItems { get; }
:
public DiagramViewModel(MainWindowViewModel mainWindowViewModel, int width, int height)
{
:
AllItems = Layers.CollectionChangedAsObservable()
.Select(_ => Layers.Select(x => x.LayerItemsChangedAsObservable()).Merge()
.Merge(this.ObserveProperty(y => y.BackgroundItem.Value).ToUnit()))
.Switch()
.Select(_ => Layers.SelectRecursive<LayerTreeViewItemBase, LayerTreeViewItemBase>(x => x.Children)
.Where(x => x.GetType() == typeof(LayerItem))
.Select(y => (y as LayerItem).Item.Value)
.Union(new SelectableDesignerItemViewModelBase[] { BackgroundItem.Value })
.ToArray())
.ToReadOnlyReactivePropertySlim(Array.Empty<SelectableDesignerItemViewModelBase>());
:
SelectedItems = Layers.CollectionChangedAsObservable()
.Select(_ => Layers.Select(x => x.SelectedLayerItemsChangedAsObservable()).Merge()
.Merge(Layers.SelectRecursive<LayerTreeViewItemBase, LayerTreeViewItemBase>(x => x.Children)
.Where(x => x.GetType() == typeof(LayerItem))
.Select(y => (y as LayerItem).Item.Value)
.OfType<ConnectorBaseViewModel>()
.SelectMany(x => new List<SnapPointViewModel>() { x.SnapPoint0VM.Value, x.SnapPoint1VM.Value })
.ToObservableCollection()
.ObserveElementProperty(x => x.IsSelected.Value)
.ToUnit()))
.Switch()
.Select(_ => Layers.SelectRecursive<LayerTreeViewItemBase, LayerTreeViewItemBase>(x => x.Children)
.Where(x => x.GetType() == typeof(LayerItem))
.Select(y => (y as LayerItem).Item.Value)
.Except(Layers.SelectRecursive<LayerTreeViewItemBase, LayerTreeViewItemBase>(x => x.Children)
.Where(x => x.GetType() == typeof(LayerItem))
.Select(y => (y as LayerItem).Item.Value)
.OfType<ConnectorBaseViewModel>())
.Union(Layers.SelectRecursive<LayerTreeViewItemBase, LayerTreeViewItemBase>(x => x.Children)
.Where(x => x.GetType() == typeof(LayerItem))
.Select(y => (y as LayerItem).Item.Value)
.OfType<ConnectorBaseViewModel>()
.SelectMany(x => new List<SnapPointViewModel>() { x.SnapPoint0VM.Value, x.SnapPoint1VM.Value })
)
.Where(z => z.IsSelected.Value == true)
.OrderBy(z => z.SelectedOrder.Value)
.ToArray())
.ToReadOnlyReactivePropertySlim(Array.Empty<SelectableDesignerItemViewModelBase>());
}
}
internal static class Extensions
{
:
public static IEnumerable<T2> SelectRecursive<T1, T2>(this IEnumerable<T1> source, Func<T2, IEnumerable<T2>> selector) where T1 : class where T2 : class
{
foreach (var parent in source)
{
yield return parent as T2;
var children = selector(parent as T2);
foreach (var child in SelectRecursive(children, selector))
yield return child;
}
}
:
}
ちなみに、本現象をQiitaに掲載するために当プロジェクトの最小機能版を作成したのですが、
それを動かして試してみた様子をGifアニメーションで確認できるようにしてみました。
ちょっとよく見ないとわかりにくいのですが、
1. 直線を描画
2. なげなわツールで直線の頂点の片方を選択
3. 選択ツール(マウスポインターの形のツール)で何もないところをクリック(直線を矢印キーで動かすための儀式です、たぶんなんらかのバグ)
4. 再度なげなわツールで直線の頂点の片方を選択
5. 矢印キーで直線を動かす(1ピクセル単位で動くのでぱっと見わかりづらいです)
Gifアニメーションは上記のような内容です。
該当するソースコード
当プロジェクト(ネタ元)
boiler's Graphics
https://github.com/dhq-boiler/boiler-s-Graphics
コミット:aa22aeb付近(develop)
当プロジェクトの最小機能版
https://github.com/dhq-boiler/Qiita/tree/develop/Question20210925
自分で試したこと
- DiagramViewModel.SelectedItemsプロパティを見直しました
- 最小機能版を抽出し、バグが再現するか確認しました→再現した
- 最小機能版のテストプロジェクトを抽出し、テストが失敗するか確認しました→テスト失敗した
何か私の見落とし、致命的な勘違いなど気づいたところがあれば、回答していただけると助かります。よろしくお願いいたします。
追伸
このバグが取り除けたら、テストカバレッジ率を上げるべく作業したいです。
現在24%で、目標の75%~80%には遠く及ばないので、大手を振って大々的に宣伝できずにいます。
この数字は非常に恥ずかしいので、早くこのバグを取り除いて作業したいです...。