[Journal - Closed Delegates]

Closed Delegates

Saturday, January 28, 2006

I've talked about open instance and closed static delegates recently. What if we could close in on all arguments used in a delegate invocation, not just the first one? This is handy when you need to save some context, namely, local variables, for later use.

For example, if you populate a tree view control step-by-step as the user expands the nodes, you might want to save all the context, along with a specific handler, for invocation at node expansion time.

The way to do it is to wrap both the handler as well as the arguments (that is, the locals) in another object. Look at Gregor.Core.CInvocationInfo:

public class CInvocationInfo {

    public CInvocationInfo(Delegate handler, params object[] args);

    public Delegate Handler{get;}
    public object[] Arguments{get;}

    public object Invoke(){
        return m_Handler.DynamicInvoke(m_Arguments);
    }
}

As for the tree view lazy-population approach, there is a general solution in Gregor.UICore.ControlUtil, which offers a special handler for the BeforeExpand event of any TreeView control (along with the now established pattern of a pair of Add/Remove methods), as well as a way to register a CInvocationInfo object with a node, which is invoked on node expansion.

One issue is that CInvocationInfo knows nothing about the specific delegate type. While there are generic type overloads of CInvocationInfo - they use the Callback delegate types, and the likewise generically overloaded tuple types - such overloading of generics has a way of leaking into every type, module and routine involved. To keep things simple, I use the weakly-typed and dynamically invoking version shown above in ControlUtil.

The mechanism is used in the assembly browser (Gregor.AppCore project). I have used wrappers in the assembly browser class, which provide some checking on the handler and its arguments:

private TreeNode CreateDummyNode(
    VoidCallback<TreeNode, ArrayList, CAssemblyInfoEx, CNamespaceInfoEx> handler,
    TreeNode tnType,
    ArrayList list,
    CAssemblyInfoEx assyInfo,
    CNamespaceInfoEx nsInfo){
    return ControlUtil.CreateExpansionDummy(handler, tnType, list, assyInfo, nsInfo);
}

Note that the parent node (tnType) is included as part of the context in the argument list. It is used by the application when the handler is invoked, not by the UICore library.

The CreateExpansionDummy() method creates a specially texted dummy node for insertion under the node to expand (recall that the dummy is needed so the the Plus sign appears), and tags a new CInvocationInfo object to it. The app can then insert that node into the tree.

On expansion of the dummy's parent, that invocation info object is retrieved, and the dummy is removed. The invocation info is then used to invoke the delegate with the arguments saved:

private static void HandleTreeViewBeforeExpand(object sender, TreeViewCancelEventArgs e){
    try{
        // we have nowhere to return the result to
        /*(void)*/ ControlUtil.ReplaceExpansionDummy(e.Node, false);
    }catch(Exception ex){
        Dev.ProcessException(ex, true);
    }
}

public static object ReplaceExpansionDummy(TreeNode tnExpanding, bool fThrow){
    Check.Reference(tnExpanding);
    object ret = null;
    if(tnExpanding.Nodes.Count == 1
    && (0 == string.Compare(tnExpanding.Nodes[0].Text, DUMMY_TEXT, true))){
        TreeNode tnDummy = tnExpanding.Nodes[0];
        CInvocationInfo invokeInfo = (CInvocationInfo) tnDummy.Tag;
        tnExpanding.Nodes.Clear();
        ret = invokeInfo.Invoke();
    }else if(fThrow){
        throw new ArgumentException("Node '" + tnExpanding.Text + "' has already been expanded.");
    }
    return ret;
}

The ReplaceExpansionDummy() method is public so that the application can force child node population on occasions other than user-driven node expansion, such as (partial) programmatic population of the tree when searching for a not-yet-existant node corresponding to some entity.

Exercise: why did I not place this code into a special tree view class?