views:

654

answers:

6

What solutions do I have if I want to prevent the UI from freezing while I deserialize a large number of UI elements in WPF? I'm getting errors complainig that the objects belong on the UI Thread when I'm trying to load them in another thread. So, what options do I have to prevent the Vista "Program not responding" error while I'm loading my UI data? Can I rely on a single-threaded solution, or am I missing something regarding perhaps multiple UI Threads?

+4  A: 

If you only use a single thread then the UI will freeze while you do any amount of processing.

If you use a BackgroundWorker thread you'll have more control over what happens & when.

See this MSDN article for more information - it goes into detail about updating the UI from non-UI threads using Dispatcher.Invoke

ChrisF
A: 

You can still make your long processing in a separate thread, but when finished you have to synchronize with the UI thread by calling Dispatcher.BeginInvoke(your_UI_action_here)

Mart
A: 

Recommendations from the OldNewThing blog.

It is best if you do go the threaded route, to have one GUI thread and spawn your work load off to another thread that when finishes reports back to the main GUI thread that its done. The reason for this is because you will not get into thread issues with your GUI interface.

So One GUI Thread Many worker threads that do the work.

If any of your threads do hang the user is in direct control over your application can can close down the thread without effecting his experience with the application interface. This will make him happy because your user will feel in control other than him constantly click THAT STOP BUTTON AND IT WONT STOP SEARCHING.

Chad
A: 

Try freezing your UIElements. Frozen objects can be passed between threads without encountering an InvalidOperationException, so you deserialize them & freeze them on a background thread before using them on your UI thread.

Alternatively, consider dispatching the individual deserializations back to the UI thread at background priority. This isn't optimal, since the UI thread still has to do all of the work to deserialize these objects and there's some overhead added by dispatching them as individual tasks, but at least you won't block the UI - higher priority events like input will be able to be interspersed with your lower priority deserialization work.

Nicholas Armstrong
This only works for freezables though and UI elements are not freezable. So, if you're just talking about creating geometry, brushes, etc. this works, but if you're talking about building UI elements (i.e. Visual class and up the inheritance chain) it does not.
Drew Marsh
Oops, good point on UIElement not being freezable.
Nicholas Armstrong
+2  A: 

Here is a wonderful blog posting from Dwane Need that discusses all the available options for working with UI elements amongst multiple threads.

You really haven't given enough detail to give a good prescription. For example, why are you creating UI elements yourself at all instead of using databinding? You might have a good reason, but without more details it's hard to give good advice. As another example of detail that would be useful, are you looking to build complex deeply nested control hierarchies for each piece of data or do you just need to draw a simple shape?

Drew Marsh
+2  A: 

You can turn the flow of control on its head using DispatcherFrames, allowing a deserialization to proceed on the UI thread in the background.

First you need a way to get control periodically during deserialization. No matter what deserializer you are using, it will have to call property sets on your objects, so you can usually add code to the property setters. Alternatively you could modify the deserializer. In any case, make sure your code is called frequently enough

Each time you receive control, all you need to do is:

  1. Create a DispatcherFrame
  2. Queue an event to the dispatcher using BeginInvoke that sets Continue=false on the frame
  3. Use PushFrame to start the frame running on the Dispatcher

In addition, when calling the deserializer itself make sure you do it from Dispatcher.BeginInvoke, or that your calling code doesn't hold any locks etc.

Here's how it would look:

  public partial class MyWindow
  {
    SomeDeserializer _deserializer = new SomeDeserializer();

    byte[] _sourceData;
    object _deserializedObject;

    ...

    void LoadButton_Click(...)
    {
      Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
      {
        _deserializedObject = _deserializer.DeserializeObject(_sourceData);
      }));
    }
  }

  public class OneOfTheObjectsBeingDeserializedFrequently
  {
    ...

    public string SomePropertyThatIsFrequentlySet
    {
      get { ... }
      set { ...; BackgroundThreadingSolution.DoEvents(); }
    }
  }

  public class BackgroundThreadingSolution
  {
    [ThreadLocal]
    static DateTime _nextDispatchTime;

    public static void DoEvents()
    {
      // Limit dispatcher queue running to once every 200ms
      var now = DateTime.Now;
      if(now < _nextDispatchTime) return;
      _nextDispatchTime = now.AddMilliseconds(200);

      // Run the dispatcher for everything over background priority
      var frame = new DispatcherFrame();
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
      {
        frame.Continue = false;
      }));
      Dispatcher.PushFrame(frame);
    }
  }

Checking DateTime.Now in DoEvents() isn't actually required for this technique to work, but will improve performance if SomeProperty is set very frequently during deserialization.

Edit: Right after I wrote this I realized there is an easier way to implement the DoEvents method. Instead of using DispatcherFrame, simply use Dispatcher.Invoke with an empty action:

    public static void DoEvents()
    {
      // Limit dispatcher queue running to once every 200ms
      var now = DateTime.Now;
      if(now < _nextDispatchTime) return;
      _nextDispatchTime = now.AddMilliseconds(200);

      // Run the dispatcher for everything over background priority
      Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new Action(() => {}));
    }
Ray Burns