Display traits in tree view with check boxes

This commit is contained in:
Dustin Campbell
2015-12-04 14:30:45 -08:00
parent 8ceecdcf03
commit f1879e1a06
12 changed files with 296 additions and 156 deletions
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
namespace xunit.runner.wpf
{
public static partial class Extensions
{
private class FuncComparer<T> : IComparer<T>
{
private readonly Func<T, T, int> _comparison;
public FuncComparer(Func<T, T, int> comparison)
{
_comparison = comparison;
}
public int Compare(T x, T y) => _comparison(x, y);
}
}
}
+76 -4
View File
@@ -1,13 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace xunit.runner.wpf
{
public static class Extensions
public static partial class Extensions
{
public static void AddRange<TList, TEnumerable>(this ICollection<TList> list, IEnumerable<TEnumerable> items) where TEnumerable : TList
{
@@ -24,5 +21,80 @@ namespace xunit.runner.wpf
list.Add(i);
}
}
public static int BinarySearch<T, TValue>(this ObservableCollection<T> collection, int index, int length, TValue value, IComparer<TValue> comparer, Func<T, TValue> selector)
{
comparer = comparer ?? Comparer<TValue>.Default;
var low = index;
var high = (index + length) - 1;
while (low <= high)
{
var mid = low + ((high - low) / 2);
var comp = comparer.Compare(selector(collection[mid]), value);
if (comp == 0)
{
return mid;
}
if (comp < 0)
{
low = mid + 1;
}
else
{
high = mid - 1;
}
}
return ~low;
}
public static int BinarySearch<T, TValue>(this ObservableCollection<T> collection, TValue value, IComparer<TValue> comparer, Func<T, TValue> selector)
{
return collection.BinarySearch(0, collection.Count, value, comparer, selector);
}
public static int BinarySearch<T, TValue>(this ObservableCollection<T> collection, int index, int length, TValue value, Func<TValue, TValue, int> comparison, Func<T, TValue> selector)
{
return collection.BinarySearch(index, length, value, new FuncComparer<TValue>(comparison), selector);
}
public static int BinarySearch<T, TValue>(this ObservableCollection<T> collection, TValue value, Func<TValue, TValue, int> comparison, Func<T, TValue> selector)
{
return collection.BinarySearch(0, collection.Count, value, new FuncComparer<TValue>(comparison), selector);
}
public static int BinarySearch<T, TValue>(this ObservableCollection<T> collection, TValue value, Func<T, TValue> selector)
{
return collection.BinarySearch(0, collection.Count, value, comparer: null, selector: selector);
}
public static int BinarySearch<T>(this ObservableCollection<T> collection, int index, int length, T value, IComparer<T> comparer)
{
return collection.BinarySearch(index, length, value, comparer, x => x);
}
public static int BinarySearch<T>(this ObservableCollection<T> collection, T value, IComparer<T> comparer)
{
return collection.BinarySearch(0, collection.Count, value, comparer, x => x);
}
public static int BinarySearch<T>(this ObservableCollection<T> collection, T value)
{
return collection.BinarySearch(0, collection.Count, value, Comparer<T>.Default, x => x);
}
public static int BinarySearch<T>(this ObservableCollection<T> collection, int index, int length, T value, Func<T, T, int> comparison)
{
return collection.BinarySearch(index, length, value, new FuncComparer<T>(comparison), x => x);
}
public static int BinarySearch<T>(this ObservableCollection<T> collection, T value, Func<T, T, int> comparison)
{
return collection.BinarySearch(0, collection.Count, value, new FuncComparer<T>(comparison), x => x);
}
}
}
+2 -10
View File
@@ -45,11 +45,7 @@ namespace xunit.runner.wpf
protected virtual void OnItemChanged(T sender, PropertyChangedEventArgs args)
{
var itemChanged = this.ItemChanged;
if (itemChanged != null)
{
itemChanged(sender, args);
}
this.ItemChanged?.Invoke(sender, args);
}
private TFilterArg filterArgument;
@@ -187,11 +183,7 @@ namespace xunit.runner.wpf
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
var collectionChanged = this.CollectionChanged;
if (collectionChanged != null)
{
collectionChanged(this, args);
}
this.CollectionChanged?.Invoke(this, args);
}
public void Add(T item)
+36 -21
View File
@@ -119,13 +119,13 @@
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Reload" Command="{Binding AssemblyReloadCommand}" />
@@ -139,29 +139,44 @@
<Label Content="Traits:"
Grid.Row="4" />
<ListBox Grid.Row="5"
ItemsSource="{Binding Traits}">
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:TraitViewModel">
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/>
<TreeView Grid.Row="5"
ItemsSource="{Binding Traits}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:TraitViewModel}"
ItemsSource="{Binding Children}">
<CheckBox IsChecked="{Binding IsChecked}" Content="{Binding Text}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Checked">
<cmd:EventToCommand Command="{Binding DataContext.TraitCheckedChangedCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}" />
</i:EventTrigger>
<i:EventTrigger EventName="Unchecked">
<cmd:EventToCommand Command="{Binding DataContext.TraitCheckedChangedCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</CheckBox>
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ContextMenu>
</TreeView.ItemContainerStyle>
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="Clear" Command="{Binding TraitsClearCommand}" />
</ContextMenu>
</ListBox.ContextMenu>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<cmd:EventToCommand Command="{Binding TraitSelectionChangedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
</TreeView.ContextMenu>
</TreeView>
</Grid>
</GroupBox>
+22 -14
View File
@@ -50,7 +50,7 @@ namespace xunit.runner.wpf.ViewModel
this.WindowLoadedCommand = new RelayCommand(OnExecuteWindowLoaded);
this.RunCommand = new RelayCommand(OnExecuteRun, CanExecuteRun);
this.CancelCommand = new RelayCommand(OnExecuteCancel, CanExecuteCancel);
this.TraitSelectionChangedCommand = new RelayCommand(OnExecuteTraitSelectionChanged);
this.TraitCheckedChangedCommand = new RelayCommand<TraitViewModel>(OnExecuteTraitCheckedChanged);
this.TraitsClearCommand = new RelayCommand(OnExecuteTraitsClear);
this.AssemblyReloadCommand = new RelayCommand(OnExecuteAssemblyReload, CanExecuteAssemblyReload);
this.AssemblyReloadAllCommand = new RelayCommand(OnExecuteAssemblyReloadAll);
@@ -113,6 +113,7 @@ namespace xunit.runner.wpf.ViewModel
public ICommand WindowLoadedCommand { get; }
public RelayCommand RunCommand { get; }
public RelayCommand CancelCommand { get; }
public ICommand TraitCheckedChangedCommand { get; }
public ICommand TraitSelectionChangedCommand { get; }
public ICommand TraitsClearCommand { get; }
public ICommand AssemblyReloadCommand { get; }
@@ -354,7 +355,7 @@ namespace xunit.runner.wpf.ViewModel
this.traitCollectionView.Collection.Clear();
foreach (var testCase in this.allTestCases)
{
this.traitCollectionView.Add(testCase.Traits);
this.traitCollectionView.AddRange(testCase.Traits);
}
}
@@ -476,18 +477,18 @@ namespace xunit.runner.wpf.ViewModel
}
}
private void OnTestDiscovered(List<TestCaseData> testCaseData)
private void OnTestDiscovered(List<TestCaseData> testCaseDataList)
{
var allTraits = new SortedDictionary<string, SortedSet<string>>();
foreach (var data in testCaseData)
foreach (var data in testCaseDataList)
{
AddTraits(allTraits, data);
}
this.allTestCases.AddRange(testCaseData.Select(d =>
new TestCaseViewModel(d.DisplayName, d.AssemblyPath, Convert(CreateSortedTraits(d)).ToImmutableArray())));
this.traitCollectionView.Add(Convert(allTraits));
this.allTestCases.AddRange(testCaseDataList.Select(d =>
new TestCaseViewModel(d.DisplayName, d.AssemblyPath, Convert(CreateSortedTraits(d)))));
this.traitCollectionView.AddRange(Convert(allTraits));
}
private SortedDictionary<string, SortedSet<string>> CreateSortedTraits(TestCaseData data)
@@ -499,7 +500,16 @@ namespace xunit.runner.wpf.ViewModel
private static IEnumerable<TraitViewModel> Convert(SortedDictionary<string, SortedSet<string>> allTraits)
{
return allTraits.SelectMany(pair => pair.Value.Select(value => new TraitViewModel(pair.Key, value)));
foreach (var trait in allTraits)
{
var name = trait.Key;
var values = trait.Value;
var viewModel = new TraitViewModel(name);
viewModel.AddValues(values);
yield return viewModel;
}
}
private static void AddTraits(SortedDictionary<string, SortedSet<string>> allTraits, TestCaseData data)
@@ -562,11 +572,9 @@ namespace xunit.runner.wpf.ViewModel
this.cancellationTokenSource.Cancel();
}
private void OnExecuteTraitSelectionChanged()
private void OnExecuteTraitCheckedChanged(TraitViewModel trait)
{
this.searchQuery.TraitSet = new HashSet<TraitViewModel>(
this.traitCollectionView.Collection.Where(x => x.IsSelected),
TraitViewModelComparer.Instance);
this.searchQuery.TraitSet = this.traitCollectionView.GetCheckedTraits();
FilterAfterDelay();
}
@@ -574,7 +582,7 @@ namespace xunit.runner.wpf.ViewModel
{
foreach (var cur in this.traitCollectionView.Collection)
{
cur.IsSelected = false;
cur.IsChecked = false;
}
}
+2 -6
View File
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace xunit.runner.wpf.ViewModel
{
@@ -12,6 +8,6 @@ namespace xunit.runner.wpf.ViewModel
public bool IncludePassedTests = true;
public bool IncludeSkippedTests = true;
public string SearchString = string.Empty;
public HashSet<TraitViewModel> TraitSet = new HashSet<TraitViewModel>(TraitViewModelComparer.Instance);
public ISet<TraitViewModel> TraitSet = new HashSet<TraitViewModel>(TraitViewModel.EqualityComparer);
}
}
@@ -1,12 +1,7 @@
using GalaSoft.MvvmLight;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using xunit.runner.data;
using System.Collections.Generic;
namespace xunit.runner.wpf.ViewModel
{
@@ -14,11 +9,11 @@ namespace xunit.runner.wpf.ViewModel
{
private TestState _state = TestState.NotRun;
public TestCaseViewModel(string displayName, string assemblyFileName, ImmutableArray<TraitViewModel> traits)
public TestCaseViewModel(string displayName, string assemblyFileName, IEnumerable<TraitViewModel> traits)
{
this.DisplayName = displayName;
this.AssemblyFileName = assemblyFileName;
this.Traits = traits;
this.Traits = traits.ToImmutableArray();
}
public string DisplayName { get; }
@@ -1,54 +1,36 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace xunit.runner.wpf.ViewModel
{
public sealed partial class TraitCollectionView
{
private readonly TraitViewModelComparer _comparer = TraitViewModelComparer.Instance;
private readonly ObservableCollection<TraitViewModel> _collection = new ObservableCollection<TraitViewModel>();
public ObservableCollection<TraitViewModel> Collection { get; } = new ObservableCollection<TraitViewModel>();
public ObservableCollection<TraitViewModel> Collection => _collection;
public TraitCollectionView()
public void AddRange(IEnumerable<TraitViewModel> traits)
{
}
public void Add(IEnumerable<TraitViewModel> traitList)
{
foreach (var traitViewModel in traitList)
foreach (var trait in traits)
{
InsertIfNotPresent(traitViewModel);
var index = Collection.BinarySearch(trait, TraitViewModel.Comparer);
if (index < 0)
{
Collection.Insert(~index, trait);
}
else
{
// This trait already exists, add more values.
var originalTrait = Collection[index];
originalTrait.AddValues(trait.Children.Select(x => x.Text));
}
}
}
private void InsertIfNotPresent(TraitViewModel trait)
public ISet<TraitViewModel> GetCheckedTraits()
{
// TODO: make it a binary search
for (int i = 0; i < _collection.Count; i++)
{
var current = _collection[i];
var result = _comparer.Compare(trait, current);
if (result < 0)
{
_collection.Insert(i, trait);
return;
}
if (result == 0)
{
return;
}
}
_collection.Add(trait);
return new HashSet<TraitViewModel>(
Collection.SelectMany(x => x.Children).Where(x => x.IsChecked == true),
comparer: TraitViewModel.EqualityComparer);
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
namespace xunit.runner.wpf.ViewModel
{
public partial class TraitViewModel
{
private static readonly TraitViewModelComparer _comparer = new TraitViewModelComparer();
internal static IComparer<TraitViewModel> Comparer => _comparer;
internal static IEqualityComparer<TraitViewModel> EqualityComparer => _comparer;
private class TraitViewModelComparer : IEqualityComparer<TraitViewModel>, IComparer<TraitViewModel>
{
public int Compare(TraitViewModel x, TraitViewModel y) => StringComparer.Ordinal.Compare(x.Text, y.Text);
public bool Equals(TraitViewModel x, TraitViewModel y) => StringComparer.Ordinal.Equals(x.Text, y.Text);
public int GetHashCode(TraitViewModel obj) => obj.Text.GetHashCode();
}
}
}
+94 -17
View File
@@ -1,31 +1,108 @@
using GalaSoft.MvvmLight;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using GalaSoft.MvvmLight;
namespace xunit.runner.wpf.ViewModel
{
public sealed class TraitViewModel : ViewModelBase
public partial class TraitViewModel : ViewModelBase
{
private bool _isSelected;
private readonly TraitViewModel _parent;
private bool? _isChecked;
private bool _isExpanded;
private string _text;
public string Name { get; }
public string Value { get; }
public string DisplayName { get; }
public ObservableCollection<TraitViewModel> Children { get; }
public bool IsSelected
public TraitViewModel(string text)
: this(null, text)
{
get { return _isSelected; }
set { Set(ref _isSelected, value); }
}
public TraitViewModel(string name, string value)
private TraitViewModel(TraitViewModel parent, string text)
{
Name = name;
Value = value;
DisplayName = $"{Name}={Value}";
this._parent = parent;
this._isChecked = false;
this._isExpanded = true;
this._text = text;
this.Children = new ObservableCollection<TraitViewModel>();
}
private void SetIsChecked(bool? value, bool updateChildren, bool updateParent)
{
if (value == this._isChecked)
{
return;
}
this._isChecked = value;
if (updateChildren && value != null)
{
foreach (var child in this.Children)
{
child.SetIsChecked(value, updateChildren: true, updateParent: false);
}
}
if (updateParent && _parent != null)
{
_parent.VerifyCheckState();
}
this.RaisePropertyChanged(nameof(IsChecked));
}
private void VerifyCheckState()
{
bool? state = null;
var isFirst = true;
foreach (var child in this.Children)
{
if (isFirst)
{
state = child.IsChecked;
isFirst = false;
}
else if (state != child.IsChecked)
{
state = null;
break;
}
}
this.SetIsChecked(state, updateChildren: false, updateParent: true);
}
public void AddValues(IEnumerable<string> values)
{
foreach (var value in values)
{
var index = this.Children.BinarySearch(value, StringComparer.Ordinal.Compare, v => v.Text);
if (index < 0)
{
this.Children.Insert(~index, new TraitViewModel(this, value));
}
}
}
public bool? IsChecked
{
get { return _isChecked; }
set { SetIsChecked(value, updateChildren: true, updateParent: true); }
}
public bool IsExpanded
{
get { return _isExpanded; }
set { Set(ref _isExpanded, value); }
}
public string Text
{
get { return _text; }
set { Set(ref _text, value); }
}
}
}
@@ -1,38 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace xunit.runner.wpf.ViewModel
{
internal sealed class TraitViewModelComparer : IEqualityComparer<TraitViewModel>, IComparer<TraitViewModel>
{
internal static readonly TraitViewModelComparer Instance = new TraitViewModelComparer();
private readonly StringComparer _comparer = StringComparer.Ordinal;
public int Compare(TraitViewModel x, TraitViewModel y)
{
var result = _comparer.Compare(x.Name, y.Name);
if (result != 0)
{
return result;
}
return _comparer.Compare(x.Value, y.Value);
}
public bool Equals(TraitViewModel x, TraitViewModel y)
{
return _comparer.Equals(x.Name, y.Name)
&& _comparer.Equals(x.Value, y.Value);
}
public int GetHashCode(TraitViewModel obj)
{
return obj.Name.GetHashCode();
}
}
}
+2 -1
View File
@@ -102,14 +102,15 @@
</Compile>
<Compile Include="Storage.cs" />
<Compile Include="Storage.WindowPlacement.cs" />
<Compile Include="Extensions.FuncComparer.cs" />
<Compile Include="ViewModel\TraitCollectionView.cs" />
<Compile Include="ViewModel\AssemblyAndConfigFile.cs" />
<Compile Include="ViewModel\MainViewModel.cs" />
<Compile Include="ViewModel\SearchQuery.cs" />
<Compile Include="ViewModel\TestCaseViewModel.cs" />
<Compile Include="ViewModel\TestAssemblyViewModel.cs" />
<Compile Include="ViewModel\TraitViewModel.Comparer.cs" />
<Compile Include="ViewModel\TraitViewModel.cs" />
<Compile Include="ViewModel\TraitViewModelComparer.cs" />
<Compile Include="ViewModel\ViewModelLocator.cs" />
<Page Include="LoadingDialog.xaml">
<SubType>Designer</SubType>