5

aListBoxと 2 つのComboBoxes のビューがあります。でアイテムを選択すると、選択したアイテムのプロパティの値に基づいて esListBoxのコンテンツ/値が更新されます。ComboBox私のシナリオでは、ListBoxはクライアントのリストをComboBox保持し、最初は国のリストを保持します。選択された項目は、クライアントの原産国です。2 つ目ComboBoxは、都市のリストを保持します。選択された都市は、クライアントの出身都市です。

2 番目のItemsSourceプロパティは、フィルターを使用してすべての都市の に基づいてComboBoxバインドされます。国の選択が変わると、フィルターを更新して、選択した国に属する都市のみを表示します。ListViewCollectionObservableCollectionListBox

クライアント A がニュージーランドのオークランド出身で、クライアント B がカナダのトロント出身であるとします。Aを選択すると、すべて正常に動作します。2 つ目ComboBoxはニュージーランドの都市のみが入力されており、オークランドが選択されています。ここで B を選択すると、選択した国はカナダになり、都市のリストにはカナダの都市のみが含まれ、トロントが選択されました。ここで A に戻ると、国でニュージーランドが選択され、都市のリストにはニュージーランドの都市のみが含まれますが、オークランドは選択されていません。

このシナリオをデバッグすると、B を選択すると、ListCollectionView.Refresh()最初に選択されたクライアント A の都市の値が への呼び出しによって設定されることnullがわかります (Refresh への呼び出しにブレークポイントを配置し、モデルの都市セッターに別のブレークポイントを配置し、以下のコードを参照してください)。

100% 確実ではありませんが、都市に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
}

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
}

この単純化された例では、 my のコンストラクターでビュー モデルを作成して設定し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 を使用しています。

最後に、この記事(およびその他の記事) を読み、指定された推奨事項を自分のケースに適応/適用しようとしましたが、成功しませんでした。私は間違ったことをしていると思います:

私もこの質問を読みましたが、ListBox大きくなると、可能であればやりたくない何百ものアイテムを明示的に追跡する必要が生じる可能性があります。

4

1 に答える 1

2

少し冗長なモデルがあります。国のリストがあり、すべての国に都市のリストがあります。次に、都市の全体的なリストを作成し、選択が変更されたときに更新します。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>

都市がnullに設定されている理由について、あなたは正しい推測をしています。

ただし、上記のようにモデルを維持したい場合は、メソッド呼び出しの順序を変更する必要があります。これを行うには、Application.Current.Dispatcherプロパティを使用する必要があります (上記のComboBoxを変更する必要はありません)。

private void OnCountryChanged()
{
    var uiDispatcher = System.Windows.Application.Current.Dispatcher;
    uiDispatcher.BeginInvoke(new Action(this.CitiesView.Refresh));
}
于 2013-04-28T12:50:51.423 に答える