Search code examples
spreadsheetgear

Using workbookView.ActiveCell resets _undoList of workbookView.ActiveCommandManager


Version - SpreadsheetGear 9.2.44 - WPF as UI

I don't think 9.2.59 fixes this, I can't seem to find anything in the changes that mentions the ActiveCommandManager problem. I can't use 9.2.59 due to the invalid formula bug that causes hard-crashes with WPF.

I have a scenario where we want the cell to have a specific font color if certain conditions are met but we want the text to reset to black when they begin editing the cell for better readability. My solution was to use the BeginCellEdit event to set the wbv.ActiveCell.Font.Color to black and then swap it back on CellEditEnd. However, I found that doing wbv.ActiveCell.Font.Color clears the wbv.ActiveCommandManager so only one call to Undo can be done after the cell edit ends. Is there a way to NOT have the undo list nuked? Is there a way that I can just manage it myself if the actual intended behavior is to clear the list when using the .ActiveCell?

Here is sample code to showcase the issue:

Note: I am not using reflection in any way in my actual project, here it's just so we can see what is happening to that data.
MainWindow.xaml

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ssgtesting"
        xmlns:sg="http://schemas.spreadsheetgear.com/controls/xaml" x:Class="ssgtesting.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>

        <sg:WorkbookView CellBeginEdit="wbv_CellBeginEdit" Name="wbv"/>
        <Button Content="Undo" HorizontalAlignment="Left" Margin="203,40,0,0" VerticalAlignment="Top" Click="Button_Click" Width="265" Height="130"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Reflection;
using System.Windows;

namespace ssgtesting
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                wbv.GetLock();
                var activeCommandManager = wbv.ActiveCommandManager;
                Type type = activeCommandManager.GetType();
                FieldInfo undoListField = type.GetField("_undoList", BindingFlags.NonPublic | BindingFlags.Instance);
                if (undoListField != null)
                {
                    var undoList = undoListField.GetValue(activeCommandManager);
                    // Set breakpoint here and check the count of the list
                    if (undoList != null) { }
                }
                wbv.ActiveCommandManager.Undo();
            }
            // This might throw depending on when you click undo because if you double click it since the list is only 1 then it cant undo twice.
            catch (Exception) { throw; }
            finally { wbv.ReleaseLock(); }
        }

        private void wbv_CellBeginEdit(object sender, SpreadsheetGear.Windows.Controls.CellBeginEditEventArgs e)
        {
            var activeCommandManager = wbv.ActiveCommandManager;
            Type type = activeCommandManager.GetType();
            FieldInfo undoListField = type.GetField("_undoList", BindingFlags.NonPublic | BindingFlags.Instance);
            if (undoListField != null)
            {
                var undoList = undoListField.GetValue(activeCommandManager);
                // Set breakpoint here and check the count of the list
                if (undoList != null) { }
            }

            // This nukes the undo list
            // Why does it do that? How can I avoid this?
            // Comment out this line to show that it clears the undo list
            wbv.ActiveCell.Font.Color = SpreadsheetGear.Color.FromArgb(0x00, 0x00, 0x00);

            activeCommandManager = wbv.ActiveCommandManager;
            type = activeCommandManager.GetType();
            undoListField = type.GetField("_undoList", BindingFlags.NonPublic | BindingFlags.Instance);
            if (undoListField != null)
            {
                var undoList = undoListField.GetValue(activeCommandManager);
                // Set breakpoint here and check the count of the list
                if (undoList != null) { }
            }
        }
    }
}

Solution

  • There are really two parts to this question:

    1. Persisting undo capabilities
    2. The end goal of reverting a cell's font color upon entering Edit Mode

    Persisting undo capabilities

    The behavior you are seeing with the undo stack clearing is by design. Undo / redo is only supported when a given task is executed as a Command through the ActiveCommandManager of a WorkbookView. When interacting with the WorkbookView, most actions taken by a user are executed as various Commands under the hood and so undo / redo is persisted.

    Direct modifications to workbook contents outside of a Command (e.g., direct calls to IRange / IWorksheet / IWorkbook / etc., members that modify a workbook in some way) will clear the undo stack because the CommandManagber can no longer reliably get back to the state it was tracking before the direct API call was made.

    In your case you are setting wbv.ActiveCell.Font.Color outside of an executing Command, which clears the stack. To preserve undo you would need to wrap this action in a Command, for instance:

    using SpreadsheetGear.Commands;
    
    public class ResetFontColorCommand : CommandRange
    {
        public ResetFontColorCommand(IRange range) : base(range) { }
    
        protected override bool Execute()
        {
            Range.Font.ColorIndex = SpreadsheetGear.ColorIndex.Automatic;
            return base.Execute();
        }
    
        public override CommandUndoSupport UndoSupport => CommandUndoSupport.Full;
    
        public override string DisplayText => "Reset Font Color";
    }
    

    You could then execute this command by doing the following:

    var command = new ResetFontColorCommand(workbookView.ActiveCell);
    workbookView.ActiveCommandManager.Execute(command);
    

    Note that Full CommandUndoSupport means this Command will be added to the undo stack (it can be undone). There is also an Ignore flag that will execute the command but not add the command to the undo stack. So hitting Ctrl+Z in this case would "skip" over this command (e.g., your black text would persist) and undo whatever was executed prior. None is another option, which would clear the undo stack after executing.

    Reverting a cell's font color upon entering Edit Mode

    If you try executing the above ResetFontColorCommand inside CellBeginEdit, you'll see a problem: your cell's font color does not immediately revert to black in edit mode. However, it will do so after you commit a change in Edit Mode or cancel it.

    This is because by the time CellBeginEdit gets called, Edit Mode has already been initialized enough that the font color cannot be changed, and so the color change must wait until Edit Mode is dismissed.

    There's not any good way around this CellBeginEdit behavior, so executing your command in this event is not an option, unfortunately, nor can I really think of any great alternatives. About the only thing that comes to mind is to execute this command on more basic events that occur before Edit Mode is initialized (e.g., a user might start typing to edit a cell value, so detect key press events when the WorkbookView has focus and an ActiveCell is present; detect F2 key presses; or detect double-clicks on a cell, etc.)