/***********************************************************************/ /* Copyright (C) 2002 Definitive Solutions, Inc. All Rights Reserved. */ /* THIS COMPUTER PROGRAM IS PROPRIETARY AND CONFIDENTIAL TO DEFINITIVE */ /* SOLUTIONS, INC. AND ITS LICENSORS AND CONTAINS TRADE SECRETS OF */ /* DEFINITIVE SOLUTIONS, INC. THAT ARE PROVIDED PURSUANT TO A WRITTEN */ /* AGREEMENT CONTAINING RESTRICTIONS ON USE AND DISCLOSURE. ANY USE, */ /* REPRODUCTION, OR TRANSFER EXCEPT AS PROVIDED IN SUCH AGREEMENT */ /* IS STRICTLY PROHIBITED. */ /***********************************************************************/ #include "stdafx.h" #include "MyAdoListCtrl.h" #include "Generic.h" #include "MyApp.h" #include "MyLog.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // Macros // Items to protect each end. #define MYADOLISTCRL_STALE_BUFFER 100 // When should we bunch up the staleing? #define MYADOLISTCTRL_STALE_DELAY_MS (10 * 1000) // When does staleing kick in. This is the maximum desired number of live // items at any moment. #define MYADOLISTCTRL_STALE_THRESHOLD 10000 // How big is the area around the hint area. #define MYADOLISTCTRL_STALE_NEARHINT 100 ///////////////////////////////////////////////////////////////////////////// // MyAdoListCtrlCache // Constructor. MyAdoListCtrlCache::MyAdoListCtrlCache(_RecordsetPtr& pRS) : m_pListCtrl(NULL) , m_pRS(pRS) , m_dwCurRec(0) { } // Destructor. /* virtual */ MyAdoListCtrlCache::~MyAdoListCtrlCache() { Flush(); } // Gives the parent a way to pass its pointer in to this cache class. No, it // can't do that in its constructor (the "this" is incomplete at that point). void MyAdoListCtrlCache::SetOwnerListCtrl(MyAdoListCtrl* pListCtrl) { ASSERT_VALID(pListCtrl); m_pListCtrl = pListCtrl; } // Retrieve the cached value for this row and column; retrieves it if needed, // as the hint provided by MFC is just that - a hint - and is not guaranteed // to predict every item that will be needed. CString MyAdoListCtrlCache::GetString(DWORD dwRow, DWORD dwCol) { VALIDATE; HRESULT hr(S_OK); CString s(" << SqlPP Error >>"); // A shortcut: if this is the zeroth column, just sent back the row. if (dwCol) { // See if this row is in the map. CStringArray* psa = NULL; if (m_map.Lookup(dwRow, psa)) { _ASSERTE(dwCol < psa->GetSize() && "There aren't that many columns in the array!"); s = psa->GetAt(dwCol); } else { // This row is not in the map, so let's add it and call ourself // again. This is deliberate recursion. EC_H(AddRowsToCache(dwRow, dwRow)); if (SUCCEEDED(hr)) { s = GetString(dwRow, dwCol); } else { s = ""; } } } else { s.Format("%d", dwRow + 1); } return s; } // Fill the cache for the sent range of database rows. The caller has no idea // if any (or all) of these are already cached or not. HRESULT MyAdoListCtrlCache::Prepare(int nLower, int nUpper) { VALIDATE; _ASSERTE(0 <= nLower && nLower <= nUpper); HRESULT hr(S_OK); DOMYLOGD ("Preparing cache for rows <%d> through <%d>...\n", nLower, nUpper); // First, we can save ourselves some work if some of the rows are already // cached. (And even more work if all of them are cached.) bool bAllCached(true); CStringArray* psa = NULL; for (DWORD dwRow = nLower; dwRow <= nUpper; ++dwRow) { if (! m_map.Lookup(dwRow, psa)) { bAllCached = false; break; } } if (! bAllCached) { // Add the sent rows to the cache; the first one, at least, needs to be // cached. EC_H(AddRowsToCache(dwRow, nUpper)); // Now delete stale rows, but be sure to never toss out the first "n" // rows, not the last "n" rows, as they are frequently accessed. EC_H_(StaleCache(nLower, nUpper)); } return hr; } // Add these row(s) to the cache. It's okay to send the lower and upper row // values as the same. Although the caller knows that the first one is not // already cached, it knows nothing about the subsequent ones. HRESULT MyAdoListCtrlCache::AddRowsToCache(DWORD dwRowLower, DWORD dwRowUpper) { VALIDATE; _ASSERTE(NULL != m_pRS && adStateOpen == m_pRS->GetState()); #ifdef _DEBUG CStringArray* psa = NULL; _ASSERTE(! m_map.Lookup(dwRowLower, psa) && "The first one should never be in the map"); #endif HRESULT hr(S_OK); // Move the recordset cursor to the lower row. (This is never wasted // effort, because we know the first one always needs to be cached.) if (! (m_pRS->BOF && m_pRS->adoEOF)) { DOMYLOGD ("Caching (maybe) rows <%d> through <%d>.\n", dwRowLower, dwRowUpper); // If the current position is already past the lower one, we start // over at the beginning (the downside of forward-only cursors). if (m_dwCurRec > dwRowLower) { EC_HEC_RS(m_pRS->MoveFirst(), m_pRS); m_dwCurRec = 0; DOMYLOGD ("Cache rewound to the first record.\n"); } // Move the cursor forward to the lower one. We can't do even a // paint-only message loop in here, as it causes this function to be // interrupted. if (SUCCEEDED(hr) && m_dwCurRec < dwRowLower) { DOMYLOGD ("Cache moving from record <%d> to lower record <%d>...\n", m_dwCurRec, dwRowLower); for ( ; m_dwCurRec < dwRowLower; ++m_dwCurRec) { _ASSERTE(! m_pRS->adoEOF); EC_HEC_RS(m_pRS->MoveNext(), m_pRS); } _ASSERTE(m_dwCurRec == dwRowLower && "The for loop didn't work"); DOMYLOGD ("Cache moved to lower record <%d>.\n", dwRowLower); } } // Now iterate each row, up to the upper one, and request it be added to // the cache (if need be). DOMYLOGD ("Caching records <%d> thru <%d>...\n", m_dwCurRec, dwRowUpper); for ( ; SUCCEEDED(hr) && ! m_pRS->adoEOF && m_dwCurRec <= dwRowUpper; ++m_dwCurRec) { EC_H(AddCurrentRowToCache()); if (FAILED(hr)) break; EC_HEC_RS(m_pRS->MoveNext(), m_pRS); if (FAILED(hr)) break; } DOMYLOGD ("Cached through upper record <%d>.\n", dwRowUpper); return hr; } // Add the record the recordset is currently positioned at to the cache. // This assumes that the cursor is at the correct position! The caller has no // idea whether the current record is already cached or not. HRESULT MyAdoListCtrlCache::AddCurrentRowToCache() { VALIDATE; _ASSERTE(NULL != m_pRS && adStateOpen == m_pRS->GetState()); _ASSERTE(! m_pRS->BOF && ! m_pRS->adoEOF); HRESULT hr(S_OK); // Don't bother if it's already cached. CStringArray* psa = NULL; if (! m_map.Lookup(m_dwCurRec, psa)) { // Iterate the fields in this database record, adding them to the string // array. CStringArray* psa = new CStringArray; CString sValue; sValue.Format("%d", m_dwCurRec + 1); EC_VEM(psa->SetAtGrow(0, sValue)); FieldsPtr pFields; EC_VEC_RS(pFields = m_pRS->GetFields(), m_pRS); _ASSERTE(NULL != pFields); long nCols(0); EC_VEC_RS(nCols = pFields->GetCount(), m_pRS); for (long nCol = 1; nCol <= nCols; ++nCol) { FieldPtr pField; EC_VEC_RS(pField = pFields->GetItem(nCol - 1L), m_pRS); _ASSERTE(NULL != pField); _variant_t var; EC_VEC_RS(var = pField->GetValue(), m_pRS); if (SUCCEEDED(hr)) { if (VT_NULL == var.vt || VT_EMPTY == var.vt) { sValue.Empty(); } else { _bstr_t bstr((_bstr_t) var); sValue = (LPCTSTR) bstr; } // There's no point in storing anything larger than the list // control can display. sValue = sValue.Left(255); EC_VEM(psa->SetAtGrow(nCol, sValue)); DOMYLOGD ("Cached: [%d,%d] <%s>\n", m_dwCurRec, nCol, sValue); } else { //AfxMessageBox("Error encountered displaying column!", // MB_ICONWARNING); break; } } // Add the string array, representing this record, to the map. EC_V(m_map.SetAt(m_dwCurRec, psa)); } return hr; } // Clear the cache. void MyAdoListCtrlCache::Flush() { VALIDATE; DWORD dwRow(0); POSITION pos(m_map.GetStartPosition()); while (pos) { CStringArray* psa = NULL; m_map.GetNextAssoc(pos, dwRow, psa); psa->RemoveAll(); delete psa; psa = NULL; } m_map.RemoveAll(); _ASSERTE(0 == m_map.GetCount()); m_dwCurRec = 0; DOMYLOGD ("Flushed the cache.\n"); } // Delete some items from the cache. HRESULT MyAdoListCtrlCache::StaleCache(int nLower, int nUpper) { VALIDATE; ASSERT_VALID(m_pListCtrl); _ASSERTE(nLower <= nUpper); CWaitCursor wc; HRESULT hr(S_OK); // Set up a delay so we don't stale ten times a second. static DWORD dwLastStaleing(::GetTickCount()); if (::GetTickCount() - dwLastStaleing > MYADOLISTCTRL_STALE_DELAY_MS) { dwLastStaleing = ::GetTickCount(); // Get the number of extra ones near the hint area that we won't stale. int nExtraLower(max(0, nLower - MYADOLISTCTRL_STALE_NEARHINT)); int nExtraUpper(min(m_map.GetCount() - 1, nUpper + MYADOLISTCTRL_STALE_NEARHINT)); // Don't stale until we have too many. POSITION pos(m_map.GetStartPosition()); while (pos && MYADOLISTCTRL_STALE_THRESHOLD <= m_map.GetCount()) { // Which list control item is this map entry for? CStringArray* psa = NULL; DWORD dwListControlItem(-1); m_map.GetNextAssoc(pos, dwListControlItem, psa); ASSERT_VALID(psa); int nItem(dwListControlItem); // Don't stale items in the low-end buffer. if (nItem <= MYADOLISTCRL_STALE_BUFFER - 1) { DOMYLOGD ("Item <%d> is in the lower buffer of [%d - %d].\n", nItem, 0, MYADOLISTCRL_STALE_BUFFER - 1); continue; } // Don't stale items in the high-end buffer. if (nItem >= m_pListCtrl->GetItemCount() - MYADOLISTCRL_STALE_BUFFER) { DOMYLOGD ("Item <%d> is in the upper buffer of [%d - %d].\n", nItem, m_pListCtrl->GetItemCount() - MYADOLISTCRL_STALE_BUFFER, m_pListCtrl->GetItemCount() - 1); continue; } // Don't stale items really near the hint area. if (nExtraLower <= nItem && nExtraUpper >= nItem) { DOMYLOGD ("Item <%d> is near the hint area of [%d - %d].\n", nItem, nExtraLower, nExtraUpper); continue; } // Don't stale visible items. int nTopItem(m_pListCtrl->GetTopIndex()); int nLastItem(nTopItem + m_pListCtrl->GetCountPerPage()); if (nItem >= nTopItem && nItem <= nLastItem) { DOMYLOGD ("Item <%d> is visible.\n", nItem); continue; } // Okay, try to now stale this item. if (m_map.Lookup((DWORD&) nItem, psa)) { DOMYLOGD ("Staleing item <%d>.\n", nItem); psa->RemoveAll(); EC_B(m_map.RemoveKey((DWORD&) nItem)); delete psa; psa = NULL; } } _ASSERTE(MYADOLISTCTRL_STALE_THRESHOLD > m_map.GetCount() && "Should have staled it down to this many"); } return hr; } ///////////////////////////////////////////////////////////////////////////// // MyAdoListCtrl // Constructor. MyAdoListCtrl::MyAdoListCtrl(_RecordsetPtr& pRS) : m_cache(pRS) , m_pRS(pRS) { } // Destructor. /* virtual */ MyAdoListCtrl::~MyAdoListCtrl() { } BEGIN_MESSAGE_MAP(MyAdoListCtrl, MyListCtrl) //{{AFX_MSG_MAP(MyAdoListCtrl) ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnGetdispinfo) ON_NOTIFY_REFLECT(LVN_ODCACHEHINT, OnOdcachehint) ON_NOTIFY_REFLECT(LVN_ODFINDITEM, OnOdfinditem) //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // MyAdoListCtrl message handlers // LVN_GETDISPINFO void MyAdoListCtrl::OnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult) { VALIDATE; LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR; LV_ITEM* pItem = &(pDispInfo)->item; if (pItem->mask & LVIF_TEXT) { if (0 == pItem->iSubItem) { DOMYLOGD ("OnGetDispInfo wants row <%d>.\n", pItem->iItem); } CString s(m_cache.GetString(pItem->iItem, pItem->iSubItem)); lstrcpyn(pItem->pszText, s, pItem->cchTextMax); } *pResult = 0; } // LVN_ODCACHEHINT void MyAdoListCtrl::OnOdcachehint(NMHDR* pNMHDR, LRESULT* pResult) { VALIDATE; HRESULT hr(S_OK); NMLVCACHEHINT* pCacheHint = (NMLVCACHEHINT*) pNMHDR; DOMYLOGD ("Hint received for <%d> thru <%d>.\n", pCacheHint->iFrom, pCacheHint->iTo); EC_H(m_cache.Prepare(pCacheHint->iFrom, pCacheHint->iTo)); } // LVN_ODFINDITEM void MyAdoListCtrl::OnOdfinditem(NMHDR* pNMHDR, LRESULT* pResult) { VALIDATE; NMLVFINDITEM* pFindInfo = (NMLVFINDITEM*)pNMHDR; _ASSERTE(! "Not supported!"); *pResult = 0; } // The caller is telling the list control that the recordset is about to have // new data put into it, and so the list control should be ready. // // 'bCancel' holds a flag the caller will set if the user wants to cancel; this // class will check that variable once in a while. // // 'pwndCount' should be populated with the record count every so often. HRESULT MyAdoListCtrl::PrepareForNewRecords(bool& bCancel, CWnd* pwndCount /* = NULL */ ) { VALIDATE; _ASSERTE(! bCancel && "Can't be stopped right off the bat!"); ASSERT_NULL_OR_POINTER(pwndCount, CWnd); HRESULT hr(S_OK); // Delete all our items. EC_B(DeleteAllItems()); // Delete all our columns. while (DeleteColumn(0)) { ; } // Insert the columns in the the results list control. if (adStateOpen == m_pRS->GetState()) { FieldsPtr pFields; EC_VEC_RS(pFields = m_pRS->GetFields(), m_pRS); _ASSERTE(NULL != pFields); CRect rcResults; GetClientRect(rcResults); long nFields(pFields->GetCount()); int nWidth(rcResults.Width() - ::GetSystemMetrics(SM_CXVSCROLL)); int nColWidth(nWidth / (1 + nFields)); EC_B(0 == InsertColumn(0, "ROWNUM", LVCFMT_RIGHT, nColWidth, 0)); for (long nField = 1; nField <= nFields; ++nField) { FieldPtr pField; EC_VEC_RS(pField = pFields->GetItem(nField - 1L), m_pRS); _ASSERTE(NULL != pField); if (FAILED(hr)) break; CString sName((LPCTSTR) pField->GetName()); _ASSERTE(! sName.IsEmpty()); EC_B(nField == InsertColumn(nField, sName, LVCFMT_LEFT, nColWidth, nField)); } // Delete everything in the cache. m_cache.Flush(); // First add one list control full of items. The problem we're // having to work around is that we can't cache and populate the // list control at the same time, as this is a forward-only // cursor. EC_HEC_RS(AddItems(bCancel, GetCountPerPage() + 1, pwndCount), m_pRS); // Now position to the next one to add (assuming the recordset isn't // empty). if (! (m_pRS->BOF && m_pRS->adoEOF)) { EC_HEC_RS(m_pRS->MoveFirst(), m_pRS); for (m_cache.m_dwCurRec = 0; SUCCEEDED(hr) && m_cache.m_dwCurRec < GetItemCount(); ++m_cache.m_dwCurRec) { EC_HEC_RS(m_pRS->MoveNext(), m_pRS); } // Now, do a spin to allow those items to cache and paint, and // then add the rest of the items. This gives the user some // immediate feedback. Invalidate(true); Generic::SpinTheMessageLoop(); EC_HEC_RS(AddItems(bCancel, -1, pwndCount), m_pRS); } DOMYLOGD ("Added <%d> items to the list control.\n", m_cache.m_dwCurRec); } else { // Must not be a SELECT statement. This is not an error. } return hr; } // Add this many items. -1 means add all there are. HRESULT MyAdoListCtrl::AddItems(bool& bCancel, int nItems /* = -1 */ , CWnd* pwndCount /* = NULL */ ) { VALIDATE; ASSERT_NULL_OR_POINTER(pwndCount, CWnd); _ASSERTE(NULL != m_pRS && adStateOpen == m_pRS->GetState()); HRESULT hr(S_OK); // Be sure the cache can get to the list control. (Idempotent.) m_cache.SetOwnerListCtrl(this); // Add items for all the records. This is the line that takes up all // the memory if there are lots of really wide records. DOMYLOGD ("Adding <%d> items to the list control.\n", nItems); if (! (m_pRS->BOF && m_pRS->adoEOF)) { CWaitCursor* pwc = new CWaitCursor; DWORD dwStart(::GetTickCount()); for ( ; ! m_pRS->adoEOF && ! bCancel && (-1 == nItems || m_cache.m_dwCurRec < nItems); ++m_cache.m_dwCurRec) { EC_HEC_RS(m_pRS->MoveNext(), m_pRS); if (FAILED(hr)) break; // Update the control every once in a while, if requested. if (500 < ::GetTickCount() - dwStart) { if (pwndCount) { CString sCount; sCount.Format("%d", -1 == m_cache.m_dwCurRec ? 0 : m_cache.m_dwCurRec); pwndCount->SetWindowText(sCount); dwStart = ::GetTickCount(); } // Manage the hourglass. Spin so that we can get the "Cancel" // button clicks. delete pwc; pwc = NULL; Generic::SpinTheMessageLoop(); pwc = new CWaitCursor; } } // Set the list control items. SetItemCountEx(m_cache.m_dwCurRec, LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL); DOMYLOGD ("Set item count to <%d>.\n", m_cache.m_dwCurRec); // Update once at end. CString sCount; sCount.Format("%d", -1 == m_cache.m_dwCurRec ? 0 : m_cache.m_dwCurRec); pwndCount->SetWindowText(sCount); // Set the column widths of the list control. for (int nCol = 0; nCol < GetHeaderCtrl()->GetItemCount(); ++nCol) { EC_B(SetColumnWidth(nCol, LVSCW_AUTOSIZE_USEHEADER)); } // Done. delete pwc; pwc = NULL; } return hr; }