1

Yet another question about updating from background threads.

To get to the point: In the application, background threads need to update UI. I've considered using an in-between collection to buffer messages and have a timer to display them. At the moment we are trying a simplest approach.

Code attempt #1:

void foo(string status)
{
    if (this.InvokeRequired)
    {
        BeginInvoke(new MethodInvoker(delegate()
        {
            InsertStatusMessage(status);
        }));

    }
    else
    {
        InsertStatusMessage(status);
    }  
}

This seems to have some flaws. Msdn states that InvokeRequired also returns false if the window handle hasn't been created yet (not available, in my opinion). So the code should be:

void foo(string status)
{
    if (this.InvokeRequired)
    {
        BeginInvoke(new MethodInvoker(delegate()
        {
            InsertStatusMessage(status);
        }));

        // wait until status is set
        EndInvoke(result);
    }
    else if(this.IsHandleCreated)
    {
        InsertStatusMessage(status);
    }
    else
    {
        _logger.Error("Could not update status");
    } 
}

The code above somehow also throws (for an unknown and not replicated reason). We use DevExpress and this is the unhandled exception message (no information nor any clue on what/where the error happened):

System.NullReferenceException: object reference not set to an instance of an object in DevExpress.Utils.Text.FontsCache.GetFontCacheByFont(Graphics graphics, Font font) in DevExpress.Utils.Text.TextUtils.GetFontAscentHeight(Graphics g, Font font) in DevExpress.XtraEditors.ViewInfo.BaseEditViewInfo.GetTextAscentHeight() in DevExpress.XtraEditors.ViewInfo.TextEditViewInfo.CalcTextBaseline(Graphics g) in DevExpress.XtraEditors.ViewInfo.BaseControlViewInfo.ReCalcViewInfo(Graphics g, MouseButtons buttons, Point mousePosition, Rectangle bounds) in DevExpress.XtraGrid.Views.Grid.ViewInfo.GridViewInfo.UpdateCellEditViewInfo(GridCellInfo cell, Point mousePos, Boolean canFastRecalculate, Boolean calc) in DevExpress.XtraGrid.Views.Grid.ViewInfo.GridViewInfo.CreateCellEditViewInfo(GridCellInfo cell, Boolean calc, Boolean allowCache) in DevExpress.XtraGrid.Views.Grid.ViewInfo.GridViewInfo.RequestCellEditViewInfo(GridCellInfo cell) in DevExpress.XtraGrid.Views.Grid.Drawing.GridPainter.DrawRegularRowCell(GridViewDrawArgs e, GridCellInfo ci) in DevExpress.XtraGrid.Views.Grid.Drawing.GridPainter.DrawRegularRow(GridViewDrawArgs e, GridDataRowInfo ri) in DevExpress.XtraGrid.Views.Grid.Drawing.GridPainter.DrawRow(GridViewDrawArgs e, GridRowInfo ri) in DevExpress.XtraGrid.Views.Grid.Drawing.GridPainter.DrawRows(GridViewDrawArgs e) in DevExpress.XtraGrid.Views.Grid.Drawing.GridPainter.DrawContents(GridViewDrawArgs e) in DevExpress.XtraGrid.Views.Grid.Drawing.GridPainter.Draw(ViewDrawArgs ee) in DevExpress.XtraGrid.Views.Base.BaseView.Draw(GraphicsCache e) in DevExpress.XtraGrid.GridControl.OnPaint(PaintEventArgs e)
in System.Windows.Forms.Control.PaintWithErrorHandling(PaintEventArgs e, Int16 layer, Boolean disposeEventArgs) in System.Windows.Forms.Control.WmPaint(Message& m) in System.Windows.Forms.Control.WndProc(Message& m) in DevExpress.XtraEditors.Container.EditorContainer.WndProc(Message& m)
in DevExpress.XtraGrid.GridControl.WndProc(Message& m) in System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
in System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

I want to use Begin/End Invoke instead of Invoke because it requires less stuff (method delegates) and it is more readable.

What have I missed, how can I safely do thread invoking? I just want to add a message in a listbox. I really don't care if the calling thread will waits for a few milliseconds.

Odys
  • 8,951
  • 10
  • 69
  • 111
  • Why is the method triggert before the handle is created ? This looks a little obvious to me – Boas Enkler Apr 12 '12 at 09:37
  • @BoasEnkler I didn't say it does this. I am not sure on what fails – Odys Apr 12 '12 at 09:38
  • There are some DevExpress controls that simply CAN'T be update from a background call using Invoke. They might be busy doing something which will be broken by using Invoke. Use another approach to fix this and include the way the data structure is changed/datasource is set into you're question to give us a chance to help you. – CodingBarfield Apr 12 '12 at 09:44
  • @CodingBarfield any links to support your suggestion? – Odys Apr 12 '12 at 09:49
  • I agree with the others re DevExpress. Try it with MS controls to see if the problem goes away. Not sure what you mean by Begin/End Invoke requires less stuff. Using BeginInvoke() and then waiting with an EndInvoke() is just as inefficient as creating a child thread and waiting for it to end. Either call Invoke() OR just BeginInvoke() on its own. Avoid excessive calls to BeginInvoke() as there is only so much space on the message pump –  Apr 12 '12 at 09:57
  • Maybe http://www.devexpress.com/Support/Center/p/AK2981.aspx would help. I can't remember where I've read it exactly. I've seen code go bad when they where changing the data while the Xtragrid was still bound to the data. We've implemented some Loading And LoadingDone events to disable databinding and prevent 'strange' timing issues to occur. – CodingBarfield Apr 12 '12 at 10:08

3 Answers3

2

You can call directly "Invoke" with "MethodInvoker".

void foo(string status)
{
    Invoke(new MethodInvoker(() => {InsertStatusMessage(status);}));
}

I used this also with DevExpress controls (especially to async update the data sources on several Xtragrids on one form).

For more information about MethodInvoker there is an excellent post.

Dimi Takis
  • 4,924
  • 3
  • 29
  • 41
  • This is similar to what I do. Are you sure this is 100% correct? I think you ought to check if everything is set up and can be invoked – Odys Apr 12 '12 at 09:55
  • 1
    This one-liner runs without any problems. Just go ahead & try it out :). Updated my post with the article link. – Dimi Takis Apr 12 '12 at 10:00
1
class Test : Form
{
        delegate void FooCallback(string status);


        public Test()
        {
        }

        private void foo(string status)
        {
            if (this.InvokeRequired == true)
            {
                FooCallback = new FooCallback(foo);
                this.Invoke
                    (d, status);

            }
            else
            {
              //Do Things

            }
        }     
}

using MethodInvoker costs to much Performance

Pwnstar
  • 2,333
  • 2
  • 29
  • 52
0

I have reverse engineered the applied logic and in all cases there was 1 common factor that is the cause of errors:

  1. Object reference not set to an instance of an object.
  2. Object is currently in use elsewhere.

The error is the introduction of

DevExpress.Utils.Text.FontsCache.GetFontCacheByFont(Graphics graphics, Font font)

This class shares windows fonts between threads and that is STRICTLY FORBIDDEN. Windows usage of GDI resources, during paint message handling but also stuff like

DevExpress.Utils.Text.TextUtils.GetStringSize(Graphics g, String text, 
    Font font, StringFormat stringFormat, Int32 maxWidth, Int32 maxHeight, 
    IWordBreakProvider wordBreakProvider, Boolean& isCropped)

is restricted to the thread that has created the GDI resource. In other words:

Only the thread that has created a font or window may use it, all other threads are not allowed to send windows paint messages to those windows.

Measuring the size of a font in a window is also some type of paint action (although invisible because only the size of the painted text is returned).

Solution (only one of both should be implemented by DevExpress):

  1. The class Text.FontsCache must add the current-thread-ID to the ID that is used to store the created font in the cache.
  2. Each thread must make its own instance of the class Text.FontsCache.
Keale
  • 3,924
  • 3
  • 29
  • 46