none
BindingSource and INotifyPropertyChanged on a non-UI thread

    Question

  • Hi,

    while working on my entry for Code'n My Way to PDC, I have encountered two problem that are not easy to work around.

    Here's the situation - I have a master/detail "views", master is DataGridView bound through BindingSource to an IEnumerator<Detail> (where Detail is some class) and detail is a user control with a few Labels bound through another BindingSource to properties of Detail object. I manually track which Detail object is "current" by responding to PositionChanged event of the first BindingSource object.

    Due to the nature of application, I receive asynchronous callbacks on threads from a thread pool and update the values in Detail object(s) accordingly. Then I (naturally) fire PropertyChanged (Detail implements INotifyPropertyChanged) event. Unfortunately, the event gets fired in the context of non-UI thread and one would expect that neither DataGridView nor the Labels in detail view would get updated, or even worse that the app would crash or freeze.

    Tests show that DataGridView does get updated, but Label controls in the detail view do not. Why does update of DataGridView work??

    Observation 1: as noted in another thread of this forum, if I update Detail object represented in non-current row of DataGridView, that other row is not (in UI sense) updated/refreshed, but I understood that this has been fixed in a post-beta 2 build.

    Observation 2: when I simulate conditions and update my Detail objects through a Windows.Forms.Timer and thus always stay on the UI thread, everything works fine, so it's just the threading issue and not the data binding code issue.


    I cannot avoid getting callbacks from "random" non-UI threads. Once I do, my code runs inside an object that does not "see" any kind of UI (it's fully decoupled from anything :) ), so Control.Invoke and IsInvokeRequired and such do not really work for me. The best bet for me is to try to switch to UI thread somehow and then call PropertyChanged. What is the best way to achieve this?

    Finally, because it's BindingSource that "sees" the UI elements to which data is bound to, can't it detect that the thread switch is required and call Control.Invoke before it tries to set a property on a control?

    Thanks in advance for any help.
    Friday, July 29, 2005 11:32 PM

Answers

  • You've asked a lot of good, but unfortunately hard, questions:

    >> Tests show that DataGridView does get updated, but Label controls in the detail view do not. Why does update of DataGridView work??

    You must access a Windows Forms control on the thread it was created on.  If you run this under the debugger in VS 2005, you will always get an exception.  For compatibility reasons with earlier versions we don't throw when not debugging. The core issue is there are some fundamental synchronization issues at work and as with all synchronization issues, it may work sometimes and not others.  Do no rely on this consistently working.

    >> Observation 1: ...

    If you use BindingList<T> rather than IEnumerator<T>, then this will work correctly (due to a recent fix). 

    >> Observation 2: ...

    I'm not sure why you are seeing this working as this is only supported when using BindingList<T> with INotifyPropertyChanged.  There are ways you can get this to appears as if it works (e.g. calling ResetBindings(false) on BindingSource when you make changes to the underlying list).

    >> The best bet for me is to try to switch to UI thread somehow and then call PropertyChanged. What is the best way to achieve this?

    You can use the AsyncOperationManager to marshall the call back to the UI thread.  The following short sample shows how to do this for a ListChanged handler, but the same applies for a PropertyChanged handler:

    Declare a variable in your Detail type:


    private             AsyncOperation      _oper = null;
     


    Initialize it in your constructor:


    _oper = AsyncOperationManager.CreateOperation(null);
     


    Implement your PropertyChanged handler (sample below shows ListChanged - but this should give you the idea):


    private void PostCallback(object state)
    {
        /* Make sure we fire ListChanged on the UI thread */
        ListChangedEventArgs args = (state as ListChangedEventArgs);
        base.OnListChanged(args);
    }

    protected override void OnListChanged(System.ComponentModel.ListChangedEventArgs e)
    {
        if (!_suspend)
        {
            /* We need to marshall changes back to the UI  */
            _oper.Post(new SendOrPostCallback(PostCallback), e);
        }
    }

     



    >> Can't it detect that the thread switch is required and call Control.Invoke before it tries to set a property on a control?

    It's actually the combination of the Binding Type and the BindToObject Type that does the data binding and these are not thread safe.  It would be a fair amount of work to get this right - but is something we'll consider post VS 2005.

    Joe Stegman
    The Windows Forms Team
    Microsoft Corp.

    This posting is provided "AS IS" with no warranties, and confers no rights.

    Saturday, July 30, 2005 3:22 AM

All replies

  • You've asked a lot of good, but unfortunately hard, questions:

    >> Tests show that DataGridView does get updated, but Label controls in the detail view do not. Why does update of DataGridView work??

    You must access a Windows Forms control on the thread it was created on.  If you run this under the debugger in VS 2005, you will always get an exception.  For compatibility reasons with earlier versions we don't throw when not debugging. The core issue is there are some fundamental synchronization issues at work and as with all synchronization issues, it may work sometimes and not others.  Do no rely on this consistently working.

    >> Observation 1: ...

    If you use BindingList<T> rather than IEnumerator<T>, then this will work correctly (due to a recent fix). 

    >> Observation 2: ...

    I'm not sure why you are seeing this working as this is only supported when using BindingList<T> with INotifyPropertyChanged.  There are ways you can get this to appears as if it works (e.g. calling ResetBindings(false) on BindingSource when you make changes to the underlying list).

    >> The best bet for me is to try to switch to UI thread somehow and then call PropertyChanged. What is the best way to achieve this?

    You can use the AsyncOperationManager to marshall the call back to the UI thread.  The following short sample shows how to do this for a ListChanged handler, but the same applies for a PropertyChanged handler:

    Declare a variable in your Detail type:


    private             AsyncOperation      _oper = null;
     


    Initialize it in your constructor:


    _oper = AsyncOperationManager.CreateOperation(null);
     


    Implement your PropertyChanged handler (sample below shows ListChanged - but this should give you the idea):


    private void PostCallback(object state)
    {
        /* Make sure we fire ListChanged on the UI thread */
        ListChangedEventArgs args = (state as ListChangedEventArgs);
        base.OnListChanged(args);
    }

    protected override void OnListChanged(System.ComponentModel.ListChangedEventArgs e)
    {
        if (!_suspend)
        {
            /* We need to marshall changes back to the UI  */
            _oper.Post(new SendOrPostCallback(PostCallback), e);
        }
    }

     



    >> Can't it detect that the thread switch is required and call Control.Invoke before it tries to set a property on a control?

    It's actually the combination of the Binding Type and the BindToObject Type that does the data binding and these are not thread safe.  It would be a fair amount of work to get this right - but is something we'll consider post VS 2005.

    Joe Stegman
    The Windows Forms Team
    Microsoft Corp.

    This posting is provided "AS IS" with no warranties, and confers no rights.

    Saturday, July 30, 2005 3:22 AM
  •  Joe Stegman wrote:

    You've asked a lot of good, but unfortunately hard, questions:



    Well, that's me - always going for the non-trivial (hard) cases :) However, you've practically answered all of them and for that I am truly thankful.

     Joe Stegman wrote:

    You must access a Windows Forms control on the thread it was created on.  If you run this under the debugger in VS 2005, you will always get an exception.  For compatibility reasons with earlier versions we don't throw when not debugging. The core issue is there are some fundamental synchronization issues at work and as with all synchronization issues, it may work sometimes and not others.  Do no rely on this consistently working.



    Actually, I do not plan on relying on it. I am puzzled to see it working at all. Actually, no exception is thrown whatsoever and I am always running under debugger. But since this is not supposed to work anyway, it doesn't matter if it works now.

     Joe Stegman wrote:

    >> Observation 1: ...

    If you use BindingList<T> rather than IEnumerator<T>, then this will work correctly (due to a recent fix).



    Due to the contest rules, I have to use beta 2 - does the fix apply to beta 2?


     Joe Stegman wrote:

    >> Observation 2: ...

    I'm not sure why you are seeing this working as this is only supported when using BindingList<T> with INotifyPropertyChanged.  There are ways you can get this to appears as if it works (e.g. calling ResetBindings(false) on BindingSource when you make changes to the underlying list).



    Sorry, I was not precise enough. I can see updates to both views working if I stick to UI thread, but the problem with DataGridView not updating non-current row is still there as you said it would.

     Joe Stegman wrote:

    >> The best bet for me is to try to switch to UI thread somehow and then call PropertyChanged. What is the best way to achieve this?

    You can use the AsyncOperationManager to marshall the call back to the UI thread.  The following short sample shows how to do this for a ListChanged handler, but the same applies for a PropertyChanged handler:


    I will try this tonight.


    Thanks a lot. This kind of support directly from a developer is priceless.

    Saturday, July 30, 2005 7:20 PM
  •  Drazen Dotlic wrote:

    Due to the contest rules, I have to use beta 2 - does the fix apply to beta 2?


    Unfortunately no, but you can make it work by sub-classing BindingList<T> and hooking property change on the child objects and turning the child PropertyChanged events into ListChanged events (its a bit of work).  As part of the sub-classing, you'll also need to override RaisesItemChangedEvents and set it to true.


    Joe Stegman
    The Windows Forms Team
    Microsoft Corp.

    This posting is provided "AS IS" with no warranties, and confers no rights.

    Saturday, July 30, 2005 8:15 PM
  •  Joe Stegman wrote:

     Drazen Dotlic wrote:

    Due to the contest rules, I have to use beta 2 - does the fix apply to beta 2?


    Unfortunately no, but you can make it work by sub-classing BindingList<T> and hooking property change on the child objects and turning the child PropertyChanged events into ListChanged events (its a bit of work).  As part of the sub-classing, you'll also need to override RaisesItemChangedEvents and set it to true.



    Well, at least there is a workaround. I think I might simply document this as an already solved problem in post-beta 2 builds and ignore it :)

    Saturday, July 30, 2005 8:21 PM
  •  Joe Stegman wrote:

    >> The best bet for me is to try to switch to UI thread somehow and then call PropertyChanged. What is the best way to achieve this?

    You can use the AsyncOperationManager to marshall the call back to the UI thread.  The following short sample shows how to do this for a ListChanged handler, but the same applies for a PropertyChanged handler:

    Declare a variable in your Detail type:


    private             AsyncOperation      _oper = null;
     


    Initialize it in your constructor:


    _oper = AsyncOperationManager.CreateOperation(null);
     



    Just a note so that others don't waste hours as I just have - the example code you've given works like a charm, but...
    It is important when you are creating the AsyncOperation. Turned out I was creating it before any form was created - somewhere in Main before Application.Run(new MyForm). The consequence was that SynchronizationContext member of AsyncOperation was not set correctly thus all Post calls ended up on the same thread from which they were called.
    Documentation also states and provides implementation of a Component derived class that uses this mechanism (AsyncOperationManager). I have a class that is not a Component yet everything works fine.

    Saturday, July 30, 2005 11:42 PM
  • Joe,

    I implemented your suggestion regarding marshalling calls back to a UI thread - using AsyncOperationManger.  My DataGridView is now properly displaying all changes from all threads that are changing properties of objects that are contained in a BindingList.

    Now I need to know if there is a way to get the DataGridView object to not cancel edits when it is also repainting the its other cells (from the async property updates). 

    Do you know which technique is available for essentially allowing me to edit one cell, while another cell is being updated in the background?

    Thanks,

    Bill Perlman

    Thursday, October 19, 2006 7:05 PM