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

问题描述 投票:0回答:3

我在异步等待方面相当落后,所以这可能是一个“duh”问题。

我正在开发一个非常简单的 UI 应用程序,它使用 WPF NotifyIcon 库从系统托盘运行。

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

  • 节目开始
  • 如有必要,会有一个启动屏幕告诉用户程序正在运行并提示他们登录(如果他们在之前的迭代中尚未登录)。
  • WPF NotifyIcon 出现在系统托盘中。
  • 异步执行开始

我遇到的问题是“异步执行开始”部分。在此之前发生的所有事情都运行良好,但是当程序开始尝试“运行”时,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( ); }
    }
}

我在这里做错了什么?

c# wpf asynchronous async-await
3个回答
2
投票

我认为你的问题完全在于你的

Start
方法。

但首先要注意的事情。您的断点没有执行您期望的操作。一旦

Start
方法实际完成并且执行剩余的函数,它就会中断,而不是在 UI 线程再次释放时中断。您必须了解,一旦执行离开
Start
函数中的 UI 同步,UI 线程就可以再次自由运行。

了解方法实际释放执行需要多长时间的一个好方法是等待它返回

Task
对象。

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

Task

 方法命中内部异步执行的函数,
Start
 对象就会返回。一旦 
await
 实际完成,
pendingTask
 就会返回。

在你的情况下,我认为时间会相似,因为

Start

方法没有向后台发送足够的工作。

有几种方法可以解决这个问题。如果您的

Start

 方法没有与 UI 交互,那就没问题。您只需将整个方法发送到后台即可完成。这工作起来很简单:

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

这会将任务发送到 ThreadPool 的线程中,并立即再次释放 UI。

Task.Run

 方法具有重载,可自动解开 
Task
 方法返回的内部 
Start

如果您的方法与 UI 交互,则必须在内部更改该方法。查找方法内耗时较长且不与 UI 交互的部分,并将它们包装到

Task.Run

 方法的调用中,如上所示。

每个

await

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

所以像这样的事情可以毫无问题地工作:

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

我希望有帮助。如果不知道您的

Start

 方法的作用,我无法给您更多提示。但我希望这个答案能让您走上正轨。

我通常的免责声明:我通常使用 VB.net,因此我的 C# 代码可能在语法方面存在缺陷。如果您发现任何错误,请随时编辑它或告诉我出了什么问题。


1
投票
只需创建一个新的

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

,这是一种很好的异步方式来运行后台操作,同时从后台线程接收进度更新,允许您更新诸如加载栏之类的内容像那样。如果您想了解更多信息,请告诉我

编辑:抱歉,我没有注意到您正在运行异步方法。我会避免所有这些,只需以正常方法和线程运行它(假设您不需要进度更新或完成时发出警报,否则使用后台工作程序)。我觉得你把这件事搞得太复杂了。编辑我的代码以反映这一点

编辑 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() }
    

0
投票
聚会已经很晚了,但是有一个很好的例子可以在

Microsoft Learn 上有所帮助

© www.soinside.com 2019 - 2024. All rights reserved.