【解決方法】MVVM に続いてカスケード コンボ ボックスを適切に実装するにはどうすればよいですか?

プログラミングQA


WPF c# アプリで mvvm に続くカスケード コンボ ボックスを実装する方法に関して、インターネット上にかなりの数の投稿があることは知っていますが、私の場合はまだ機能しませんでした。

誰でもこれで私を助けることができますか?

いくつかのプロパティを持つモデルがあります。 コンボ ボックスの 1 つで、Name というプロパティの一意の値を itemsource として設定したいと考えています。そのコンボ ボックスから項目を選択すると、他のコンボ ボックスはその選択を使用して、一致する GSTIN というプロパティの一意の値を見つける必要があります。 現在、これらのプロパティは両方とも同じクラスの一部です。

私が試したこと:

C#
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Data.SQLite;
using System.Runtime.CompilerServices;
using System.Linq;
using System.Windows;
using System.Windows.Input;

namespace Purchase
{
	public partial class Window1 : Window
	{
		public Window1()
		{
			InitializeComponent();
			this.DataContext=new BVM();
		}
	}
	
	#region Icommand
	public class RelayCommand : ICommand
	{
		private readonly Action<object> _execute;
		private readonly Predicate<object> _canExecute;

		public RelayCommand(Action<object> execute)
			: this(execute, null)
		{
		}

		public RelayCommand(Action<object> execute, Predicate<object> canExecute)
		{
			if (execute == null)
				throw new ArgumentNullException("execute");
			_execute = execute;
			_canExecute = canExecute;
		}

		public bool CanExecute(object parameter)
		{
			return _canExecute == null ? true : _canExecute(parameter);
		}

		public event EventHandler CanExecuteChanged
		{
			add { CommandManager.RequerySuggested += value; }
			remove { CommandManager.RequerySuggested -= value; }
		}

		public void Execute(object parameter)
		{
			_execute(parameter);
		}

	}
	#endregion
	
	#region Base viewmodel
	public class ViewModelBase : INotifyPropertyChanged
	{
		public event PropertyChangedEventHandler PropertyChanged;

		protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
		{
			var handler = PropertyChanged;
			if (handler != null)
				handler(this, new PropertyChangedEventArgs(propertyName));
		}
		protected virtual void SetValue<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
		{
			field = value;
			OnPropertyChanged(propertyName);
		}
		
	}
	#endregion
	
	#region Model
	public class Bills : ViewModelBase
	{
		
		private int _id;
		public int ID
		{
			get
			{
				return _id;
			}
			set
			{
				SetValue(ref _id, value);
			}
		}

		private string _Name;
		public string Name
		{
			get
			{
				return _Name;
			}
			set
			{
				SetValue(ref _Name, value);
			}
		}

		...
		
		private string _GSTIN;
		public string GSTIN
		{
			get
			{
				return _GSTIN;
			}
			set
			{
				SetValue(ref _GSTIN, value);
			}
		}

		...

	}
	#endregion
	
	#region Main viewmodel
	public class BVM : ViewModelBase
	{
		private ObservableCollection<Bills> _allBills;
		public ObservableCollection<Bills> AllBillss
		{
			get
			{
				if (this._allBills == null) this._allBills = GetAllBillsFromDB();
				return _allBills;
			}

		}
		
		//1st combo box source
		private ObservableCollection<string> _comboItems;
		public ObservableCollection<string> ComboItems
		{
			get
			{
				if (_comboItems == null) this._comboItems = new ObservableCollection<string>(AllBillss.Select(b => b.Name).Distinct().OrderBy(b => b).ToList());
				return this._comboItems;
			}
			set
			{
				_comboItems = value;
				OnPropertyChanged("ComboItems");
			}
		}
		
		//2nd combo box source
		private ObservableCollection<string> _comboItems2;
		public ObservableCollection<string> ComboItems2
		{
			get
			{
				if (_comboItems2 == null) this._comboItems2 = new ObservableCollection<string>(AllBillss.Where(t=>t.Name != null && t.Name == _comboItems.ToString()).Select(s=>s.GSTIN).Distinct().ToList());
				return this._comboItems2;
			}

			set
			{
				_comboItems2 = value;
				OnPropertyChanged("ComboItems2");
			}
		}
		
		//1st combo box selected item
		private string _SelectedCBItem;
		public string SelectedCBItem
		{
			get { return _SelectedCBItem; }
			set
			{
				_SelectedCBItem = value;
				OnPropertyChanged("SelectedCBItem");
				this._comboItems2 = new ObservableCollection<string>(AllBillss.Where(t=>t.Name != null && t.Name == _comboItems.ToString()).Select(s=>s.GSTIN).Distinct().ToList());
				OnPropertyChanged("ComboItems2");
			}
		}
		
		//2nd combo box selected item
		private string _SelectedCBItem2;
		public string SelectedCBItem2
		{
			get { return _SelectedCBItem2; }
			set
			{
				SetValue(ref _SelectedCBItem2, value);
			}
		}
		
		public BVM()
		{
			
		}
		
	}
	...
}

XAML

XML
<Label Margin="5" Grid.Column="0" Content="Vendor :" Height="25" Width="60" />
<ComboBox Margin="5" Grid.Column="1" Height="25" Width="350" ItemsSource="{Binding ComboItems}" SelectedItem="SelectedCBItem" />
<Label Margin="5" Grid.Column="2" Content="GSTIN :" Height="25" Width="60" />
<ComboBox Margin="5" Grid.Column="3" Height="25" Width="220" ItemsSource="{Binding ComboItems2}" SelectedItem="SelectedCBItem2" />

解決策 1

を使用する必要があります CollectionViewSource[^] サブコレクションをフィルタリングします。 Microsoft には次の例があります。 方法: ビューでデータをフィルター処理する – WPF .NET Framework | マイクロソフト ラーン[^]

以下は、MVVM パターンを使用して実装する方法を示すためにまとめられた実際の例です。

1. データソース提供: 世界の主要都市 – データセット – DataHub – 摩擦のないデータ[^]

2. 分離コード:

C#
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace WpfCascadingComboBoxes;

public partial class MainWindow : INotifyPropertyChanged
{
    public MainWindow()
    {
        InitializeComponent();
        _ = LoadData();
    }

    private string _url = "https://pkgstore.datahub.io/core/world-cities/world-cities_json/data/5b3dd46ad10990bca47b04b4739a02ba/world-cities_json.json";

    private CountryModel? selectedCountry;
    private ListCollectionView filteredCitiesView;

    public List<CityModel> Cities { get; } = new();
    
    public ObservableCollection<CountryModel> Countries { get; } = new();

    public ListCollectionView FilteredCitiesView
    {
        get => filteredCitiesView;
        private set
        {
            if (Equals(value, filteredCitiesView)) return;

            filteredCitiesView = value;
            OnPropertyChanged();
        }
    }

    public CountryModel? SelectedCountry
    {
        get => selectedCountry;
        set
        {
            if (Equals(value, selectedCountry)) return;

            selectedCountry = value;
            ApplyFilter();
            OnPropertyChanged();
        }
    }

    private async Task LoadData()
    {
        JsonSerializerOptions options = new();

        await using Stream stream = await new HttpClient().GetStreamAsync(_url);

        // deserialize the stream an object at a time...
        await foreach (CountryDTO item in JsonSerializer
                           .DeserializeAsyncEnumerable<CountryDTO>(stream, options))
        {
            Process(item);
        }

        PrepareFiltering();

        SelectedCountry = Countries.First();

        ApplyFilter();
    }

    private void PrepareFiltering()
    {
        FilteredCitiesView = (ListCollectionView)CollectionViewSource.GetDefaultView(Cities);
        FilteredCitiesView.SortDescriptions.Clear();
        FilteredCitiesView.SortDescriptions.Add(new SortDescription("Name",
            ListSortDirection.Ascending));
    }

    // Filter
    public bool Contains(object item)
    {
        CityModel? city = item as CityModel;
        return (city?.ParentId ?? -999) == (SelectedCountry?.Id ?? -1);
    }

    private void ApplyFilter()
        => FilteredCitiesView.Filter = Contains;

    // prepared models from JSON data
    private void Process(CountryDTO dto)
    {
        CountryModel? country = Countries
            .FirstOrDefault(x => x.Name.Equals(dto.Country));

        if (country == null)
        {
            country = new CountryModel(Countries.Count + 1, dto.Country);
            Countries.Add(country);
        }

        var city = new CityModel(Cities.Count + 1, country.Id, dto.Name);
        Cities.Add(city);
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

public class CountryDTO
{
    [JsonPropertyName("country")]
    public string? Country { get; set; }

    [JsonPropertyName("name")]
    public string? Name { get; set; }
}

public record CountryModel(int Id, string Name);

public record CityModel(int Id, int ParentId, string Name);

3. XAML:

XML
<Window x:Class="WpfCascadingComboBoxes.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfCascadingComboBoxes"
        x:Name="Window"
        mc:Ignorable="d" Title="MainWindow" Height="450" Width="800">
    <Grid DataContext="{Binding ElementName=Window}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <ComboBox Grid.Column="0"
                  Width="200"
                  VerticalAlignment="Center"
                  ItemsSource="{Binding Countries}"
                  DisplayMemberPath="Name"
                  SelectedItem="{Binding SelectedCountry}"/>

        <ComboBox Grid.Column="1"
                  Width="200"
                  VerticalAlignment="Center"
                  DisplayMemberPath="Name"
                  ItemsSource="{Binding FilteredCitiesView}"/>
    </Grid>
</Window>

ノート:
※アプリ起動時にデータを取り込み準備中です。 選択する前に、国の ComboBox がいっぱいになるのを待ちます。
* 国を選択すると、City ComboBox が CollectionViewSource[^].

コメント

タイトルとURLをコピーしました