2013-04-28 7 views
5

Ho una vista con uno ListBox e due ComboBox es. Quando seleziono un articolo nello ListBox, il contenuto/valore di ComboBox viene aggiornato in base al valore delle proprietà dell'elemento selezionato. Nel mio scenario, lo ListBox contiene un elenco di client, il primo ComboBox contiene un elenco di paesi. L'articolo selezionato è il paese di origine del cliente. Il secondo ComboBox contiene un elenco di città. La città selezionata è la città di origine del cliente.Refreshing ListCollectionView imposta il valore dell'elemento selezionato in ComboBox su null

La ItemsSource di proprietà del secondo ComboBox è destinata a un ListViewCollection sulla base di una ObservableCollection di tutte le città utilizzando un filtro. Quando la selezione nel paese ListBox cambia, aggiorno il filtro per visualizzare solo le città che appartengono al paese selezionato.

Supponiamo che il cliente A sia di Auckland, Nuova Zelanda e il cliente B di Toronto, Canada. Quando seleziono A, tutto funziona correttamente. Il secondo ComboBox viene popolato solo dalle città della Nuova Zelanda e Auckland è selezionato. Ora seleziono B e il paese selezionato è ora Canada e l'elenco delle città contiene solo città canadesi, viene selezionato Toronto. Se ora torno ad A, la Nuova Zelanda è selezionata nei paesi, la lista delle città contiene solo città della Nuova Zelanda ma Auckland non è selezionata.

Quando il debug questo scenario, mi accorgo che quando seleziono B, la chiamata al ListCollectionView.Refresh() imposta il valore della città sul client Una inizialmente selezionato per null (mettere un punto di interruzione alla chiamata per rinfrescare e un altro sulla city ​​setter sul modello, vedi il codice sotto).

mi immagino - anche se non sono sicuro al 100% - che sta accadendo perché ho un legame TwoWay sul SelectedItem della città ComboBox e quando il filtro aggiorna l'elenco delle città canadesi, Auckland e scompare questa informazione viene rinviata alla proprietà che viene quindi aggiornata a null. Il che, in un certo senso, ha senso.

La mia domanda è: come posso evitare che ciò accada? Come posso evitare che la proprietà sul mio modello venga aggiornata quando lo ItemsSource è solo aggiornato?

Qui di seguito è il mio codice (è un po 'lungo, anche se ho cercato di rendere la più piccola quantità possibile di codice che rende il problema è riproducibile):

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 
} 

Ora ecco il 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> 

Se è di aiuto, ecco anche il codice della mia ordinazione ComboBox per gestire la notifica delle modifiche nella selezione del paese.

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 
} 

Per questo esempio semplificato, ho creare e popolare il modello vista nel costruttore della mia Window, qui:

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; 
} 

Dovrebbe essere possibile riprodurre il problema semplicemente copiare/incollare tutti il codice sopra. Sto usando .NET 4.0.

Infine, ho letto this article (e alcuni altri) e ho cercato di adattare/applicare le raccomandazioni fornite al mio caso ma senza alcun successo. Suppongo che sto facendo le cose in modo errato:

Ho letto anche this question ma se il mio ListBox diventa grande, potrei finire per dover tenere traccia di centinaia di elementi esplicitamente che non voglio fare se possibile.

risposta

2

Si dispone di un modello ridondante. Hai una lista di paesi, e ogni nazione ha una lista di città. E poi componi l'elenco generale delle città, che aggiorni quando la selezione è cambiata. comportamento se si cambia la sorgente di dati delle città ComboBox, otterrete desiderata:

<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> 

Hai un indovinare a destra sul perché la città è impostato su nulla.

Ma se si desidera mantenere il modello come descritto sopra, è necessario modificare l'ordine dei metodi di chiamata. Per fare questo, è necessario utilizzare Application.Current.Dispatcher proprietà (e non è necessario modificare ComboBox di cui sopra):

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

ho provato entrambe le soluzioni e funzionano. Vado per il primo, sarà meno codice e più facile da capire e da mantenere. Tuttavia, mi piacerebbe davvero sapere come mai utilizzare il dispatcher dell'interfaccia utente aiuta a risolvere il problema ?? Non ho eseguito il debug del codice per vedere cosa sta succedendo, ma dal momento che ho capito che tutto era già in esecuzione sul thread dell'interfaccia utente, non riesco a capire perché usare il dispatcher dell'interfaccia utente possa aiutare in qualche modo ... – Guillaume

+0

Method BeginInvoke() del dispatcher dell'interfaccia utente pianificherà il richiamo del metodo sul thread dell'interfaccia utente, quindi quando il thread dell'interfaccia utente sarà libero di eseguire un'azione, eseguirà l'azione specificata. Quindi qui SelectedClient verrà selezionato per primo, e dopo che il SelectedClient sarà cambiato, verrà applicato il filtro per CitiesView. – stukselbax

+0

Ok, capito. Grazie per l'aiuto! – Guillaume