Search code examples
c#wpfdata-bindingicommandrelaycommand

How to bind Command to Check Box in ListView WPF?


Update

I edited the code below to match the suggestions and it works correctly now.

I've seen several stack overflow questions similar to this one, but I haven't quite been able to put it all together. I have the following xaml code.

<UserControl x:Class="AuditEfficiencyMVVM.View.AuditTestsMain"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:AuditEfficiencyMVVM.View"
         xmlns:viewmodel="clr-namespace:AuditEfficiencyMVVM.ViewModel"
         mc:Ignorable="d" 
         d:DesignHeight="500" d:DesignWidth="1000">

<UserControl.DataContext>
    <viewmodel:AuditTests/>
</UserControl.DataContext>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>
        <RowDefinition Height="50"/>
        <RowDefinition/>
        <RowDefinition Height="50"/>
    </Grid.RowDefinitions>

    <ListView Grid.Row="1" Grid.Column="0" Margin="10" ItemsSource="{Binding Path=Tests}">
        <ListView.View>
            <GridView>
                <GridViewColumn>
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <CheckBox Name="TestSelected" IsChecked="{Binding Path=Selected, Mode=TwoWay}" Command="{Binding Path=TestSelected, RelativeSource={RelativeSource AncestorType=ListView}}"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Test Type" DisplayMemberBinding="{Binding Type, Mode=OneWay}"/>
                <GridViewColumn Header="Progress">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <ProgressBar Name="TestProgress" Width="50" Height="20" Value="{Binding Progress, Mode=OneWay}"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
                <GridViewColumn Header="Status" DisplayMemberBinding="{Binding Status, Mode=OneWay}"/>                    
            </GridView>
        </ListView.View>

        <ListView.GroupStyle>
            <GroupStyle>
                <GroupStyle.ContainerStyle>
                    <Style TargetType="{x:Type GroupItem}">
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate>
                                    <Expander IsExpanded="True">
                                        <Expander.Header>
                                            <TextBlock FontWeight="Bold" FontSize="14" Text="{Binding Name}"/>
                                        </Expander.Header>
                                        <ItemsPresenter/>
                                    </Expander>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </GroupStyle.ContainerStyle>
            </GroupStyle>
        </ListView.GroupStyle>
    </ListView>

    <ListView Grid.Row="1" Grid.Column="1" Margin="10" ItemsSource="{Binding Path=Files}">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="File Type" DisplayMemberBinding="{Binding Type, Mode=OneWay}"/>
                <GridViewColumn Header="File Location" Width="250">
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Path=Location, Mode=TwoWay}" Width="225"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>                    
                <GridViewColumn>
                    <GridViewColumn.CellTemplate>
                        <DataTemplate>
                            <Button Width="30" Height="20">...</Button>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
            </GridView>
        </ListView.View>
    </ListView>

    <Button Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right" Margin="10" Width="50" Height="30">Run</Button>
</Grid>
</UserControl>

Here is my code behind

public class AuditTests : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private RelayCommand _testSelected;

    private void AddTest()
    {
        MessageBox.Show("Success");
    }

    public RelayCommand TestSelected
    {
        get
        {
            return _testSelected;
        }
        private set
        {
            if (_testSelected != value)
            {
                _testSelected = value;
                RaisePropertyChanged("TestSelected");
            }                
        }
    }

    private void RaisePropertyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    public AuditTests()
    {
        TestSelected = new RelayCommand(AddTest);
    }

    public ObservableCollection<Model.File> Files
    {
        get;
        set;
    }

    public ObservableCollection<Model.Test> Tests
    {
        get;
        set;
    }

    public void LoadFiles()
    {
        ObservableCollection<Model.File> files = new ObservableCollection<Model.File>();

        foreach (Model.Test test in Tests)
        {
            foreach (Enums.FileType type in test.ExpectedSources)
            {
                Boolean containsType = false;
                foreach (Model.File file in files)
                {
                    if (file.Type == type)
                    {
                        containsType = true;
                        break;
                    }
                }

                if (!containsType)
                {
                    files.Add(new Model.File { Type = type, Location = "", Tests = new List<Enums.TestType> { test.Type } });
                }
                else
                {
                    files.Where(t => t.Type == type).First().Tests.Add(test.Type);
                }
            }
        }

        Files = files;
    }

    public void LoadTests()
    {
        ObservableCollection<Model.Test> tests = new ObservableCollection<Model.Test>();

        foreach (var prop in Enum.GetValues(typeof(Enums.TestType)).Cast<Enums.TestType>().ToList())
        {
            tests.Add(new Model.Test { Category = prop.GetCategory(), Type = prop, Progress = 0, Selected = true, Status = Enums.TestStatus.NotStarted, ExpectedSources = prop.GetExpectedFiles() });
        }

        Tests = tests;
    }        
}
}

From what I've read this seems like it should work, but when I check/uncheck the check box the message box is not activated. What am I missing here to get the check/uncheck command to work?


Solution

  • First, I'd make AuditTests implement INotifyPropertyChanged, and raise PropertyChanged in all its property setters when their values change. There's a lot of documentation out there on doing that.

    Second, you've got to give the user control a copy of it. I can't tell if you gave your UserControl any properties of its own that MainWindow may want to put bindings on; if that's the case, it's a more complicated question. But the simplest thing is this:

    public AuditTestsMain()
    {
        InitializeComponent();
    
        //  Now its parent view can't bind this guy's properties to properties of the 
        //  parent's view's viewmodel, because we've broken DataContext inheritance. 
        DataContext = new ViewModels.AuditTests();
    }
    

    Then look at ASh's answer for how to bind the command. That's another DataContext thing: The thing there is that the listview items are Model.Test instances. So inside that DataTemplate for the items, bindings bind to the properties of Model.Test by default, because that's the DataContext. But the command is not a property of Model.Test; it's a property of AuditTests. AuditTests is the UserControl's DataContext, and therefore it's the ListView's DataContext as well. Controls inherit the parent's DataContext unless something interferes with that -- like the line I'm adding to your AuditTestsMain constructor, or like the way the ListView creates child items which have the listview's items as their DataContext.