WPF / C # Ne bloquez pas l’interface utilisateur

J’ai une application WPF existante, qui comporte plusieurs sections. Chaque section est un UserControl, qui implémente une interface.

L’interface spécifie deux méthodes: void LoadData([...]) et bool UnloadData() .

Ces méthodes sont appelées par le thread d’interface utilisateur. Nous devons donc effectuer notre travail dans backgroundworker si cela prend du temps.

Aucun problème avec LoadData car nous pouvons mettre à jour l’interface utilisateur de manière asynchrone. Le problème est avec UnloadData ().

Cela devrait revenir si nous pouvons vraiment quitter la vue actuelle.

Ceci est calculé avec le statut actuel des données (sauvegardé / modifié / invalide):

  • Retour sauvegardé vrai,
  • Invalid vous demande si vous souhaitez restr pour sauvegarder des données correctes ou quitter sans sauvegarder
  • Modified vous indique que vous pouvez annuler votre modification (renvoyer true), continuer à modifier (renvoyer false) ou vous sauvegarder les données actuelles (renvoyer true).

Le problème vient de “Modified -> Save”. Cette méthode prend beaucoup de temps. Par conséquent, pour respecter la philosophie de l’application, vous devez l’exécuter dans un thread en arrière-plan (avec un indicateur occupé).

Mais si nous lançons simplement le fil et allons à la section suivante, cela retournera “true” à l’appel de la méthode, et nous lancerons directement la vue suivante.

Dans mon cas, le chargement de la vue suivante avant que nos données locales soient sauvegardées peut poser problème.

Alors:

Est-il possible d’attendre que le thread d’arrière-plan termine avant de renvoyer “true”, SANS bloquer l’interface utilisateur?

 public bool UnloadData(){ if(...){ LaunchMyTimeConsumingMethodWithBackgroundWorker(); return true;//Only when my time consuming method ends } //[...] } 

Important EDIT Peut-être que je n’étais pas assez clair: je sais comment utiliser un BackgroundWorker, ou TPL. Mon problème est que la classe parente (celle qui appelle UnloadData () “est une classe que je ne peux pas éditer (pour plusieurs raisons: c’est dans une autre DLL qui ne sera pas rechargée, elle fonctionne déjà avec plus de 70 userControls, tous séparés projets (dll), chargés par reflection.

Ce n’était pas mon choix, je ne le trouve pas bien, mais je dois y faire face maintenant. Je cherche surtout le moyen de faire attendre ma méthode pour la rendre. Je ne sais pas si c’est possible. Mais je cherche une solution de contournement, cela me fera économiser des semaines de travail.

Ok maintenant je suis excité, parce que je pense avoir découvert quelque chose par moi-même …

Voici ce que vous faites: vous créez un DispatcherFrame, poussez ce cadre sur le Dispatcher et, dans le composant RunWorkerCompleted, définissez la valeur Continue du cadre sur false.

C’est le code jusqu’à présent:

 public void Function() { BackgroundWorker worker = new BackgroundWorker(); worker.DoWork += TimeConsumingFunction; var frame = new DispatcherFrame(); worker.RunWorkerCompleted += (sender, args) => { frame.Continue = false; }; worker.RunWorkerAsync(); Dispatcher.PushFrame(frame); } private void TimeConsumingFunction(object sender, DoWorkEventArgs doWorkEventArgs) { Console.WriteLine("Entering"); for (int i = 0; i < 3; i++) { Thread.Sleep(1000); } Console.WriteLine("Exiting"); } private void ButtonBase_OnClick(object sender, RoutedEventArgs e) { Function(); Console.WriteLine("Returns"); } 

Vous devez implémenter une propriété de dépendance “IsBusy” de type bool, que vous définissez sur TRUE avant de démarrer BackgoundWorker, puis sur FALSE à la fin du travail.

Sur l’interface utilisateur, vous liez à cette propriété toute fonctionnalité que vous souhaitez désactiver pendant le traitement (comme le bouton permettant de charger la vue suivante, etc.); ou peut-être en affichant un bouton “Annuler”.

Vous ne devez pas “attendre” que l’opération se termine, vous pouvez récupérer le résultat dans une variable supplémentaire, que BackgroundWorker définira:

  BackgroundWorker _bw; bool _returnValue = false; private void button_Click(object sender, RoutedEventArgs e) { // if starting the processing by clicking a button _bw = new BackgroundWorker(); IsBusy = true; _bw.DoWork += new DoWorkEventHandler(_bw_DoWork); _bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(_bw_RunWorkerCompleted); _bw.RunWorkerAsync(); } void _bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { IsBusy = false; // resortingeve the result of the operation in the _returnValue variable } void _bw_DoWork(object sender, DoWorkEventArgs e) { _returnValue = UnloadData(); } private bool UnloadData() { if (...) { LaunchTimeConsumingMethod(); return true; } else return false; //etc ... } public bool IsBusy { get { return (bool)GetValue(IsBusyProperty); } set { SetValue(IsBusyProperty, value); } } // Using a DependencyProperty as the backing store for IsBusy. This enables animation, styling, binding, etc... public static readonly DependencyProperty IsBusyProperty = DependencyProperty.Register( ... ) 

Vous pourrez peut-être essayer d’utiliser les nouvelles fonctionnalités “en attente ” de .NET 4.5.

Le mot-clé wait vous permet d’attendre la fin d’un object Task sans bloquer l’interface utilisateur.

Essayez cette modification:

 public async bool UnloadData() { if(...) { await Task.Factory.StartNew(() => { LaunchMyTimeConsumingMethod(); }); return true;//Only when my time consuming method ends } //[...] } 

Traitez UnloadData comme une opération asynchrone et laissez les fonctionnalités async / wait traiter à la fois le cas quand il se termine de manière synchrone et quand il doit se terminer de manière asynchrone:

 public async Task UnloadData(){ if(...){ // The await keyword will segment your method execution and post the continuation in the UI thread // The Task.Factory.StartNew will run the time consuming method in the ThreadPool await Task.Factory.StartNew(()=>LaunchMyTimeConsumingMethodWithBackgroundWorker()); // The return statement is the continuation and will run in the UI thread after the consuming method is executed return true; } // If it came down this path, the execution is synchronous and is completely run in the UI thread return false; } private async void button_Click(object sender, RoutedEventArgs e) { // Put here your logic to prevent user interaction during the operation's execution. // Ex: this.mainPanel.IsEnabled = false; // Or: this.modalPanel.Visibility = Visible; // etc try { bool result = await this.UnloadData(); // Do whatever with the result } finally { // Reenable the user interaction // Ex: this.mainPanel.IsEnabled = true; } } 

MODIFIER

Si vous ne pouvez pas modifier UnloadData, exécutez-le simplement sur le ThreadPool, comme l’a noté @BTownTKD:

  private async void button_Click(object sender, RoutedEventArgs e) { // Put here your logic to prevent user interaction during the operation's execution. // Ex: this.mainPanel.IsEnabled = false; // Or: this.modalPanel.Visibility = Visible; // etc try { // The await keyword will segment your method execution and post the continuation in the UI thread // The Task.Factory.StartNew will run the time consuming method in the ThreadPool, whether it takes the long or the short path bool result = await The Task.Factory.StartNew(()=>this.UnloadData()); // Do whatever with the result } finally { // Reenable the user interaction // Ex: this.mainPanel.IsEnabled = true; } } 

Vous devriez probablement utiliser TPL si votre version de framework est 4.0:

 var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); // this will work only if you're running this code from UI thread, for example, by clicking a button Task.Factory.StartNew(() => UnloadData()).ContinueWith(t => /*update ui using t.Result here*/, uiScheduler); 

J’espère que cela t’aides.

Vous devez implémenter une fonction de rappel (RunWorkerCompleted), elle est appelée à la fin du processus de travail en arrière-plan.

Découvrez un exemple ici: http://msdn.microsoft.com/en-us/library/cc221403(v=vs.95).aspx