Multi-threaded view model using Update Controls

Multithreaded programming doesn't have to be hard. You just need to learn a few patterns.

Here's a common problem. How do you notify the UI thread that something has changed on a background thread? The INotifyPropertyChanged contract requires that the PropertyChanged event be fired on the UI thread. There are ways of marshalling that event from the background to the foreground, but wouldn't it be nice if you didn't have to?

Update Controls takes care of this problem for you. You make sure your code is thread-safe, and Update Controls will make sure that dependents are updated, no matter what thread they are on.

A thread-safe model
For Update Controls to work properly -- and indeed for your entire application to work properly -- you need your data model to be thread safe. That means that anything that can change is protected with a lock. If it can change (and you are using Update Controls), then it should have an Independent sentry. Just be sure to call OnGet and OnSet inside the lock.

public class Playlist
{
    private int _id;
    private string _name;

    public Playlist(int id)
    {
        _id = id;
    }

    public int ID
    {
        get { return _id; }
    }

    #region Independent properties
    // Generated by Update Controls --------------------------------
    private Independent _indName = new Independent();

    public string Name
    {
        get
        {
            lock (this)
            {
                _indName.OnGet();
                return _name;
            }
        }
        set
        {
            lock (this)
            {
                _indName.OnSet();
                _name = value;
            }
        }
    }
    // End generated code --------------------------------
    #endregion
}

When dealing with collections, we need to be a little careful. If you return an IEnumerable that traverses a collection, that IEnumerable will leave the lock. This will cause problems, as another thread can come in and modify the collection while you are traversing it. To be safe, we need to create a copy of the collection within the lock.

public class MusicLibrary
{
    private List<Playlist> _playlists = new List<Playlist>();

    #region Independent properties
    // Generated by Update Controls --------------------------------
    private Independent _indPlaylists = new Independent();

    public Playlist GetPlaylist(int playlistID)
    {
        lock (this)
        {
            // This method changes the list.
            _indPlaylists.OnSet();

            // See if we already have this one.
            Playlist playlist = _playlists.Where(p => p.ID == playlistID).FirstOrDefault();
            if (playlist != null)
                return playlist;

            // If not, create it.
            playlist = new Playlist(playlistID);
            _playlists.Add(playlist);
            return playlist;
        }
    }

    public void DeletePlaylist(Playlist playlist)
    {
        lock (this)
        {
            _indPlaylists.OnSet();
            _playlists.Remove(playlist);
        }
    }

    public IEnumerable<Playlist> Playlists
    {
        get
        {
            lock (this)
            {
                _indPlaylists.OnGet();
                // Return a copy of the list so that it can be accessed in a
                // thread-safe manner.
                return new List<Playlist>(_playlists);
            }
        }
    }
    // End generated code --------------------------------
    #endregion
}

With this, your model is safe to access from multiple threads. So let's create one.

Create a thread to communicate with external systems
The UI thread is for the user. If you use it to communicate with external systems, it might hang. Then the user will have to decide whether to "Continue Waiting" or "Switch To...". Switch to what, a Mac? But seriously, it's better if you spawn a new thread to do all of your communication with external systems.

Commuter communicates with iTunes. So I created a separate thread to synchronize between iTunes and my data model. It wakes up every 10 seconds and refreshes the playlists.

public class ITunesSynchronizationService
{
    private MusicLibrary _musicLibrary;
    private Thread _thread;
    private ManualResetEvent _stop;
    private bool _first = true;

    private string _lastError = string.Empty;
    private Independent _indLastError = new Independent();

    public ITunesSynchronizationService(MusicLibrary musicLibrary)
    {
        _musicLibrary = musicLibrary;
        _thread = new Thread(ThreadProc);
        _thread.Name = "iTunes synchronization service";
        _stop = new ManualResetEvent(false);
    }

    public void Start()
    {
        _thread.Start();
    }

    public void Stop()
    {
        _stop.Set();

        // Give it 30 seconds to shut down.
        _thread.Join(30000);
    }

    public string LastError
    {
        get
        {
            lock (this)
            {
                _indLastError.OnGet();
                return _lastError;
            }
        }

        private set
        {
            lock (this)
            {
                _indLastError.OnSet();
                _lastError = value;
            }
        }
    }

    private void ThreadProc()
    {
        while (ShouldContinue())
        {
            try
            {
                // Connect to iTunes.
                iTunesApp itApp = new iTunesAppClass();

                // List all of the playlists.
                List<Playlist> oldPlaylists = _musicLibrary.Playlists.ToList();
                foreach (IITUserPlaylist itPlaylist in itApp.LibrarySource.Playlists.OfType<IITUserPlaylist>())
                {
                    string name = itPlaylist.Name;
                    Playlist playlist = _musicLibrary.GetPlaylist(itPlaylist.playlistID);
                    if (name != null && playlist.Name != name)
                        playlist.Name = name;
                    oldPlaylists.Remove(playlist);
                }

                // Delete all that are no longer in iTunes.
                foreach (Playlist playlist in oldPlaylists)
                    _musicLibrary.DeletePlaylist(playlist);

                LastError = string.Empty;
            }
            catch (Exception ex)
            {
                LastError = ex.Message;
            }
        }
    }
    private bool ShouldContinue()
    {
        // Don't wait the first time.
        if (_first)
        {
            _first = false;
            return true;
        }

        // Wake up every 10 seconds, or when the thread should stop.
        return !_stop.WaitOne(10000);
    }
}

Notice the LastError property. This is a thread-safe way for the service to communicate its status with the user. This property is bound to a TextBlock on the UI.

The iTunes synchronization service makes changes directly to the data model. Because the data model is thread-safe, the UI thread and the background thread can share this resource. Update Controls will notify the UI thread whenever the background thread changes the data model.

Try it out
Download the code. When you run Commuter, it will launch iTunes. Even as iTunes launches, the Commuter user interface is responsive. If you are fast enough, you can open the Playlist drop-down before it is populated. But even if COM is faster than the mouse, you can try a few experiments.

Select a playlist in Commuter. Then switch to iTunes and rename that playlist. Within 10 seconds you'll see the selected playlist's name change.

Bring iTunes to the front, but leave Commuter visible underneath it. Place your mouse on the drop-down. Press Ctrl+N to create a new playlist, then click the mouse to bring Commuter to the front and open the drop-down. Within 10 seconds, "untitled playlist" will appear in the list.

Multi-threaded programming still requires some care, but Update Controls can at least take care of updating the UI when the background thread changes the data model.

Leave a Reply

You must be logged in to post a comment.