为未定义数量的 ComboBoxes 创建一个集合过滤器

Make a collection filter for an undefined number of ComboBoxes

背景

有一个包含 2 列的 ListView:文件列(字符串)和工作表列(组合框)。

  1. 我有一个不固定数量的组合框(从 1 到无穷大),这取决于文件中的列数(这些是“ImportColumns”以避免混淆)。
  2. 程序中有列的列表(从 2 到无穷大)。最小的情况如下:“-None-”,“1”。但也可以是“-None-”、"col1"、"element2"、"any name"、...、"item99999"。这些是“WorksheetColumns”。
  3. ImportColumns 和 WorksheetColumns 的计数完全没有关联:可以是1:2、1:200、200:25等
  4. 每个 ComboBox 都有一个 common WorksheetColumns 下拉列表。在这个列表中,“- None -”是默认的,甚至可以被所有的ComboBox取用。但是,所有其他项目一次只能由 1 个 ComboBox 选择。

例如列表为“-None-”、“1”、“2”、“3”、“4”、“5”、“6”、“7”、“ 8"... 当每个ComboBox都以"- None -"开头时,每个ComboBox都有所有列表项可供选择。但是当 ComboBox 选择例如“1”时,所有其他 ComboBox 的下拉列表中不再有它。如果“1”然后切换为其他任何内容,ComboBoxes 可以再次 select 它。因此,只有未被 select 编辑的项目和“- None -”必须显示在下拉列表中。

  1. 每个 WorksheetItem 都具有 ID(整数)、名称(字符串)和 Selected(布尔)等属性。

  2. 最后也是最重要的:一个 ComboBox,其中 selected 例如“1”应该在其下拉列表中仍然有“1”.

用一个简短的例子来总结,组合框是"BOY",工作表列是"girl"。试想BOY1选女朋友:他可以单身("-None-"),选girl1(因为她不是任何BOY选择的CURRENTLY),girl2(同),但不是 girl3(因为她是被 BOY2 选择的)。同时,BOY2可以选择单身(“-None-”),转为girl1或girl2,或留在girl3。任务是仅将 girl3 包含在 BOY2 的选项列表中。

由于 ComboBox 的数量与 WorksheetColumns 一样未定义,我无法在 XAML 或 C# 中创建变量。

以前

在包含 WorksheetItems 的 class 中放置一个方法来获取 WorksheetItems 的子集合。方法return所有非Selected项,ID等于0的项和项select被当前ComboBox编辑(由参数"keepColumn"):

public List<WorksheetColumn> GetWorksheetColumnHeaders(int keepColumn)
    {
        return WorksheetColumns.Where(header => header.ID == 0 || header.Selected == false || header.ID == keepColumn).ToList();
    }

问题是:我不知道在哪里调用这个方法。我读到不可能从 XAML 调用方法,但该方法的结果应设置为 ItemsSource 到多个组合框(由于 "keepColumn" 参数,每个组合框必须具有不同的 ItemsSource它使 WorksheetItem select 由这个 ComboBox 编辑 - 这就是目标)。

我有 XAML、ComboBox_SelectionChanged 事件和 OnWorksheetItemPropertyChanged(在 "Selected" 更改时发生)。

代码

现在我的 XAML 使用 属性,它没有 return 需要的项目,而不是方法:

<ListView Grid.Row="0" Name="listView" IsSynchronizedWithCurrentItem="True" SelectionMode="Single" Margin="10" ItemsSource="{Binding ImportColumns}" >

            <ListView.View>
                <GridView AllowsColumnReorder="False">

                    <GridViewColumn Header="File column" DisplayMemberBinding="{Binding FileColumnHeader}"/>

                    <GridViewColumn Header="Worksheet column" >
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>

                                <ComboBox VerticalAlignment="Center" DataContext="{Binding DataContext,RelativeSource={RelativeSource AncestorType={x:Type ListView}}}" ItemsSource="{Binding ListOfWorksheetColumns.UnselectedWorksheetColumns, UpdateSourceTrigger=PropertyChanged}" SelectionChanged="ComboBox_SelectionChanged" SelectedIndex="0">

                                </ComboBox>

                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                </GridView>
            </ListView.View>

        </ListView>

我的主要ViewModel:

public class ImportManagerViewModel : BaseViewModel
{
    public List<ImportColumn> ImportColumns { get; set; }
    public ListOfWorksheetColumnHeaders ListOfWorksheetColumns { get; set; }

    public bool IsAnyColumnImported
    {
        get;
        set;
    }

    public void OnImportItemPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        IsAnyColumnImported = ImportColumns.Any(x => x.TargetColumnIndex != 0);
    }

    public void OnWorksheetItemPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        ListOfWorksheetColumns.UnselectedWorksheetColumns = new ObservableCollection<WorksheetColumn>(ListOfWorksheetColumns.WorksheetColumns.Where(header => header.ID == 0 || header.Selected == false /*|| header.ID == 1*/));
    }
}

我的 ListOfWorksheetColumnHeaders class(包含集合、子集合和方法):

public class ListOfWorksheetColumnHeaders : BaseViewModel
{

    public ObservableCollection<WorksheetColumn> WorksheetColumns { get; set; } = new ObservableCollection<WorksheetColumn>();

    public ObservableCollection<WorksheetColumn> UnselectedWorksheetColumns
    {
        get;
        set;
    }

    public List<WorksheetColumn> GetWorksheetColumnHeaders(int keepColumn)
    {
        return WorksheetColumns.Where(header => header.ID == 0 || header.Selected == false || header.ID == keepColumn).ToList();
    }
}

我的工作表列class:

public class WorksheetColumn : BaseViewModel
{
    public int ID;

    public string ColumnName { get; set; }
    public bool Selected
    {
        get;
        set;
    }

    public override string ToString()
    {
        return ColumnName;
    }
}

截至目前

我唯一需要的是 - 让 selected WorksheetItem 留在 selected 的 ComboBox 的下拉列表中。否则,ComboBox 会立即从下拉列表中丢失此项,因为它的 "Select" 属性 变为 true不重要:不再被ComboBoxselect编辑后,其"Select"属性变为false,它出现在列表中,但 ComboBox 当前没有任何内容 selected;换句话说,如果你 select 任何东西,它只会清除组合框的 selection。

The problem is: I don't know where to call this method

很可能,您根本不应该调用它。我基于以下声明:

the result of the method should be set as ItemsSource

这告诉我你有这些项目控件(例如 ComboBox),你 应该 做的是间接绑定 WorksheetColumns 集合,通过一个 CollectionViewSource 对象。 CollectionViewSource 对象将允许您过滤视图,您可以通过提供方法中当前拥有的谓词来实现,即 header => header.ID == 0 || header.Selected == false || header.ID == keepColumn.

尚不清楚 keepColumn 参数的来源,但如果它不是某个视图模型中的 属性,那么它可能应该是。无论如何,一旦您更多地研究了集合绑定模型并了解 CollectionViewSource 的工作原理,我希望您能够弄清楚如何在您的过滤器中获取 keepColumn 值而无需太多麻烦了。

如果在研究了相关文档并尝试解决问题后,您仍然无法弄清楚如何让它工作并且您的同事也无法帮助您,请随时post 一个新问题。但是在那个问题中,请确保你包含一个很好的 Minimal, Reproducible Example 来清楚地显示你已经尝试过的内容,并详细解释该代码的作用,你希望它做什么,以及你具体需要什么帮助.

顺便说一下……

I've read that it's impossible to call a method from XAML

嗯,那不是真的。方法至少有四种关键方式 "called from XAML" (取决于其中一种的确切含义):

  • 事件处理程序。 XAML 发布事件的对象将使这些事件作为属性出现在 XAML 中。您可以将属性值设置为一个方法的名称,该方法将为您订阅事件。
  • ICommand绑定。大多数地方您可以提供 ICommand,您也可以选择传递 CommandParameter。这是传递给 ICommandCanExecute()Execute() 方法的 object 引用。您可以通过传递数组来传递多个参数。 ICommand.Execute() 方法在由于任何原因执行命令时被调用。
  • IValueConverterIMultiValueConverter。这些也有参数。需要进行值转换时调用接口中的相关方法,一般在绑定的上下文中。
  • 最后,虽然以上都是特殊场景,对某些人来说可能不符合 "calling a method from XAML"(因为它们不是 通用 调用),您始终可以使用 ObjectDataProvider 来调用可以 return 任何类型值的任何方法。您可以调用实例上的方法或静态方法。您可以将参数传递给该方法。您甚至可以调用构造函数来创建对象的实例。 (警告: 尝试将其强加到您的场景中可能很诱人......不要这样做!它可能无论如何都行不通,因为 ObjectDataProvider 是一个一次性的事情。但即使你以某种方式破解它,处理这种情况肯定是错误的方法。)

您可以将您的方法更改为 属性,它允许 Binding:

public class ImportColumn {
    private int keepColumn;

    public List<WorksheetColumn> WorksheetColumnHeaders => WorksheetColumns.Where(header => header.ID == 0 || header.Selected == false || header.ID == keepColumn).ToList();
}

您必须将其放入新的 class 并将每个 ItemsSource 绑定到此 属性,如下所示:

<ComboBox ItemsSource="{Binding WorksheetColumnHeaders}"/>

但这不是进行绑定的正确方法,因为您不应混合视图和数据,您应该查看 MVVM 模式并查看 ICommand,如其他答案所建议的那样。

尝试使用转换器

[ValueConversion(typeof(List<WorksheetColumn>), typeof(List<WorksheetColumn>))]
public class ListFilterConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        List<WorksheetColumn> worksheets = value as List<WorksheetColumn>;
        int keepColumn = (int)parameter;

        return worksheets.Where(header => header.ID == 0 || header.Selected == false || header.ID == keepColumn).ToList();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

XAML 是..

    <ListView Grid.Row="0" Name="listView" IsSynchronizedWithCurrentItem="True" SelectionMode="Single" Margin="10" ItemsSource="{Binding ImportColumns}" >
        <ListView.Resources>
            <local:ListFilterConverter x:Key="myConverter" />
            <sys:Int32 x:Key="keepcolumn1">1</sys:Int32>
        </ListView.Resources>

        <ListView.View>
            <GridView AllowsColumnReorder="False">

                <GridViewColumn Header="File column" DisplayMemberBinding="{Binding FileColumnHeader}"/>

                <GridViewColumn Header="Worksheet column" >

                    <GridViewColumn.CellTemplate>
                        <DataTemplate>

                            <ComboBox VerticalAlignment="Center" DataContext="{Binding DataContext,RelativeSource={RelativeSource AncestorType={x:Type ListView}}}" 
                                      ItemsSource="{Binding ListOfWorksheetColumns.UnselectedWorksheetColumns, Converter={StaticResource myConverter}, ConverterParameter={StaticResource keepcolumn1}}" 
                                      SelectionChanged="ComboBox_SelectionChanged" SelectedIndex="0">

                            </ComboBox>

                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>

            </GridView>
        </ListView.View>

    </ListView>