在 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
子句控制。没有它,数据库 can 和 will 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;
}
我的程序在 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
子句控制。没有它,数据库 can 和 will 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;
}