Anche i controlli WPF sono soggetti a thread affinity, questo significa che, come nei controlli WinForm attuali, non possiamo accedervi da un thread diverso da quello che ha creato il controllo stesso.
Il motivo percui questo vincolo è rimasto è perchè rendere i controlli thread-safe sarebbe stato eccessivamente complesso anche alla luce del fatto che il più semplice dei controlli WPF è notevolmente più complesso se messo a confronto con la controparte WinForm, inoltre l'introdurre logiche di sincronizzazione di accesso avrebbero introdotto delle inaccettabili penalizzazioni a livello di performances.
In WPF quasi tutte le classi che interagiscono con la UI (inclusi quindi tutti i vari Button, Label...) ereditano dalla classe DispatcherObject la quale wrappa la classe Dispatcher (L'equivalente WPF della message pump di Win32) e mette a disposizione due metodi: CheckAccess e VerifyAccess.
Il primo ritorna true se possiamo accedere al controllo direttamente

[EditorBrowsable(EditorBrowsableState.Never)]
public bool CheckAccess()
{
      return (this.Thread == Thread.CurrentThread);
}

Il secondo invece genera un eccezione se siamo in un thread diverso da quello che ha creato il controllo.
Entrambi i metodi sono marcati con EditorBrowsable.Never quindi non appaiono nell'intellisense (non ho idea del motivo...)
Una volta stabilito che non possiamo accedere direttamente, le cose non sono molto diverse da quanto dobbiamo fare oggi con i controlli Windows Forms
Se btn1 è un pulsante definito via XAML, posso cambiare la proprietà Content da un thread secondario usando il metodo
Invoke esposto dalla proprietà Dispatcher.
Esempio:
public delegate void MyDelegate();
Thread t = new Thread(new ThreadStart(Exec));
t.Start();
...
private void Exec ()
{
for (int i = 0; i < 100; i++)
{
if(!btn1.CheckAccess())
btn1.Dispatcher.Invoke(DispatcherPriority.Normal,(MyDelegate)delegate(){btn1.Content = i.ToString();});
}
}

A differenza del metodo Invoke dei controlli WinForms abbiamo la possibilità di stabilire la priorità con il quale il nostro delegate verrà invocato.
Ad esempio se non è importante che l'aggiornamento della UI avvenga immediatamente potremmo specificare DispatcherPriority.ApplicationIdle e
in questo caso il delegate verrà invocato non appena l'applicazione avrà un momento "libero".