2013-04-28 167 views
5

我有一個ListBox和兩個ComboBox es的視圖。當我在ListBox中選擇一個項目時,將根據所選項目的屬性值刷新ComboBox es的內容/值。在我的場景中,ListBox包含一個客戶列表,第一個ComboBox包含一個國家列表。所選項目是客戶的原籍國。第二個ComboBox擁有一個城市列表。選定的城市是客戶的來源城市。刷新ListCollectionView將ComboBox中所選項目的值設置爲空

第二ComboBoxItemsSource屬性基於ObservableCollection所有使用濾波器城市綁定到ListViewCollection。當國家ListBox中的選擇發生變化時,我刷新過濾器以僅顯示屬於所選國家/地區的城市。

我們假設客戶A來自新西蘭的奧克蘭,客戶B來自加拿大多倫多。當我選擇A時,一切正常。第二個ComboBox只填充新西蘭城市,並選擇奧克蘭。現在我選擇B,選定的國家現在是加拿大,城市名單隻包含加拿大城市,選擇多倫多。如果現在我回到A,在這些國家選擇新西蘭,城市名單隻包含來自新西蘭的城市,但奧克蘭沒有被選中。

當我調試這種情況下,我注意到,當我選擇B,調用ListCollectionView.Refresh()設置城最初選擇null客戶A的值(把在調用斷點刷新,並在另一個在模型上的城市二傳手,見下面的代碼)。

- 雖然我不是100%肯定 - 它正在發生的事情,因爲我對這個城市ComboBoxSelectedItem當過濾器更新列表的加拿大城市,奧克蘭消失和TwoWay結合這些信息將被髮送回該物業,然後更新至null。從某種意義上說,這是有道理的。

我的問題是:我該如何避免這種情況發生?如何防止ItemsSource僅更新時更新我的​​型號的屬性?

下面是我的代碼(這是一個有點長,雖然我試圖使它的代碼,使問題重現的可能的最小量):

public class Country 
{ 
    public string Name { get; set; } 
    public IEnumerable<City> Cities { get; set; } 
} 

public class City 
{ 
    public string Name { get; set; } 
    public Country Country { get; set; } 
} 

public class ClientModel : NotifyPropertyChanged 
{ 
    #region Fields 
    private string name; 
    private Country country; 
    private City city; 
    #endregion 

    #region Properties 
    public string Name 
    { 
     get 
     { 
      return this.name; 
     } 

     set 
     { 
      this.name = value; 
      this.OnPropertyChange("Name"); 
     } 
    } 

    public Country Country 
    { 
     get 
     { 
      return this.country; 
     } 

     set 
     { 
      this.country = value; 
      this.OnPropertyChange("Country"); 
     } 
    } 

    public City City 
    { 
     get 
     { 
      return this.city; 
     } 

     set 
     { 
      this.city = value; 
      this.OnPropertyChange("City"); 
     } 
    } 
    #endregion 
} 

public class ViewModel : NotifyPropertyChanged 
{ 
    #region Fields 
    private ObservableCollection<ClientModel> models; 
    private ObservableCollection<Country> countries; 
    private ObservableCollection<City> cities; 
    private ListCollectionView citiesView; 

    private ClientModel selectedClient; 
    #endregion 

    #region Constructors 
    public ViewModel(IEnumerable<ClientModel> models, IEnumerable<Country> countries, IEnumerable<City> cities) 
    { 
     this.Models = new ObservableCollection<ClientModel>(models); 
     this.Countries = new ObservableCollection<Country>(countries); 
     this.Cities = new ObservableCollection<City>(cities); 
     this.citiesView = (ListCollectionView)CollectionViewSource.GetDefaultView(this.cities); 
     this.citiesView.Filter = city => ((City)city).Country.Name == (this.SelectedClient != null ? this.SelectedClient.Country.Name : string.Empty); 

     this.CountryChangedCommand = new DelegateCommand(this.OnCountryChanged); 
    } 
    #endregion 

    #region Properties 
    public ObservableCollection<ClientModel> Models 
    { 
     get 
     { 
      return this.models; 
     } 

     set 
     { 
      this.models = value; 
      this.OnPropertyChange("Models"); 
     } 
    } 

    public ObservableCollection<Country> Countries 
    { 
     get 
     { 
      return this.countries; 
     } 

     set 
     { 
      this.countries = value; 
      this.OnPropertyChange("Countries"); 
     } 
    } 

    public ObservableCollection<City> Cities 
    { 
     get 
     { 
      return this.cities; 
     } 

     set 
     { 
      this.cities = value; 
      this.OnPropertyChange("Cities"); 
     } 
    } 

    public ListCollectionView CitiesView 
    { 
     get 
     { 
      return this.citiesView; 
     } 
    } 

    public ClientModel SelectedClient 
    { 
     get 
     { 
      return this.selectedClient; 
     } 

     set 
     { 
      this.selectedClient = value; 
      this.OnPropertyChange("SelectedClient"); 
     } 
    } 

    public ICommand CountryChangedCommand { get; private set; } 

    #endregion 

    #region Methods 
    private void OnCountryChanged(object obj) 
    { 
     this.CitiesView.Refresh(); 
    } 
    #endregion 
} 

現在,這裏的XAML:

<Grid Grid.Column="0" DataContext="{Binding SelectedClient}"> 
     <Grid.ColumnDefinitions> 
      <ColumnDefinition/> 
      <ColumnDefinition/> 
     </Grid.ColumnDefinitions> 
     <Grid.RowDefinitions> 
      <RowDefinition Height="25"/> 
      <RowDefinition Height="25"/> 
     </Grid.RowDefinitions> 

     <TextBlock Grid.Column="0" Grid.Row="0" Text="Country"/> 
     <local:ComboBox Grid.Column="1" Grid.Row="0" SelectedItem="{Binding Country}" 
         Command="{Binding DataContext.CountryChangedCommand, RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}" 
         ItemsSource="{Binding DataContext.Countries, RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"> 
      <local:ComboBox.ItemTemplate> 
       <DataTemplate> 
        <TextBlock Text="{Binding Name}"/> 
       </DataTemplate> 
      </local:ComboBox.ItemTemplate> 
     </local:ComboBox> 

     <TextBlock Grid.Column="0" Grid.Row="1" Text="City"/> 
     <ComboBox Grid.Column="1" Grid.Row="1" SelectedItem="{Binding City}" 
        ItemsSource="{Binding DataContext.CitiesView, RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"> 
      <ComboBox.ItemTemplate> 
       <DataTemplate> 
        <TextBlock Text="{Binding Name}"/> 
       </DataTemplate> 
      </ComboBox.ItemTemplate> 
     </ComboBox> 
    </Grid> 

    <ListBox Grid.Column="1" ItemsSource="{Binding Models}" SelectedItem="{Binding SelectedClient}"> 
     <ListBox.ItemTemplate> 
      <DataTemplate> 
       <TextBlock Text="{Binding Name}"/> 
      </DataTemplate> 
     </ListBox.ItemTemplate> 
    </ListBox> 
</Grid> 

如果有任何幫助,這裏還有我自定義的ComboBox的代碼來處理國家選擇變化的通知。

public class ComboBox : System.Windows.Controls.ComboBox, ICommandSource 
{ 
    #region Fields 
    public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
     "Command", 
     typeof(ICommand), 
     typeof(ComboBox)); 

    public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(
     "CommandParameter", 
     typeof(object), 
     typeof(ComboBox)); 

    public static readonly DependencyProperty CommandTargetProperty = DependencyProperty.Register(
     "CommandTarget", 
     typeof(IInputElement), 
     typeof(ComboBox)); 
    #endregion 

    #region Properties 
    public ICommand Command 
    { 
     get { return (ICommand)this.GetValue(CommandProperty); } 
     set { this.SetValue(CommandProperty, value); } 
    } 

    public object CommandParameter 
    { 
     get { return this.GetValue(CommandParameterProperty); } 
     set { this.SetValue(CommandParameterProperty, value); } 
    } 

    public IInputElement CommandTarget 
    { 
     get { return (IInputElement)this.GetValue(CommandTargetProperty); } 
     set { this.SetValue(CommandTargetProperty, value); } 
    } 
    #endregion 

    #region Methods 

    protected override void OnSelectionChanged(System.Windows.Controls.SelectionChangedEventArgs e) 
    { 
     base.OnSelectionChanged(e); 

     var command = this.Command; 
     var parameter = this.CommandParameter; 
     var target = this.CommandTarget; 

     var routedCommand = command as RoutedCommand; 
     if (routedCommand != null && routedCommand.CanExecute(parameter, target)) 
     { 
      routedCommand.Execute(parameter, target); 
     } 
     else if (command != null && command.CanExecute(parameter)) 
     { 
      command.Execute(parameter); 
     } 
    } 
    #endregion 
} 

對於這個簡單的例子,我創建並填充視圖模型在我Window的構造,在這裏:

public MainWindow() 
{ 
    InitializeComponent(); 

    Country canada = new Country() { Name = "Canada" }; 
    Country germany = new Country() { Name = "Germany" }; 
    Country vietnam = new Country() { Name = "Vietnam" }; 
    Country newZealand = new Country() { Name = "New Zealand" }; 

    List<City> canadianCities = new List<City> 
    { 
     new City { Country = canada, Name = "Montréal" }, 
     new City { Country = canada, Name = "Toronto" }, 
     new City { Country = canada, Name = "Vancouver" } 
    }; 
    canada.Cities = canadianCities; 

    List<City> germanCities = new List<City> 
    { 
     new City { Country = germany, Name = "Frankfurt" }, 
     new City { Country = germany, Name = "Hamburg" }, 
     new City { Country = germany, Name = "Düsseldorf" } 
    }; 
    germany.Cities = germanCities; 

    List<City> vietnameseCities = new List<City> 
    { 
     new City { Country = vietnam, Name = "Ho Chi Minh City" }, 
     new City { Country = vietnam, Name = "Da Nang" }, 
     new City { Country = vietnam, Name = "Hue" } 
    }; 
    vietnam.Cities = vietnameseCities; 

    List<City> newZealandCities = new List<City> 
    { 
     new City { Country = newZealand, Name = "Auckland" }, 
     new City { Country = newZealand, Name = "Christchurch" }, 
     new City { Country = newZealand, Name = "Invercargill" } 
    }; 
    newZealand.Cities = newZealandCities; 

    ObservableCollection<ClientModel> models = new ObservableCollection<ClientModel> 
    { 
     new ClientModel { Name = "Bob", Country = newZealand, City = newZealandCities[0] }, 
     new ClientModel { Name = "John", Country = canada, City = canadianCities[1] } 
    }; 

    List<Country> countries = new List<Country> 
    { 
     canada, newZealand, vietnam, germany 
    }; 

    List<City> cities = new List<City>(); 
    cities.AddRange(canadianCities); 
    cities.AddRange(germanCities); 
    cities.AddRange(vietnameseCities); 
    cities.AddRange(newZealandCities); 

    ViewModel vm = new ViewModel(models, countries, cities); 

    this.DataContext = vm; 
} 

應該可以重現該問題通過簡單的複製/粘貼所有的上面的代碼。我正在使用.NET 4.0。

最後,我讀this article(和其他一些),並試圖修改/應用給出的建議,我的情況,但沒有任何成功。我想我做錯了事:

我也讀this question但如果我的ListBox增長很大,我可能最終不得不跟蹤數百個項目,如果可能的話我不想做。

回答

2

您有一點冗餘模型。你有國家名單,每個國家都有城市名單。然後你編寫城市的總體列表,當你選擇改變時你會更新。如果你將改變城市組合框的數據來源,你會得到期望的行爲:

<ComboBox Grid.Column="1" Grid.Row="1" SelectedItem="{Binding City}" 
       ItemsSource="{Binding Country.Cities}"> 
     <ComboBox.ItemTemplate> 
      <DataTemplate> 
       <TextBlock Text="{Binding Name}"/> 
      </DataTemplate> 
     </ComboBox.ItemTemplate> 
    </ComboBox> 

您有關於爲什麼城市設定爲一個猜對了。

但是,如果你想保持你的模型,如上所述 - 你應該改變方法調用的順序。要做到這一點,你應該使用Application.Current.Dispatcher屬性(你不需要改變組合框上面提到):

private void OnCountryChanged() 
{ 
    var uiDispatcher = System.Windows.Application.Current.Dispatcher; 
    uiDispatcher.BeginInvoke(new Action(this.CitiesView.Refresh)); 
} 
+0

我想這兩種解決方案和他們的工作。我會選擇第一個,它會減少代碼,更易於理解和維護。但是,我真的很想知道如何使用UI調度程序來解決問題?我沒有調試代碼,看看發生了什麼,但據我所知,一切都已經在UI線程上運行,我無法弄清楚爲什麼使用UI調度程序可以以某種方式提供幫助... – Guillaume 2013-04-28 15:08:45

+0

方法BeginInvoke()的UI調度程序將在UI線程上調度方法調用,因此當UI線程可以自由執行某些操作時,它將執行您指定的操作。因此,這裏將首先選擇SelectedClient,並且在更改SelectedClient之後,將應用CitiesView的過濾器。 – stukselbax 2013-04-28 17:51:30

+0

好吧,明白了。謝謝你的幫助! – Guillaume 2013-04-29 01:24:56