在 BeginInvoke 内部调用时阻塞长操作

Long operation blocking when called inside BeginInvoke

我的程序在 MDB 中的每个 Table&Row 中搜索给定字符串。 当我开始搜索 (Search_Button_Click) 时,进度条没有显示并且 ui 被阻塞(无法移动 window)。看不出有什么问题。

ListViewData 是一个 ObservableCollection<ListViewData>

     private async void Search_Button_Click(object sender, RoutedEventArgs e)
    {
        LoadBar.Visibility = Visibility.Visible;
        await StartSearch();
        LoadBar.Visibility = Visibility.Hidden;
    }

    private async Task StartSearch()
    {
        await Task.Run(() =>
        {
            SearchMDB();
        });
    }

    private void SearchMDB()
    {
        this.Dispatcher.BeginInvoke(new Action(() =>
        {
            ListViewData.Clear();

            foreach (KeyValuePair<string, DataTable> _KVP in MDBContent)
            {
                for (int RowIndex = 0; RowIndex < _KVP.Value.Rows.Count; RowIndex++)
                {
                    DataRow _DR = _KVP.Value.Rows[RowIndex];
                    for (int i = 0; i < _DR.ItemArray.Length; i++)
                    {
                        if (_DR[i].ToString().Contains(Search_TextBox.Text))
                        {
                            ListViewItemClass _LC = new ListViewItemClass();
                            _LC.Page = _KVP.Key;
                            _LC.Column = _DR.Table.Columns[i].ToString();
                            _LC.Row = (RowIndex + 1).ToString();
                            _LC.ItemValue = _DR[i].ToString();
                            ListViewData.Add(_LC);
                        }
                    }
                }
            }
        }));
    }

我能看到两个问题:

A)。 SearchMDB() 方法已异步执行。您可以删除 this.Dispatcher.BeginInvoke() 行,因为您将再次回到 UI 线程上的 运行。

B)。但!您正在更新该委托中的 UI!更好的做法是异步线程从数据库中获取您需要的所有数据(如果需要,创建一个小的 DTO class),然后 populate/refresh UI 上的 ListView线程。

private async Task StartSearch()
{
    var data = await SearchAndFetchMDBDataAsync();
    RefreshListView(data);
}

private Task<List<object>> SearchAndFetchMDBDataAsync()
{
    return Task.Run(() =>
    {
        List<MdbDto> data = new List<MdbDto>();

        foreach (KeyValuePair<string, DataTable> _KVP in MDBContent)
            // ...

        return data;
    });
}

您的代码是同步的。 BeginInvoke 用于将调用编组回 UI 线程。使用 Task.Run 调用 BeginInvoke 不会改变任何东西。

我根据名称 SearchMDB 假设您正在尝试对 MDB 数据库执行 LIKE 搜索。最好的选择是让 Access 执行此操作。 Access 有索引。你的代码没有。它被迫扫描所有数据。更好的是,找到一个可以处理 MDB 文件的全文搜索库。将所有内容加载到内存中实际上会使事情变慢 .

如果您希望此代码按原样 运行,只需使用 Task.Run 并将过滤器字符串作为参数传递给 SearchMDB,例如 StartSearch(Search_TextBox.Text)

private async void Search_Button_Click(object sender, RoutedEventArgs e)
{
    LoadBar.Visibility = Visibility.Visible;
    await Task.Run(StartSearch(Search_TextBox.Text));
    LoadBar.Visibility = Visibility.Hidden;
}

private void SearchMDB()
{
    ListViewData.Clear();

    foreach (KeyValuePair<string, DataTable> _KVP in MDBContent)
    {
        .....
    }
}

更好的是,避免 全局 ListViewData 容器。使用全局状态时,很难编写正确的多线程代码。错误处理也更难 - 如果 SearchMDB 失败你打算做什么?

假设ListViewData是一个List<ListViewItemClass>,你应该写成:

private async void Search_Button_Click(object sender, RoutedEventArgs e)
{
    LoadBar.Visibility = Visibility.Visible;
    ListViewData=await Task.Run(StartSearch(Search_TextBox.Text));
    LoadBar.Visibility = Visibility.Hidden;
}

private List<ListViewItemClass> SearchMDB()
{
    var newData=new List<ListViewItemClass>();

    foreach (KeyValuePair<string, DataTable> _KVP in MDBContent)
    {
      for ()
      {
        .....
        newData.Add(_LC);
      }
    }
    return newData();
}

这样可以避免并发错误 并且 不会在 SearchMDB 抛出时破坏您的 UI。

更新

整个方法可以重写为单个 LINQ 查询:

var items = from KeyValuePair<string, DataTable> pair in MDBContent
            from DataRow row in pair.Value.Rows
            from DataColumn column in pair.Value.Columns
            let field=row[column].ToString()
            where field.Contains(searchText)
            select new ListViewItemClass
            {
                Page = pair.Key,
                Column = column.Caption,
//                Row = (RowIndex + 1).ToString(),
                ItemValue = field
            };

不仅更清楚发生了什么,您可以通过一次调用 `.AsParallel() 轻松地将其转换为 PLINQ,例如:

var items = from KeyValuePair<string, DataTable> pair in MDBContent.AsParallel()
            from DataRow row in pair.Value.Rows
            from DataColumn column in pair.Value.Columns
            let field=row[column].ToString()
            where field.Contains(searchText)
            select new ListViewItemClass
            {
                Page = pair.Key,
                Column = column.Caption,
//                Row = (RowIndex + 1).ToString(),
                ItemValue = field
            };
return items.ToList();

请注意,没有 Row 字段。 Table 行 没有 行索引。它们在结果中的位置由 ORDER BY 子句控制。没有它,数据库 canwill return 结果乱序。

如果您使用 Select() 重载将索引和项目传递给项目,则可以引入行索引:

var items = from pair in MDBContent.AsParallel()
            let indexedRows =pair.Value.Rows.OfType<DataRow>().Select((row,idx)=>new {Row=row,Idx=idx})                
            from indexedRow in indexedRows
            from DataColumn column in pair.Value.Columns
            let field=indexedRow.Row[column].ToString()
            where field.Contains(searchText)
            select new ListViewItemClass
            {
                Page = pair.Key,
                Column = column.Caption,
                Row = (indexedRow.Idx +1).ToString(),
                ItemValue = field
            };

更新 2

另一个问题中的评论表明 ListViewData 是一个 ObservableCollection。这不会改变任何东西。数据应该仍然在一边处理。 ObservableCollection 是为了观察个别物品的变化。

在这种情况下,整个集合都发生了变化。处理此问题的最简单方法是替换集合并发出通知,告知其对应的 属性 已更改,从而强制 UI 重新加载数据。这就是 WPF 数据绑定的工作方式,通过绑定到 properties 而不是字段。它也更便宜 - 清除集合并逐一添加项目会引发 lot 的通知。

单击事件处理程序应更改为:

private async void Search_Button_Click(object sender, RoutedEventArgs e)
{
    LoadBar.Visibility = Visibility.Visible;
    var data=await Task.Run(StartSearch(Search_TextBox.Text));

    ListViewData=new ObservableCollection(data);

    //Raise a change notification if `ListViewData` isn't a property
    //or doesn't raise the event itself
    //RaisePropertyChanged("ThatPropertyName);

    LoadBar.Visibility = Visibility.Hidden;
}