How can I insert a “Select All” item at the top of

2019-08-26 21:49发布

问题:

I have a ComboBox with its ItemsSource set to an IList of MyClass objects. I overrode the ComboBox's ItemTemplate to display a CheckBox next to the items. I want to have an item at the top that says "Select All" and when the user checks that CheckBox, the code checks all CheckBoxes. My question is, what is the MVVM way of doing this?

I don't want to add a separate MyClass object to the IList. That seems like it would involve too much coupling of the view and the model. Is there a way to add an item directly in the XAML code and give it a Command that checks all of the Checkboxes?

My ComboBox XAML right now is:

<ComboBox ItemsSource="{Binding MyList}" Width="200">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding Name}" IsChecked="{Binding Selected}" />
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

That looks like:

I'd like it to look like:

My MyClass is simply this:

public class MyClass
{
    public string Name { get; set; }
    public bool Selected { get; set; }
}

Edit: I found a way to add an item to the collection in the XAML code using the example here. I would still need a way run code when the user checks the checkbox for that "Select All" item. To just add the item, the code is this:

<ComboBox Width="200">

    <ComboBox.Resources>
        <CollectionViewSource x:Key="comboBoxSource" Source="{Binding Path=MyList}" />
    </ComboBox.Resources>

    <ComboBox.ItemsSource>
        <CompositeCollection>
            <local:MyClass Name="Select All" Selected="False">
            </local:MyClass>
            <CollectionContainer Collection="{Binding Source={StaticResource comboBoxSource}}" />
        </CompositeCollection>
    </ComboBox.ItemsSource>

    <ComboBox.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding Name}" IsChecked="{Binding Selected}" />
        </DataTemplate>
    </ComboBox.ItemTemplate>

</ComboBox>

回答1:

Id personally just modify the CheckBox's Template and add custom CheckBox there with Click handler, nothing too fancy, easy to understand.

http://msdn.microsoft.com/en-us/library/ms752094(v=vs.110).aspx

From there you can modify this part:

<ScrollViewer Margin="4,6,4,6"
                                SnapsToDevicePixels="True">
                    <StackPanel IsItemsHost="True"
                                KeyboardNavigation.DirectionalNavigation="Contained" />
</ScrollViewer>

The other way I would model is this simply as to create

public class MyClassViewModel
{
    public string Name { get; set; }
    public bool Selected { get; set; }

    public ICommand Execute {get; set;}
}

and add custom object to your IList. This will work nicely, without any crap, your viewmodel has no idea about view, + it's testable. win-win for everyone.



回答2:

Whenever your Button to select all is pressed you can invoke action, binding to itemssource, enumerating throughout collection setting Selected value to true. This solutiond requires Blend interface.

 <local:MyClass Name="Select All" Selected="False">
          <i:Interaction.Triggers>
                <ic:DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Selected}" Value="True">
                            <local:SelectAll TargetObject="{Binding RelativeSource={RelativeSource AncestorType=ComboBox}, Path=ItemsSource}"/>
                </ic:DataTrigger>
           </i:Interaction.Triggers>
</local:MyClass>

and then create class SelectAll as follows

public class SelectAll : TargetedTriggerAction<List<MyClass>>
{
    protected override void Invoke(object parameter)
    {
        if (Target is List<MyClass>)
            foreach (var elem in (List<MyClass>)Target)
                elem.Selected = true;
    }
}


回答3:

I figured out a way to do it that uses some simpler concepts.

My first problem was that I wanted to add a "Select All" item to the top of the list without actually adding an item to the databound IList<MyClass>. I edited my original post to show that I can use a CompositeCollection to do this. I made the "Select All" databound object a subclass of MyClass and called the class MyClassSelectAll. More on this below.

My second problem was that I needed to handle the CheckBox.Click event differently for the "Select All" item than for the regular MyClass items. I gave each CheckBox a Command that I can use to check the CheckBoxes. To do this, I need a reference to both the IList<MyClass> and the CheckBox on which the user clicked (more specfically the MyClass object databound to that CheckBox).

I found one way to solve my second problem is to set the CheckBox.CommandParameter to a MultiBinding that contains those two objects. In the Command.Execute(object) method, I can easily check if the current MyClass is a MyClassSelectAll, and if so, I can loop through the IList<MyClass> list (which doesn't contain the "Select All" item) and set the "Selected" property appropriately.

Here's my relevant XAML code:

<Grid.Resources>
    <local:MyCommand x:Key="kMyCommand" />
    <local:MyConverter x:Key="kConv" />
</Grid.Resources>

<ComboBox Width="200">

    <ComboBox.Resources>
        <CollectionViewSource x:Key="comboBoxSource" Source="{Binding Path=MyList}" />
    </ComboBox.Resources>

    <ComboBox.ItemsSource>
        <CompositeCollection>
            <local:MyClassSelectAll Name="Select All" Selected="False" />
            <CollectionContainer Collection="{Binding 
                Source={StaticResource comboBoxSource}}" />
        </CompositeCollection>
    </ComboBox.ItemsSource>

    <ComboBox.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding Name}" 
                IsChecked="{Binding Selected}" 
                Command="{StaticResource kMyCommand}">
                <CheckBox.CommandParameter>
                    <MultiBinding Converter="{StaticResource kConv}">
                        <Binding RelativeSource="{RelativeSource 
                            FindAncestor, AncestorType={x:Type Window}}" 
                            Path="DataContext.MyList" />
                        <Binding Path="." />
                    </MultiBinding>
                </CheckBox.CommandParameter>
            </CheckBox>
        </DataTemplate>
    </ComboBox.ItemTemplate>

</ComboBox>

I updated MyClass to implement INotifyPropertyChanged appropriately. The code is simple, so I won't post it here.

MyConverter is pretty simple as well; it just packages the list and current item object into a MyCommandArgs object:

class MyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, 
        object parameter, System.Globalization.CultureInfo culture)
    {
        MyCommandArgs result = new MyCommandArgs()
        {
            MyList = values[0] as IList<MyClass>,
            CurrentItem = values[1] as MyClass
        };
        return result;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, 
        object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The MyCommandArgs class that I use to pass around the MyList and CurrentItem references is a simple container class so I won't post it here.

MyClassSelectAll is the simplest; its only job is to flag the "Select All" MyClass object as something that selects all.

class MyClassSelectAll : MyClass
{
}

Finally, The MyCommand object that handles the click event.

class MyCommand : ICommand
{
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        var mylist = parameter as MyCommandArgs;
        if (mylist != null && mylist.CurrentItem is MyClassSelectAll)
        {
            foreach (var item in mylist.MyList)
            {
                item.Selected = mylist.CurrentItem.Selected;
            }
        }
    }
}

Thanks everyone. I was really stuck on this. It seems like many times when I have a problem doing something in WPF, the solution is to use a Converter.



回答4:

You have to registed the event when selecting a item, and when selecting the item,check that the item selected is the first one(using index) and then,change the selection to what do you need. (Dont have the IDE here,sorry for not code).