This commit is contained in:
Wiesław Šoltés 2022-02-04 23:09:19 +01:00
Родитель 2dd487f1fc
Коммит 4af5ed488a
25 изменённых файлов: 1923 добавлений и 1948 удалений

Просмотреть файл

@ -3,27 +3,26 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using ReactiveHistorySample.Views;
namespace ReactiveHistorySample
namespace ReactiveHistorySample;
public class App : Application
{
public class App : Application
public override void Initialize()
{
public override void Initialize()
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
AvaloniaXamlLoader.Load(this);
desktopLifetime.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView();
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = new MainWindow();
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
{
singleViewLifetime.MainView = new MainView();
}
base.OnFrameworkInitializationCompleted();
}
base.OnFrameworkInitializationCompleted();
}
}

Просмотреть файл

@ -3,29 +3,28 @@ using Avalonia.Controls;
using Avalonia.Media;
using ReactiveHistorySample.ViewModels;
namespace ReactiveHistorySample.Controls
{
public class LayerCanvas : Canvas
{
public override void Render(DrawingContext context)
{
base.Render(context);
namespace ReactiveHistorySample.Controls;
var layer = DataContext as LayerViewModel;
if (layer != null)
public class LayerCanvas : Canvas
{
public override void Render(DrawingContext context)
{
base.Render(context);
var layer = DataContext as LayerViewModel;
if (layer != null)
{
foreach (var shape in layer.Shapes)
{
foreach (var shape in layer.Shapes)
if (shape is LineShapeViewModel)
{
if (shape is LineShapeViewModel)
{
var line = shape as LineShapeViewModel;
context.DrawLine(
new Pen(Brushes.Red, 2.0),
new Point(line.Start.Value.X.Value, line.Start.Value.Y.Value),
new Point(line.End.Value.X.Value, line.End.Value.Y.Value));
}
var line = shape as LineShapeViewModel;
context.DrawLine(
new Pen(Brushes.Red, 2.0),
new Point(line.Start.Value.X.Value, line.Start.Value.Y.Value),
new Point(line.End.Value.X.Value, line.End.Value.Y.Value));
}
}
}
}
}
}

Просмотреть файл

@ -1,27 +1,26 @@

namespace ReactiveHistorySample.Models
namespace ReactiveHistorySample.Models;
public abstract class BaseObject : ObservableObject
{
public abstract class BaseObject : ObservableObject
private object _owner;
private string _name;
public object Owner
{
private object _owner;
private string _name;
public object Owner
{
get { return _owner; }
set { Update(ref _owner, value); }
}
public string Name
{
get { return _name; }
set { Update(ref _name, value); }
}
public BaseObject(object owner, string name)
{
_owner = owner;
_name = name;
}
get { return _owner; }
set { Update(ref _owner, value); }
}
}
public string Name
{
get { return _name; }
set { Update(ref _name, value); }
}
public BaseObject(object owner, string name)
{
_owner = owner;
_name = name;
}
}

Просмотреть файл

@ -1,10 +1,9 @@

namespace ReactiveHistorySample.Models
namespace ReactiveHistorySample.Models;
public abstract class BaseShape : BaseObject
{
public abstract class BaseShape : BaseObject
public BaseShape(object owner, string name) : base(owner, name)
{
public BaseShape(object owner, string name) : base(owner, name)
{
}
}
}
}

Просмотреть файл

@ -1,20 +1,19 @@
using System.Collections.ObjectModel;
namespace ReactiveHistorySample.Models
namespace ReactiveHistorySample.Models;
public class Layer : BaseObject
{
public class Layer : BaseObject
private ObservableCollection<LineShape> _shapes;
public ObservableCollection<LineShape> Shapes
{
private ObservableCollection<LineShape> _shapes;
public ObservableCollection<LineShape> Shapes
{
get { return _shapes; }
set { Update(ref _shapes, value); }
}
public Layer(object owner, string name) : base(owner, name)
{
_shapes = new ObservableCollection<LineShape>();
}
get { return _shapes; }
set { Update(ref _shapes, value); }
}
}
public Layer(object owner, string name) : base(owner, name)
{
_shapes = new ObservableCollection<LineShape>();
}
}

Просмотреть файл

@ -1,27 +1,26 @@

namespace ReactiveHistorySample.Models
namespace ReactiveHistorySample.Models;
public class LineShape : BaseShape
{
public class LineShape : BaseShape
private PointShape _start;
private PointShape _end;
public PointShape Start
{
private PointShape _start;
private PointShape _end;
public PointShape Start
{
get { return _start; }
set { Update(ref _start, value); }
}
public PointShape End
{
get { return _end; }
set { Update(ref _end, value); }
}
public LineShape(object owner, string name, PointShape start, PointShape end) : base(owner, name)
{
_start = start;
_end = end;
}
get { return _start; }
set { Update(ref _start, value); }
}
}
public PointShape End
{
get { return _end; }
set { Update(ref _end, value); }
}
public LineShape(object owner, string name, PointShape start, PointShape end) : base(owner, name)
{
_start = start;
_end = end;
}
}

Просмотреть файл

@ -1,28 +1,27 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ReactiveHistorySample.Models
namespace ReactiveHistorySample.Models;
public abstract class ObservableObject : INotifyPropertyChanged
{
public abstract class ObservableObject : INotifyPropertyChanged
{
#pragma warning disable CS8618
public event PropertyChangedEventHandler? PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
#pragma warning restore CS8618
public void Notify([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public bool Update<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (!Equals(field, value))
{
field = value;
Notify(propertyName);
return true;
}
return false;
}
public void Notify([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public bool Update<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (!Equals(field, value))
{
field = value;
Notify(propertyName);
return true;
}
return false;
}
}

Просмотреть файл

@ -1,28 +1,27 @@

namespace ReactiveHistorySample.Models
namespace ReactiveHistorySample.Models;
public class PointShape : BaseShape
{
public class PointShape : BaseShape
private double _x;
private double _y;
public double X
{
private double _x;
private double _y;
public double X
{
get { return _x; }
set { Update(ref _x, value); }
}
public double Y
{
get { return _y; }
set { Update(ref _y, value); }
}
public PointShape(object owner, string name, double x, double y)
: base(owner, name)
{
_x = x;
_y = y;
}
get { return _x; }
set { Update(ref _x, value); }
}
}
public double Y
{
get { return _y; }
set { Update(ref _y, value); }
}
public PointShape(object owner, string name, double x, double y)
: base(owner, name)
{
_x = x;
_y = y;
}
}

Просмотреть файл

@ -5,46 +5,45 @@ using Reactive.Bindings.Extensions;
using ReactiveHistory;
using ReactiveHistorySample.Models;
namespace ReactiveHistorySample.ViewModels
namespace ReactiveHistorySample.ViewModels;
public class LayerViewModel : IDisposable
{
public class LayerViewModel : IDisposable
private CompositeDisposable Disposable { get; set; }
public ReactiveProperty<string> Name { get; set; }
public ReadOnlyReactiveCollection<LineShapeViewModel> Shapes { get; set; }
public ReactiveCommand UndoCommand { get; set; }
public ReactiveCommand RedoCommand { get; set; }
public ReactiveCommand ClearCommand { get; set; }
public LayerViewModel(Layer layer, IHistory history)
{
private CompositeDisposable Disposable { get; set; }
Disposable = new CompositeDisposable();
public ReactiveProperty<string> Name { get; set; }
public ReadOnlyReactiveCollection<LineShapeViewModel> Shapes { get; set; }
this.Name = layer.ToReactivePropertyAsSynchronized(l => l.Name)
.SetValidateNotifyError(name => string.IsNullOrWhiteSpace(name) ? "Name can not be null or whitespace." : null)
.AddTo(this.Disposable);
public ReactiveCommand UndoCommand { get; set; }
public ReactiveCommand RedoCommand { get; set; }
public ReactiveCommand ClearCommand { get; set; }
this.Shapes = layer.Shapes
.ToReadOnlyReactiveCollection(x => new LineShapeViewModel(x, history))
.AddTo(this.Disposable);
public LayerViewModel(Layer layer, IHistory history)
{
Disposable = new CompositeDisposable();
this.Name.ObserveWithHistory(name => layer.Name = name, layer.Name, history).AddTo(this.Disposable);
this.Name = layer.ToReactivePropertyAsSynchronized(l => l.Name)
.SetValidateNotifyError(name => string.IsNullOrWhiteSpace(name) ? "Name can not be null or whitespace." : null)
.AddTo(this.Disposable);
UndoCommand = new ReactiveCommand(history.CanUndo, false);
UndoCommand.Subscribe(_ => history.Undo()).AddTo(this.Disposable);
this.Shapes = layer.Shapes
.ToReadOnlyReactiveCollection(x => new LineShapeViewModel(x, history))
.AddTo(this.Disposable);
RedoCommand = new ReactiveCommand(history.CanRedo, false);
RedoCommand.Subscribe(_ => history.Redo()).AddTo(this.Disposable);
this.Name.ObserveWithHistory(name => layer.Name = name, layer.Name, history).AddTo(this.Disposable);
UndoCommand = new ReactiveCommand(history.CanUndo, false);
UndoCommand.Subscribe(_ => history.Undo()).AddTo(this.Disposable);
RedoCommand = new ReactiveCommand(history.CanRedo, false);
RedoCommand.Subscribe(_ => history.Redo()).AddTo(this.Disposable);
ClearCommand = new ReactiveCommand(history.CanClear, false);
ClearCommand.Subscribe(_ => history.Clear()).AddTo(this.Disposable);
}
public void Dispose()
{
this.Disposable.Dispose();
}
ClearCommand = new ReactiveCommand(history.CanClear, false);
ClearCommand.Subscribe(_ => history.Clear()).AddTo(this.Disposable);
}
}
public void Dispose()
{
this.Disposable.Dispose();
}
}

Просмотреть файл

@ -5,68 +5,67 @@ using Reactive.Bindings.Extensions;
using ReactiveHistory;
using ReactiveHistorySample.Models;
namespace ReactiveHistorySample.ViewModels
namespace ReactiveHistorySample.ViewModels;
public class LineShapeViewModel : IDisposable
{
public class LineShapeViewModel : IDisposable
private CompositeDisposable Disposable { get; set; }
public ReactiveProperty<string> Name { get; set; }
public ReactiveProperty<PointShapeViewModel> Start { get; set; }
public ReactiveProperty<PointShapeViewModel> End { get; set; }
public ReactiveCommand DeleteCommand { get; set; }
public ReactiveCommand UndoCommand { get; set; }
public ReactiveCommand RedoCommand { get; set; }
public ReactiveCommand ClearCommand { get; set; }
public LineShapeViewModel(LineShape line, IHistory history)
{
private CompositeDisposable Disposable { get; set; }
Disposable = new CompositeDisposable();
public ReactiveProperty<string> Name { get; set; }
public ReactiveProperty<PointShapeViewModel> Start { get; set; }
public ReactiveProperty<PointShapeViewModel> End { get; set; }
var lineHistoryScope = new StackHistory().AddTo(this.Disposable);
public ReactiveCommand DeleteCommand { get; set; }
this.Name = line.ToReactivePropertyAsSynchronized(l => l.Name)
.SetValidateNotifyError(name => string.IsNullOrWhiteSpace(name) ? "Name can not be null or whitespace." : null)
.AddTo(this.Disposable);
public ReactiveCommand UndoCommand { get; set; }
public ReactiveCommand RedoCommand { get; set; }
public ReactiveCommand ClearCommand { get; set; }
var startInitialValue = new PointShapeViewModel(line.Start, lineHistoryScope).AddTo(this.Disposable);
this.Start = new ReactiveProperty<PointShapeViewModel>(startInitialValue)
.SetValidateNotifyError(start => start == null ? "Point can not be null." : null)
.AddTo(this.Disposable);
public LineShapeViewModel(LineShape line, IHistory history)
var endInitialValue = new PointShapeViewModel(line.End, lineHistoryScope).AddTo(this.Disposable);
this.End = new ReactiveProperty<PointShapeViewModel>(endInitialValue)
.SetValidateNotifyError(end => end == null ? "Point can not be null." : null)
.AddTo(this.Disposable);
this.Name.ObserveWithHistory(name => line.Name = name, line.Name, lineHistoryScope).AddTo(this.Disposable);
this.DeleteCommand = new ReactiveCommand();
this.DeleteCommand.Subscribe((x) => Delete(line, history)).AddTo(this.Disposable);
UndoCommand = new ReactiveCommand(lineHistoryScope.CanUndo, false);
UndoCommand.Subscribe(_ => lineHistoryScope.Undo()).AddTo(this.Disposable);
RedoCommand = new ReactiveCommand(lineHistoryScope.CanRedo, false);
RedoCommand.Subscribe(_ => lineHistoryScope.Redo()).AddTo(this.Disposable);
ClearCommand = new ReactiveCommand(lineHistoryScope.CanClear, false);
ClearCommand.Subscribe(_ => lineHistoryScope.Clear()).AddTo(this.Disposable);
}
private void Delete(LineShape line, IHistory history)
{
if (line.Owner != null && line.Owner is Layer layer)
{
Disposable = new CompositeDisposable();
var lineHistoryScope = new StackHistory().AddTo(this.Disposable);
this.Name = line.ToReactivePropertyAsSynchronized(l => l.Name)
.SetValidateNotifyError(name => string.IsNullOrWhiteSpace(name) ? "Name can not be null or whitespace." : null)
.AddTo(this.Disposable);
var startInitialValue = new PointShapeViewModel(line.Start, lineHistoryScope).AddTo(this.Disposable);
this.Start = new ReactiveProperty<PointShapeViewModel>(startInitialValue)
.SetValidateNotifyError(start => start == null ? "Point can not be null." : null)
.AddTo(this.Disposable);
var endInitialValue = new PointShapeViewModel(line.End, lineHistoryScope).AddTo(this.Disposable);
this.End = new ReactiveProperty<PointShapeViewModel>(endInitialValue)
.SetValidateNotifyError(end => end == null ? "Point can not be null." : null)
.AddTo(this.Disposable);
this.Name.ObserveWithHistory(name => line.Name = name, line.Name, lineHistoryScope).AddTo(this.Disposable);
this.DeleteCommand = new ReactiveCommand();
this.DeleteCommand.Subscribe((x) => Delete(line, history)).AddTo(this.Disposable);
UndoCommand = new ReactiveCommand(lineHistoryScope.CanUndo, false);
UndoCommand.Subscribe(_ => lineHistoryScope.Undo()).AddTo(this.Disposable);
RedoCommand = new ReactiveCommand(lineHistoryScope.CanRedo, false);
RedoCommand.Subscribe(_ => lineHistoryScope.Redo()).AddTo(this.Disposable);
ClearCommand = new ReactiveCommand(lineHistoryScope.CanClear, false);
ClearCommand.Subscribe(_ => lineHistoryScope.Clear()).AddTo(this.Disposable);
}
private void Delete(LineShape line, IHistory history)
{
if (line.Owner != null && line.Owner is Layer layer)
{
layer.Shapes.RemoveWithHistory(line, history);
}
}
public void Dispose()
{
this.Disposable.Dispose();
layer.Shapes.RemoveWithHistory(line, history);
}
}
}
public void Dispose()
{
this.Disposable.Dispose();
}
}

Просмотреть файл

@ -5,40 +5,39 @@ using Reactive.Bindings.Extensions;
using ReactiveHistory;
using ReactiveHistorySample.Models;
namespace ReactiveHistorySample.ViewModels
namespace ReactiveHistorySample.ViewModels;
public class PointShapeViewModel : IDisposable
{
public class PointShapeViewModel : IDisposable
private CompositeDisposable Disposable { get; set; }
public ReactiveProperty<string> Name { get; set; }
public ReactiveProperty<double> X { get; set; }
public ReactiveProperty<double> Y { get; set; }
public PointShapeViewModel(PointShape point, IHistory history)
{
private CompositeDisposable Disposable { get; set; }
Disposable = new CompositeDisposable();
public ReactiveProperty<string> Name { get; set; }
public ReactiveProperty<double> X { get; set; }
public ReactiveProperty<double> Y { get; set; }
this.Name = point.ToReactivePropertyAsSynchronized(p => p.Name)
.SetValidateNotifyError(name => string.IsNullOrWhiteSpace(name) ? "Name can not be null or whitespace." : null)
.AddTo(this.Disposable);
public PointShapeViewModel(PointShape point, IHistory history)
{
Disposable = new CompositeDisposable();
this.X = point.ToReactivePropertyAsSynchronized(p => p.X)
.SetValidateNotifyError(x => double.IsNaN(x) || double.IsInfinity(x) ? "X can not be NaN or Infinity." : null)
.AddTo(this.Disposable);
this.Name = point.ToReactivePropertyAsSynchronized(p => p.Name)
.SetValidateNotifyError(name => string.IsNullOrWhiteSpace(name) ? "Name can not be null or whitespace." : null)
.AddTo(this.Disposable);
this.Y = point.ToReactivePropertyAsSynchronized(p => p.Y)
.SetValidateNotifyError(y => double.IsNaN(y) || double.IsInfinity(y) ? "Y can not be NaN or Infinity." : null)
.AddTo(this.Disposable);
this.X = point.ToReactivePropertyAsSynchronized(p => p.X)
.SetValidateNotifyError(x => double.IsNaN(x) || double.IsInfinity(x) ? "X can not be NaN or Infinity." : null)
.AddTo(this.Disposable);
this.Y = point.ToReactivePropertyAsSynchronized(p => p.Y)
.SetValidateNotifyError(y => double.IsNaN(y) || double.IsInfinity(y) ? "Y can not be NaN or Infinity." : null)
.AddTo(this.Disposable);
this.Name.ObserveWithHistory(name => point.Name = name, point.Name, history).AddTo(this.Disposable);
this.X.ObserveWithHistory(x => point.X = x, point.X, history).AddTo(this.Disposable);
this.Y.ObserveWithHistory(y => point.Y = y, point.Y, history).AddTo(this.Disposable);
}
public void Dispose()
{
this.Disposable.Dispose();
}
this.Name.ObserveWithHistory(name => point.Name = name, point.Name, history).AddTo(this.Disposable);
this.X.ObserveWithHistory(x => point.X = x, point.X, history).AddTo(this.Disposable);
this.Y.ObserveWithHistory(y => point.Y = y, point.Y, history).AddTo(this.Disposable);
}
}
public void Dispose()
{
this.Disposable.Dispose();
}
}

Просмотреть файл

@ -1,18 +1,17 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ReactiveHistorySample.Views
{
public class LineShapeView : UserControl
{
public LineShapeView()
{
this.InitializeComponent();
}
namespace ReactiveHistorySample.Views;
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public class LineShapeView : UserControl
{
public LineShapeView()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

Просмотреть файл

@ -1,18 +1,17 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ReactiveHistorySample.Views
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
namespace ReactiveHistorySample.Views;
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

Просмотреть файл

@ -1,18 +1,17 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ReactiveHistorySample.Views
{
public class PointShapeView : UserControl
{
public PointShapeView()
{
this.InitializeComponent();
}
namespace ReactiveHistorySample.Views;
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public class PointShapeView : UserControl
{
public PointShapeView()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

Просмотреть файл

@ -1,17 +1,16 @@
using Avalonia;
namespace ReactiveHistorySample.Avalonia
{
class Program
{
static void Main(string[] args)
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
namespace ReactiveHistorySample.Avalonia;
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
class Program
{
static void Main(string[] args)
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}

Просмотреть файл

@ -1,54 +1,53 @@
using System;
namespace ReactiveHistory
namespace ReactiveHistory;
/// <summary>
/// Undo/redo action history contract.
/// </summary>
public interface IHistory
{
/// <summary>
/// Undo/redo action history contract.
/// Gets or sets flag indicating whether history is paused.
/// </summary>
public interface IHistory
{
/// <summary>
/// Gets or sets flag indicating whether history is paused.
/// </summary>
bool IsPaused { get; set; }
bool IsPaused { get; set; }
/// <summary>
/// Gets or sets flag indicating whether undo action can execute.
/// </summary>
IObservable<bool> CanUndo { get; }
/// <summary>
/// Gets or sets flag indicating whether undo action can execute.
/// </summary>
IObservable<bool> CanUndo { get; }
/// <summary>
/// Gets or sets flag indicating whether redo action can execute.
/// </summary>
IObservable<bool> CanRedo { get; }
/// <summary>
/// Gets or sets flag indicating whether redo action can execute.
/// </summary>
IObservable<bool> CanRedo { get; }
/// <summary>
/// Gets or sets flag indicating whether clear action can execute.
/// </summary>
IObservable<bool> CanClear { get; }
/// <summary>
/// Gets or sets flag indicating whether clear action can execute.
/// </summary>
IObservable<bool> CanClear { get; }
/// <summary>
/// Makes undo/redo history snapshot.
/// </summary>
/// <param name="undo">The undo state action.</param>
/// <param name="redo">The redo state action.</param>
void Snapshot(Action undo, Action redo);
/// <summary>
/// Makes undo/redo history snapshot.
/// </summary>
/// <param name="undo">The undo state action.</param>
/// <param name="redo">The redo state action.</param>
void Snapshot(Action undo, Action redo);
/// <summary>
/// Executes undo action.
/// </summary>
/// <returns>True if undo action was executed.</returns>
bool Undo();
/// <summary>
/// Executes undo action.
/// </summary>
/// <returns>True if undo action was executed.</returns>
bool Undo();
/// <summary>
/// Executes redo action.
/// </summary>
/// <returns>True if redo action was executed.</returns>
bool Redo();
/// <summary>
/// Executes redo action.
/// </summary>
/// <returns>True if redo action was executed.</returns>
bool Redo();
/// <summary>
/// Clears undo/redo actions history.
/// </summary>
void Clear();
}
}
/// <summary>
/// Clears undo/redo actions history.
/// </summary>
void Clear();
}

Просмотреть файл

@ -2,181 +2,180 @@
using System.Collections.Generic;
using System.Linq;
namespace ReactiveHistory
namespace ReactiveHistory;
/// <summary>
/// Stack history extension methods for the generic list implementations.
/// </summary>
public static class IListExtensions
{
/// <summary>
/// Stack history extension methods for the generic list implementations.
/// Adds item to the source list with history.
/// </summary>
public static class IListExtensions
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="item">The item to add.</param>
/// <param name="history">The history object.</param>
public static void AddWithHistory<T>(this IList<T> source, T item, IHistory history)
{
/// <summary>
/// Adds item to the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="item">The item to add.</param>
/// <param name="history">The history object.</param>
public static void AddWithHistory<T>(this IList<T> source, T item, IHistory history)
if (source == null)
throw new ArgumentNullException(nameof(source));
if (item == null)
throw new ArgumentNullException(nameof(item));
if (history == null)
throw new ArgumentNullException(nameof(history));
int index = source.Count;
void redo() => source.Insert(index, item);
void undo() => source.RemoveAt(index);
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Inserts item to the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="index">The item insertion index.</param>
/// <param name="item">The item to insert.</param>
/// <param name="history">The history object.</param>
public static void InsertWithHistory<T>(this IList<T> source, int index, T item, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (index < 0)
throw new IndexOutOfRangeException("Index can not be negative.");
if (item == null)
throw new ArgumentNullException(nameof(item));
if (history == null)
throw new ArgumentNullException(nameof(history));
void redo() => source.Insert(index, item);
void undo() => source.RemoveAt(index);
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Replaces item at specified index in the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="index">The item index to replace.</param>
/// <param name="item">The replaced item.</param>
/// <param name="history">The history object.</param>
public static void ReplaceWithHistory<T>(this IList<T> source, int index, T item, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (index < 0)
throw new IndexOutOfRangeException("Index can not be negative.");
if (item == null)
throw new ArgumentNullException(nameof(item));
if (history == null)
throw new ArgumentNullException(nameof(history));
var oldValue = source[index];
var newValue = item;
void redo() => source[index] = newValue;
void undo() => source[index] = oldValue;
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Removes item at specified index from the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="item">The item to remove.</param>
/// <param name="history">The history object.</param>
public static void RemoveWithHistory<T>(this IList<T> source, T item, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (item == null)
throw new ArgumentNullException(nameof(item));
if (history == null)
throw new ArgumentNullException(nameof(history));
int index = source.IndexOf(item);
void redo() => source.RemoveAt(index);
void undo() => source.Insert(index, item);
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Removes item from the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="index">The item index to remove.</param>
/// <param name="history">The history object.</param>
public static void RemoveWithHistory<T>(this IList<T> source, int index, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (index < 0)
throw new IndexOutOfRangeException("Index can not be negative.");
if (history == null)
throw new ArgumentNullException(nameof(history));
var item = source[index];
void redo() => source.RemoveAt(index);
void undo() => source.Insert(index, item);
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Removes all items from the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="history">The history object.</param>
public static void ClearWithHistory<T>(this IList<T> source, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (history == null)
throw new ArgumentNullException(nameof(history));
if (source.Count > 0)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (item == null)
throw new ArgumentNullException(nameof(item));
if (history == null)
throw new ArgumentNullException(nameof(history));
int index = source.Count;
void redo() => source.Insert(index, item);
void undo() => source.RemoveAt(index);
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Inserts item to the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="index">The item insertion index.</param>
/// <param name="item">The item to insert.</param>
/// <param name="history">The history object.</param>
public static void InsertWithHistory<T>(this IList<T> source, int index, T item, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (index < 0)
throw new IndexOutOfRangeException("Index can not be negative.");
if (item == null)
throw new ArgumentNullException(nameof(item));
if (history == null)
throw new ArgumentNullException(nameof(history));
void redo() => source.Insert(index, item);
void undo() => source.RemoveAt(index);
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Replaces item at specified index in the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="index">The item index to replace.</param>
/// <param name="item">The replaced item.</param>
/// <param name="history">The history object.</param>
public static void ReplaceWithHistory<T>(this IList<T> source, int index, T item, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (index < 0)
throw new IndexOutOfRangeException("Index can not be negative.");
if (item == null)
throw new ArgumentNullException(nameof(item));
if (history == null)
throw new ArgumentNullException(nameof(history));
var oldValue = source[index];
var newValue = item;
void redo() => source[index] = newValue;
void undo() => source[index] = oldValue;
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Removes item at specified index from the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="item">The item to remove.</param>
/// <param name="history">The history object.</param>
public static void RemoveWithHistory<T>(this IList<T> source, T item, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (item == null)
throw new ArgumentNullException(nameof(item));
if (history == null)
throw new ArgumentNullException(nameof(history));
int index = source.IndexOf(item);
void redo() => source.RemoveAt(index);
void undo() => source.Insert(index, item);
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Removes item from the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="index">The item index to remove.</param>
/// <param name="history">The history object.</param>
public static void RemoveWithHistory<T>(this IList<T> source, int index, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (index < 0)
throw new IndexOutOfRangeException("Index can not be negative.");
if (history == null)
throw new ArgumentNullException(nameof(history));
var item = source[index];
void redo() => source.RemoveAt(index);
void undo() => source.Insert(index, item);
history.Snapshot(undo, redo);
redo();
}
/// <summary>
/// Removes all items from the source list with history.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="source">The source list.</param>
/// <param name="history">The history object.</param>
public static void ClearWithHistory<T>(this IList<T> source, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (history == null)
throw new ArgumentNullException(nameof(history));
if (source.Count > 0)
var items = source.ToArray();
void redo()
{
var items = source.ToArray();
void redo()
foreach (var item in items)
{
foreach (var item in items)
{
source.Remove(item);
}
source.Remove(item);
}
void undo()
{
foreach (var item in items)
{
source.Add(item);
}
}
history.Snapshot(undo, redo);
redo();
}
void undo()
{
foreach (var item in items)
{
source.Add(item);
}
}
history.Snapshot(undo, redo);
redo();
}
}
}
}

Просмотреть файл

@ -1,47 +1,46 @@
using System;
using System.Reactive.Linq;
namespace ReactiveHistory
namespace ReactiveHistory;
/// <summary>
/// Observable extension methods for the generic observable implementations.
/// </summary>
public static class IObservableExtensions
{
/// <summary>
/// Observable extension methods for the generic observable implementations.
/// Observe property changes with history.
/// </summary>
public static class IObservableExtensions
/// <param name="source">The property value observable.</param>
/// <param name="update">The property update action.</param>
/// <param name="currentValue">The property current value.</param>
/// <param name="history">The history object.</param>
/// <returns>The property value changes subscription.</returns>
public static IDisposable ObserveWithHistory<T>(this IObservable<T> source, Action<T> update, T currentValue, IHistory history)
{
/// <summary>
/// Observe property changes with history.
/// </summary>
/// <param name="source">The property value observable.</param>
/// <param name="update">The property update action.</param>
/// <param name="currentValue">The property current value.</param>
/// <param name="history">The history object.</param>
/// <returns>The property value changes subscription.</returns>
public static IDisposable ObserveWithHistory<T>(this IObservable<T> source, Action<T> update, T currentValue, IHistory history)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (source == null)
throw new ArgumentNullException(nameof(source));
if (update == null)
throw new ArgumentNullException(nameof(update));
if (update == null)
throw new ArgumentNullException(nameof(update));
if (history == null)
throw new ArgumentNullException(nameof(history));
if (history == null)
throw new ArgumentNullException(nameof(history));
var previous = currentValue;
var previous = currentValue;
return source.Skip(1).Subscribe(
next =>
return source.Skip(1).Subscribe(
next =>
{
if (!history.IsPaused)
{
if (!history.IsPaused)
{
var undoValue = previous;
var redoValue = next;
void undo() => update(undoValue);
void redo() => update(redoValue);
history.Snapshot(undo, redo);
}
previous = next;
});
}
var undoValue = previous;
var redoValue = next;
void undo() => update(undoValue);
void redo() => update(redoValue);
history.Snapshot(undo, redo);
}
previous = next;
});
}
}
}

Просмотреть файл

@ -3,146 +3,145 @@ using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace ReactiveHistory
namespace ReactiveHistory;
/// <summary>
/// Undo/redo stack based action history.
/// </summary>
public class StackHistory : IHistory, IDisposable
{
private readonly Subject<bool> _canUndo;
private readonly Subject<bool> _canRedo;
private readonly Subject<bool> _canClear;
private volatile bool _isPaused;
/// <summary>
/// Undo/redo stack based action history.
/// Gets or sets undo states stack.
/// </summary>
public class StackHistory : IHistory, IDisposable
public Stack<State> Undos { get; set; }
/// <summary>
/// Gets or sets redo states stack.
/// </summary>
public Stack<State> Redos { get; set; }
/// <inheritdoc/>
public bool IsPaused
{
private readonly Subject<bool> _canUndo;
private readonly Subject<bool> _canRedo;
private readonly Subject<bool> _canClear;
private volatile bool _isPaused;
get { return _isPaused; }
set { _isPaused = value; }
}
/// <summary>
/// Gets or sets undo states stack.
/// </summary>
public Stack<State> Undos { get; set; }
/// <inheritdoc/>
public IObservable<bool> CanUndo
{
get { return _canUndo.AsObservable(); }
}
/// <summary>
/// Gets or sets redo states stack.
/// </summary>
public Stack<State> Redos { get; set; }
/// <inheritdoc/>
public IObservable<bool> CanRedo
{
get { return _canRedo.AsObservable(); }
}
/// <inheritdoc/>
public bool IsPaused
/// <inheritdoc/>
public IObservable<bool> CanClear
{
get { return _canClear.AsObservable(); }
}
/// <summary>
/// Initializes a new <see cref="StackHistory"/> instance.
/// </summary>
public StackHistory()
{
Undos = new Stack<State>();
Redos = new Stack<State>();
_isPaused = false;
_canUndo = new Subject<bool>();
_canRedo = new Subject<bool>();
_canClear = new Subject<bool>();
}
/// <inheritdoc/>
public void Snapshot(Action undo, Action redo)
{
if (undo == null)
throw new ArgumentNullException(nameof(undo));
if (redo == null)
throw new ArgumentNullException(nameof(redo));
if (Redos.Count > 0)
{
get { return _isPaused; }
set { _isPaused = value; }
Redos.Clear();
_canRedo.OnNext(false);
}
Undos.Push(new State(undo, redo, string.Empty, string.Empty));
_canUndo.OnNext(true);
_canClear.OnNext(true);
}
/// <inheritdoc/>
public IObservable<bool> CanUndo
/// <inheritdoc/>
public bool Undo()
{
if (Undos.Count > 0)
{
get { return _canUndo.AsObservable(); }
}
/// <inheritdoc/>
public IObservable<bool> CanRedo
{
get { return _canRedo.AsObservable(); }
}
/// <inheritdoc/>
public IObservable<bool> CanClear
{
get { return _canClear.AsObservable(); }
}
/// <summary>
/// Initializes a new <see cref="StackHistory"/> instance.
/// </summary>
public StackHistory()
{
Undos = new Stack<State>();
Redos = new Stack<State>();
_isPaused = false;
_canUndo = new Subject<bool>();
_canRedo = new Subject<bool>();
_canClear = new Subject<bool>();
}
/// <inheritdoc/>
public void Snapshot(Action undo, Action redo)
{
if (undo == null)
throw new ArgumentNullException(nameof(undo));
if (redo == null)
throw new ArgumentNullException(nameof(redo));
if (Redos.Count > 0)
IsPaused = true;
var state = Undos.Pop();
if (Undos.Count == 0)
{
_canUndo.OnNext(false);
}
state.Undo.Invoke();
Redos.Push(state);
_canRedo.OnNext(true);
_canClear.OnNext(true);
IsPaused = false;
return true;
}
return false;
}
/// <inheritdoc/>
public bool Redo()
{
if (Redos.Count > 0)
{
IsPaused = true;
var state = Redos.Pop();
if (Redos.Count == 0)
{
Redos.Clear();
_canRedo.OnNext(false);
}
Undos.Push(new State(undo, redo, string.Empty, string.Empty));
state.Redo.Invoke();
Undos.Push(state);
_canUndo.OnNext(true);
_canClear.OnNext(true);
IsPaused = false;
return true;
}
/// <inheritdoc/>
public bool Undo()
{
if (Undos.Count > 0)
{
IsPaused = true;
var state = Undos.Pop();
if (Undos.Count == 0)
{
_canUndo.OnNext(false);
}
state.Undo.Invoke();
Redos.Push(state);
_canRedo.OnNext(true);
_canClear.OnNext(true);
IsPaused = false;
return true;
}
return false;
}
/// <inheritdoc/>
public bool Redo()
{
if (Redos.Count > 0)
{
IsPaused = true;
var state = Redos.Pop();
if (Redos.Count == 0)
{
_canRedo.OnNext(false);
}
state.Redo.Invoke();
Undos.Push(state);
_canUndo.OnNext(true);
_canClear.OnNext(true);
IsPaused = false;
return true;
}
return false;
}
/// <inheritdoc/>
public void Clear()
{
Undos.Clear();
Redos.Clear();
_canUndo.OnNext(false);
_canRedo.OnNext(false);
_canClear.OnNext(false);
}
/// <inheritdoc/>
public void Dispose()
{
Undos.Clear();
Redos.Clear();
_canUndo.Dispose();
_canRedo.Dispose();
_canClear.Dispose();
}
return false;
}
}
/// <inheritdoc/>
public void Clear()
{
Undos.Clear();
Redos.Clear();
_canUndo.OnNext(false);
_canRedo.OnNext(false);
_canClear.OnNext(false);
}
/// <inheritdoc/>
public void Dispose()
{
Undos.Clear();
Redos.Clear();
_canUndo.Dispose();
_canRedo.Dispose();
_canClear.Dispose();
}
}

Просмотреть файл

@ -1,45 +1,44 @@
using System;
namespace ReactiveHistory
namespace ReactiveHistory;
/// <summary>
/// Undo/redo action pair.
/// </summary>
public struct State
{
/// <summary>
/// Undo/redo action pair.
/// The undo state action.
/// </summary>
public struct State
public readonly Action Undo;
/// <summary>
/// The redo state action.
/// </summary>
public readonly Action Redo;
/// <summary>
/// The undo state name.
/// </summary>
public readonly string UndoName;
/// <summary>
/// The redo state name.
/// </summary>
public readonly string RedoName;
/// <summary>
/// Initializes a new <see cref="State"/> instance.
/// </summary>
/// <param name="undo">The undo state action.</param>
/// <param name="redo">The redo state action.</param>
/// <param name="undoName">The undo state name.</param>
/// <param name="redoName">The redo state name.</param>
public State(Action undo, Action redo, string undoName, string redoName)
{
/// <summary>
/// The undo state action.
/// </summary>
public readonly Action Undo;
/// <summary>
/// The redo state action.
/// </summary>
public readonly Action Redo;
/// <summary>
/// The undo state name.
/// </summary>
public readonly string UndoName;
/// <summary>
/// The redo state name.
/// </summary>
public readonly string RedoName;
/// <summary>
/// Initializes a new <see cref="State"/> instance.
/// </summary>
/// <param name="undo">The undo state action.</param>
/// <param name="redo">The redo state action.</param>
/// <param name="undoName">The undo state name.</param>
/// <param name="redoName">The redo state name.</param>
public State(Action undo, Action redo, string undoName, string redoName)
{
Undo = undo;
Redo = redo;
UndoName = undoName;
RedoName = redoName;
}
Undo = undo;
Redo = redo;
UndoName = undoName;
RedoName = redoName;
}
}
}

Просмотреть файл

@ -2,32 +2,31 @@
using System.Collections.Generic;
using System.Reactive.Disposables;
namespace ReactiveHistory.UnitTests
namespace ReactiveHistory.UnitTests;
internal class HistoryHelper : IDisposable
{
internal class HistoryHelper : IDisposable
CompositeDisposable _disposable;
IList<bool> _canUndos;
IList<bool> _canRedos;
IList<bool> _canClears;
public IList<bool> CanUndos { get { return _canUndos; } }
public IList<bool> CanRedos { get { return _canRedos; } }
public IList<bool> CanClears { get { return _canClears; } }
public HistoryHelper(IHistory target)
{
CompositeDisposable _disposable;
IList<bool> _canUndos;
IList<bool> _canRedos;
IList<bool> _canClears;
public IList<bool> CanUndos { get { return _canUndos; } }
public IList<bool> CanRedos { get { return _canRedos; } }
public IList<bool> CanClears { get { return _canClears; } }
public HistoryHelper(IHistory target)
{
_disposable = new CompositeDisposable();
_canUndos = new List<bool>();
_canRedos = new List<bool>();
_canClears = new List<bool>();
_disposable.Add(target.CanUndo.Subscribe(x => _canUndos.Add(x)));
_disposable.Add(target.CanRedo.Subscribe(x => _canRedos.Add(x)));
_disposable.Add(target.CanClear.Subscribe(x => _canClears.Add(x)));
}
public void Dispose()
{
_disposable.Dispose();
}
_disposable = new CompositeDisposable();
_canUndos = new List<bool>();
_canRedos = new List<bool>();
_canClears = new List<bool>();
_disposable.Add(target.CanUndo.Subscribe(x => _canUndos.Add(x)));
_disposable.Add(target.CanRedo.Subscribe(x => _canRedos.Add(x)));
_disposable.Add(target.CanClear.Subscribe(x => _canClears.Add(x)));
}
}
public void Dispose()
{
_disposable.Dispose();
}
}

Просмотреть файл

@ -3,97 +3,96 @@ using System.Reactive.Linq;
using System.Reactive.Subjects;
using Xunit;
namespace ReactiveHistory.UnitTests
namespace ReactiveHistory.UnitTests;
public class ObservableHistoryExtensionsTests
{
public class ObservableHistoryExtensionsTests
[Fact]
[Trait("ReactiveHistory", "ObservableHistoryExtensions")]
public void ObserveWithHistory_Skips_First_Value()
{
[Fact]
[Trait("ReactiveHistory", "ObservableHistoryExtensions")]
public void ObserveWithHistory_Skips_First_Value()
{
var target = new StackHistory();
var target = new StackHistory();
using (var subject = new Subject<int>())
using (subject.AsObservable().ObserveWithHistory(x => { }, 0, target))
{
subject.OnNext(1);
Assert.Empty(target.Undos);
}
using (var subject = new Subject<int>())
using (subject.AsObservable().ObserveWithHistory(x => { }, 0, target))
{
subject.OnNext(1);
Assert.Empty(target.Undos);
}
}
[Fact]
[Trait("ReactiveHistory", "ObservableHistoryExtensions")]
public void ObserveWithHistory_Creates_History_Snapshot()
[Fact]
[Trait("ReactiveHistory", "ObservableHistoryExtensions")]
public void ObserveWithHistory_Creates_History_Snapshot()
{
var target = new StackHistory();
using (var subject = new Subject<int>())
using (subject.AsObservable().ObserveWithHistory(x => { }, 0, target))
{
var target = new StackHistory();
subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);
using (var subject = new Subject<int>())
using (subject.AsObservable().ObserveWithHistory(x => { }, 0, target))
{
subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);
Assert.Equal(2, target.Undos.Count);
}
Assert.Equal(2, target.Undos.Count);
}
}
[Fact]
[Trait("ReactiveHistory", "ObservableHistoryExtensions")]
public void ObserveWithHistory_Does_Not_Create_History_Snapshot_When_IsPaused_True()
[Fact]
[Trait("ReactiveHistory", "ObservableHistoryExtensions")]
public void ObserveWithHistory_Does_Not_Create_History_Snapshot_When_IsPaused_True()
{
var target = new StackHistory();
using (var subject = new Subject<int>())
using (subject.AsObservable().ObserveWithHistory(x => { }, 0, target))
{
var target = new StackHistory();
subject.OnNext(1);
subject.OnNext(2);
using (var subject = new Subject<int>())
using (subject.AsObservable().ObserveWithHistory(x => { }, 0, target))
{
subject.OnNext(1);
subject.OnNext(2);
target.IsPaused = true;
subject.OnNext(3);
subject.OnNext(4);
target.IsPaused = false;
target.IsPaused = true;
subject.OnNext(3);
subject.OnNext(4);
target.IsPaused = false;
subject.OnNext(5);
subject.OnNext(6);
subject.OnNext(5);
subject.OnNext(6);
Assert.Equal(3, target.Undos.Count);
}
Assert.Equal(3, target.Undos.Count);
}
}
[Fact]
[Trait("ReactiveHistory", "ObservableHistoryExtensions")]
public void ObserveWithHistory_Sets_CurrentValue()
[Fact]
[Trait("ReactiveHistory", "ObservableHistoryExtensions")]
public void ObserveWithHistory_Sets_CurrentValue()
{
var history = new StackHistory();
using (var subject = new Subject<int>())
{
var history = new StackHistory();
var target = new List<int>();
var initialValue = 10;
using (var subject = new Subject<int>())
using (subject.AsObservable().ObserveWithHistory(x => target.Add(x), currentValue: initialValue, history: history))
{
var target = new List<int>();
var initialValue = 10;
subject.OnNext(initialValue); // empty -> 10 (the initial state of variable)
subject.OnNext(2); // empty -> 10 -> 2
subject.OnNext(3); // empty -> 10 -> 2 -> 3
using (subject.AsObservable().ObserveWithHistory(x => target.Add(x), currentValue: initialValue, history: history))
{
subject.OnNext(initialValue); // empty -> 10 (the initial state of variable)
subject.OnNext(2); // empty -> 10 -> 2
subject.OnNext(3); // empty -> 10 -> 2 -> 3
history.Undo(); // 3 -> 2
history.Undo(); // 2 -> 10 (finally restores initial state)
history.Undo(); // 3 -> 2
history.Undo(); // 2 -> 10 (finally restores initial state)
Assert.Empty(history.Undos);
Assert.Equal(2, history.Redos.Count);
Assert.Equal(new int[] { 2, 10 }, target);
Assert.Empty(history.Undos);
Assert.Equal(2, history.Redos.Count);
Assert.Equal(new int[] { 2, 10 }, target);
history.Redo(); // 10 -> 2
history.Redo(); // 2 -> 3
history.Redo(); // 10 -> 2
history.Redo(); // 2 -> 3
Assert.Equal(2, history.Undos.Count);
Assert.Empty(history.Redos);
Assert.Equal(new int[] { 2, 10, 2, 3 }, target);
}
Assert.Equal(2, history.Undos.Count);
Assert.Empty(history.Redos);
Assert.Equal(new int[] { 2, 10, 2, 3 }, target);
}
}
}
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -2,206 +2,205 @@
using System.Linq;
using Xunit;
namespace ReactiveHistory.UnitTests
namespace ReactiveHistory.UnitTests;
public class StackHistoryTests
{
public class StackHistoryTests
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Undos_And_Redos_Shuould_Be_Initialized()
{
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Undos_And_Redos_Shuould_Be_Initialized()
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
Assert.NotNull(target.Undos);
Assert.NotNull(target.Redos);
Assert.Empty(target.Undos);
Assert.Empty(target.Redos);
Assert.False(target.IsPaused);
Assert.Equal(new bool[] { }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Dispose_Should_Release_Allocated_Resources()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => { }, () => { });
target.Snapshot(() => { }, () => { });
var result = target.Undo();
Assert.Single(target.Undos);
Assert.Single(target.Redos);
Assert.True(result);
target.Dispose();
Assert.Empty(target.Undos);
Assert.Empty(target.Redos);
Assert.Throws<ObjectDisposedException>(() => target.CanUndo.Subscribe(_ => { }));
Assert.Throws<ObjectDisposedException>(() => target.CanRedo.Subscribe(_ => { }));
Assert.Throws<ObjectDisposedException>(() => target.CanClear.Subscribe(_ => { }));
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void First_Snapshot_Should_Push_One_Undo_State()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => { }, () => { });
Assert.Single(target.Undos);
Assert.Empty(target.Redos);
Assert.Equal(new bool[] { true }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { true }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Snapshot_Should_Clear_Redos()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => { }, () => { });
Assert.Single(target.Undos);
Assert.Empty(target.Redos);
var result = target.Undo();
Assert.Empty(target.Undos);
Assert.Single(target.Redos);
Assert.True(result);
target.Snapshot(() => { }, () => { });
Assert.Single(target.Undos);
Assert.Empty(target.Redos);
Assert.Equal(new bool[] { true, false, true }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { true, false }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { true, true, true }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Invoking_Undo_Should_Not_Throw_When_Undos_Are_Empty()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
var result = target.Undo();
Assert.False(result);
Assert.Equal(new bool[] { }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Invoking_Redo_Should_Not_Throw_When_Redos_Are_Empty()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
var result = target.Redo();
Assert.False(result);
Assert.Equal(new bool[] { }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Invoking_Undo_Should_Invoke_Undo_Action_And_Push_State_To_Redos()
{
int undoCount = 0;
int redoCount = 0;
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => undoCount++, () => redoCount++);
var undo = target.Undos.Peek();
var result = target.Undo();
Assert.Equal(1, undoCount);
Assert.Equal(0, redoCount);
Assert.Empty(target.Undos);
Assert.Single(target.Redos);
Assert.True(result);
Assert.Equal(new bool[] { true, false }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { true }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { true, true }, helper.CanClears.ToArray());
var redo = target.Redos.Peek();
Assert.Equal(undo, redo);
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Invoking_Redo_Should_Invoke_Redo_Action_And_Push_State_To_Undos()
{
int undoCount = 0;
int redoCount = 0;
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => undoCount++, () => redoCount++);
var undo1 = target.Undos.Peek();
var result1 = target.Undo();
var redo1 = target.Redos.Peek();
var result2 = target.Redo();
Assert.Single(target.Undos);
Assert.Empty(target.Redos);
Assert.True(result1);
Assert.True(result2);
Assert.Equal(new bool[] { true, false, true }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { true, false }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { true, true, true }, helper.CanClears.ToArray());
var undo2 = target.Undos.Peek();
Assert.Equal(undo1, undo2);
Assert.Equal(undo1, redo1);
Assert.Equal(1, undoCount);
Assert.Equal(1, redoCount);
Assert.True(result1);
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Undo_Sets_IsPaused_True_While_Invoking_Undo_Redo_State()
{
var target = new StackHistory();
target.Snapshot(
undo: () => Assert.True(target.IsPaused),
redo: () => Assert.True(target.IsPaused));
Assert.False(target.IsPaused);
target.Undo();
Assert.False(target.IsPaused);
Assert.False(target.IsPaused);
target.Redo();
Assert.NotNull(target.Undos);
Assert.NotNull(target.Redos);
Assert.Empty(target.Undos);
Assert.Empty(target.Redos);
Assert.False(target.IsPaused);
Assert.Equal(new bool[] { }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { }, helper.CanClears.ToArray());
}
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Dispose_Should_Release_Allocated_Resources()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => { }, () => { });
target.Snapshot(() => { }, () => { });
var result = target.Undo();
Assert.Single(target.Undos);
Assert.Single(target.Redos);
Assert.True(result);
target.Dispose();
Assert.Empty(target.Undos);
Assert.Empty(target.Redos);
Assert.Throws<ObjectDisposedException>(() => target.CanUndo.Subscribe(_ => { }));
Assert.Throws<ObjectDisposedException>(() => target.CanRedo.Subscribe(_ => { }));
Assert.Throws<ObjectDisposedException>(() => target.CanClear.Subscribe(_ => { }));
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void First_Snapshot_Should_Push_One_Undo_State()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => { }, () => { });
Assert.Single(target.Undos);
Assert.Empty(target.Redos);
Assert.Equal(new bool[] { true }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { true }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Snapshot_Should_Clear_Redos()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => { }, () => { });
Assert.Single(target.Undos);
Assert.Empty(target.Redos);
var result = target.Undo();
Assert.Empty(target.Undos);
Assert.Single(target.Redos);
Assert.True(result);
target.Snapshot(() => { }, () => { });
Assert.Single(target.Undos);
Assert.Empty(target.Redos);
Assert.Equal(new bool[] { true, false, true }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { true, false }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { true, true, true }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Invoking_Undo_Should_Not_Throw_When_Undos_Are_Empty()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
var result = target.Undo();
Assert.False(result);
Assert.Equal(new bool[] { }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Invoking_Redo_Should_Not_Throw_When_Redos_Are_Empty()
{
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
var result = target.Redo();
Assert.False(result);
Assert.Equal(new bool[] { }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { }, helper.CanClears.ToArray());
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Invoking_Undo_Should_Invoke_Undo_Action_And_Push_State_To_Redos()
{
int undoCount = 0;
int redoCount = 0;
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => undoCount++, () => redoCount++);
var undo = target.Undos.Peek();
var result = target.Undo();
Assert.Equal(1, undoCount);
Assert.Equal(0, redoCount);
Assert.Empty(target.Undos);
Assert.Single(target.Redos);
Assert.True(result);
Assert.Equal(new bool[] { true, false }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { true }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { true, true }, helper.CanClears.ToArray());
var redo = target.Redos.Peek();
Assert.Equal(undo, redo);
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Invoking_Redo_Should_Invoke_Redo_Action_And_Push_State_To_Undos()
{
int undoCount = 0;
int redoCount = 0;
var target = new StackHistory();
using (var helper = new HistoryHelper(target))
{
target.Snapshot(() => undoCount++, () => redoCount++);
var undo1 = target.Undos.Peek();
var result1 = target.Undo();
var redo1 = target.Redos.Peek();
var result2 = target.Redo();
Assert.Single(target.Undos);
Assert.Empty(target.Redos);
Assert.True(result1);
Assert.True(result2);
Assert.Equal(new bool[] { true, false, true }, helper.CanUndos.ToArray());
Assert.Equal(new bool[] { true, false }, helper.CanRedos.ToArray());
Assert.Equal(new bool[] { true, true, true }, helper.CanClears.ToArray());
var undo2 = target.Undos.Peek();
Assert.Equal(undo1, undo2);
Assert.Equal(undo1, redo1);
Assert.Equal(1, undoCount);
Assert.Equal(1, redoCount);
Assert.True(result1);
}
}
[Fact]
[Trait("ReactiveHistory", "StackHistory")]
public void Undo_Sets_IsPaused_True_While_Invoking_Undo_Redo_State()
{
var target = new StackHistory();
target.Snapshot(
undo: () => Assert.True(target.IsPaused),
redo: () => Assert.True(target.IsPaused));
Assert.False(target.IsPaused);
target.Undo();
Assert.False(target.IsPaused);
Assert.False(target.IsPaused);
target.Redo();
Assert.False(target.IsPaused);
}
}

Просмотреть файл

@ -1,21 +1,20 @@
using System;
using Xunit;
namespace ReactiveHistory.UnitTests
namespace ReactiveHistory.UnitTests;
public class StateTests
{
public class StateTests
[Fact]
[Trait("ReactiveHistory", "State")]
public void Constructor_Should_Set_Undo_And_Redo_Fields()
{
[Fact]
[Trait("ReactiveHistory", "State")]
public void Constructor_Should_Set_Undo_And_Redo_Fields()
{
void undo()
{ }
void redo()
{ }
var target = new State(undo, redo, string.Empty, string.Empty);
Assert.Equal(undo, target.Undo);
Assert.Equal(redo, target.Redo);
}
void undo()
{ }
void redo()
{ }
var target = new State(undo, redo, string.Empty, string.Empty);
Assert.Equal(undo, target.Undo);
Assert.Equal(redo, target.Redo);
}
}
}