有人可能會說這有什麼好寫的。不就是一行代碼就能搞定的嗎?而且為什麼需要用代碼設置SelectedItem呢?用戶所點的Item不就自動是SelectedItem嗎?在這裡將要討論我們的,就是ListBox自己沒有能自己把SelectedItem設置正確的情況。本來想當作一個WPF Bug清單的一篇文章的,但是又感覺也許就是有這樣變態的需求呢。
我們用一個非常簡單的代碼的XAML就可以重現這個問題。
Demo Window
1<Window x:Class="SelectListBoxItem.DemoWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:s="clr-namespace:System;assembly=mscorlib" 5 Title="ListBox Selection Problem" 6 SizeToContent="Height" 7 Width="300"> 8 <ListBox> 9 <ListBox.ItemTemplate> 10 <DataTemplate> 11 <TextBox Text="{Binding .}" Width="100"/> 12 </DataTemplate> 13 </ListBox.ItemTemplate> 14 <s:String>a</s:String> 15 <s:String>b</s:String> 16 <s:String>c</s:String> 17 </ListBox> 18</Window>
運行的效果如下。
圖1. TextBox得到焦點
其實這就是個問題,一個ListBoxItem已經被MouseDown了,可是沒有被選中。MouseDown已經被TextBox吃了。結果有可能出現下面的狀況。
圖2. 焦點與選中項不一致
這個問題在WPF裡的其它控件也有,在智者千慮的【WPF】如何讓TreeView實現右鍵選中的功能裡就描述了TreeView上的相似問題。感覺很惡心。
一開始使用的是PreviewMouseDown解決,在MouseDown的時候,通過DataContext也好,通過FindAncestor也好,反正是在獲得焦點的同時選擇上了。
但是隨著項目的進行,這種方法造成DataBinding的Validation出現了問題。Validation應該是在LostFocus是對DataContext進行驗證;但是使用PreviewMouseDown更改選中項,這個LostFocus就是在別的項被選中之後發現,結果就是用一個無關的數據在新的DataContext上進行驗證。
這個問題又普遍存在於項目各個DataBinding中,分別修改肯定是不行的。只能是不用PreviewMouseDown。用GotFocus,用它的Item的GotFocus來設置選中項。
為了在現有系統中方便應用,使用了AttachedProperty來實現這個功能。代碼如下:
ListBoxService
1using System.Diagnostics; 2using System.Windows; 3using System.Windows.Controls; 4 5namespace SelectListBoxItem 6{ 7 /**//// <summary> 8 /// 9 /// </summary> 10 public class ListBoxService 11 { 12 AutoSelect Property#region AutoSelect Property 13 14 public static readonly DependencyProperty AutoSelectProperty = DependencyProperty.RegisterAttached("AutoSelect", typeof(bool), typeof(ListBoxService), new PropertyMetadata(OnAutoSelectPropertyChanged)); 15 16 public static bool GetAutoSelect(DependencyObject element) 17 { 18 if (element == null) 19 return false; 20 21 return (bool)element.GetValue(AutoSelectProperty); 22 } 23 24 public static void SetAutoSelect(DependencyObject element, bool value) 25 { 26 if (element == null) 27 return; 28 29 element.SetValue(AutoSelectProperty, value); 30 } 31 32 #endregion 33 34 private static void OnAutoSelectPropertyChanged(DependencyObject element, DependencyPropertyChangedEventArgs e) 35 { 36 if (!(element is UIElement)) 37 return; 38 39 if ((bool)e.NewValue) 40 (element as UIElement).GotFocus += new RoutedEventHandler(OnElementGotFocus); 41 else 42 (element as UIElement).GotFocus -= new RoutedEventHandler(OnElementGotFocus); 43 } 44 45 private static void OnElementGotFocus(object sender, RoutedEventArgs e) 46 { 47 Debug.Assert(e.OriginalSource is DependencyObject); 48 49 ListBoxItem item = (e.OriginalSource as DependencyObject).FindAncestor<ListBoxItem>(); 50 if (item != null) 51 item.IsSelected = true; 52 else 53 Debug.WriteLine(string.Format("Cannot find ListBoxItem from {0}", sender)); 54 } 55 } 56} 57
其中FindAncestor是自己定義的一個方法,因為單純地使用VisualTreeHelper是不足以在所有情況下找到Parent的。代碼可參見源代碼。
寫好了怎麼用呢?我們說了,要以對現有代碼最小的變動實現這個功能。可能有人已經想到了,用Style,那個Window的代碼根本不用動。只要在App.xaml裡加上一個Resource就OK了。代碼如下,簡單吧。
App Resource
1<Application x:Class="SelectListBoxItem.App" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:l="clr-namespace:SelectListBoxItem" 5 StartupUri="DemoWindow.xaml"> 6 <Application.Resources> 7 <Style TargetType="{x:Type ListBox}"> 8 <Setter Property="l:ListBoxService.AutoSelect" Value="True"/> 9 </Style> 10 </Application.Resources> 11</Application> 12
到此,ListBox的行為算是正常些了。正常的運行截圖就不發了。