Gui*_*ume 5 .net c# wpf mvvm listcollectionview
我有一个ListBox和两个ComboBoxes 的视图.当我在中选择一个项目时ListBox,ComboBoxes 的内容/值将根据所选项目的属性值进行刷新.在我的场景中,它ListBox拥有一个客户列表,第一个ComboBox拥有一个国家列表.所选项目是客户的原产国.第二个ComboBox包含城市列表.选定的城市是客户的起源城市.
在ItemsSource第二个的属性ComboBox绑定到一个ListViewCollection基于一ObservableCollection的所有使用过滤器的城市.当国家/地区的选择ListBox发生变化时,我刷新过滤器以仅显示属于所选国家/地区的城市.
我们假设客户A来自新西兰奥克兰,客户B来自加拿大多伦多.当我选择A时,一切正常.第二个ComboBox城市只有新西兰城市,奥克兰被选中.现在我选择B,所选国家现在是加拿大,城市列表只包含加拿大城市,多伦多被选中.如果现在我回到A,新西兰在这些国家/地区被选中,则城市列表仅包含来自新西兰的城市,但未选择奥克兰.
当我调试这个场景时,我注意到当我选择B时,调用ListCollectionView.Refresh()设置最初选择的客户端A上的城市值null(在刷新调用时设置断点,在模型上的城市设置器上设置另一个断点)见下面的代码).
我猜 - 虽然我不是百分百肯定 - 它发生了,因为我对城市有TwoWay约束力,当过滤器将列表更新到加拿大城市时,奥克兰消失,这些信息被送回物业.然后更新为.在某种程度上,这是有道理的.SelectedItemComboBoxnull
我的问题是:我怎样才能避免这种情况发生?如果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
}
Run Code Online (Sandbox Code Playgroud)
现在这是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>
Run Code Online (Sandbox Code Playgroud)
如果有任何帮助,这里也是我的自定义代码,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
}
Run Code Online (Sandbox Code Playgroud)
对于这个简化的例子,我在我的构造函数中创建并填充视图模型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;
}
Run Code Online (Sandbox Code Playgroud)
应该可以通过简单地复制/粘贴所有上述代码来重现该问题.我正在使用.NET 4.0.
最后,我阅读了这篇文章(以及其他一些文章),并试图将给定的建议适用于我的案例但没有任何成功.我想我做错了:
我也读过这个问题,但如果我的ListBox成长很大,我可能最终必须明确地跟踪数百个项目,如果可能的话我不想做.
你的模型有点多余。您有国家/地区列表,每个国家/地区都有城市列表。然后,您编写城市的总体列表,并在选择更改时更新该列表。如果您要更改 Cities ComboBox的数据源,您将获得所需的行为:
<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>
Run Code Online (Sandbox Code Playgroud)
您对为什么城市设置为null 的猜测是正确的。
但是,如果您想保持上面描述的模型 - 您应该更改方法调用的顺序。为此,您应该使用Application.Current.Dispatcher属性(并且不需要更改上面提到的ComboBox ):
private void OnCountryChanged()
{
var uiDispatcher = System.Windows.Application.Current.Dispatcher;
uiDispatcher.BeginInvoke(new Action(this.CitiesView.Refresh));
}
Run Code Online (Sandbox Code Playgroud)