Pregunta

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.

¿Fue útil?

Solución

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.

Otros consejos

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

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.
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top