From 9c80a8931dff4ce0790fec8e228fd2c54f1a8cee Mon Sep 17 00:00:00 2001 From: anmcgrath Date: Sat, 3 Jun 2023 14:03:48 +1000 Subject: [PATCH] Add cell metadata --- .../Commands/SetMetaDataCommand.cs | 33 ++++++++ src/BlazorDatasheet/Data/Cell.cs | 36 +++++++++ src/BlazorDatasheet/Data/Sheet.cs | 75 ++++++++++++++++--- .../Events/CellMetaDataChangeEventArgs.cs | 19 +++++ .../Interfaces/IReadOnlyCell.cs | 2 + test/BlazorDatasheet.Test/MetaDataTests.cs | 46 ++++++++++++ 6 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 src/BlazorDatasheet/Commands/SetMetaDataCommand.cs create mode 100644 src/BlazorDatasheet/Events/CellMetaDataChangeEventArgs.cs create mode 100644 test/BlazorDatasheet.Test/MetaDataTests.cs diff --git a/src/BlazorDatasheet/Commands/SetMetaDataCommand.cs b/src/BlazorDatasheet/Commands/SetMetaDataCommand.cs new file mode 100644 index 00000000..b5e2b22b --- /dev/null +++ b/src/BlazorDatasheet/Commands/SetMetaDataCommand.cs @@ -0,0 +1,33 @@ +using BlazorDatasheet.Data; + +namespace BlazorDatasheet.Commands; + +public class SetMetaDataCommand : IUndoableCommand +{ + private readonly int _row; + private readonly int _col; + private readonly string _name; + private readonly object? _value; + private object? _oldValue; + + public SetMetaDataCommand(int row, int col, string name, object? value) + { + _row = row; + _col = col; + _name = name; + _value = value; + } + + public bool Execute(Sheet sheet) + { + _oldValue = sheet.GetMetaData(_row, _col, _name); + sheet.SetMetaDataImpl(_row, _col, _name, _value); + return true; + } + + public bool Undo(Sheet sheet) + { + sheet.SetMetaDataImpl(_row, _col, _name, _oldValue); + return true; + } +} \ No newline at end of file diff --git a/src/BlazorDatasheet/Data/Cell.cs b/src/BlazorDatasheet/Data/Cell.cs index 37c5a7dd..96c376d0 100644 --- a/src/BlazorDatasheet/Data/Cell.cs +++ b/src/BlazorDatasheet/Data/Cell.cs @@ -58,6 +58,9 @@ public class Cell : IReadOnlyCell, IWriteableCell /// public Type DataType { get; set; } + private Dictionary? _metaData; + public IReadOnlyDictionary MetaData => _metaData ?? new Dictionary(); + /// /// Represents an individual datasheet cell /// @@ -151,6 +154,17 @@ public T GetValue() } } + public void ClearMetadata() + { + _metaData?.Clear(); + } + + public void Clear() + { + ClearMetadata(); + ClearValue(); + } + public void ClearValue() { var currentVal = GetValue(); @@ -199,6 +213,28 @@ public bool TrySetValue(T val) return TrySetValue(val, typeof(T)); } + internal void SetCellMetaData(string name, object? value) + { + if (_metaData == null) + _metaData = new Dictionary(); + + if (!_metaData.ContainsKey(name)) + _metaData.Add(name, value); + _metaData[name] = value; + } + + public object? GetMetaData(string name) + { + if (HasMetaData(name)) + return _metaData[name]; + return null; + } + + public bool HasMetaData(string name) + { + return _metaData != null && _metaData.ContainsKey(name); + } + public bool DoTrySetValue(object? val, Type type) { try diff --git a/src/BlazorDatasheet/Data/Sheet.cs b/src/BlazorDatasheet/Data/Sheet.cs index 19fe214a..e0d2b704 100644 --- a/src/BlazorDatasheet/Data/Sheet.cs +++ b/src/BlazorDatasheet/Data/Sheet.cs @@ -123,6 +123,8 @@ public class Sheet public event EventHandler? CellsSelected; + public event EventHandler? MetaDataChanged; + /// /// Fired when cells are merged /// @@ -303,9 +305,12 @@ internal bool RemoveRowAtImpl(int rowIndex) /// index of inserted row\column /// count of inserted or removed rows\columns. count > 0 when inserted, count < 0 when reomved /// list of affected regions (before operation state) and list of new regions (state after operation) - internal (IReadOnlyList mergesPerformed, IReadOnlyList overridenMergedRegions) RerangeMergedCells(Axis axis, int index, int count) + internal (IReadOnlyList mergesPerformed, IReadOnlyList overridenMergedRegions) + RerangeMergedCells(Axis axis, int index, int count) { - var afterInserted = axis == Axis.Row ? new Region(index, NumRows, 0, NumCols) : new Region(0, NumRows, index, NumCols); + var afterInserted = axis == Axis.Row + ? new Region(index, NumRows, 0, NumCols) + : new Region(0, NumRows, index, NumCols); var envelope = afterInserted.ToEnvelope(); var mergesPerformed = MergedCells.Search(envelope); var overridenMergedRegions = new List(); @@ -343,7 +348,8 @@ internal bool RemoveRowAtImpl(int rowIndex) MergedCells.Delete(item); - if ((region.Top != region.Bottom && region.Left != region.Right) || region is RowRegion || region is ColumnRegion) + if ((region.Top != region.Bottom && region.Left != region.Right) || region is RowRegion || + region is ColumnRegion) { var merge = new CellMerge(region); MergedCells.Insert(merge); @@ -353,17 +359,20 @@ internal bool RemoveRowAtImpl(int rowIndex) return (mergesPerformed, overridenMergedRegions.AsReadOnly()); } + /// /// Undo rerange operation to restore state before Insert\Remove rows\columns commands /// /// state to return on /// state to undo - internal void UndoRerangeMergedCells(IReadOnlyList _mergesPerformed, IReadOnlyList _overridenMergedRegions) + internal void UndoRerangeMergedCells(IReadOnlyList _mergesPerformed, + IReadOnlyList _overridenMergedRegions) { foreach (var item in _overridenMergedRegions) { MergedCells.Delete(item); } + foreach (var item in _mergesPerformed) { MergedCells.Insert(item); @@ -425,8 +434,8 @@ public BRange Range(IEnumerable regions) public IEnumerable GetCellsInRegion(IRegion region) { return (new BRange(this, region)) - .Positions - .Select(x => this.GetCell(x.row, x.col)); + .Positions + .Select(x => this.GetCell(x.row, x.col)); } /// @@ -477,9 +486,9 @@ public IReadOnlyCell GetCell(CellPosition position) internal IEnumerable<(int row, int col)> GetNonEmptyCellPositions(IRegion region) { return _cellDataStore.GetNonEmptyPositions(region.TopLeft.Row, - region.BottomRight.Row, - region.TopLeft.Col, - region.BottomRight.Col); + region.BottomRight.Row, + region.TopLeft.Col, + region.BottomRight.Col); } #endregion @@ -535,6 +544,47 @@ internal bool TrySetCellValueImpl(int row, int col, object? value, bool raiseEve return setValue; } + /// + /// Sets cell metadata, specified by name, for the cell at position row, col + /// + /// + /// + /// + /// + /// Whether setting the cell metadata was successful + public bool SetCellMetaData(int row, int col, string name, object? value) + { + var cmd = new SetMetaDataCommand(row, col, name, value); + return Commands.ExecuteCommand(cmd); + } + + internal void SetMetaDataImpl(int row, int col, string name, object? value) + { + var cell = _cellDataStore.Get(row, col); + if (cell == null) + { + cell = new Cell(); + _cellDataStore.Set(row, col, cell); + } + + var oldMetaData = cell.GetMetaData(name); + + cell.SetCellMetaData(name, value); + this.MetaDataChanged?.Invoke(this, new CellMetaDataChangeEventArgs(row, col, name, oldMetaData, value)); + } + + /// + /// Returns the metadata with key "name" for the cell at row, col. + /// + /// + /// + /// + /// + public object? GetMetaData(int row, int col, string name) + { + return GetCell(row, col)?.GetMetaData(name); + } + internal void SetCell(int row, int col, Cell cell) { @@ -568,6 +618,7 @@ public bool SetCellValues(List changes) var cmd = new SetCellValuesCommand(changes); return Commands.ExecuteCommand(cmd); } + /// /// Set read only state for specified cell /// @@ -591,6 +642,7 @@ internal void SetCellReadOnlyImpl(int row, int col, bool readOnly) var cell = _cellDataStore.Get(row, col); cell.IsReadOnly = readOnly; } + /// /// Performs the actual setting of cell values, including raising events for any changes made. /// @@ -910,7 +962,7 @@ private IEnumerable SetColumnFormatImpl(CellFormat cellFormat foreach (var rowInterval in RowFormats.GetAllIntervals()) { overlappingRegions.Add(new Region(rowInterval.Start, rowInterval.End, region.Start.Col, - region.End.Col)); + region.End.Col)); } foreach (var overlapRegion in overlappingRegions) @@ -961,7 +1013,7 @@ private IEnumerable SetRowFormatImpl(CellFormat cellFormat, R foreach (var colInterval in ColFormats.GetAllIntervals()) { overlappingRegions.Add(new Region(region.Start.Row, region.End.Row, colInterval.Start, - colInterval.End)); + colInterval.End)); } foreach (var overlapRegion in overlappingRegions) @@ -1048,6 +1100,7 @@ internal CellChangedFormat SetCellFormat(int row, int col, CellFormat cellFormat strBuilder.Append(value); } } + if (col != c1) strBuilder.Append(tabDelimiter); } diff --git a/src/BlazorDatasheet/Events/CellMetaDataChangeEventArgs.cs b/src/BlazorDatasheet/Events/CellMetaDataChangeEventArgs.cs new file mode 100644 index 00000000..3cb268c4 --- /dev/null +++ b/src/BlazorDatasheet/Events/CellMetaDataChangeEventArgs.cs @@ -0,0 +1,19 @@ +namespace BlazorDatasheet.Events; + +public class CellMetaDataChangeEventArgs +{ + public CellMetaDataChangeEventArgs(int row, int col, string name, object? oldValue, object? newValue) + { + Name = name; + OldValue = oldValue; + NewValue = newValue; + Row = row; + Col = col; + } + + public int Row { get; } + public int Col { get; } + public string Name { get; } + public object? OldValue { get; } + public object? NewValue { get; } +} \ No newline at end of file diff --git a/src/BlazorDatasheet/Interfaces/IReadOnlyCell.cs b/src/BlazorDatasheet/Interfaces/IReadOnlyCell.cs index ba9ca4c5..b0a71e9c 100644 --- a/src/BlazorDatasheet/Interfaces/IReadOnlyCell.cs +++ b/src/BlazorDatasheet/Interfaces/IReadOnlyCell.cs @@ -15,4 +15,6 @@ public interface IReadOnlyCell public int Col { get; } public bool IsValid { get; } public object? Data { get; } + object? GetMetaData(string name); + bool HasMetaData(string name); } \ No newline at end of file diff --git a/test/BlazorDatasheet.Test/MetaDataTests.cs b/test/BlazorDatasheet.Test/MetaDataTests.cs new file mode 100644 index 00000000..03b07da8 --- /dev/null +++ b/test/BlazorDatasheet.Test/MetaDataTests.cs @@ -0,0 +1,46 @@ +using BlazorDatasheet.Data; +using NUnit.Framework; + +namespace BlazorDatasheet.Test; + +public class MetaDataTests +{ + [Test] + public void Set_Cell_MetaData_And_Undo_Works() + { + var sheet = new Sheet(3, 3); + sheet.SetCellMetaData(1, 1, "test", 7); + Assert.AreEqual(7, sheet.GetMetaData(1, 1, "test")); + sheet.SetCellMetaData(1, 1, "test", 8); + Assert.AreEqual(8, sheet.GetMetaData(1, 1, "test")); + sheet.Commands.Undo(); + Assert.AreEqual(7, sheet.GetMetaData(1, 1, "test")); + } + + [Test] + public void Set_Cell_MetaData_Fires_Event() + { + var sheet = new Sheet(3, 3); + var fired = false; + var rowFired = -1; + var colFired = -1; + var nameFired = ""; + object? newData = null; + sheet.MetaDataChanged += (sender, args) => + { + fired = true; + rowFired = args.Row; + colFired = args.Col; + newData = args.NewValue; + nameFired = args.Name; + }; + + sheet.SetCellMetaData(1, 2, "test", "value"); + + Assert.True(fired); + Assert.AreEqual(1, rowFired); + Assert.AreEqual(2, colFired); + Assert.AreEqual("test", nameFired); + Assert.AreEqual("value", newData); + } +} \ No newline at end of file