Detect If Formula Was Invoked Via F2+{Enter} Or Double-click+{Enter}

133 views
Skip to first unread message

Alexander Rickman

unread,
Apr 19, 2023, 8:35:37 AM4/19/23
to Excel-DNA

Hi all,

I have built a custom RTD server (deriving from ExcelRtdServer) and I am trying to determine a way to preserve the manual (single-cell) recalculation functionality of non-RTD functions in my custom RTD wrapper functions. I am aware that I could create a button on the ribbon that runs a callback to manually refresh data, in fact I have already done so, but my users are very disturbed by the fact that they can’t get live data by manually refreshing a cell themselves.

Some background: In a standard Excel function, one can manually refresh a single cell via keying F2+{Enter} or double-clicking into a cell and then keying {Enter}. In both cases, Excel first enters “Edit Mode”, and then keying {Enter} actually invokes the function. Is there any way to detect this within a function call? And if not from within a function call, could I somehow hook window events (possibly borrowing some logic from ExcelDna.IntelliSense) to detect if Excel is in “Edit Mode”, set a global boolean, and then read that from within a function call?

Here’s how I plan on using that info from within an RTD wrapper function call: If a function was invoked via keying F2+{Enter} or double-clicking into a cell and then keying {Enter}, I can use the function execution info (i.e. a string comprised of the function name + parameters), read from a dictionary keyed on function execution info (with live RTD topics as its values), pull out the associated topic, and then update it via topic.UpdateValue(). This would also refresh any other existing cells with the same topic, which is ideal.

I am at a standstill here, so I’ll take all the suggestions I can get.

Thanks,

Alexander

1585471449

unread,
Apr 19, 2023, 8:35:48 AM4/19/23
to Alexander Rickman
您的邮件已收到,谢谢!

Govert van Drimmelen

unread,
Apr 19, 2023, 9:00:41 AM4/19/23
to exce...@googlegroups.com

Hi Alexander,

 

Excel sheets in Manual recalculation mode don’t work easily with RTD-based functions.

 

Maybe you need non-RTD functions that are just marked IsVolatile=true, and then do some caching of the values at the back.

This will let the functions recalculate on every F9 press.

Do you need push-updates in addition to the Manual calculation mode updates?

 

Another options might be to skip the RTD part for sheets that are doing manual recalculation, thus making those function calls synchronous.

To check this, you need to mark your function as IsMacroType=true, then you can call

    var calculationMode = XlCall.Excel(XlCall.xlfGetDocument, 14)

 

1 = Automatic 

2 = Automatic except tables 

3 = Manual

 

Another approach is to switch to the “native async functions” of Excel. If you look for “ExcelAsyncHandle” on the Google Group or GitHub, you should find some examples.

Native async functions work different to RTD-based function, in that the user can’t interact with the workbook while the functions are running.

So the interaction can be quite awkward.

But they do work nicely with Manual calculation mode.

 

Trying to build a different input and calculation model on top of Excel, detecting editing events etc. seem challenging.

Calling topic.UpdateValue() does not help you at all – your user (or some macro, I suppose) needs to do the recalculation after Excel has responded to the UpdateValue() and fetched the data, which might be some seconds later.

 

-Govert

--
You received this message because you are subscribed to the Google Groups "Excel-DNA" group.
To unsubscribe from this group and stop receiving emails from it, send an email to exceldna+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/exceldna/00ad6f2a-319c-4abc-863d-8fdb2ab3cc13n%40googlegroups.com.

Kedar Kulkarni

unread,
Apr 19, 2023, 9:22:54 AM4/19/23
to Excel-DNA
I think we had a similar situation a while back. We use reactive extensions and not ExcelRTDServer so a bit unsure if the below is useful. 

We created 2 different functions, one that returns RTD/stream and one that returns point-in-time information. For getting RTD - we used ExcelAsyncUtil.Observe(functionName, paramInfo) which returns RX observable sequence and continues to refresh RTD till the time observable produces value. For non RTD we just get point in time data without using Observe but just get the latest value. We use reactive extensions and there is a way to just get the latest value and complete the observable sequence. 

If we do not change the parameters for an Excel function - the cached value would be served by ExcelAsyncUtil.Observe and it would not refresh if we press F2 and enter, so we bypassed that for the function that refreshes when we press F2 and enter. 

Till the time we do not use a volatile precedent to the function, the formula would not refresh and the user must press F2 and enter to refresh the value which would execute the function again. 

not sure if this is helpful 
thanks

Alexander Rickman

unread,
Apr 19, 2023, 10:22:36 AM4/19/23
to Excel-DNA
Hi Govert, 

"Do you need push-updates in addition to the Manual calculation mode updates?"
Yes. I have implemented a system that automatically refreshes groups of functions based on an interval that users can set. This way they consistently receive near real-time data. 

I am using `RTD` functions to take advantage of the built in caching, ability to batch function calls, multithreaded execution, and all while allowing users to interact with Excel. I know I could achieve the batching and multithreaded execution with "native async functions" using something similar to AsyncBatchSample, but then my users lose out on the ability to interact with the Excel while the functions are running.

"Calling topic.UpdateValue() does not help you at all" 
I should have been more clear. What I meant when I said I would call  topic.UpdateValue() was this: I would run the required code to retrieve the live data point and then call  topic.UpdateValue() using that newly retrieved value. 

"Trying to build a different input and calculation model on top of Excel, detecting editing events etc. seem challenging."
I really am only concerned about manual recalculation for the single cell case . My users can live without F9 force recalculation of multiple cells and just use the "Refresh All" button I added to the ribbon for that purpose. If I just had the logic to determine how the function was invoked, I really think I could get this to work. 
On Wednesday, April 19, 2023 at 9:00:41 AM UTC-4 Govert van Drimmelen wrote:

Alexander Rickman

unread,
Apr 19, 2023, 12:59:27 PM4/19/23
to Excel-DNA
Hi Kedar, 

This is interesting.

"If we do not change the parameters for an Excel function - the cached value would be served by ExcelAsyncUtil.Observe and it would not refresh if we press F2 and enter, so we bypassed that for the function that refreshes when we press F2 and enter." 

Are you saying that you were actually able to to detect when a function was invoked via F2+{Enter}? If so, would you mind sharing how? As I mentioned, If I can detect this, I would just run conditional logic that pulls either a live or cached value. 

Kedar Kulkarni

unread,
Apr 19, 2023, 2:00:51 PM4/19/23
to Excel-DNA
Actually, I do not have to detect it. As these 2 are different formulas but reuse the same code, users would use the correct formula as per their needs. 

The user will use either Clock.Live which is RTD and Clock.F2 for a refresh on demand and both have the same signatures so the user could use them interchangeably by just replacing ".Live("  to ".F2(" in the worksheet. I mean if users are flexible then I think this approach would work but if they want the exact same function to do both then below won't work. 

This is a very basic example and uses reactive extensions. 

try using following code and the excel formula =Clock.Live("aaa") and =Clock.F2("aaa")


using ExcelDna.Integration;
using System;
using System.Linq;
using System.Reactive.Linq;

namespace DCTools
{
    public static class RxExample
    {
        [ExcelFunction(Name ="Clock.Live") ]
        public static object LiveClock(object name)
        {
            var intervalSeconds = 2;
            return RxExcel.Observe("LiveClock", new[] { name, intervalSeconds }, () =>
                    Observable.Interval(TimeSpan.FromSeconds(intervalSeconds)).Select(_ => GetClock(name)));
        }

        [ExcelFunction(Name = "Clock.F2")]
        public static object F2Clock(object name)
        {
            return GetClock(name);
        }

        private static string GetClock(object name)
        {
            return name.ToString() + " - " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        }
    }

    public static class RxExcel
    {
        public static IExcelObservable ToExcelObservable<T>(this IObservable<T> observable)
        {
            return new ExcelObservable<T>(observable);
        }

        public static object Observe<T>(string functionName, object parameters, Func<IObservable<T>> observableSource)
        {
            return ExcelAsyncUtil.Observe(functionName, parameters, () => observableSource().ToExcelObservable());
        }
    }

    public class ExcelObservable<T> : IExcelObservable
    {
        readonly IObservable<T> _observable;

        public ExcelObservable(IObservable<T> observable)
        {
            _observable = observable;
        }

        public IDisposable Subscribe(IExcelObserver observer)
        {
            return _observable.Subscribe(value => observer.OnNext(value), observer.OnError, observer.OnCompleted);
        }
    }
}

Alexander Rickman

unread,
Apr 20, 2023, 3:18:52 PM4/20/23
to Excel-DNA
Thanks for the info Kedar. That would work, but unfortunately my users are unwilling to make the compromise.

I have discovered a possible solution, but I can't seem to get it to work. Here is a link to the StackOverflow post about it. 

Toby Jacobs

unread,
Apr 22, 2023, 6:40:26 AM4/22/23
to exce...@googlegroups.com
One thing to note is that when a user manually computes a cell, there will be exactly one cell computed in a short time (e.g. say 50ms).

As a consequence, you could use an windowed observable (Rx / Reactive Extensions) to count the number of compute calls at the same time as you set up your ID for the RTD observable. If you only get one, then you trigger a call to generate a fresh ID so that the old observable is invalidated, and you SHOULD get a fresh computation. And users likely wouldn't have to wait TOO long.

NB this is assuming you're using the ExcelObservable mechanism to do RTD, rather than a direct RTD server. No idea how to handle that beyond general hooking of excel events and passing e.g. selection events and crucially keyboard events for the enter key hooked at the window level to indicate when a user got there. That kind of approach would be prone to weird behaviour if not done exactly right though.

Alexander Rickman

unread,
Apr 22, 2023, 3:06:17 PM4/22/23
to Excel-DNA

After working on this for a while, I finally came with a solution. Originally, I was attempting to sub class Excel’s editor window using the WinAPI and NativeWindow to listen for specific messages that would indicate that Excel’s edit mode state had changed, but the WndProc override was never getting invoked. I remembered reading about a post on Reddit a couple of months ago where someone described using the undocumented LPenHelper function (found in the the Excel SDK) to monitor the state of Excel’s edit mode by periodically calling it and inspecting inspecting the return value. I also noticed that  ExcelDna.IntelliSense  uses this function, albeit for a different purpose. All that being said, I dislike polling, but I don’t really see any other way of accomplishing this, so I wrote a monitor class to poll Excel’s edit mode state every 10ms, using a System.Threading.Timer, which runs on a ThreadPool thread. In my application, I create a globally scoped singleton instance of the class in the AutoOpen method and reference it where needed. Below is the how it all works together:

  • An RTD server scoped ConcurrentDictionary keyed on function execution info (i.e. a string comprised of the function name + parameters), with live RTD topics as its values is maintained.
  • A static method was defined on the RTD server to retrieve the current running instance of it.
  • An instance method was added to retrieve a topic from the aforementioned dictionary.
  • Within a function call, Excel’s last edit mode state is checked. If the the edit mode is != ready, then the running instance of the RTD server is retrieved (if it exists) and a key consisting of the function’s execution info is created and passed to the dictionary to retrieve the topic. If that topic exists, then the required code to retrieve the live data point is run, topic.UpdateValue is called and then topic.Value is returned to the caller. If the edit mode == ready and/or the topic is not found in the dictionary, then it is retrieved from the RTD server.

So far, testing has proven this to be a reliable way to handle single-cell manual recalculation, and I very happy with it.

Below is the code for my ExcelEditModeMonitor class and a sample of how to use it.

ExcelEditModeMonitor

using System; using System.Runtime.InteropServices; using System.Threading; /// <summary> /// Represents the "cell edit Mode" of Excel by periodically polling /// its state via the XLCALL32.dll function "LPenHelper" and inspecting /// its return value. /// </summary> public sealed class ExcelEditModeMonitor: IDisposable { // Constants, struct, and LPenHelper are all defined in the XLCALL32.h file private const int xlSpecial = 0x4000; private const int xlGetFmlaInfo = (14 | xlSpecial); [StructLayout(LayoutKind.Sequential)] private struct FmlaInfo { public int wPointMode; // current edit mode. 0 => rest of struct undefined public int cch; // count of characters in formula public IntPtr lpch; // pointer to formula characters. READ ONLY!!! public int ichFirst; // char offset to start of selection public int ichLast; // char offset to end of selection (may be > cch) public int ichCaret; // char offset to blinking caret } [DllImport("XLCALL32.DLL")] private static extern int LPenHelper(int wCode, ref FmlaInfo fmlaInfo); private Timer _timer; private long _disposedCount = 0; private const int PollingFrequency = 10; // milliseconds private readonly CancellationTokenSource _internalTokenSource; private readonly CancellationToken _internalToken; private readonly CancellationToken _externalToken; private xlEditMode _lastEditMode = xlEditMode.Ready; private xlEditMode _currentEditMode = xlEditMode.Ready; /// <summary> /// Represents the the current cell editing mode displayed on the left side of /// Excel's status bar. /// </summary> public enum xlEditMode { /// <summary> /// This value is does not correlate with an actual Excel cell edit mode, /// but is used as a return value when an exception occurs while attempting /// to poll the current state. /// </summary> Undefined = -1, /// <summary> /// Represents the default state. /// </summary> Ready = 0, /// <summary> /// Indicates when a cell is selected and typing has begun, or when F2 is keyed /// twice. /// </summary> Enter = 1, /// <summary> /// Indicates in-cell editing, which occurs as a result of double clicking a /// cell, keying F2 to enter data. /// </summary> Edit = 2, /// <summary> /// Indicates in-cell formula selection mode, which occurs as a result of entering /// a formula, and clicking the cells/ranges to include in that formula. /// </summary> Point = 4 } public xlEditMode LastEditMode => _lastEditMode; public xlEditMode CurrentEditMode => _currentEditMode; public event EventHandler<ExcelEditModeMonitorEventArgs> EditModeChange; public ExcelEditModeMonitor(CancellationToken cancellationToken) { _internalTokenSource = new CancellationTokenSource(); _internalToken = _internalTokenSource.Token; _externalToken = cancellationToken; _timer = new Timer(_timer_Elapsed, null, 0, PollingFrequency); } public void Dispose() { // don't dispose more than once if (Interlocked.Read(ref _disposedCount) > 0) return; _internalTokenSource.Cancel(); _timer.Change(Timeout.Infinite, Timeout.Infinite); _timer.Dispose(); EditModeChange = null; Interlocked.Increment(ref _disposedCount); } private void _timer_Elapsed(object state) { if (_externalToken.IsCancellationRequested || _internalToken.IsCancellationRequested) { _timer.Change(Timeout.Infinite, Timeout.Infinite); return; } _lastEditMode = _currentEditMode; _currentEditMode = GetEditMode(); if(_currentEditMode != _lastEditMode) { EditModeChange?.Invoke(this, new ExcelEditModeMonitorEventArgs(_lastEditMode, _currentEditMode)); } } private xlEditMode GetEditMode() { try { FmlaInfo fmlaInfo = new FmlaInfo(); int retVal = LPenHelper(xlGetFmlaInfo, ref fmlaInfo); if (retVal != 0) { return xlEditMode.Undefined; } return (xlEditMode)fmlaInfo.wPointMode; } catch { return xlEditMode.Undefined; } } } public sealed class ExcelEditModeMonitorEventArgs : EventArgs { public ExcelEditModeMonitor.xlEditMode LastEditMode { get; set; } public ExcelEditModeMonitor.xlEditMode CurrentEditMode { get; set; } public ExcelEditModeMonitorEventArgs(ExcelEditModeMonitor.xlEditMode lastEditMode, ExcelEditModeMonitor.xlEditMode currentEditMode) { LastEditMode = lastEditMode; CurrentEditMode = currentEditMode; } }

Example Usage

public sealed class ExcelDnaAddIn : IExcelAddIn { CancellationTokenSource _globalCancellationTokenSource; ExcelEditModeMonitor _editModeMonitor; public void AutoOpen() { _globalCancellationTokenSource = new CancellationTokenSource(); _editModeMonitor = new ExcelEditModeMonitor(_globalCancellationTokenSource); _editModeMonitor.EditModeChange += OnEditModeChange; } public void AutoClose() { _editModeMonitor.Dispose(); } private void OnExcelEditModeChange(object sender, ExcelEditModeMonitorEventArgs e) { var last = Enum.GetName(typeof(ExcelEditModeMonitor.xlEditMode), e.LastEditMode); var current = Enum.GetName(typeof(ExcelEditModeMonitor.xlEditMode), e.CurrentEditMode); System.Diagnostics.Debug.WriteLine($"Excel Edit Mode Changed From {last} to {current}"); } }

Govert van Drimmelen

unread,
Apr 22, 2023, 3:16:38 PM4/22/23
to Excel-DNA
Hi Alexander,

Thank you for writing back with details of your plan.

In the IntelliSense we had some occasional Excel crashes when calling the LPenHelper function from other threads, so eventually put those calls on the main Excel thread.
See this discussion and related PR:
-Govert

Alexander Rickman

unread,
Apr 24, 2023, 9:10:53 AM4/24/23
to Excel-DNA

Govert,

Glad to contribute to the community! Many thanks for the heads up!

Based on the suggestions in Issue#87, I made some revisions to ExcelEditModeMonitor. See below:

using System; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; /// <summary> /// Represents the "cell edit Mode" of Excel by periodically polling /// its state via the XLCALL32.dll function "LPenHelper" and inspecting /// its return value. /// </summary> public sealed class ExcelEditModeMonitor: IDisposable { // Constants, struct, and LPenHelper are all defined in the XLCALL32.h file private const int xlSpecial = 0x4000; private const int xlGetFmlaInfo = (14 | xlSpecial); [StructLayout(LayoutKind.Sequential)] private struct FmlaInfo { public int wPointMode; // current edit mode. 0 => rest of struct undefined public int cch; // count of characters in formula public IntPtr lpch; // pointer to formula characters. READ ONLY!!! public int ichFirst; // char offset to start of selection public int ichLast; // char offset to end of selection (may be > cch) public int ichCaret; // char offset to blinking caret } [DllImport("XLCALL32.DLL")] private static extern int LPenHelper(int wCode, ref FmlaInfo fmlaInfo); private readonly SynchronizationContext _excelMainThreadSyncContext; private System.Threading.Timer _timer; private long _disposedCount = 0; private const int PollingFrequency = 10; // milliseconds private readonly CancellationTokenSource _internalTokenSource; private readonly CancellationToken _internalToken; private readonly CancellationToken _externalToken; private xlEditMode _lastEditMode = xlEditMode.Ready; private xlEditMode _currentEditMode = xlEditMode.Ready; /// <summary> /// Represents the the current cell editing mode displayed on the left side of /// Excel's status bar. /// </summary> public enum xlEditMode { /// <summary> /// This value is does not correlate with an actual Excel cell edit mode, /// but is used as a return value when an exception occurs while attempting /// to poll the current state. /// </summary> Undefined = -1, /// <summary> /// Represents the default state. /// </summary> Ready = 0, /// <summary> /// Indicates when a cell is selected and typing has begun, or when F2 is keyed /// twice. /// </summary> Enter = 1, /// <summary> /// Indicates in-cell editing, which occurs as a result of double clicking a /// cell, keying F2 to enter data. /// </summary> Edit = 2, /// <summary> /// Indicates in-cell formula selection mode, which occurs as a result of entering /// a formula, and clicking the cells/ranges to include in that formula. /// </summary> Point = 4 } public xlEditMode LastEditMode => _lastEditMode; public xlEditMode CurrentEditMode => _currentEditMode; public ExcelEditModeMonitor(CancellationToken cancellationToken) { _excelMainThreadSyncContext = new WindowsFormsSynchronizationContext(); _internalTokenSource = new CancellationTokenSource(); _internalToken = _internalTokenSource.Token; _externalToken = cancellationToken; _timer = new System.Threading.Timer(_timer_Elapsed, null, 0, PollingFrequency); } public void Dispose() { // don't dispose more than once if (Interlocked.Read(ref _disposedCount) > 0) return; _internalTokenSource.Cancel(); _timer.Change(Timeout.Infinite, Timeout.Infinite); // Stop timer _timer.Dispose(); Interlocked.Increment(ref _disposedCount); } private void _timer_Elapsed(object state) { if (_externalToken.IsCancellationRequested || _internalToken.IsCancellationRequested) { _timer.Change(Timeout.Infinite, Timeout.Infinite); // Stop timer return; } // Switches Excel's Main UI thread, and updates state _excelMainThreadSyncContext.Post(_ => { _lastEditMode = _currentEditMode; _currentEditMode = GetEditMode(); }, null); } [HandleProcessCorruptedStateExceptions] private xlEditMode GetEditMode() { try { FmlaInfo fmlaInfo = new FmlaInfo(); int retVal = LPenHelper(xlGetFmlaInfo, ref fmlaInfo); if (retVal != 0) { return xlEditMode.Undefined; } return (xlEditMode)fmlaInfo.wPointMode; } catch { return xlEditMode.Undefined; } } }

Do you think this is sufficient?

Alexander Rickman

unread,
Apr 24, 2023, 10:12:54 AM4/24/23
to Excel-DNA

For got to dispose of _excelMainThreadSyncContext in the Dispose method.

See revision below:

public void Dispose() { // don't dispose more than once if (Interlocked.Read(ref _disposedCount) > 0) return; _internalTokenSource.Cancel(); _timer.Change(Timeout.Infinite, Timeout.Infinite); // Stop timer _timer.Dispose(); _excelMainThreadSyncContext.Dispose(); Interlocked.Increment(ref _disposedCount); }
Reply all
Reply to author
Forward
0 new messages