如何在WPF应用程序上执行异步启动

本文关键字:程序上 执行 异步 启动 应用程序 应用 WPF | 更新日期: 2023-09-27 18:02:35

我在async-await方面很落后,所以这可能是一个"duh"问题。

我正在研究一个应该是非常小的UI应用程序,它使用WPF NotifyIcon库从系统托盘运行。

应用程序应该以以下方式非常简单地(对用户而言)运行:

  • 程序启动
  • 如果有必要,有一个闪屏告诉用户程序正在运行,并提示他们登录(如果他们在之前的迭代中还没有这样做)。
  • WPF通知图标出现在系统托盘中
  • 异步执行开始

我正在运行的问题是"异步执行开始"部分。在此之前发生的一切都很好,但是当程序开始试图"运行"时,UI就会锁定(我的意思是,用户可以像疯子一样点击托盘图标,而上下文菜单却拒绝出现)。

此锁定发生的时间长得令人无法接受。

这是启动代码:

private async void AppStartup( object sender, StartupEventArgs e ) {
    this.TRSIcon = this.FindResource( "TRSIcon" ) as TaskbarIcon;
    if ( Settings.Default.DoUpgrade ) { //Upgrade if necessary.
        Settings.Default.Upgrade( );
        Settings.Default.DoUpgrade = false;
        Settings.Default.Save( );
    }
    if ( string.IsNullOrEmpty( Settings.Default.Username ) || string.IsNullOrEmpty( Settings.Default.Password ) ) {
        new Help( ).ShowDialog( );
        Tuple<string, string> UP;
        if ( ( UP = Login.Instance.GetUserPassword( ) ) != null ) {
            Settings.Default.Username = UP.Item1;
            Settings.Default.Password = UP.Item2;
            Settings.Default.Save( );
        } else
            return;
    }
    await this.Start( ); //<-----This is where the meat of the program runs and it hangs the UI until it finishes.
    return; //<-----This is just so that I have a break point to see that await this.Start is blocking (I have to do it like that right? or do I?)
}

这是Resources.xaml:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Tools="clr-namespace:WPFTools.TaskbarNotification;assembly=WPFTools"
    xmlns:TR="clr-namespace:TriviaRetriever">
    <ContextMenu x:Key="TSRInterfaceMenu" x:Shared="false">
        <MenuItem Header="Login" Command="{Binding cmdLogin}"/>
        <MenuItem Header="Get My Trivia" Command="{Binding cmdDownload}"/>
        <MenuItem Header="Register" Command="{Binding cmdRegister}"/>
        <MenuItem Header="Lost Password" Command="{Binding cmdLostPassword}"/>
        <MenuItem Header="About" Command="{Binding cmdAbout}"/>
        <MenuItem Header="Log Out" Command="{Binding cmdLogout}"/>
        <MenuItem Header="Exit" Command="{Binding cmdExit}"/>
    </ContextMenu>
    <Tools:TaskbarIcon
        x:Key="TRSIcon"
        MenuActivation="LeftOrDoubleClick"
        IconSource="/TRIcon.ico"
        DoubleClickCommand="{Binding cmdAbout}"
        ContextMenu="{StaticResource TSRInterfaceMenu}">
        <Tools:TaskbarIcon.DataContext>
            <TR:TRSIViewModel/>
        </Tools:TaskbarIcon.DataContext>
    </Tools:TaskbarIcon>
</ResourceDictionary>

这是上下文菜单命令的MVVM:

public class TRSIViewModel {
    public ICommand cmdLogin {
        get {
            return new DelegateCommand {
                fncCanExecute = ( ) => ( Application.Current as App ).Core == null,
                actCommand = async ( ) => {
                    Tuple<string, string> LoginPassword = Login.Instance.GetUserPassword( );
                    if ( LoginPassword != null ) {
                        Settings.Default.Username = LoginPassword.Item1;
                        Settings.Default.Password = LoginPassword.Item2;
                        Settings.Default.Save( );
                        await ( Application.Current as App ).Start( );
                    }
                }
            };
        }
    }
    public ICommand cmdLogout {
        get {
            return new DelegateCommand {
                fncCanExecute = ( ) => ( Application.Current as App ).Core != null,
                actCommand = ( ) => {
                    ( Application.Current as App ).Core.Terminate( );
                    ( Application.Current as App ).Core = null;
                }
            };
        }
    }
    public ICommand cmdRegister {
        get {
            return new DelegateCommand {
                fncCanExecute = ( ) => true,
                actCommand = ( ) => Process.Start( @"https://www.digigames.com/weekly_subscriptions/index.php" )
            };
        }
    }
    public ICommand cmdLostPassword {
        get {
            return new DelegateCommand {
                fncCanExecute = ( ) => true,
                actCommand = ( ) => Process.Start( @"https://www.digigames.com/weekly_subscriptions/lost_password.php" )
            };
        }
    }
    public ICommand cmdAbout {
        get {
            return new DelegateCommand {
                fncCanExecute = ( ) => true,
                actCommand = ( ) => ( Application.Current as App ).TRSIcon.ShowCustomBalloon( new About( ), PopupAnimation.Slide, 5000 )
            };
        }
    }
    public ICommand cmdExit {
        get {
            return new DelegateCommand {
                fncCanExecute = ( ) => true,
                actCommand = ( ) => {
                    if ( ( Application.Current as App ).Core != null )
                        ( Application.Current as App ).Core.Terminate( );
                    Application.Current.Shutdown( 0 );
                }
            };
        }
    }
    public ICommand cmdDownload {
        get {
            return new DelegateCommand {
                fncCanExecute = ( ) => ( Application.Current as App ).Core != null,
                actCommand = async ( ) => await ( Application.Current as App ).Core.DownloadTrivia( true )
            };
        }
    }
    public class DelegateCommand : ICommand {
        public Action actCommand { get; set; }
        public Func<bool> fncCanExecute { get; set; }
        public bool CanExecute( object parameter ) {
            return this.fncCanExecute != null && this.fncCanExecute( );
        }
        public event EventHandler CanExecuteChanged {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
        public void Execute( object parameter ) { this.actCommand( ); }
    }
}

我在这里做错了什么?

如何在WPF应用程序上执行异步启动

我认为你的问题都是关于你的Start方法。

但重要的事情先做。您的断点没有做您期望它做的事情。一旦Start方法实际完成并执行剩余的函数,而不是一旦UI线程再次释放,它就会中断。您必须理解,一旦执行离开Start函数中的UI同步,UI线程就可以自由地再次运行。

了解方法实际释放执行所需时间的一个好方法是等待它返回Task对象。

var pendingTask = this.Start();
Debugger.Break();
await pendingTask;

Start方法碰到内部异步执行的函数时,Task对象返回。一旦pendingTask实际完成,await返回。

在你的情况下,我认为时间将是相似的,因为Start方法没有将足够的工作发送到后台。

有几种方法可以做到这一点。如果您的Start方法没有与UI交互,则没有问题。你只需要把整个方法发送到后台就可以了。这很简单:

await Task.Run(() => this.Start());

这将任务发送到ThreadPool的一个线程中,并立即释放UI。Task.Run方法有一个重载,它可以自动打开由Start方法返回的内部Task

如果你的方法与UI交互,你必须在内部修改方法。查找方法中需要很长时间且不与UI交互的部分,并将它们包装成上面所示的Task.Run方法的调用。

每个await将再次建立之前存在的SynchronizationContext。因此,线程中每个能够改变UI的await都将确保在同一个线程中执行延续。

所以像这样的事情没有问题:

someLabel.Label = "Working…";
await Task.Run(() => DoManyThings());
someLabel.Label = "Done! :D"

我希望这对你有帮助。在不知道Start方法的作用之前,我无法给您更多提示。但我希望这个答案能让你走上正确的道路。

我通常的免责声明:我通常使用VB.net,所以我的c#代码可能在语法方面有缺陷。

如果您发现任何错误,请随时编辑或告诉我错在哪里。

简单地创建一个新的Thread并运行它。

   private void App_OnStartup(object sender, StartupEventArgs e)
    {
        //Some login logic
        if(loggedIn) //Or however you do it
        {
            Thread thread = new Thread(() =>
            {
                this.RunWhateverMethodWillDoABunchOfStuff()
            });
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
        }

根据确切的程序和你想要的,你也可以使用BackgroundWorker,这是一个很好的同步方式来运行后台操作,同时从后台线程接收进度更新,允许你更新像加载条或类似的东西。如果你想了解更多信息,请告诉我

编辑:对不起,我没有注意到你正在运行一个同步方法。我会避免这一切,并简单地运行它在一个正常的方法,只是一个线程(假设你不需要进度更新或警报,当它完成,否则使用backgroundworker)。我觉得你把事情弄得太复杂了。编辑我的代码以反映

编辑2:下面是BackgroundWorker方法的一个例子

        BackgroundWorker bw = new BackgroundWorker();
        bw.DoWork += BwOnDoWork;
        bw.ProgressChanged += BwOnProgressChanged;
        bw.RunWorkerCompleted += BwOnRunWorkerCompleted;
        bw.WorkerSupportsCancellation = true;
        bw.WorkerReportsProgress = true;
        //This line here is what starts the asynchronous work
        bw.RunWorkerAsync();

    private void BwOnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs runWorkerCompletedEventArgs)
    {
         //Do whatever you want to do when it is done with its asynchronous task
         //for example
         Label.Content = "Yay, Were done doing whatever it was that we were doing!!!!"
    }
    private void BwOnProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        //Here is where we can send progress reports to the UI, like updating a loading bar
        MyProgressBar.EditValue = e.ProgressPercentage;
    }
    private void BwOnDoWork(object sender, DoWorkEventArgs doWorkEventArgs)
    {
        //This is where we will put anything we want to be ran asynchronously
        this.RunWhateverMethodWillDoABunchOfStuff()
    }