Skip to content

Commit

Permalink
Minor refactoring + unit tests added
Browse files Browse the repository at this point in the history
  • Loading branch information
hwndmaster committed Jul 14, 2021
1 parent 42e8ecb commit d4ae2a1
Show file tree
Hide file tree
Showing 32 changed files with 966 additions and 132 deletions.
14 changes: 12 additions & 2 deletions PriceChecker.Core/Services/PriceSeeker.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
Expand Down Expand Up @@ -37,14 +38,23 @@ public async Task<PriceSeekResult[]> SeekAsync(Product product, CancellationToke
});

return await Task.WhenAll(result)
.ContinueWith(x => x.Result?.Where(x => x != null).ToArray() ?? new PriceSeekResult[0]);
.ContinueWith(x => x.Result?.Where(x => x != null).ToArray() ?? new PriceSeekResult[0], TaskContinuationOptions.OnlyOnRanToCompletion);
}

private async Task<PriceSeekResult> Seek(ProductSource productSource, CancellationToken cancel)
{
var agent = productSource.Agent;
var url = string.Format(agent.Url, productSource.AgentArgument);
var content = await _trickyHttpClient.DownloadContent(url, cancel);
string content;
try
{
content = await _trickyHttpClient.DownloadContent(url, cancel);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed loading content for source `{productSource.AgentId}`, url = `{url}`");
throw;
}
if (content == null)
return null;

Expand Down
1 change: 0 additions & 1 deletion PriceChecker.Infrastructure/Events/EventBus.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Reactive;
using System.Reactive.Linq;

Expand Down
12 changes: 1 addition & 11 deletions PriceChecker.UI.Forms/AutoGrid/AttachingBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private void OnItemsSourceChanged(object sender, EventArgs e)
}

if (AssociatedObject.SelectionMode == DataGridSelectionMode.Extended &&
typeof(ISelectable).IsAssignableFrom(GetItemType()))
typeof(ISelectable).IsAssignableFrom(Helpers.GetListItemType(AssociatedObject.ItemsSource)))
{
BindIsSelected();
}
Expand Down Expand Up @@ -118,15 +118,5 @@ private void BindIsSelected()
rowStyle.Setters.Add(new Setter(DataGrid.IsSelectedProperty, binding));
AssociatedObject.RowStyle = rowStyle;
}

private Type GetItemType()
{
var sourceCollection = AssociatedObject.ItemsSource;
if (sourceCollection is ListCollectionView listCollectionView)
{
sourceCollection = listCollectionView.SourceCollection;
}
return sourceCollection.GetType().GetGenericArguments().Single();
}
}
}
4 changes: 2 additions & 2 deletions PriceChecker.UI.Forms/AutoGrid/Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ public static IEnumerable GetItemsSource(DependencyObject element)
return (IEnumerable) element.GetValue(ItemsSourceProperty);
}

public static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var itemType = e.NewValue.GetType().GetGenericArguments().Single();
var itemType = Helpers.GetListItemType(e.NewValue);
var properties = itemType.GetProperties();
var groupByProps = properties
.Where(x => x.GetCustomAttributes(false).OfType<GroupByAttribute>().Any())
Expand Down
26 changes: 26 additions & 0 deletions PriceChecker.UI.Forms/Helpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Data;

namespace Genius.PriceChecker.UI.Forms
{
internal class Helpers
{
public static Type GetListItemType(object value)
{
if (value is ListCollectionView listCollectionView)
value = listCollectionView.SourceCollection;

if (value is ITypedObservableList typedObservableList)
return typedObservableList.ItemType;

return value.GetType().GetGenericArguments().Single();
}

public static string MakeCaptionFromPropertyName(string propertyName)
{
return Regex.Replace(propertyName, @"(?<=[^$])([A-Z])", " $1");
}
}
}
26 changes: 26 additions & 0 deletions PriceChecker.UI.Forms/TypedObservableList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace Genius.PriceChecker.UI.Forms
{
public interface ITypedObservableList
{
Type ItemType { get; }
}

public class TypedObservableList<TContract, TType> : ObservableCollection<TContract>, ITypedObservableList, ITypedList
{
public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
{
return TypeDescriptor.GetProperties(typeof(TType));
}

public string GetListName(PropertyDescriptor[] listAccessors)
{
return null;
}

public Type ItemType => typeof(TType);
}
}
2 changes: 1 addition & 1 deletion PriceChecker.UI.Forms/ViewModels/TabViewModelBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Genius.PriceChecker.UI.Forms.ViewModels
{
public interface ITabViewModel
public interface ITabViewModel : IViewModel
{
IActionCommand Activated { get; }
IActionCommand Deactivated { get; }
Expand Down
34 changes: 28 additions & 6 deletions PriceChecker.UI.Forms/ViewModels/ViewModelBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@

namespace Genius.PriceChecker.UI.Forms.ViewModels
{
public abstract class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
public interface IViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
bool TryGetPropertyValue(string propertyName, out object value);
}

public abstract class ViewModelBase : IViewModel
{
protected readonly ConcurrentDictionary<string, object> _propertyBag = new();
private readonly Dictionary<string, List<ValidationRule>> _validationRules = new();
private readonly Dictionary<string, List<string>> _errors = new();

protected bool PropertiesAreInitialized = false;
private bool _suspendDirtySet = false;

public ViewModelBase()
{
Expand All @@ -34,11 +38,29 @@ public IEnumerable GetErrors(string propertyName)
new List<string>();
}

internal bool TryGetPropertyValue(string propertyName, out object value)
public bool TryGetPropertyValue(string propertyName, out object value)
{
return _propertyBag.TryGetValue(propertyName, out value);
}

protected void InitializeProperties(Action action)
{
_suspendDirtySet = true;
try
{
action();

if (this is IHasDirtyFlag hasDirtyFlag)
{
hasDirtyFlag.IsDirty = false;
}
}
finally
{
_suspendDirtySet = false;
}
}

protected T GetOrDefault<T>(T defaultValue = default(T), [CallerMemberName] string name = null)
{
if (name == null)
Expand Down Expand Up @@ -74,9 +96,9 @@ protected void RaiseAndSetIfChanged<T>(T value, Action<T, T> valueChangedHandler
_propertyBag.AddOrUpdate(name, _ => value, (_, __) => value);
OnPropertyChanged(name);

if (this is IHasDirtyFlag hasDirtyFlag &&
if (!_suspendDirtySet &&
this is IHasDirtyFlag hasDirtyFlag &&
name != nameof(IHasDirtyFlag.IsDirty) &&
PropertiesAreInitialized &&
(this is not ISelectable || name != nameof(ISelectable.IsSelected)))
{
hasDirtyFlag.IsDirty = true;
Expand Down
4 changes: 2 additions & 2 deletions PriceChecker.UI.Forms/ViewModels/ViewModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ namespace Genius.PriceChecker.UI.Forms.ViewModels
public static class ViewModelExtensions
{
public static IDisposable WhenChanged<TViewModel, TProperty>(this TViewModel viewModel, Expression<Func<TViewModel, TProperty>> propertyAccessor, Action<TProperty> handler)
where TViewModel : ViewModelBase
where TViewModel : IViewModel
{
var propName = ExpressionHelpers.GetPropertyName(propertyAccessor);

return WhenChanged(viewModel, propName, handler);
}

public static IDisposable WhenChanged<TProperty>(this ViewModelBase viewModel, string propertyName, Action<TProperty> handler)
public static IDisposable WhenChanged<TProperty>(this IViewModel viewModel, string propertyName, Action<TProperty> handler)
{
PropertyChangedEventHandler fn = (_, args) =>
{
Expand Down
2 changes: 1 addition & 1 deletion PriceChecker.UI.Forms/WpfHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static void AddFlyout<T>(FrameworkElement owner, string isOpenBindingPath

public static DataGridTemplateColumn CreateButtonColumn(string commandPath, string iconName)
{
var caption = commandPath.Replace("Command", "");
var caption = Helpers.MakeCaptionFromPropertyName(commandPath.Replace("Command", ""));

var buttonFactory = new FrameworkElementFactory(typeof(Button));
buttonFactory.SetBinding(Button.CommandProperty, new Binding(commandPath));
Expand Down
137 changes: 137 additions & 0 deletions PriceChecker.UI.Tests/Helpers/TrackerScanContextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System;
using System.Reactive.Subjects;
using AutoFixture;
using Genius.PriceChecker.Core.Messages;
using Genius.PriceChecker.Core.Models;
using Genius.PriceChecker.UI.Helpers;
using Xunit;

namespace Genius.PriceChecker.UI.Tests.Helpers
{
public class TrackerScanContextTests : TestBase
{
private readonly Fixture _fixture = new();
private readonly TrackerScanContext _sut;

// Session values:
private readonly Subject<ProductAutoScanStartedEvent> _productAutoScanStartedEventSubject;
private TrackerScanStatus? _lastStatus = null;
private double? _lastProgress = null;

public TrackerScanContextTests()
{
_productAutoScanStartedEventSubject = CreateEventSubject<ProductAutoScanStartedEvent>();

_sut = new TrackerScanContext(EventBusMock.Object);

_sut.ScanProgress.Subscribe(x => {
_lastStatus = x.Status;
_lastProgress = x.Progress;
});
}

[Fact]
public void NotifyStarted__Resets_state_and_calculates_initial_progress()
{
// Arrange
var count = 10;

// Act
_sut.NotifyStarted(count);

// Verify
var expectedProgress = 1d / (count * 2);
Assert.True(_sut.IsStarted);
Assert.False(_sut.HasErrors);
Assert.False(_sut.HasNewLowestPrice);
Assert.Equal(0, _sut.FinishedJobs);
Assert.Equal(TrackerScanStatus.InProgress, _lastStatus);
Assert.Equal(expectedProgress, _lastProgress);
}

[Fact]
public void NotifyProgressChange__When_ScannedOk__Increases_progress()
{
// Arrange
var count = 2;
_sut.NotifyStarted(count);

// Act
_sut.NotifyProgressChange(ProductScanStatus.ScannedOk);

// Verify
var expectedProgress = 0.5d; // 50% of 2 jobs
Assert.True(_sut.IsStarted);
Assert.False(_sut.HasErrors);
Assert.False(_sut.HasNewLowestPrice);
Assert.Equal(1, _sut.FinishedJobs);
Assert.Equal(TrackerScanStatus.InProgress, _lastStatus);
Assert.Equal(expectedProgress, _lastProgress);
}

[Fact]
public void NotifyProgressChange__When_scanned_last_job__Finishes_progress()
{
// Arrange
_sut.NotifyStarted(2);

// Act
_sut.NotifyProgressChange(ProductScanStatus.ScannedOk);
_sut.NotifyProgressChange(ProductScanStatus.ScannedOk);

// Verify
Assert.False(_sut.IsStarted);
Assert.False(_sut.HasErrors);
Assert.False(_sut.HasNewLowestPrice);
Assert.Equal(2, _sut.FinishedJobs);
Assert.Equal(TrackerScanStatus.Finished, _lastStatus);
Assert.Equal(1, _lastProgress);
}

[Fact]
public void NotifyProgressChange__When_ScannedWithErrors__Reports_about_errors()
{
// Arrange
_sut.NotifyStarted(_fixture.Create<int>());

// Act
_sut.NotifyProgressChange(ProductScanStatus.ScannedWithErrors);

// Verify
Assert.True(_sut.HasErrors);
Assert.Equal(TrackerScanStatus.InProgressWithErrors, _lastStatus);
}

[Fact]
public void NotifyProgressChange__When_ScannedNewLowest__Reports_about_new_lowest()
{
// Arrange
_sut.NotifyStarted(_fixture.Create<int>());

// Act
_sut.NotifyProgressChange(ProductScanStatus.ScannedNewLowest);

// Verify
Assert.True(_sut.HasNewLowestPrice);
}

[Fact]
public void ProductAutoScanStartedEvent_fired__Calls_NotifyStarted()
{
// Arrange
var count = 10;

// Act
_productAutoScanStartedEventSubject.OnNext(new ProductAutoScanStartedEvent(count));

// Verify
var expectedProgress = 1d / (count * 2);
Assert.True(_sut.IsStarted);
Assert.False(_sut.HasErrors);
Assert.False(_sut.HasNewLowestPrice);
Assert.Equal(0, _sut.FinishedJobs);
Assert.Equal(TrackerScanStatus.InProgress, _lastStatus);
Assert.Equal(expectedProgress, _lastProgress);
}
}
}
Loading

0 comments on commit d4ae2a1

Please sign in to comment.