How do I delegate an AsyncCallback method for Control.BeginInvoke? (.NET)
Asked Answered
H

3

6

Is it possible to use Control.BeginInvoke in anything other than a "fire & forget" manner? I want to change the following request to delegate a callback method so that i can do something when each of my asynchronous calls complete.

this.BeginInvoke(new RefreshRulesDelegate(RefreshRules), new object[] { ctrl, ctrl.DsRules, ctrl.CptyId });  

I would be able to do this with a normal delegate.BeginInvoke e.g.

RefreshRulesDelegate del = new RefreshRulesDelegate(RefreshRules);
            del.BeginInvoke(ctrl, ctrl.DsRules, ctrl.CptyId, new AsyncCallback(RefreshCompleted), del);  

But because I'm calling Control.BeginInvoke I can't do this as I get the "cross-thread operation not valid" error.
Anyone help?

Further to some of the answers received, I will clarify the "why". I need to load/refresh a Control on my GUI without locking up the rest of the app. The control contains numerous controls (ruleListCtls) which all require a dataset to be retrieved and passed to them. i.e.

public void RefreshAll()
{
    foreach (LTRFundingRuleListControl ctrl in ruleListCtls)
    {
        this.BeginInvoke(new RefreshRulesDelegate(RefreshRules), new object[]{ctrl,ctrl.DsRules, ctrl.CptyId });   
    }
}  

I have found that I can do this if I provide a delegate callback method and move any code which amends the controls back onto the main GUI thread on which they were created (to avoid the cross-thread error)

public void RefreshAll()
{
    IntPtr handle; 
    foreach (LTRFundingRuleListControl ctrl in ruleListCtls)
    {
        handle = ctrl.Handle;
        RefreshRulesDsDelegate del = new RefreshRulesDsDelegate(RefreshRulesDs);
        del.BeginInvoke(ctrl.DsRules, ctrl.CptyId, handle, out handle, new AsyncCallback(RefreshCompleted), del);
    }        
}

private void RefreshCompleted(IAsyncResult result)
{
    CptyCatRuleDataSet dsRules;
    string cptyId;
    IntPtr handle;

    AsyncResult res = (AsyncResult) result;

    // Get the handle of the control to update, and the dataset to update it with
    RefreshRulesDsDelegate del = (RefreshRulesDsDelegate) res.AsyncDelegate;
    dsRules = del.EndInvoke(out handle,res);

    // Update the control on the thread it was created on
    this.BeginInvoke(new UpdateControlDatasetDelegate(UpdateControlDataset), new object[] {dsRules, handle});
}

public delegate CptyCatRuleDataSet RefreshRulesDsDelegate(CptyCatRuleDataSet dsRules, string cptyId, IntPtr ctrlId, out IntPtr handle);
private CptyCatRuleDataSet RefreshRulesDs(CptyCatRuleDataSet dsRules, string ruleCptyId, IntPtr ctrlId, out IntPtr handle)
{
    try
    {
        handle = ctrlId;
        int catId = ((CptyCatRuleDataSet.PSLTR_RULE_CAT_CPTY_SelRow)dsRules.PSLTR_RULE_CAT_CPTY_Sel.Rows[0]).RULE_CAT_ID;
            return ltrCptyRulesService.GetCptyRules(ruleCptyId, catId);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}  

Here's what we delgate to the main thread having received the callback:

private delegate void UpdateControlDatasetDelegate(CptyCatRuleDataSet dsRules, IntPtr ctrlId);
private void UpdateControlDataset(CptyCatRuleDataSet dsRules, IntPtr ctrlId)
{
    IEnumerator en = ruleListCtls.GetEnumerator();
    while (en.MoveNext())
    {
        LTRFundingRuleListControl ctrl = en.Current as LTRFundingRuleListControl;
        if (ctrl.Handle == ctrlId)
        {
            ctrl.DsRules = dsRules;
        }
    }
}  

This now works fine. However, the main problem, apart from that I don't think this is particularly elegant, is exception handling. Maybe this is another question, but if RefreshRulesDs throws an exception then my app crashes as the error is not bubbled back up the GUI thread (obviously) but as an unhandled exception. Until I can catch these then I will have to do this whole operation synchronously. How do I successfully catch an error and load up the rest of my controls? Or how do I do achieve this asynchronous operation another way, with proper exception handling?

Heathenize answered 27/11, 2009 at 12:58 Comment(1)
The exception handling is a bit messy, but you should be able to catch them by surrounding EndInvoke. BackgroundWorker and Task (Fx 4) do it nicer though.Rash
R
5

Regarding the "Is it possible" part: No, Control.BeginInvoke uses Windows' PostMessage() and that means there is no answer. It also means that the RefreshRulesDelegate is executed on the main thread, not on a background thread.

So, use delegate.BeginInvoke or the ThreadPool and when they are completed use Control.[Begin]Invoke() to update the UI.

Rash answered 27/11, 2009 at 13:32 Comment(1)
Henk, your help is appreciated. I've amended my code as you suggested. See above. The (million dollar) question now is how do I handle any exceptions thrown?Heathenize
J
2

You could do this:

this.BeginInvoke(delegate
{
    RefreshRules(ctrl, ctrl.DsRules, ctrl.CptyId);
    RefreshCompleted();
});

EDIT:

I would consider removing the IAsyncResult argument from the method RefreshCompleted and use the solution above.

If for some reason you really need to keep the IAsyncResult argument. You could implement an extension method for Control:

public static IAsyncResult BeginInvoke(this Control control, Delegate del, object[] args, AsyncCallback callback, object state)
{
    CustomAsyncResult asyncResult = new CustomAsyncResult(callback, state);
    control.BeginInvoke(delegate
    {
        del.DynamicInvoke(args);
        asyncResult.Complete();
    }, args);

    return asyncResult;
}

public static void EndInvoke(this Control control, IAsyncResult asyncResult)
{
    asyncResult.EndInvoke();
}

You would need to define your CustomAsyncResult class, you can get documentation on how to do this here

Jackshaft answered 27/11, 2009 at 13:5 Comment(1)
Thanks. RefreshCompleted has the IAsyncResult parameter though which I do not have access to at that point. Also I want RefreshCompleted to be called as the AsyncCallbackHeathenize
B
0

So you want the "extra thing" to happen on a worker thread? (else you'd just run it in th RefreshRules method). Perhaps just use ThreadPool.QueueUserItem:

ThreadPool.QueueUserWorkItem(delegate { /* your extra stuff */ });

at the end of (or after) your RefreshRules method?

For info, you may find it easier/tidier to call BeginInvoke with an anonymous method too:

this.BeginInvoke((MethodInvoker) delegate {
    RefreshRules(ctrl, ctrl.DsRules, ctrl.CptyId);
    ThreadPool.QueueUserWorkItem(delegate { /* your extra stuff */ });
});

this avoids creating a delegate type, and provides type-checking on your call to RefreshRules - note that it captures ctrl, though - so if you are in a loop you'll need a copy:

var tmp = ctrl;
this.BeginInvoke((MethodInvoker) delegate {
    RefreshRules(tmp, tmp.DsRules, tmp.CptyId);
    ThreadPool.QueueUserWorkItem(delegate { /* your extra stuff */ });
});
Brecciate answered 27/11, 2009 at 13:3 Comment(2)
Thanks. Not sure that helps though. I just want RefreshCompleted to be called when RefreshRules has finished. I thought that would be simpleHeathenize
it would be easier to explain, if you explain a little the cause of the question. What problem caused your question. Maybe there are a easier way to do it.Mario

© 2022 - 2024 — McMap. All rights reserved.