5

Excel VBA is a flexible development environment. It is pesudo-compiled. However, sometimes during development a "state loss" can occur. A "state loss" is when all variables are torn down. Indeed, VBA has an option "Notify before state loss" option for triage. It is unsurprising that one cannot Edit and Continue code in all cases. However, sometimes state losses happen whilst running in production because opening some other workbook may cause trauma to your application session (trust me, it happens!)

I know one can persist data to a worksheet cell or even a file but this is inappropriate for trying to retain an instance of a class, especially if that is the anchor for a whole object graph.

So in the case where one insists on a memory held variable how do you persist state over a state loss?

S Meaden
  • 8,050
  • 3
  • 34
  • 65

2 Answers2

9

One way to keep the data persistent during the lifetime of Excel is to store them on the default .Net domain attached to the instance:

Sub Usage()
    Dim dict As Object
    Set dict = GetPersistentDictionary()
End Sub
Public Function GetPersistentDictionary() As Object
    ' References:
    '  mscorlib.dll
    '  Common Language Runtime Execution Engine

    Const name = "weak-data"
    Static dict As Object

    If dict Is Nothing Then
      Dim host As New mscoree.CorRuntimeHost
      Dim domain As mscorlib.AppDomain
      host.Start
      host.GetDefaultDomain domain

      If IsObject(domain.GetData(name)) Then
        Set dict = domain.GetData(name)
      Else
        Set dict = CreateObject("Scripting.Dictionary")
        domain.SetData name, dict
      End If
    End If

    Set GetPersistentDictionary = dict
End Function
Florent B.
  • 41,537
  • 7
  • 86
  • 101
  • That is hugely interesting. I will google this to get the full story. – S Meaden Aug 15 '16 at 14:18
  • So does every Excel.exe session already have a .NET session running? – S Meaden Aug 15 '16 at 22:49
  • @S Meaden, the .Net runtime is automatically started when requested. So it shouldn't matter. But if I had to guess, I'd say that it depends whether there's some .Net addon installed or not. – Florent B. Aug 16 '16 at 11:17
  • Old question but just wondering; what are the restrictions on the sorts of objects I can do this with - does `domain.SetData` work with only dictionaries, or other stuff too (like a custom VBA class, or other library ones I could reference). Why have you used `Object` everywhere; does `GetData` not necessarily return an Object that can be cast to Dictionary, or is it just Late binding? Finally, how does the dictionary stay up to date, do you have to re`SetData` manually every time the dictionary changes, or does `dict` internally expose some event to `domain` for when the cache needs updating? – Greedo Jul 31 '19 at 15:26
  • 1
    @Greedo, `domain.SetData` should work with any type of data that can by coerced to a Variant. I use `Object` for late binding, but it should also work with early binding. An `Object` is just a pointer COM structure with only one copy, so the dictionary will remaing the same during the lifetime of Excel. – Florent B. Jul 31 '19 at 15:38
  • Oh hang on, does that mean you aren't making a shallow copy of anything, you're just increasing the reference count on the object so that it doesn't go out of scope when I trigger a "state loss" in my code? That would mean the objects persist until the Excel process terminates, but I won't get any memory leaks right? Out of curiosity, is there a way to release the reference prematurely - if that is what's going on here? (sorry the documentation on `mscorlib` is sparse and hard to find;) – Greedo Jul 31 '19 at 16:27
  • 1
    @Greedo, yes there's no copy, it just keeps a reference to the object/dictionary. There's no memory leak since there's only one dictionary and it is used during the lifetime of Excel. You could release the dictionary with `domain.SetData name, Empty`, Though, I dont't see the point since it will automatically be released once Excel is terminated. – Florent B. Jul 31 '19 at 16:38
0

Attempting answer of my own question ...

The solution is to have a simple container, I choose Scripting.Dictionary, compiled into a DLL and make accessible to VBA using COM. In the old days one could have used VB6.

These days, one can also use C++ but here I present a C# solution (uses COM interop).

using System.Runtime.InteropServices;

namespace VBAStateLossProofStorageLib
{
    // Code curated by S Meaden from Microsoft documentation

    // 1. C# Shared Class library
    // 2. In AssemblyInfo.cs set ComVisible(true)
    // 3. In Project Properties->Build check 'Register for Interop'
    // 4. Add Com reference to Microsoft Scripting Runtime

    public interface IVBAStateLossProofStorage
    {
        Scripting.Dictionary getGlobalDictionary();
    }

    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(IVBAStateLossProofStorage))]
    public class VBAStateLossProofStorage : IVBAStateLossProofStorage
    {
        public Scripting.Dictionary getGlobalDictionary()
        { return CVBAStateLossProofStorage.m_dictionary; }
    }


    // https://msdn.microsoft.com/en-gb/library/79b3xss3.aspx
    // "a static class remains in memory for the lifetime of the application domain in which your program resides. "
    [ComVisible(false)]
    static class CVBAStateLossProofStorage
    {
        public static Scripting.Dictionary m_dictionary;

        static CVBAStateLossProofStorage()
        {
            m_dictionary = new Scripting.Dictionary();
        }
    }
}

And here is some client VBA code to demonstrate. Requires a Tools->Reference to the type library (.tlb file) created alongside the Dll.

Option Explicit

Public gdicLossy As New Scripting.Dictionary
Public gdicPermanent As Scripting.Dictionary

Sub RunFirst()

    Set gdicLossy = New Scripting.Dictionary
    gdicLossy.add "Greeting", "Hello world!"

    Dim o As VBAStateLossProofStorageLib.VBAStateLossProofStorage
    Set o = New VBAStateLossProofStorageLib.VBAStateLossProofStorage

    Set gdicPermanent = o.getGlobalDictionary
    gdicPermanent.RemoveAll '* clears it down
    gdicPermanent.add "Greeting", "Bonjour!"

    End '* THIS PROVOKES A STATE LOSS - ALL VARIABLES ARE TORN DOWN - EVENT HANDLERS DISAPPEAR
End Sub

Sub RunSecond()

    Debug.Assert gdicLossy.Count = 0  '* sadly we have lost "Hello world!" forever

    Dim o As VBAStateLossProofStorageLib.VBAStateLossProofStorage
    Set o = New VBAStateLossProofStorageLib.VBAStateLossProofStorage

    Set gdicPermanent = o.getGlobalDictionary
    Debug.Assert gdicPermanent.Count = 1 '* Happily we have retained "Bonjour!" as it was safe in its compiled Dll
    Debug.Assert gdicPermanent.Item("Greeting") = "Bonjour!"

End Sub
S Meaden
  • 8,050
  • 3
  • 34
  • 65