从ActionBlock更新UI控件

3

我一直在尝试通过创建一个示例应用程序来理解TPL Dataflow。其中一个我一直在尝试做的事情是从ActionBlock更新TextBox控件。使用TPL Dataflow的原因是在保持顺序的同时执行并行异步操作。以下函数由我编写:

private TaskScheduler scheduler = null;

public Form1()
    {
        this.scheduler = TaskScheduler.FromCurrentSynchronizationContext();
        InitializeComponent();
    }

public async void btnTPLDataFlow_Click(object sender, EventArgs e)
    {
        Stopwatch watch = new Stopwatch();
        watch.Start();

        txtOutput.Clear();

        ExecutionDataflowBlockOptions execOptions = new ExecutionDataflowBlockOptions();
        execOptions.MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded;
        execOptions.TaskScheduler = scheduler;

        ActionBlock<int> actionBlock = new ActionBlock<int>(async v =>
        {
            bool x = await InsertIntoDatabaseAsync(v);

            if (x)
                txtOutput.Text += "Value Inserted for: " + v + Environment.NewLine;
            else
                txtOutput.Text += "Value Failed for: " + v + Environment.NewLine;

        }, execOptions);


        for (int i = 1; i <= 200; i++)
        {
            actionBlock.Post(i);
        }

        actionBlock.Complete();
        await actionBlock.Completion;            

        watch.Stop();
        lblTPLDataFlow.Text = Convert.ToString(watch.ElapsedMilliseconds / 1000);
    }


private async Task<bool> InsertIntoDatabaseAsync(int id)
    {
        try
        {
            string connString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=D:\\TPLDatabase.accdb;Persist Security Info=False;";

            using (OleDbConnection conn = new OleDbConnection(connString))
            {
                string commandText = "INSERT INTO tblRecords (ProductName, ProductDescription, IsProcessed) VALUES (@ProductName, @ProductDescription, @IsProcessed)";

                await conn.OpenAsync();
                using (OleDbCommand command = new OleDbCommand(commandText, conn))
                {
                    command.CommandType = CommandType.Text;

                    command.Parameters.AddWithValue("@ProductName", "Product " + id);
                    command.Parameters.AddWithValue("@ProductDescription", "Description " + id);
                    command.Parameters.AddWithValue("@IsProcessed", false);

                    if (await command.ExecuteNonQueryAsync() > 0)
                        return true;
                    else
                        return false;
                }
            }
        }
        catch
        {
            return false;
        }
    }

现在上面的代码可以正常运行。它按顺序将记录插入到我的样本MS Access数据库中,并按顺序更新UI。但是,这个问题是它会阻塞UI,这是可以理解的,因为我正在使用TaskScheduler.FromCurrentSynchronizationContext,它将在UI线程上更新TextBox
我对代码进行了小改动,并从ExecutionDataflowBlockOptions中删除了调度程序。相反,我使用以下代码更新UI:
txtOutput.Invoke(new MethodInvoker(delegate
            {
                if (x)
                    txtOutput.Text += "Value Inserted for: " + v + Environment.NewLine;
                else
                   txtOutput.Text += "Value Failed for: " + v + Environment.NewLine;
            }));

现在这个更改不会冻结用户界面,但是数据库中的值顺序和文本框中显示的值顺序都受到了严重干扰。新的顺序如下:
ID   ProductName    ProductDescription  IsProcessed
6847    Product 6     Description 6       False
6848    Product 7     Description 7       False
6849    Product 8     Description 8       False
6850    Product 10    Description 10      False
6851    Product 11    Description 11      False
6852    Product 12    Description 12      False
6853    Product 9     Description 9       False
6854    Product 13    Description 13      False
6855    Product 14    Description 14      False

现在,在我的情况下,更新UI并保持顺序的最佳方法是什么?
1个回答

3
TPL数据流块在输出时保留输入顺序。它们不在块内部保留执行顺序,这就是为什么您会看到所有内容都是无序的原因。
您可能想要做的是将ActionBlock替换为TransformBlock,以并行地执行实际工作,并将其链接到一个ActionBlock,该块逐个更新UI。
您也可以使此块在UI线程上运行,这样您就不需要使用Invoke了:
var transformBlock = new TransformBlock<int, int>(
    v => InsertIntoDatabaseAsync(v), 
    execOptions);

var actionBlock = new ActionBlock<int>(x =>
{
    if (x)
        txtOutput.Text += "Value Inserted for: " + v + Environment.NewLine;
    else
        txtOutput.Text += "Value Failed for: " + v + Environment.NewLine;
}, new ExecutionDataflowBlockOptions { TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext() })

transformBlock.LinkTo(ActionBlock, new DataflowLinkOptions { PropagateCompletion = true } );

太好了。谢谢你的回答,以及提供解决方案的建议。由于我是新手,所以很好奇 TransformBlock 相对于 ActionBlock 的优势在哪里。(我认为双参数<int,int>表示一个输入和一个输出。我是对的吗?此外,我需要等待 TransformBlock 或 ActionBlock 完成吗?最重要的是,如果任何块中发生异常,并且如果我抛出异常,那么所有块是否都会终止?) - Adnan Yaseen
好处是将工作分成两个块。由于您需要将它们链接在一起,因此第一个块需要是“TrasnformBlock”。 - i3arnon
是的,第一种类型是输入,第二种类型是输出。 - i3arnon
你只需要等待最后一个块,因为我已经使用了 PropagateCompletion - i3arnon
@AdnanYaseen 是的,当一个块有异常时,它会被故障并停止处理其他消息。 - i3arnon
1
再次感谢您的所有回复。现在我完全明白您提出的建议。干杯! - Adnan Yaseen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接