Govind
Govind

Reputation: 143

Check box in Datagrid wpf c# application

I am creating an C# wpf application for student records using datagrid. How to create check box in header to select/deselect all check boxes in a row ? How to select check box in row with one click so that we can edit/delete record ? and how we can select multiple check boxes to delete ?

Upvotes: 2

Views: 5100

Answers (2)

Arthur Nunes
Arthur Nunes

Reputation: 7038

I have created a behavior to allow a property in a control to be bound to a property of a collection of items, in a way that:

  • If you change the property in the control, all of the items are updated.
  • If you change the property in a item, if all the items have the same property the control will reflect it. If not, the property of the control will be given a fallback value (like null).

With this behavior, you can add a checkbox to your DataGrid header and bind its IsChecked property to the DataGrid's ItemSource, to a property of the collection type.

We can deal with the selection logic using the MVVM pattern. For example, we have the following ViewModel for the collection entity:

public class ItemViewModel : INotifyPropertyChanged
{
    #region INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion

    private bool isSelected;
    public bool IsSelected {
        get { return this.isSelected; }

        set
        {
            if (this.isSelected == value)
                return;

            this.isSelected = value;
            this.OnPropertyChanged("IsSelected");
        }
    }

    private string myProperty;
    public string MyProperty
    {
        get { return this.myProperty; }
        set
        {
            if (this.myProperty != value)
            {
                this.myProperty = value;
                this.OnPropertyChanged("MyProperty");
            }
        }
    }
}

Then we have the MainViewModel, which controls the MainWindow's logic:

public class MainViewModel: INotifyPropertyChanged
{
    #region INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion

    #region Items (INotifyPropertyChanged Property)
    private ObservableCollection<ItemViewModel> items;

    public ObservableCollection<ItemViewModel> Items
    {
        get { return this.items; }
        set
        {
            if (this.items != value)
            {
                this.items = value;
                this.OnPropertyChanged("Items");
            }
        }
    }
    #endregion

    public MainViewModel()
    {
        this.Items = new ObservableCollection<ItemViewModel>();
        for (int i = 0; i < 10; i++)
            this.Items.Add(new ItemViewModel() { MyProperty = "Item" + i });
    }
}

In our Window, we can then declare the DataGrid. To achieve what you want, we will have to to the following:

  • Assign the MainViewModel to the Window.DataContext
  • Bind the DataGrid.ItemsSource to the Items property in the MainViewModel
  • Define the columns of the DataGrid. In the example I opted to have a "IsSelected" column and add the "SelectAll" CheckBox to its header, as you specified, but with the behavior you could have a CheckBox anywhere to control the selecion.
  • We need to make the selection of a row update the "IsSelected" property of our items and vice-versa. For that we modify the RowStyle so we can bind the "IsSelected" property of the row to the "IsSelected" of its item. With that, the selection logic now can be totally driven by the ViewModel.
  • The last thing to do is to make the "SelectAll" CheckBox do its job. We apply the CollectionPropertyBehavior and configure it so its "SourcePropertyPath" points to the property in the CheckBox we want to bind ("IsChecked") and the CollectionPropertyPath points to the property in the items ("IsSelected"). Then we just need to bind its ItemsSource to the DataGridItemsSource. Note the default value is "null" which means that when the items have different values in its property, the CheckBox will recive "null" and be in the undefined state.

The final xaml will be like this:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication2" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" x:Class="WpfApplication2.MainWindow"
    Title="MainWindow" mc:Ignorable="d" Height="350" Width="525">
<Window.Resources>
    <Style x:Key="DataGridRowStyle" TargetType="{x:Type DataGridRow}">
        <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
    </Style>
</Window.Resources>
<Window.DataContext>
    <local:MainViewModel/>
</Window.DataContext>
<Grid>
    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Items}" CanUserAddRows="False" RowStyle="{DynamicResource DataGridRowStyle}">
        <DataGrid.Columns>
            <DataGridCheckBoxColumn Binding="{Binding IsSelected}">
                <DataGridCheckBoxColumn.Header>
                    <CheckBox>
                        <i:Interaction.Behaviors>
                            <local:CollectionPropertyBehavior CollectionPropertyPath="IsSelected" SourcePropertyPath="IsChecked" ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
                        </i:Interaction.Behaviors>
                    </CheckBox>                                 
                </DataGridCheckBoxColumn.Header>            
            </DataGridCheckBoxColumn>
            <DataGridTextColumn Width="*" Binding="{Binding MyProperty}"/>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

Finally, the behavior:

public class CollectionPropertyBehavior : Behavior<DependencyObject>
{
    private IEnumerable<ValueProxy> proxies;
    private bool syncking;

    public string SourcePropertyPath
    {
        get { return (string)GetValue(SourcePropertyPathProperty); }
        set { SetValue(SourcePropertyPathProperty, value); }
    }
    public static readonly DependencyProperty SourcePropertyPathProperty =
        DependencyProperty.Register("SourcePropertyPath", typeof(string), typeof(CollectionPropertyBehavior), new PropertyMetadata(null));

    public string CollectionPropertyPath
    {
        get { return (string)GetValue(CollectionPropertyPathProperty); }
        set { SetValue(CollectionPropertyPathProperty, value); }
    }
    public static readonly DependencyProperty CollectionPropertyPathProperty =
        DependencyProperty.Register("CollectionPropertyPath", typeof(string), typeof(CollectionPropertyBehavior), new PropertyMetadata(null));


    private IEnumerable<object> Items { get { return  this.ItemsSource == null ? null : this.ItemsSource.OfType<object>(); } }
    public IEnumerable ItemsSource
    {
        get { return (IEnumerable)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }
    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(CollectionPropertyBehavior), new PropertyMetadata(null, ItemsSourceChanged));


    private object Value
    {
        get { return (object)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }
    private static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(object), typeof(CollectionPropertyBehavior), new PropertyMetadata(null, ValueChanged));


    public object DefaultValue
    {
        get { return (object)GetValue(DefaultValueProperty); }
        set { SetValue(DefaultValueProperty, value); }
    }
    public static readonly DependencyProperty DefaultValueProperty =
        DependencyProperty.Register("DefaultValue", typeof(object), typeof(CollectionPropertyBehavior), new PropertyMetadata(null));



    private static void ValueChanged(object sender, DependencyPropertyChangedEventArgs args)
    {
        var element = sender as CollectionPropertyBehavior;
        if (element == null || element.ItemsSource == null) return;

        element.UpdateCollection();
    }

    private static void ItemsSourceChanged(object sender, DependencyPropertyChangedEventArgs args)
    {
        var element = sender as CollectionPropertyBehavior;
        if (element == null || element.ItemsSource == null) return;
        element.ItemsSourceChanged();
    }

    private void ItemsSourceChanged()
    {
        this.proxies = null;

        if (this.Items == null || !this.Items.Any() || this.CollectionPropertyPath == null) return;

        // Cria os proxies
        this.proxies = this.Items.Select(o =>
        {
            var proxy = new ValueProxy();
            proxy.Bind(o, this.CollectionPropertyPath);
            proxy.ValueChanged += (s, e) => this.UpdateSource();
            return proxy;
        }).ToArray();

        this.UpdateSource();
    }

    private void UpdateSource()
    {
        if (this.syncking) return;

        // Atualiza o valor 
        using (new SynckingScope(this))
        {
            object value = this.proxies.First().Value;
            foreach (var proxy in this.proxies.Skip(1))
            {
                value = object.Equals(proxy.Value, value) ? value : this.DefaultValue;
            }

            this.Value = value;
        }
    }

    private void UpdateCollection()
    {
        // Se o valor estiver mudando em função da atualização de algum 
        // elemento da coleção, não faz nada
        if (this.syncking) return;

        using (new SynckingScope(this))
        {
            // Atualiza todos os elementos da coleção,
            // atrávés dos proxies
            if (this.proxies != null)
                foreach (var proxy in this.proxies)
                    proxy.Value = this.Value;
        }
    }

    protected override void OnAttached()
    {
        base.OnAttached();


        // Bind da propriedade do objeto fonte para o behavior
        var binding = new Binding(this.SourcePropertyPath);
        binding.Source = this.AssociatedObject;
        binding.Mode = BindingMode.TwoWay;
        BindingOperations.SetBinding(this, ValueProperty, binding);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        // Limpa o binding de value para a propriedade do objeto associado
        this.ClearValue(ValueProperty);
    }

    internal class SynckingScope : IDisposable
    {
        private readonly CollectionPropertyBehavior parent;

        public SynckingScope(CollectionPropertyBehavior parent)
        {
            this.parent = parent;
            this.parent.syncking = true;
        }

        public void Dispose()
        {
            this.parent.syncking = false;
        }
    }

    internal class ValueProxy : DependencyObject
    {
        public event EventHandler ValueChanged;

        public object Value
        {
            get { return (object)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register("Value", typeof(object), typeof(ValueProxy), new PropertyMetadata(null, OnValueChanged));


        private static void OnValueChanged(object sender, DependencyPropertyChangedEventArgs args)
        {
            var element = sender as ValueProxy;
            if (element == null || element.ValueChanged == null) return;

            element.ValueChanged(element, EventArgs.Empty);
        }

        public void Bind(object source, string path)
        {
            // Realiza o binding de value com o objeto desejado
            var binding = new Binding(path);
            binding.Source = source;
            binding.Mode = BindingMode.TwoWay;

            BindingOperations.SetBinding(this, ValueProperty, binding);
        }
    }
}

The advantages of this approach is that it is fully reusable. In this example we used it to handle selection, but you could have a enum property in you ItemViewModel bound to a ComboBox in the header and so on.

The behavior was ported from a Silverlight 4 project of mine, but I tested and it worked fine in a WPF application. However, I think that in WPF we can perhaps reach a nicer approach adapting the behavior to a MarkupExtension. I might take a look on that if I have time. In addition, one could also adapt it so it can be bound to the SelectedItems in a way that, when there is selected items, it updates them and when there is not, it updates all.

Upvotes: 2

Ramin
Ramin

Reputation: 2133

use sth like this:

DataGridCheckBoxColumn cbc = new DataGridCheckBoxColumn();
dataGrid.Columns.Add(cbc);
CheckBox cb = new CheckBox();
cbc.Header = cb;

and handle Checked and UnChecked events:

cb.Checked+=new RoutedEventHandler(cb_Checked); 
cb.Unchecked+=new RoutedEventHandler(cb_Unchecked);

Upvotes: 0

Related Questions