Det er nok ikke ukendt for de fleste, at Windows kontroller kun må tilgås af den tråd, som oprettede kontrollen.  Sådan er det også i WPF.  Heldigvis kan man sige.  Det er svært at forestille sig et brugerinterface, der kan opdateres af flere tråde.

Forleden lavede jeg en WPF applikation med et antal arbejdertråde, som hver især kommunikerede status tilbage til brugerinterfacet. Statusfelterne i brugerinterfacet gjorde brug af binding til properties på de bagvedliggende viewmodels.  Der var en viewmodel og et view til hver tråd, så der var ikke brug for synkronisering imellem trådene, men pludselig gik det op for mig, at disse properties blev opdateret af en anden tråd end min GUI tråd!   

Eftersom det hele virkede upåklageligt, måtte det være WPF binding, der gjorde min marshalling for mig.  Det måtte undersøges.

Til at illustrere har jeg lavet en lille WPF applikation med et vindue (view) indeholdende en textbox og en button.  Indholdet af textbox’en er bundet op vha. binding til en property (SomeData) på en klasse, som implementerer INotifyPropertyChanged.  Klassen ser således ud:

class ViewModel : INotifyPropertyChanged
{
    int _someData = 0;

    public int SomeData
    {
        get { return _someData; }
        set
        {
            _someData = value;
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs("SomeData"));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

 

I koden til vinduet sættes binding op.  Når brugeren trykker på knappen, startes en tråd op, som opdaterer SomeData property, hvorved textbox’ens indhold opdateres.  Det er her magien sker, og WPF tager over og på en eller anden måde sørger for, at min binding sker på den rigtige tråd.  Koden til vinduet ser således ud:

 

public partial class Window1 : Window
{
    ViewModel _viewModel;
    Thread _t;

    public Window1()
    {
        InitializeComponent();

        _viewModel = new ViewModel();
        gridLayout.DataContext = _viewModel;

        textBoxSomeData.SetBinding(TextBox.TextProperty, new Binding("SomeData"));
    }

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        _t = new Thread(new ThreadStart(ThreadStart));
        _t.IsBackground = true;
        _t.Start();
    }

    void ThreadStart()
    {
        for (int i = 0; i < 100; i++)
        {
            _viewModel.SomeData = i;
            Thread.Sleep(100);
        }
    }
}

 

Sætter man et breakpoint i ThreadStart funktionen og begynder at debugge sig vej et stykke ind i Microsoft’s kode, rammer man på et tidspunkt en intern klasse ved navn MS.Internal.Data.ClrBindingWorker.  Denne klasse har en metode OnSourcePropertyChanged.  Inden i den metode finder man (blandt så meget andet) følgende kode:

 

if (Dispatcher.Thread == Thread.CurrentThread)
{ 
    PW.OnPropertyChangedAtLevel(level);
} 
else 
{
    // otherwise invoke an operation to do the work on the right context 
    SetTransferIsPending(true);
    Dispatcher.BeginInvoke(
        DispatcherPriority.DataBind,
        new DispatcherOperationCallback(ScheduleTransferOperation), 
        new object[]{o, propName});
} 

 

Af denne kode fremgår det, at WPF selv finder ud af, at der sker binding på tværs af tråde, og derfor benyttes Dispatcher.BeginInvoke og korrekt marshalling sker.

Det er smukt.  Endnu engang stor respekt for WPF binding.

Kommentarer

Rasmus Kromann-Larsen Denmark siger:

28. november 2009 09:39

Så kan jeg jo man næsten heller ikke lade være med at nævne klassen SynchronizationContext som dukkede op i 3.5 og som ofte kan give nogle meget mere elegante løsninger på alt det thread-kontrol-sjov end det gamle InvokeRequired / Invoke paradigme :-) Den er dog ikke WPF specifik.

http://msdn.microsoft.com/en-us/library/system.threading.synchronizationcontext.aspx
http://www.codeproject.com/KB/cpp/SyncContextTutorial.aspx

- Rasmus.

t4rzsan Denmark siger:

30. november 2009 14:01

Hvis jeg ikke husker meget galt, så bliver SynchronizationContext brugt af Invoke/BeginInvoke... ?

Kommentarerne er lukkede