c# - WPF ViewModel在另一个线程中运行方法,报告进度,更改Button的IsEnabled并显示结果

标签 c# wpf multithreading mvvm

我正在尝试找到一种使用 ViewModel 中创建的线程来运行现有方法的正确方法。主要目的是提供响应式 UI。我决定使用基于任务的异步模式,但我需要将其与 WPF 和 MVVM 正确集成。

到目前为止,我找到了一种在另一个线程中运行冗长任务并报告其进度的方法。但是,我找不到更新启动任务的按钮的方法,以便仅在任务未运行时才启用它。以下 ViewModel 描述了我所做的事情:

public class MainViewModel : INotifyPropertyChanged
{
    public void NotifyPropertyChanged(string info)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(info));
    }
    public event PropertyChangedEventHandler PropertyChanged;

    // Do some time consuming work.
    int SomeTask()
    {
        //SCENARIO: Consider that it takes longer than usual to start the worker thread.
        Thread.Sleep(1000);

        // Prevent executing the task by two threads at the same time.
        lock ("IsReady")
        {
            if (IsReady == false)
                throw new ApplicationException("Task is already running");
            IsReady = false;
        }

        // The actual work that this task consists of.
        TaskProgress = 0;
        for (int i = 1; i <= 100; i++)
        {
            Thread.Sleep(50);
            TaskProgress = i;
        }

        // Mark task as completed to allow rerunning it.
        IsReady = true;

        return 123;
    }

    // True when not started or completed.
    bool _isReady = true;
    public bool IsReady
    {
        get { return _isReady; }
        set
        {
            _isReady = value;
            NotifyPropertyChanged("IsReady");
            StartTaskCommand.RaiseCanExecuteChanged();
        }
    }

    // Indicate the current progress when running SomeTask.
    int _taskProgress;
    public int TaskProgress
    {
        get { return _taskProgress; }
        set
        {
            _taskProgress = value;
            NotifyPropertyChanged("TaskProgress");
        }
    }

    // ICommand to start task asynchronously.
    RelayCommand _startTask;
    public RelayCommand StartTaskCommand
    {
        get
        {
            if (_startTask == null)
            {
                _startTask = new RelayCommand(
                    obj =>
                    {
                        Task<int> task = Task.Run((Func<int>)SomeTask);
                        task.ContinueWith(t =>
                        {
                            // SomeTask method may throw an ApplicationException.
                            if (!t.IsFaulted)
                                Result = t.Result.ToString();
                        });
                    },
                    obj => IsReady);

            }
            return _startTask;
        }
    }

    string _result;
    public string Result
    {
        get { return _result; }
        set { _result = value; NotifyPropertyChanged("Result"); }
    }
}

我使用以下 RelayCommand 实现:

public class RelayCommand : ICommand
{
    private Action<object> execute;
    private Func<object, bool> canExecute;

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void RaiseCanExecuteChanged()
    {
        CommandManager.InvalidateRequerySuggested();
    }

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return this.canExecute == null || this.canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        this.execute(parameter);
    }
}

主要问题是执行命令的 Button 不会根据 IsReady 更新其状态。我还尝试使用 IsEnabled="{Binding IsReady}" 显式设置它,但它仍然不起作用。我发现与此问题相关的最好的文章是:Raising CanExecuteChanged .

XAML 非常简单:

<DockPanel Margin="4">
    <TextBox DockPanel.Dock="Right" Text="{Binding Result}" Width="100"/>
    <Button DockPanel.Dock="Right" Content="Start" Margin="5,0"
            Command="{Binding StartTaskCommand}"/>
    <ProgressBar Value="{Binding TaskProgress}"/>
</DockPanel>

如何修复 IsReady 以由按钮的状态反射(reflect)?

有人推荐一个满足我需要的简约工作实现吗?

感谢您花时间阅读。

最佳答案

在启动任务之前,将 UI 线程上的 IsReady 属性设置为 false,然后在任务完成后将其设置回 true已完成:

public RelayCommand StartTaskCommand
{
    get
    {
        if (_startTask == null)
        {
            _startTask = new RelayCommand(
                obj =>
                {
                    if (IsReady)
                    {
                        //1. Disable the button on the UI thread
                        IsReady = false;
                        //2. Execute SomeTask on a background thread
                        Task.Factory.StartNew(SomeTask)
                        .ContinueWith(t =>
                        {
                            //3. Enable the button back on the UI thread
                            if (!t.IsFaulted)
                                Result = t.Result.ToString();
                            IsReady = true;
                        }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
                    }
                },
                obj => IsReady);

        }
        return _startTask;
    }
}

int SomeTask()
{
    Thread.Sleep(1000);

    TaskProgress = 0;
    for (int i = 1; i <= 100; i++)
    {
        Thread.Sleep(50);
        TaskProgress = i;
    }

    return 123;
}

关于c# - WPF ViewModel在另一个线程中运行方法,报告进度,更改Button的IsEnabled并显示结果,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48687766/

相关文章:

c# - 将 IList 转换为集合

c# - 如何根据 View 模型中的数据验证 View 控件

c++ - 从 vector 中删除已完成的线程

ruby - ActiveRecord Postgres 数据库未锁定 - 获取竞争条件

c# - 在 Windows 窗体中选择和更新数据表

c# - WPF中的Count属性的INotifyPropertyChanged?

c# - 访问流缓冲区 HttpWebRequest

c# - WPF - 将字典中定义的样式与父控件中定义的样式混合

.net - 混合 WinForms 和 WPF 应用程序中 .Net 3.5 中的 DPI 缩放

java - 尽管流量低,但 Tomcat 上的平均负载异常高