In Part V I mentioned that the ObservableItem class makes no checks to ensure any UI updates are made on the correct thread, which means it could potentially violate the threading model employed by both Windows Forms and WPF. Now if you spent much time working with WPF you’re probably aware that the data binding mechanism can actually cope with PropertyChanged events raised on different threads to the bound UI object. Windows Forms on the other hand will dutifully throw an InvalidOperationException in the same scenario if the CheckForIllegalCrossThreadCalls property is set to true. So if you only want to target WPF then you can get away with doing nothing, but I wouldn’t recommend this approach. Firstly, if you later discovered you needed to use the same data model with Windows Forms you might find it to be a non-trivial change, especially if other WPF dependencies have crept in. I know this appears to violate the YAGNI approach but we’re not really talking about adding unnecessary functionality, we’re trying not to unnecessarily prohibit reuse of the data model. Secondly, dealing with concurrency is hard enough without letting it ambush you J. I’m not saying you should never perform data model operations on a background thread, but you should always ensure that any resulting UI updates are marshalled to the correct thread. We can ensure we’re on the correct thread by making the following modifications to the ObservableItem class:
private readonly Thread m_synchronizationThread = Thread.CurrentThread; private PropertyChangedEventHandler m_propertyChangedEventHandler; public event PropertyChangedEventHandler PropertyChanged { add { VerifySynchronizationThread(); m_propertyChangedEventHandler += value; } remove { VerifySynchronizationThread(); m_propertyChangedEventHandler -= value; } } [MethodImpl(MethodImplOptions.NoInlining)] private void SetPropertyInternal<T>( string propertyName, ref T field, T value, IEqualityComparer<T> comparer) { if (VerifySetPropertyArguments) { VerifySetPropertyInternalArguments(propertyName, comparer); } VerifySynchronizationThread(); if (!comparer.Equals(field, value)) { T oldValue = field; field = value; try { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } catch { field = oldValue; throw; } } } private void VerifySynchronizationThread() { if (m_synchronizationThread != Thread.CurrentThread) { throw new InvalidOperationException( "The current object is being updated on a thread that is different from the " + "thread it was created on."); } }
Now whenever the SetPropertyInternal method is called or an event handler is registered or unregistered for the PropertyChanged event, the VerifySynchronizationThread method is called to ensure we’re on the correct thread. This protects us from unintentional UI updates on another thread, but what about when we need a data model object to perform a potentially long running operation on a background thread? Clearly we don’t want to be calling methods such as Control.Invoke or Dispatcher.Invoke from data model objects because that would tie us to either Windows Forms or WPF. This is where the SynchronizationContext class comes in, which allows us to Send (synchronous) or Post (asynchronous) updates to the UI correctly. Consider the following addition to the Person class:
public void LongRunningOperation() { var currentContext = SynchronizationContext.Current; if (currentContext == null) { currentContext = new SynchronizationContext(); } ThreadPool.QueueUserWorkItem( new WaitCallback( state1 => { string processedFirstName = GetUpperLowerString(FirstName); string processedLastName = GetUpperLowerString(LastName); currentContext.Post( new SendOrPostCallback( state2 => { FirstName = processedFirstName; LastName = processedLastName; }), null); })); } private static string GetUpperLowerString(string value) { var result = new StringBuilder(value.Length); for (int i = 0; i < value.Length; ++i) { string textElement = StringInfo.GetNextTextElement(value, i); result.Append((i % 2) == 0 ? textElement.ToUpperInvariant() : textElement.ToLowerInvariant()); } return result.ToString(); }
Now I’ve cheated a little here since my “long running” operation does nothing more than alternate the case of characters in the FirstName and LastName properties J. We use the SynchronizationContext.Current property to return a SynchronizationContext object that will update the FirstName and LastName properties, and therefore any data bound view, on the correct thread. If the property returns null then we use a default SynchronizationContext. We do this because if the default SynchronizationContext doesn’t do the correct thing, which is likely, then the resulting InvalidOperationException will be more helpful than a NullReferenceException. I’ve added a button to both sample applications so that you can test this exciting functionality out J.
Now you might be wondering why we don’t store the current SynchronizationContext instead of the current thread. Unfortunately we’re unlikely to get the same instance back in a WPF application due to the way WPF sets the current SynchronizationContext and because none of the derived classes override the Equals method, reference equality is all we can use.
Well I think that’s enough on this topic, at least for a little while J. I promise the next post will be about something different.
You would get far better performance if you changed LongRunningOperation() to be ShortRunningOperation()
ReplyDeleteHope that helps.
Thanks, that's an "exceptional" suggestion.
ReplyDelete