Quasimodo
Quasimodo

Reputation: 57

Make a collection filter for an undefined number of ComboBoxes

Background

There is a ListView which contains 2 columns: File Column (string) and Worksheet Column (ComboBox).

  1. I have an unfixed number of ComboBoxes (from 1 to infinity) which depends on the number of columns in a file (those are "ImportColumns" to avoid confusion).
  2. There is a list of columns in the program (from 2 to infinity). The least case is as follows: "- None -", "1". But it can be also "- None -", "col1", "element2", "any name", ..., "item99999". These are "WorksheetColumns".
  3. The counts of ImportColumns and WorksheetColumns are completely unconnected: it can be 1:2, 1:200, 200:25, etc.
  4. Each ComboBox has a common drop-down list of WorksheetColumns. In this list, "- None -" is default and can be taken even by all ComboBoxes. However, all other items can be picked only by 1 ComboBox at a time.

For example, the list is "- None -", "1", "2", "3", "4", "5", "6", "7", "8"... When every ComboBox starts as "- None -", every ComboBox has all list items to choose from. But when a ComboBox picks for example "1", all other ComboBoxes no longer have it in their drop-down list. If "1" is then switched to anything else, ComboBoxes can again select it. So, only not selected items and "- None -" must show up in the drop-down list.

  1. Each WorksheetItem has properties like ID (int), Name (string) and Selected (bool).

  2. Last and most important: a ComboBox which selected for example "1" should still have "1" in its drop-down list.

To sum up in a short example, ComboBoxes are "BOY"s, WorksheetColumns are "girl"s. Imagine BOY1 choosing a girlfriend: he can be single ("- None -"), choose girl1 (because she is not CURRENTLY chosen by any BOY), girl2 (the same), but not girl3 (because she was chosen by BOY2). At the same time, BOY2 can choose to become single ("- None -"), switch to girl1 or girl2, or stay with girl3. The task is to include girl3 in the list of options only for BOY2.

Since the number of ComboBoxes is just as undefined as WorksheetColumns, I cannot create variables in XAML or C#.

Previously

I was advised to put a method in a class which contains WorksheetItems to get a subcollection of WorksheetItems. The method returns all not Selected items, an item with ID equal to 0 and the item selected by the current ComboBox (specified by the argument "keepColumn"):

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

The problem is: I don't know where to call this method. I've read that it's impossible to call a method from XAML but the result of the method should be set as ItemsSource to multiple ComboBoxes (each of them must have a bit different ItemsSource because of that "keepColumn" argument which keeps the WorksheetItem selected by this very ComboBox - that's the goal).

I have XAML, ComboBox_SelectionChanged event and OnWorksheetItemPropertyChanged (happens when "Selected" gets changed).

Code

Right now my XAML uses a property, which does not return the needed item, instead of a method:

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

My main 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*/));
    }
}

My ListOfWorksheetColumnHeaders class (contains the collection, a subcollection and the method):

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

My WorksheetColumn class:

public class WorksheetColumn : BaseViewModel
{
    public int ID;

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

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

As of now

The only thing that I need - make the selected WorksheetItem stay in the drop-down list of the ComboBox that selected it. Otherwise, the ComboBox loses this item from the drop-down list immediately because its "Select" property becomes true. Not important: after it is not longer selected by the ComboBox, its "Select" property becomes false, and it appears in the list but the ComboBox has nothing currently selected; in other words, if you select anything, it just clears the selection of the combobox.

Upvotes: 0

Views: 134

Answers (3)

Byoung Sam Moon
Byoung Sam Moon

Reputation: 306

try to use converter

[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 is..

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

Upvotes: 0

Corentin Pane
Corentin Pane

Reputation: 4943

You could change your method into a property, which allows for Binding:

public class ImportColumn {
    private int keepColumn;

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

You would have to put that in a new class and bind each ItemsSource to this property like this:

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

But that is not the right way to go about bindings as you should not mix your view and your data, you should have a look at the MVVM pattern and look at ICommand as suggested by other answers.

Upvotes: 0

Peter Duniho
Peter Duniho

Reputation: 70671

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

Most likely, you shouldn't call it at all. I base that on this statement:

the result of the method should be set as ItemsSource

That tells me that you have these items controls (e.g. ComboBox) where what you should be doing is binding the WorksheetColumns collection indirectly, via a CollectionViewSource object. The CollectionViewSource object will allow you to filter the view, which you can do by providing the predicate you currently have in the method, i.e. header => header.ID == 0 || header.Selected == false || header.ID == keepColumn.

It's not clear where the keepColumn parameter comes from, but if it's not a property in a view model somewhere already it probably should be. In any case, once you've studied the collection binding model more and understand how CollectionViewSource works, I expect you'll be able to figure out how to get the keepColumn value in your filter without too much trouble.

If after studying the relevant documentation and making some attempt to solve the problem, you're still unable to figure out how to get it to work and your coworkers are unable to help you, please feel free to post a new question. But in that question, make sure you include a good Minimal, Reproducible Example that shows clearly what you've tried, with a detailed explanation of what that code does, what you want it to do instead, and what specifically you need help with.

By the way…

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

Well, that's not true. There are at least four key ways that methods can be "called from XAML" (depending on what one exactly means):

  • Event handlers. XAML objects which publish events will have those events appear as attributes in the XAML. You can set the attribute value to the name of a method, and that method will be subscribed to the event for you.
  • ICommand binding. Most places you can provide an ICommand, you also have the option of passing a CommandParameter. This is an object reference passed to your ICommand's CanExecute() and Execute() methods. You can pass multiple arguments by passing an array. The ICommand.Execute() method is called when the command is executed for any reason.
  • IValueConverter and IMultiValueConverter. These also take parameters. The relevant methods in the interfaces are called when value conversion is required, generally in the context of a binding.
  • And finally, while the above are all specialized scenarios, and to some may not qualify as "calling a method from XAML" (since they aren't general purpose calls), you can always use ObjectDataProvider to call any method that can return any type of value. You can call a method on an instance, or a static method. You can pass parameters to the method. You can even call a constructor to create an instance of an object. (Warning: it might be tempting to try to shoe-horn this into your scenario…don't do it! It likely wouldn't work anyway, since ObjectDataProvider is a one-shot sort of thing. But even if you hacked around that somehow, it would definitely be the wrong way to approach this scenario.)

Upvotes: 2

Related Questions