Multi-threading
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.
Comments
Just to clarify...
You're not saying that a data model has to be thread-safe in order to use Update Controls. You're saying that IF a program has multiple threads that depend on or change the data model, then the data model must be thread safe. Right? (Personally I avoid any multithreading that involves manual locking, it's way too easy to make a mistake.) And the same rule presumably applies to the navigation model (but if the navigation model is only used by the UI thread, maybe it doesn't have to be thread-safe.)
It might be useful to provide a different version of Independent<T> to support thread safety via locks. Its constructor would accept a lock:
(Independent<T> itself could be extended to support lock-based thread safety, but this would add extra overhead since there'd be a reference to a lock object that might not be used, and the Value property would have to test whether it needs locking on every call.)
Thread safety and Independent
You are correct; you only need to write a thread-safe data model if the program will access it from more than one thread. As long as you lock around the OnGet and OnSet, then Update Controls is thread-safe too.
I'd rather not put the lock inside the Independent, even if it is a different version. Threading is very much a concern of your application. Update Controls can't know where the boundaries should be. If you've ever used the thread-safe collections in Java, then you've probably run into this situation. Java has a thread-safe Queue, for example, but you often find that you need to do more inside the lock than just add or remove.
I agree that you should avoid lock-based concurrency, but if you need it, your app should be in complete control of it.