2012-03-29 22 views
7

Sto scrivendo un'applicazione per Windows Store App per Windows 8. Ha solo una pagina xaml con TextBlock. La pagina ha la MyTimer classe come DataContext:Aggiornamento dell'interfaccia utente di Windows Store App

this.DataContext = new MyTimer(); 

MyTimer implementa INotifyPropertyChanged e l'aggiornamento della proprietà Time è fatto con un timer:

public MyTimer(){ 
    TimerElapsedHandler f = new TimerElapsedHandler(NotifyTimeChanged); 
    TimeSpan period = new TimeSpan(0, 0, 1); 
    ThreadPoolTimer.CreatePeriodicTimer(f, period); 
} 

con

private void NotifyTimeChanged(){ 
    if (this.PropertyChanged != null){ 
     this.PropertyChanged(this, new PropertyChangedEventArgs("Time")); 
    } 
} 

il TextBlock ha un data base sul tempo

<TextBlock Text="{Binding Time}" /> 

Quando eseguo l'applicazione ho la seguente eccezione:

System.Runtime.InteropServices.COMException was unhandled by user code 

Con il messaggio

The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))

Il vero problema è che sto aggiornando la proprietà del MyTimer di classe, non la stessa GUI, Non riesco a capirlo, ma penso che la soluzione dovrebbe usare qualcosa come this one.

risposta

7

Sì, si sta notificando le modifiche alle proprietà da un thread del pool di thread anziché dal thread dell'interfaccia utente. È necessario eseguire il marshalling della notifica sul thread dell'interfaccia utente nella richiamata del timer. Ora, il tuo modello di vista è separato dalla tua vista (una buona cosa) quindi non ha un collegamento diretto all'infrastruttura Dispatcher. Quindi quello che vuoi fare è consegnare il giusto SynchronizationContext su cui comunicare. Per fare questo è necessario acquisire l'attuale SynchronizationContext durante la costruzione o consentire che venga passato esplicitamente a un costruttore che è buono per i test o se si sta inizializzando l'oggetto fuori dal thread dell'interfaccia utente per cominciare.

tutta la baracca sarebbe simile a questa:

public class MyTimer 
{ 
    private SynchronizationContext synchronizationContext; 

    public MyTimer() : this(SynchronizationContext.Current) 
    { 
    } 

    public MyTimer(SynchronizationContext synchronizationContext) 
    { 
     if(this.synchronizationContext == null) 
     { 
      throw new ArgumentNullException("No synchronization context was specified and no default synchronization context was found.") 
     } 

     TimerElapsedHandler f = new TimerElapsedHandler(NotifyTimeChanged); 
     TimeSpan period = new TimeSpan(0, 0, 1); 
     ThreadPoolTimer.CreatePeriodicTimer(f, period); 
    } 

    private void NotifyTimeChanged() 
    { 
     if(this.PropertyChanged != null) 
     { 
      this.synchronizationContext.Post(() => 
       { 
        this.PropertyChanged(this, new PropertyChangedEventArgs("Time")); 
       }); 
     } 
    } 
} 
+0

Grazie mille. Questo mi suggerisce che dovrò usare SynchronizationContext ogni volta che uso una chiamata asincrona ... penserò a fare qualcosa di simile anche con la parola chiave asincrona. – Gabber

+0

Sì, se stai utilizzando la parola chiave C# 4.5, attendi questo per impostazione predefinita. –

+0

Grazie per la risposta!Voglio solo aggiungere un sidenote: se vuoi richiamare nel contesto dell'interfaccia utente, assicurati di catturare il 'SynchronizationContext' non prima che l'UI sia stato compilato (io uso il gestore di eventi' OnLaunched' per farlo), altrimenti il contesto che afferri non servirà. Ulteriori letture: http://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I – Lvsti

5

Un modo per fare questo è in attesa Task.Delay() in un ciclo invece di utilizzare un timer:

class MyTimer : INotifyPropertyChanged 
{ 
    public MyTimer() 
    { 
     Start(); 
    } 

    private async void Start() 
    { 
     while (true) 
     { 
      await Task.Delay(TimeSpan.FromSeconds(1)); 
      PropertyChanged(this, new PropertyChangedEventArgs("Time")); 
     } 
    } 

    public event PropertyChangedEventHandler PropertyChanged = delegate { }; 

    public DateTime Time { get { return DateTime.Now; } } 
} 

Se si chiama il costruttore su il thread dell'interfaccia utente, invocherà anche lo PropertyChanged. E la cosa bella è che esattamente lo stesso codice funzionerà per esempio anche in WPF (sotto .Net 4.5 e C# 5).

+0

Non dicendo che questo codice non funzionerà, ma IME Task.Delay non ha una risoluzione molto alta. Nei test WPF, Win8 e WP8, ho impostato un CancellationTokenSource.CancelAfter su 8 minuti e l'interfaccia utente ha ottenuto solo 7:56 minuti. – Stonetip

+0

@Stonetip Dovrebbe sicuramente avere una risoluzione migliore di 1 secondo, suppongo che ci sia qualcos'altro che sta succedendo nel tuo codice. Inoltre, 'Task.Delay()' non è 'CacelAfter()', anche se mi aspetto che abbiano la stessa risoluzione. – svick

+0

Task.Delay ha una risoluzione migliore di 1 secondo, ma ci si aspetta che sia azzeccata in termini di millisecondi, quindi i miei test su WPF, Win8 e Win Phone 8 mostrano un comportamento incoerente fino a 0,5 secondi al minuto . Ho trovato, tuttavia, per gli ultimi due ambienti che la classe ThreadPoolTimer funziona alla grande. – Stonetip

Problemi correlati