我按照此处给出的示例进行操作:在 Windows 窗体 DataGridView 控件中使用实时数据加载实现虚拟模式,以实现虚拟模式DataGridView
的实时加载。这很好用,但考虑到数据库的大小,我注意到在调用我的IDataPageRetriever
期间 UI 线程被阻止。为了解决这个问题,我在IDataPageRetriever
的类中实现了async-await
模式。但是,现在有很多行没有显示任何值,或者我需要单击它们以使它们显示值。将虚拟模式DataGridView
与async-await
相结合一定有一些不直接的东西。
我认为周围有一个典型的模式,我错过了一些基本的东西。
感谢您的投入!
编辑 1:添加代码
DataGridView的CellValueNeed(英语:DataGridView's CellValueNeed(
private async void dgvCompound_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
var dgv = (DataGridView)sender;
try
{
e.Value = await memoryCache.RetrieveElement(e.RowIndex, dgv.Columns[e.ColumnIndex].DataPropertyName);
}
catch (OperationCanceledException)
{
}
dgv.InvalidateRow(e.RowIndex);
}
缓存
public class Cache
{
private static int RowsPerPage;
public event EventHandler Initialised;
public event EventHandler CacheChanged;
// Represents one page of data.
public struct DataPage
{
public CompoundDataTable table;
public DataPage(CompoundDataTable table, int rowIndex)
{
this.table = table;
LowestIndex = MapToLowerBoundary(rowIndex);
HighestIndex = MapToUpperBoundary(rowIndex);
System.Diagnostics.Debug.Assert(LowestIndex >= 0);
System.Diagnostics.Debug.Assert(HighestIndex >= 0);
}
public int LowestIndex { get; private set; }
public int HighestIndex { get; private set; }
public static int MapToLowerBoundary(int rowIndex)
{
// Return the lowest index of a page containing the given index.
return (rowIndex / RowsPerPage) * RowsPerPage;
}
private static int MapToUpperBoundary(int rowIndex)
{
// Return the highest index of a page containing the given index.
return MapToLowerBoundary(rowIndex) + RowsPerPage - 1;
}
}
private DataPage[] cachePages;
private IDataPageRetriever dataSupply;
public Cache(IDataPageRetriever dataSupplier, int rowsPerPage)
{
dataSupply = dataSupplier;
Cache.RowsPerPage = rowsPerPage;
LoadFirstTwoPages();
}
public System.Data.SqlClient.SortOrder sortOrder
{
get { return dataSupply.sortOrder; }
set { dataSupply.sortOrder = value; }
}
public string sortByColumn
{
get { return dataSupply.sortByColumn; }
set
{
dataSupply.sortByColumn = value;
Reload();
}
}
public Dictionary<int, float> sortBySimilaritySeachResult
{
get { return dataSupply.sortBySimilaritySeachResult; }
set
{
dataSupply.sortBySimilaritySeachResult = value;
Reload();
}
}
// Sets the value of the element parameter if the value is in the cache.
private bool IfPageCached_ThenSetElement(int rowIndex, int columnIndex, ref string element)
{
if (IsRowCachedInPage(0, rowIndex))
{
if (cachePages[0].table == null || cachePages[0].table.Rows.Count == 0)
{
return true;
}
try
{
element = cachePages[0].table.Rows[rowIndex % RowsPerPage][columnIndex].ToString();
}
catch (Exception exx)
{
throw;
}
return true;
}
else if (IsRowCachedInPage(1, rowIndex))
{
if (cachePages[1].table == null || cachePages[1].table.Rows.Count == 0)
{
return true;
}
try
{
element = cachePages[1].table.Rows[rowIndex % RowsPerPage][columnIndex].ToString();
}
catch (Exception exx)
{
throw;
}
return true;
}
return false;
}
public async Task<string> RetrieveElement(int rowIndex, int columnIndex)
{
string element = null;
if (IfPageCached_ThenSetElement(rowIndex, columnIndex, ref element))
{
return element;
}
else
{
return await RetrieveData_CacheIt_ThenReturnElement(rowIndex, columnIndex);
}
}
static readonly CompoundDataTable c = new CompoundDataTable();
public async Task<string> RetrieveElement(int rowIndex, string colName) => await RetrieveElement(rowIndex, c.Columns[colName].Ordinal);
private async void LoadFirstTwoPages()
{
cachePages = new DataPage[]{
new DataPage(await dataSupply.SupplyPageOfData(DataPage.MapToLowerBoundary(0), RowsPerPage), 0),
new DataPage(await dataSupply.SupplyPageOfData(DataPage.MapToLowerBoundary(RowsPerPage),RowsPerPage), RowsPerPage)
};
Initialised?.Invoke(this, EventArgs.Empty);
CacheChanged?.Invoke(this, EventArgs.Empty);
}
public async void Reload()
{
cachePages[0].table = await dataSupply.SupplyPageOfData(DataPage.MapToLowerBoundary(0), RowsPerPage);
cachePages[1].table = await dataSupply.SupplyPageOfData(DataPage.MapToLowerBoundary(RowsPerPage), RowsPerPage);
CacheChanged?.Invoke(this, EventArgs.Empty);
}
private async Task<string> RetrieveData_CacheIt_ThenReturnElement(int rowIndex, int columnIndex)
{
var IndexToUnusedPage = GetIndexToUnusedPage(rowIndex);
// Retrieve a page worth of data containing the requested value.
try
{
CompoundDataTable table = await dataSupply.SupplyPageOfData(DataPage.MapToLowerBoundary(rowIndex), RowsPerPage);
// Replace the cached page furthest from the requested cell
// with a new page containing the newly retrieved data.
cachePages[IndexToUnusedPage] = new DataPage(table, rowIndex);
return await RetrieveElement(rowIndex, columnIndex);
}
catch (OperationCanceledException)
{
cachePages[IndexToUnusedPage] = new DataPage(null, rowIndex);
throw;
}
}
// Returns the index of the cached page most distant from the given index
// and therefore least likely to be reused.
private int GetIndexToUnusedPage(int rowIndex)
{
if (rowIndex > cachePages[0].HighestIndex && rowIndex > cachePages[1].HighestIndex)
{
int offsetFromPage0 = rowIndex - cachePages[0].HighestIndex;
int offsetFromPage1 = rowIndex - cachePages[1].HighestIndex;
if (offsetFromPage0 < offsetFromPage1)
{
return 1;
}
return 0;
}
else
{
int offsetFromPage0 = cachePages[0].LowestIndex - rowIndex;
int offsetFromPage1 = cachePages[1].LowestIndex - rowIndex;
if (offsetFromPage0 < offsetFromPage1)
{
return 1;
}
return 0;
}
}
// Returns a value indicating whether the given row index is contained
// in the given DataPage.
private bool IsRowCachedInPage(int pageNumber, int rowIndex)
{
return rowIndex <= cachePages[pageNumber].HighestIndex &&
rowIndex >= cachePages[pageNumber].LowestIndex;
}
}
DataRetriver
public class DataRetriever : IDataPageRetriever
{
private SemaphoreSlim _throttle;
private static Queue<CancellationTokenSource> _tasklist;
public DataRetriever()
{
sortByColumn = "Id";
_throttle = new SemaphoreSlim(2);
_tasklist = new Queue<CancellationTokenSource>();
//just add two cancelation dummies
for (int i = 0; i < _throttle.CurrentCount; i++)
{
_tasklist.Enqueue(new CancellationTokenSource());
}
}
public int RowCount
{
get { return DB.dsTgxChemTableAdapters.CompoundTableAdapter.RowCount(); }
}
// Declare variables to be reused by the SupplyPageOfData method.
private string _sortByColumn;
public string sortByColumn
{
get { return _sortByColumn; }
set
{
if (_sortByColumn == value)
{
sortOrder = sortOrder == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending;
}
else
{
_sortByColumn = value;
sortOrder = SortOrder.Ascending;
}
}
}
public SortOrder sortOrder { get; set; }
List<int> exclusion = new List<int>();
public async Task<CompoundDataTable> SupplyPageOfData(int lowerPageBoundary, int rowsPerPage)
{
CompoundDataTable dt = new CompoundDataTable();
bool dowork = false;
lock (exclusion)
{
if (!exclusion.Contains(lowerPageBoundary))
{
exclusion.Add(lowerPageBoundary);
dowork = true;
}
}
if (dowork)
{
CancellationTokenSource cts = new CancellationTokenSource();
_tasklist.Enqueue(cts);
CancellationTokenSource prevous = _tasklist.Dequeue();
prevous.Cancel();
prevous.Dispose();
await _throttle.WaitAsync(cts.Token);
try
{
if (!cts.IsCancellationRequested)
{
await DB.dsTgxChemTableAdapters.CompoundTableAdapter.FillAsync(dt, lowerPageBoundary, rowsPerPage, sortByColumn, sortOrder, cts.Token);
}
}
finally
{
_throttle.Release();
lock (exclusion)
{
exclusion.Remove(lowerPageBoundary);
}
}
}
return dt;
}
}
就我而言,我这样做了:我在Cache
(我称之为"数据源"(上公开了一些东西 - 一个方法LoadNextPage()
和一个事件PageLoaded
(这可能只是一个异步方法,但我发现这种拆分导致了更干净的代码(,以及缓存的行计数(在您的情况下,它将是最后一个缓存页面的 HighestIndex(。
LoadNextPage()
启动数据的异步加载过程,加载和缓存数据时,将触发PageLoaded
事件。
UI 类首先调用LoadNextPage()
来加载第一页,然后触发PageLoaded
,我将网格视图的RowCount
设置为加载的缓存行计数。
之后,网格视图开始为所有单元格调用CellValueNeeded
,您可以从缓存中同步填充它。当它需要缓存最后一行的数据时,我再次调用LoadNextPage()
,然后重复该过程。
因此,网格视图一直被欺骗,因为它只有缓存的行,没有其他内容。
一个问题是CellValueNeeded
可能会为同一行多次调用,因此请确保在这种情况下不会并行加载两次内容。
我为 Windows 版 NitroGit Git 客户端中的 Git 日志做了这件事,所以它是一个单向加载过程,这意味着从 1 到 N 的页面总是被缓存。如果您有不同的情况,例如想要从中间开始向上滚动,甚至填充随机页面,则需要做更多的工作,但可以使用相同的原理欺骗网格视图,使其具有与缓存相同的行数,然后在网格视图行索引和实际数据索引之间映射, 同时在屏幕上的网格中达到缓存的边界时"在一侧"填充数据。