开发者

How to create Tri-state CheckBox that summarize state of all CheckBoxes in a ListView?

I have a WPF ListView that contains CheckBox column and other columns of the custom object. I want to put another CheckBox over this ListView--tri-state CheckBox--so its state will b开发者_如何学Ce binded to the checked checkboxes. If everyone is checked the state of upper checkbox will be checked. If only some of them the state will be indeterminate. Otherwise its state will be unchecked. (like in Gmail)


I've found some solution

<StackPanel>
  <CheckBox Name="chbxAll" Checked="chbxAll_Checked" Unchecked="chbxAll_Unchecked" Indeterminate="chbxAll_Indeterminate" IsThreeState="True" >Select All</CheckBox>
     <ListView Name="lstFoundedFiles" SelectionChanged="lstFoundedFiles_SelectionChanged" SelectionMode="Multiple" ItemsSource="{Binding Files}">
       <ListView.Resources>
         <Style TargetType="ListViewItem">
           <Style.Triggers>
             <Trigger Property="IsSelected" Value="True">
               <Setter Property="Background" Value="Aquamarine"></Setter>
             </Trigger>
           </Style.Triggers>
         </Style>
       </ListView.Resources>
       <ListView.View>
         <GridView>
           <GridViewColumn Width="50" Header="Check">
             <GridViewColumn.CellTemplate>
               <DataTemplate>
                 <CheckBox x:Name="chbxItem" IsChecked="{Binding Path=IsSelected, 
                                                    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListViewItem}}}"/>
               </DataTemplate>
             </GridViewColumn.CellTemplate>
           </GridViewColumn>
          <GridViewColumn Header="File">
            <GridViewColumn.CellTemplate>
              <DataTemplate>
                <TextBlock Text="{Binding Name}" ></TextBlock>
              </DataTemplate>
            </GridViewColumn.CellTemplate>
         </GridViewColumn>
         <GridViewColumn Header="Location">
           <GridViewColumn.CellTemplate>
             <DataTemplate>
              <TextBlock Text="{Binding Path}"></TextBlock>
             </DataTemplate>
          </GridViewColumn.CellTemplate>
       </GridViewColumn>
     </GridView>
   </ListView.View>
 </ListView>
</StackPanel>

==========================================================================

CodeBehind:

// True if we should ignore check change events.
private bool IgnoreCheckChangeEvents = false;

private void lstFoundedFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
  if (IgnoreCheckChangeEvents) return;

  int temp = lstFoundedFiles.SelectedItems.Count;
  IgnoreCheckChangeEvents = true;
  if (temp == lstFoundedFiles.Items.Count)
  {
    chbxAll.IsChecked = true;
  }
  else if (temp == 0)
  {
    chbxAll.IsChecked = false;
  }
  else
  {
    chbxAll.IsChecked = null;
  }

  IgnoreCheckChangeEvents = false;
}

private void chbxAll_Checked(object sender, RoutedEventArgs e)
{
  if (IgnoreCheckChangeEvents) return;

  IgnoreCheckChangeEvents = true;

  lstFoundedFiles.SelectAll();

  IgnoreCheckChangeEvents = false;
}

private void chbxAll_Unchecked(object sender, RoutedEventArgs e)
{
  if (IgnoreCheckChangeEvents) return;

  IgnoreCheckChangeEvents = true;

  lstFoundedFiles.UnselectAll();

  IgnoreCheckChangeEvents = false;
}

private void chbxAll_Indeterminate(object sender, RoutedEventArgs e)
{
  if (IgnoreCheckChangeEvents) return;

  chbxAll.IsChecked = false;

  IgnoreCheckChangeEvents = true;

  lstFoundedFiles.UnselectAll();

  IgnoreCheckChangeEvents = false;
}


Edit: This is a bit less abysmal than my other method, at least it is not as chaotic and it does not require as much event handling. This method rebuilds a huge multibinding whenever the source changes so it might be a bit performance intense.

<GridViewColumn.Header>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Is Active" />
        <CheckBox IsThreeState="True"
                  local:AttachedProperties.SelectAllPath="IsActive"
                  local:AttachedProperties.SelectAllItemsSource="{Binding RelativeSource={RelativeSource AncestorType=ListView}, Path=ItemsSource}" />
    </StackPanel>
</GridViewColumn.Header>
    public static readonly DependencyProperty SelectAllPathProperty =
            DependencyProperty.RegisterAttached("SelectAllPath", typeof(string), typeof(AttachedProperties), new UIPropertyMetadata(null, OnAttached));
    public static string GetSelectAllPath(DependencyObject obj)
    {
        return (string)obj.GetValue(SelectAllPathProperty);
    }
    public static void SetSelectAllPath(DependencyObject obj, string value)
    {
        obj.SetValue(SelectAllPathProperty, value);
    }

    public static readonly DependencyProperty SelectAllItemsSourceProperty =
            DependencyProperty.RegisterAttached("SelectAllItemsSource", typeof(IEnumerable), typeof(AttachedProperties), new UIPropertyMetadata(null));
    public static IEnumerable GetSelectAllItemsSource(DependencyObject obj)
    {
        return (IEnumerable)obj.GetValue(SelectAllItemsSourceProperty);
    }
    public static void SetSelectAllItemsSource(DependencyObject obj, IEnumerable value)
    {
        obj.SetValue(SelectAllItemsSourceProperty, value);
    }

    private static void OnAttached(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var cb = o as CheckBox;
        if (cb.IsLoaded)
        {
            Attach(cb);
        }
        else
        {
            cb.Loaded += (s, _) => Attach(cb);
        }
    }

    private static void Attach(CheckBox checkBox)
    {
        var itemsSource = GetSelectAllItemsSource(checkBox);
        if (itemsSource is INotifyCollectionChanged)
        {
            var isAsIcc = itemsSource as INotifyCollectionChanged;
            isAsIcc.CollectionChanged += (s, ccea) =>
                {
                    RebuildBindings(checkBox);
                };
        }
        RebuildBindings(checkBox);
        checkBox.Click += (s, cea) =>
            {
                if (!checkBox.IsChecked.HasValue)
                {
                    checkBox.IsChecked = false;
                }
            };
    }

    private static void RebuildBindings(CheckBox checkBox)
    {
        MultiBinding binding = new MultiBinding();
        var itemsSource = GetSelectAllItemsSource(checkBox);
        var path = GetSelectAllPath(checkBox);
        foreach (var item in itemsSource)
        {
            binding.Bindings.Add(new Binding(path) { Source = item });
        }
        binding.Converter = new CheckedConverter();
        checkBox.SetBinding(CheckBox.IsCheckedProperty, binding);
    }

    private class CheckedConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (values.Length == 0)
            {
                return null;
            }
            else
            {
                bool first = (bool)values[0];
                foreach (var item in values.Skip(1).Cast<bool>())
                {
                    if (first != item)
                    {
                        return null;
                    }
                }
                return first;
            }
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            var output = (bool?)value;
            var outarray = new object[targetTypes.Length];
            if (output.HasValue)
            {
                for (int i = 0; i < outarray.Length; i++)
                {
                    outarray[i] = output.Value;
                }
            }
            return outarray;
        }
    }

(Bindingless Mess:)

The following (after the XAML) has the be some of the ugliest code ever, it probably has memory leaks from the event-handling and other horrible flaws, but it does work to some degree:

<GridViewColumn.Header>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Is Active" />
        <CheckBox IsThreeState="True"
                  Tag="{Binding RelativeSource={RelativeSource AncestorType=ListView}, Path=ItemsSource}"
                  local:AttachedProperties.SelectAllPath="IsActive"/>
    </StackPanel>
</GridViewColumn.Header>
    public static readonly DependencyProperty SelectAllPathProperty =
            DependencyProperty.RegisterAttached("SelectAllPath", typeof(string), typeof(AttachedProperties), new UIPropertyMetadata(null, OnAttached));
    public static string GetSelectAllPath(DependencyObject obj)
    {
        return (string)obj.GetValue(SelectAllPathProperty);
    }
    public static void SetSelectAllPath(DependencyObject obj, string value)
    {
        obj.SetValue(SelectAllPathProperty, value);
    }

    private static void OnAttached(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var cb = o as CheckBox;
        // Needs more closures.
        Action attach = () =>
            {
                IEnumerable itemsSource = cb.Tag as IEnumerable;
                if (itemsSource == null) throw new Exception("ItemsSource for attached property 'SelectAllPath' not found.");
                string path = e.NewValue as string;
                cb.Checked += new RoutedEventHandler(cb_Checked);
                cb.Unchecked += new RoutedEventHandler(cb_Unchecked);
                PropertyChangedEventHandler propertyChangeHandler = (i, pcea) =>
                {
                    if (pcea.PropertyName == path)
                    {
                        UpdateCb(cb, itemsSource.Cast<object>(), path);
                    }
                };
                Action<object> tryAttachHandlerAction = (item) =>
                    {
                        if (item is INotifyPropertyChanged)
                        {
                            var asInpc = item as INotifyPropertyChanged;
                            asInpc.PropertyChanged += propertyChangeHandler;
                        }
                    };
                foreach (var item in itemsSource)
                {
                    tryAttachHandlerAction(item);
                }
                if (itemsSource is INotifyCollectionChanged)
                {
                    var asCC = itemsSource as INotifyCollectionChanged;
                    asCC.CollectionChanged += (s, cce) =>
                        {
                            if (cce.Action == NotifyCollectionChangedAction.Add)
                            {
                                foreach (var item in cce.NewItems)
                                {
                                    tryAttachHandlerAction(item);
                                }
                            }
                        };
                }
                UpdateCb(cb, itemsSource.Cast<object>(), path);
            };
        if (cb.IsLoaded)
        {
            attach();
        }
        else
        {
            cb.Loaded += (s, esub) => attach();
        }
    }

    private static void UpdateCb(CheckBox cb, IEnumerable<object> items, string path)
    {
        Type itemType = null;
        PropertyInfo propInfo = null;
        bool? previous = null;
        bool indeterminate = false;
        foreach (var item in items)
        {
            if (propInfo == null)
            {
                itemType = item.GetType();
                propInfo = itemType.GetProperty(path);
            }
            if (item.GetType() == itemType)
            {
                if (!previous.HasValue)
                {
                    previous = (bool)propInfo.GetValue(item, null);
                }
                else
                {
                    if (previous != (bool)propInfo.GetValue(item, null))
                    {
                        indeterminate = true;
                        break;
                    }
                }
            }
        }
        if (indeterminate)
        {
            cb.IsChecked = null;
        }
        else
        {
            if (previous.HasValue)
            {
                cb.IsChecked = previous.Value;
            }
        }
    }

    static void cb_Unchecked(object sender, RoutedEventArgs e)
    {
        SetValues(sender, false);
    }

    static void cb_Checked(object sender, RoutedEventArgs e)
    {
        SetValues(sender, true);
    }

    private static void SetValues(object sender, bool value)
    {
        var cb = sender as CheckBox;
        IEnumerable itemsSource = cb.Tag as IEnumerable;
        Type itemType = null;
        PropertyInfo propInfo = null;
        foreach (var item in itemsSource)
        {
            if (propInfo == null)
            {
                itemType = item.GetType();
                propInfo = itemType.GetProperty(GetSelectAllPath(cb));
            }
            if (item.GetType() == itemType)
            {
                propInfo.SetValue(item, value, null);
            }
        }
    }

Don't use it (at least in its current form).

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜