WPF中數據綁定的一個小疑問?

先上代碼

&
&

// 後台代碼中我通過下面函數進行DataGrid的數據修改
// _searchPairs是一個私有Dictionary&變數
private void ShowDataContent(string keyFilepath, string dir)
{
_searchPairs = new Dictionary&(); //initial
DataContent.ItemsSource = _searchPairs; //initial

// 查詢數據,對_searchPairs進行插入操作。
}
// 該函數由一個按鍵的click事件響應函數調用
// 運行後可以正常顯示。

然而當我嘗試著把 函數中前兩句初始化的移出來,無論是放在MainWindow的構造函數中,還是放在Loaded事件響應函數中。都會導致:

需要用滾輪在DataGrid上向下滾一下才會顯示,但還是可以顯示正確

這兩種方式的差別到底在哪呢?會造成這樣的不同?


1. 如其他答案所述,應該Data Binding到一個ObservableCollection. 題主應該學習一下MVVM怎麼做。

2. 在Visual Studio 2015里試了一下。MainWindow里初始化Dictionary,加一些元素進去, 設置DataGrid的ItemsSource為該Dictionary. 在Button的event handler里再添加一些元素。然後滾動DataGrid.

結果: 會拋InvalidOperationException. An ItemsControl is inconsistent with its items source.

老版本的Visual Studio估計沒有檢查這種情況。看來WPF還是有持續不斷的改進的。

從Stack trace里可以看到,滾動的時候,因為DataGrid默認開啟了Virtualizing, 或重新從ItemsSource, 也就是Dictionary里獲得最新的數據。這時會做一個Verify, 主要是檢查集合的數目是不是一樣,當然裡面的value變沒變它不會做檢查。

至於為啥要做這個Verify, 我的猜測是你既然開啟了Virtualizing,WPF一邊在為你計算下次該載入多少元素,你卻給它把前面的元素的偷偷刪除了,WPF會想你TMD這不是逗我玩嗎。

調用堆棧:

at System.Windows.Controls.ItemContainerGenerator.Verify()

at System.Windows.Controls.VirtualizingStackPanel.MeasureChild(IItemContainerGenerator generator, IContainItemStorage itemStorageProvider, IContainItemStorage parentItemStorageProvider, Object parentItem, Boolean hasUniformOrAverageContainerSizeBeenSet, Double computedUniformOrAverageContainerSize, Boolean computedAreContainersUniformlySized, IList items, Object item, IList children, Int32 childIndex, Boolean visualOrderChanged, Boolean isHorizontal, Size childConstraint, Rect viewport, VirtualizationCacheLength cacheSize, VirtualizationCacheLengthUnit cacheUnit, Boolean foundFirstItemInViewport, Double firstItemInViewportOffset, Size stackPixelSize, Size stackPixelSizeInViewport, Size stackPixelSizeInCacheBeforeViewport, Size stackPixelSizeInCacheAfterViewport, Size stackLogicalSize, Size stackLogicalSizeInViewport, Size stackLogicalSizeInCacheBeforeViewport, Size stackLogicalSizeInCacheAfterViewport, Boolean mustDisableVirtualization, Boolean isBeforeFirstItem, Boolean isAfterFirstItem, Boolean isAfterLastItem, Boolean skipActualMeasure, Boolean skipGeneration, Boolean hasBringIntoViewContainerBeenMeasured, Boolean hasVirtualizingChildren)

at System.Windows.Controls.VirtualizingStackPanel.MeasureOverrideImpl(Size constraint, Nullable`1 lastPageSafeOffset, List`1 previouslyMeasuredOffsets, Nullable`1 lastPagePixelSize, Boolean remeasure)

at System.Windows.Controls.VirtualizingStackPanel.MeasureOverride(Size constraint)

at System.Windows.Controls.Primitives.DataGridRowsPresenter.MeasureOverride(Size constraint)

at System.Windows.FrameworkElement.MeasureCore(Size availableSize)

at System.Windows.UIElement.Measure(Size availableSize)

at System.Windows.ContextLayoutManager.UpdateLayout()

at System.Windows.UIElement.UpdateLayout()

at System.Windows.Controls.VirtualizingStackPanel.&<&>c__DisplayClass5.&b__7()

at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)

at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)

at System.Windows.Threading.DispatcherOperation.InvokeImpl()

at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)

at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)

at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)

at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)

at System.Windows.Threading.DispatcherOperation.Invoke()

at System.Windows.Threading.Dispatcher.ProcessQueue()

at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean handled)

at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean handled)

at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)

at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)

at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)

at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)

at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)

at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG msg)

at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)

at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)

at System.Windows.Application.RunDispatcher(Object ignore)

at System.Windows.Application.RunInternal(Window window)

at System.Windows.Application.Run(Window window)

at System.Windows.Application.Run()

at TestWpfDataGrid.App.Main() in c:usersjianxindocumentsvisual studio 2015ProjectsTestWpfDataGridTestWpfDataGridobjDebugApp.g.cs:line 0

at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)

at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)

at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()

at System.Threading.ThreadHelper.ThreadStart_Context(Object state)

at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)

at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)

at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)

at System.Threading.ThreadHelper.ThreadStart()

異常信息:

Information for developers (use Text Visualizer to read this):

This exception was thrown because the generator for control System.Windows.Controls.DataGrid Items.Count:105 with name DataGrid1 has received sequence of CollectionChanged events that do not agree with the current state of the Items collection. The following differences were detected:

Accumulated count 100 is different from actual count 105. [Accumulated count is (Count at last Reset + #Adds - #Removes since last Reset).]

One or more of the following sources may have raised the wrong events:

System.Windows.Controls.ItemContainerGenerator

System.Windows.Controls.ItemCollection

MS.Internal.Data.EnumerableCollectionView


設置ItemsSource在先,添加字典元素在後,元祖Dictionary類沒有內置狀態變化事件,因此無法觸發綁定。改用ObservableCollection即可解決問題。

不過ItemSource設置的時候會自動取ItemsSource的內容刷新一次綁定(無論這個集合是否observable),這個過程是非同步的,所以在某些情況下可能會GUI刷新內容的時候Dictionary裡面已經有數據了(也可能只有一部分數據),這樣DataGrid裡面就會有內容,但這種情況下實際上的結果是不可預測的。


正確的做法是:

&

private void ShowDataContent(string keyFilepath, string dir)
{
_searchPairs = new Dictionary&(); //initial
DataContent.DataContext = _searchPairs; //initial
}

DataContext是每個FrameworkElement設置數據源的地方,它可以把VisualTree上的子節點繼承。

剩下所有的Dependency Property都可以直接用Biniding去綁DataContext上數據的Property或者對於collection, 用ItemSource直接綁item in collection


如果你一定要這麼做的話,可以調用UpdateLayout()手動重繪UI。


你確定會顯示嗎?我怎麼記得ItemsSource需要實現了INotifyCollectionChanged介面才能顯示更新後的數據?


wpf界面自動刷新貌似一般要麼數據類實現了INotifyPropertyChanged

要麼直接用ObservableCollection(其實它自己實現了INotifyPropertyChanged,但注意當items改變也是不刷新的額,增減是自動刷新界面的...)


印象中Dictionary是不能自動通知顯示新插入數據的。所以插入數據後DataGrid看不到變化。

DataGrid顯示數據不是一次全部載入內存,會根據顯示區域大小和數據量只載入需要顯示的那部分。雖然插入數據沒有引發DataGrid更新,但滾動滑鼠滾輪時控制項從數據源計算要顯示的數據,這樣就找到了新插入的數據。

建議的做法是使用ObservableCollection當數據源,初始化時指定給DataGrid。

強烈建議的是用MVVM模式,在xaml中進行綁定。


推薦閱讀:

如何學python-第十五課 linux下的python腳本編程
【乾貨】找不到適合自己的編程書?我自己動手寫了一個熱門編程書搜索網站
為何高版本的windows有時不兼容某些低版本程序?
interface引發的事件真相
寫c++好久不用new了,這是好的習慣嗎?

TAG:MicrosoftWindows | 編程 | C# | WindowsPresentationFoundationWPF | XAML |