在继续之前等待结果并且不阻止UI

时间:2017-08-04 19:15:44

标签: c# multithreading

我有TextBoxTextChanged事件,如果文本框的文本代表现有文件,则会调用自定义事件。在这种情况下,有一个外部dll的调用,对文件进行一些处理,这可能需要一分钟才能完成。我也做了一些后处​​理,取决于这个方法返回给我的结果。目前,这阻止了我的UI,这是非常不受欢迎的。

我看到基本上有2个“选项”/场景。

  1. 在自定义事件中,以某种方式等待dll调用完成,然后继续事件,同时保持UI免费。这似乎是我多线程未经训练的自己最简单的想法,但它在概念上也向我抛出了红色标志:这是否可能,因为自定义事件本身(从TextChanged调用)在UI线程上?
  2. 使用Task.Run()将整个自定义事件放入其自己的线程中。这里的缺点是,除了dll方法调用之外,在long方法之后,有相当多的UI元素受到getter / setter的影响。我可以根据适当的InvokeRequired编写交替的getter / setter,但如果有更正确的方法可以做到这一点,我宁愿采取这种方法。
  3. 我做了一个更短(虽然做作)的示例项目,它基本上显示了我所使用的内容,使用上面的选项2:

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
    
            comboBox1.Items.Add("Select One...");
            comboBox1.Items.Add("Item 1");
            comboBox1.Items.Add("Item 2");
    
            Value = 0;
        }
    
        public string SetMessage
        {
            set
            {
                if (lblInfo.InvokeRequired)
                    lblInfo.BeginInvoke((MethodInvoker)delegate () { lblInfo.Text = Important ? value + "!" : value; });
                else
                    lblInfo.Text = Important ? value + "!" : value;
            }
        }
    
        public bool Important
        {
            get
            {
                return chkImportant.Checked;
            }
            set
            {
                if (chkImportant.InvokeRequired)
                    chkImportant.BeginInvoke((MethodInvoker) delegate() { chkImportant.Checked = value; });
                else
                    chkImportant.Checked = value;
            }
        }
    
        public SomeValue Value
        {
            get
            {
                if (comboBox1.InvokeRequired)
                {
                    SomeValue v = (SomeValue)comboBox1.Invoke(new Func<SomeValue>(() => SomeValue.Bar));
                    return v;
                }
                else
                {
                    switch (comboBox1.SelectedIndex)
                    {
                        case 1:
                            return SomeValue.Foo;
                        case 2:
                            return SomeValue.Bar;
                        default:
                            return SomeValue.Nothing;
                    }
                }
            }
            set
            {
                if (comboBox1.InvokeRequired)
                {
                    comboBox1.BeginInvoke((MethodInvoker)delegate ()
                    {
                       switch (value)
                       {
                           case SomeValue.Nothing:
                               comboBox1.SelectedIndex = 0;
                               break;
                           case SomeValue.Foo:
                               comboBox1.SelectedIndex = 1;
                               break;
                           case SomeValue.Bar:
                               comboBox1.SelectedIndex = 2;
                               break;
                       }
                    });
                }
                else
                {
                    switch (value)
                    {
                        case SomeValue.Nothing:
                            comboBox1.SelectedIndex = 0;
                            break;
                        case SomeValue.Foo:
                            comboBox1.SelectedIndex = 1;
                            break;
                        case SomeValue.Bar:
                            comboBox1.SelectedIndex = 2;
                            break;
                    }
                }
            }
        }
    
        private void CustomEvent(object sender, EventArgs e)
        {
            if (!Important)
                Important = true;
    
            SetMessage = "Doing some stuff";
    
            if (Value == SomeValue.Foo)
                Debug.WriteLine("Foo selected");
    
            //I don't want to continue until a result is returned, 
            //but I don't want to block UI either.
            if (ReturnsTrueEventually())
            {
    
                Debug.WriteLine("True!");
            }
    
            Important = false;
            SetMessage = "Finished.";
        }
    
        public bool ReturnsTrueEventually()
        {
            //Simulates some long running method call in a dll.
            //In reality, I would interpret an integer and return 
            //an appropriate T/F value based on it.
            Thread.Sleep(5000);
    
            return true;
        }
    
        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            //Do I *need* to multithread the whole thing?
            Task.Run(() => CustomEvent(this, new EventArgs()));
        }
    }
    
    public enum SomeValue
    {
        Nothing = 0,
        Foo = 100,
        Bar = 200
    }
    

    注意:我不是要求对我的选项2代码进行代码审查。相反,我问是否有必要完成选项2,因为该选项导致我更改了相当大的代码部分,因为它只有一个方法可以阻止整个过程。

    我也意识到我可以简化这些属性中的一些代码以防止复制。为了向自己展示和调试,我暂时不论此。

    以下是我与选项1相关的内容(省略重复代码和没有调用的getter / setter):

    private async void CustomEvent(object sender, EventArgs e)
    {
        if (!Important)
            Important = true;
    
        SetMessage = "Doing some stuff";
    
        if (Value == SomeValue.Foo)
            Debug.WriteLine("Foo selected");
    
        //I don't want to continue until a result is returned, 
        //but I don't want to block UI either.
        if (await ReturnsTrueEventually())
        {
    
            Debug.WriteLine("True!");
        }
    
        Important = false;
        SetMessage = "Finished.";
    }
    
    public async Task<bool> ReturnsTrueEventually()
    {
        //Simulates some long running method call in a dll.
        //In reality, I would interpret an integer and 
        //return an appropriate T/F value based on it.
        Thread.Sleep(5000);
    
        return true;
    }
    

1 个答案:

答案 0 :(得分:0)

这基本上就是你想要的。我在这里违反了一些最佳做法,但只是表明它并不复杂。要记住的一件事是,用户现在可以连续多次单击此按钮。您可以考虑在处理之前禁用它。或者您可以执行Monitor.TryEnter()以确保它尚未运行。

    private async void buttonProcess_Click(object sender, RoutedEventArgs e)
    {
         textBlockStatus.Text = "Processing...";
         bool processed = await Task.Run(() => SlowRunningTask());
    }

    private bool SlowRunningTask()
    {
        Thread.Sleep(5000);
        return true;
    }