2013-04-28 164 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