Display “Wait” screen in WPF

前端 未结 4 1120
粉色の甜心
粉色の甜心 2020-12-13 11:26

I am trying to display a please wait dialog for a long running operation. The problem is since this is single threaded even though I tell the WaitScreen to display it never

相关标签:
4条回答
  • 2020-12-13 12:00

    Check out my comprehensive research of this very delicate topic. If there's nothing you can do to improve the actual performance, you have the following options to display a waiting message:

    Option #1 Execute a code to display a waiting message synchronously in the same method which does the real task. Just put this line before a lengthy process:

    Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Normal, (Action)(() => { /* Your code to display a waiting message */ }));
    

    It'll process pending messages on the main dispatcher thread at the end of the Invoke().

    Note: Reason for selecting Application.Current.Dispatcher but Dispatcher.CurrentDispatcher is explained here.

    Option #2 Display a “Wait” screen and update UI (process pending messages).

    To do it WinForms developers executed Application.DoEvents method. WPF offers two alternatives to achieve similar results:

    Option #2.1 With using DispatcherFrame class.

    Check a bit bulky example from MSDN:

    [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
    public void DoEvents()
    {
        DispatcherFrame frame = new DispatcherFrame();
        Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
        Dispatcher.PushFrame(frame);
    }
    
    public object ExitFrame(object f)
    {
        ((DispatcherFrame)f).Continue = false;
        return null;
    }
    

    Option #2.2 Invoke an empty Action

    Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, (Action)(() => { }));
    

    See discussions which one (2.1 or 2.2) is better here. IMHO option #1 is still better than #2.

    Option #3 Display a waiting message in a separate window.

    It comes in handy when you display not a simple waiting message, but an animation. Rendering a loading animation at the same time that we are waiting for another long rendering operation to complete is a problem. Basically, we need two rendering threads. You can't have multiple rendering threads in a single window, but you can put your loading animation in a new window with its own rendering thread and make it look like it's not a separate window.

    Download WpfLoadingOverlay.zip from this github (it was a sample from article "WPF Responsiveness: Asynchronous Loading Animations During Rendering", but I can't find it on the Web anymore) or have a look at the main idea below:

    public partial class LoadingOverlayWindow : Window
    {
        /// <summary>
        ///     Launches a loading window in its own UI thread and positions it over <c>overlayedElement</c>.
        /// </summary>
        /// <param name="overlayedElement"> An element for overlaying by the waiting form/message </param>
        /// <returns> A reference to the created window </returns>
        public static LoadingOverlayWindow CreateAsync(FrameworkElement overlayedElement)
        {
            // Get the coordinates where the loading overlay should be shown
            var locationFromScreen = overlayedElement.PointToScreen(new Point(0, 0));
    
            // Launch window in its own thread with a specific size and position
            var windowThread = new Thread(() =>
                {
                    var window = new LoadingOverlayWindow
                        {
                            Left = locationFromScreen.X,
                            Top = locationFromScreen.Y,
                            Width = overlayedElement.ActualWidth,
                            Height = overlayedElement.ActualHeight
                        };
                    window.Show();
                    window.Closed += window.OnWindowClosed;
                    Dispatcher.Run();
                });
            windowThread.SetApartmentState(ApartmentState.STA);
            windowThread.Start();
    
            // Wait until the new thread has created the window
            while (windowLauncher.Window == null) {}
    
            // The window has been created, so return a reference to it
            return windowLauncher.Window;
        }
    
        public LoadingOverlayWindow()
        {
            InitializeComponent();
        }
    
        private void OnWindowClosed(object sender, EventArgs args)
        {
            Dispatcher.InvokeShutdown();
        }
    }
    
    0 讨论(0)
  • 2020-12-13 12:15

    I found a way! Thanks to this thread.

    public static void ForceUIToUpdate()
    {
      DispatcherFrame frame = new DispatcherFrame();
    
      Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, new DispatcherOperationCallback(delegate(object parameter)
      {
        frame.Continue = false;
        return null;
      }), null);
    
      Dispatcher.PushFrame(frame);
    }
    

    That function needs to be called right before the long running operation. That will then Force the UI thread to update.

    0 讨论(0)
  • 2020-12-13 12:21

    Doing it single threaded really is going to be a pain, and it'll never work as you'd like. The window will eventually go black in WPF, and the program will change to "Not Responding".

    I would recommending using a BackgroundWorker to do your long running task.

    It's not that complicated. Something like this would work.

    private void DoWork(object sender, DoWorkEventArgs e)
    {
        //Do the long running process
    }
    
    private void WorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        //Hide your wait dialog
    }
    
    private void StartWork()
    {
       //Show your wait dialog
       BackgroundWorker worker = new BackgroundWorker();
       worker.DoWork += DoWork;
       worker.RunWorkerCompleted += WorkerCompleted;
       worker.RunWorkerAsync();
    }
    

    You can then look at the ProgressChanged event to display a progress if you like (remember to set WorkerReportsProgress to true). You can also pass a parameter to RunWorkerAsync if your DoWork methods needs an object (available in e.Argument).

    This really is the simplest way, rather than trying to do it singled threaded.

    0 讨论(0)
  • 2020-12-13 12:21

    Another option is to write your long-running routine as a function that returns IEnumerable<double> to indicate progress, and just say:

    yield return 30;
    

    That would indicate 30% of the way through, for example. You can then use a WPF timer to execute it in the "background" as a co-operative coroutine.

    It's described in some detail here, with sample code.

    0 讨论(0)
提交回复
热议问题