如何将任何 DataGridTextColumn 传递给将切换 DataGridTextColumn 的可见性的单个命令?

How can I pass any DataGridTextColumn to a single Command which will toggle the Visibility of the DataGridTextColumn?

我有一个 DataGrid 并且想使用从 ContextMenu 发送的命令来切换各个 DataGridTextColumns 的可见性。我需要一些方法将特定的 DataGridTextColumn 或其 Visibility 参数与 ContextMenu MenuItem 命令相关联。我可以在我的 ViewModel 中设置一个单独的 Visibility 变量,并使用单独的命令切换它们,每个 DataGridTextColumn 一个,工作得很好,但我有很多 DataGridTextColumns,这似乎是一种非常重复、混乱且可能不正确的解决问题的方法.

例子.xaml:

 <FrameworkElement x:Name="dummyElement" Visibility="Collapsed"/>

            <DataGrid ItemsSource="{Binding Shots}" SelectedItem="{Binding SelectedShot, Mode=TwoWay}" AutoGenerateColumns="False" HorizontalScrollBarVisibility="Auto" IsReadOnly="True" AreRowDetailsFrozen="True" HeadersVisibility="All" >

                <DataGrid.Columns>
                    <DataGridTextColumn Visibility="{Binding DataContext.ShotNumberColumnVisibility, Source={x:Reference dummyElement}}" Binding="{Binding Path=ShotNumber}" Header="Shot #" />
                </DataGrid.Columns>

                <DataGrid.ContextMenu>
                    <ContextMenu>
                        <MenuItem Header="Toggle Visibility">
                            <MenuItem Header="Shot Count" Command="{Binding ToggleVisibilityCommand}" />
                        </MenuItem>
                    </ContextMenu>
                </DataGrid.ContextMenu>

            </DataGrid >

目前,我的视图 .xaml 看起来像上面的示例,但有更多的列和每个相应的 ContextMenu MenuItem。在我的 ViewModel 中,我可以通过更改 ShotNumberVisibility 来控制可见性。


public MyViewModel()
{
    ToggleVisibilityCommand = new RelayCommand(ToggleVisibility);
}


public Visibility ShotNumberColumnVisibility { get; set; } = Visibility.Visible;


public void ToggleVisibility(object obj)
{
    ShotNumberColumnVisibility = ShotNumberColumnVisibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
    RaisePropertyChanged("ShotNumberColumnVisibility");
}

我不想为每个单独的 DataGridTextColumn 设置它。将任何 DataGridTextColumn 传递到我的 ViewModel 以便可以使用通用方法切换它的可见性的正确方法是什么?

据我所知,我似乎需要能够使用 CommandParameter 将任何 DataGridTextColumn 发送到我的 ToggleVisibility 函数。这是我想不通的部分。我在我的 .xaml 中考虑类似以下内容,但我还没有让它工作。

CommandParameter="{Binding ElementName=InclinationColumn, Path=Visibility}"

如果仍然不清楚,这里有一些我想要的命令的伪代码以及我想如何使用它。

<DataGridTextColumn Name="demoColumn" Visibility="{Binding demoColumnVisibility}" />
<MenuItem Header="Toggle Demo Column Visibility" CommandParameter="{Binding demoColumn.Visibility}" Command="{Binding ToggleVisibility}" />

public void ToggleVisibility(object obj)
{
    obj.Visibility = !obj.Visibility
    //OR MAYBE
    //Pass in the name "demoColumn" and use that select which bool to flip. In this case demoColumnVisibility


}

这是我的 RelayCommand:ICommand class 的样子...

 public class RelayCommand : ICommand
    {
        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if(execute == null)
            {
                throw new NullReferenceException("execute");
            }
            _execute = execute;
            _canExecute = canExecute;
        }

        public RelayCommand(Action<object> execute) : this(execute, null)
        {
        }


        public event EventHandler CanExecuteChanged
        { 
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute(parameter);
        }

        public void Execute(object parameter)
        {
            _execute.Invoke(parameter);
        }
    }

希望这就足够了,这个问题已经困扰我好几个小时了,我觉得我缺少一些基本的东西。非常感谢任何帮助。

您可以在后面的代码中生成 ContextMenu 及其功能。我知道这不是 MVVM 方式,但老实说,显示和隐藏列真的与业务逻辑有关吗?我认为它只是 UI 的东西,因此,它不必在视图模型中。这是一个例子:

MainWindow.xaml

<Window x:Class="GridColumnVisibilityToggle.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:s="clr-namespace:System;assembly=System.Runtime"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <DataGrid x:Name="TheDataGrid"
                  AutoGenerateColumns="False" 
                  HorizontalScrollBarVisibility="Auto" 
                  IsReadOnly="True" 
                  AreRowDetailsFrozen="True" 
                  HeadersVisibility="All" >

            <DataGrid.ItemsSource>
                <x:Array Type="{x:Type s:String}">
                    <s:String>Item 1</s:String>
                    <s:String>Item 2</s:String>
                    <s:String>Item 3</s:String>
                </x:Array>
            </DataGrid.ItemsSource>

            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding .}" Header="Header" />
            </DataGrid.Columns>

        </DataGrid >
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace GridColumnVisibilityToggle
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        protected override void OnContentRendered(EventArgs e)
        {
            base.OnContentRendered(e);

            var cm = new ContextMenu();
            var visibilityItem = new MenuItem { Header = "Toggle Visibility" };
            var columnItems = TheDataGrid.Columns.Select(a => new MenuItem
            {
                Header = a.Header,
                Command = new RelayCommand<DataGridColumn>(column => column.Visibility = column.Visibility == Visibility.Visible ? Visibility.Hidden : Visibility.Visible),
                CommandParameter = a
            });
            foreach (var item in columnItems)
            {
                visibilityItem.Items.Add(item);
            }
            cm.Items.Add(visibilityItem);
            TheDataGrid.ContextMenu = cm;
        }
    }
}

我使用的 ICommand 实现

using System;
using System.Reflection;
using System.Windows.Input;

namespace GridColumnVisibilityToggle
{
    public class RelayCommand : ICommand
    {
        private readonly Func<object, bool> _canExecute;
        private readonly Action<object> _execute;

        public RelayCommand(Action<object> execute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException(nameof(execute));
            }

            _execute = execute;
        }

        public RelayCommand(Action execute)
          : this((Action<object>)(o => execute()))
        {
            if (execute == null)
            {
                throw new ArgumentNullException(nameof(execute));
            }
        }

        public RelayCommand(Action<object> execute, Func<object, bool> canExecute)
          : this(execute)
        {
            if (canExecute == null)
            {
                throw new ArgumentNullException(nameof(canExecute));
            }

            _canExecute = canExecute;
        }

        public RelayCommand(Action execute, Func<bool> canExecute)
          : this((Action<object>)(o => execute()), (Func<object, bool>)(o => canExecute()))
        {
            if (execute == null)
            {
                throw new ArgumentNullException(nameof(execute));
            }

            if (canExecute == null)
            {
                throw new ArgumentNullException(nameof(canExecute));
            }
        }

        public bool CanExecute(object parameter)
        {
            if (_canExecute != null)
            {
                return _canExecute(parameter);
            }

            return true;
        }

        public event EventHandler CanExecuteChanged;

        public void Execute(object parameter)
        {
            _execute(parameter);
        }

        public void ChangeCanExecute()
        {
            var canExecuteChanged = CanExecuteChanged;
            if (canExecuteChanged == null)
            {
                return;
            }

            canExecuteChanged((object)this, EventArgs.Empty);
        }
    }
    public sealed class RelayCommand<T> : RelayCommand
    {
        public RelayCommand(Action<T> execute)
            : base((Action<object>)(o =>
            {
                if (!RelayCommand<T>.IsValidParameter(o))
                {
                    return;
                }

                execute((T)o);
            }))
        {
            if (execute == null)
            {
                throw new ArgumentNullException(nameof(execute));
            }
        }

        public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
            : base((Action<object>)(o =>
            {
                if (!RelayCommand<T>.IsValidParameter(o))
                {
                    return;
                }

                execute((T)o);
            }), (Func<object, bool>)(o =>
            {
                if (RelayCommand<T>.IsValidParameter(o))
                {
                    return canExecute((T)o);
                }

                return false;
            }))
        {
            if (execute == null)
            {
                throw new ArgumentNullException(nameof(execute));
            }

            if (canExecute == null)
            {
                throw new ArgumentNullException(nameof(canExecute));
            }
        }

        private static bool IsValidParameter(object o)
        {
            if (o != null)
            {
                return o is T;
            }

            var type = typeof(T);
            if (Nullable.GetUnderlyingType(type) != (Type)null)
            {
                return true;
            }

            return !type.GetTypeInfo().IsValueType;
        }
    }

}

它在OnContentRendered方法中生成DataGridContextMenu。对于每个 DataGridColumn,都会生成一个 MenuItem get,其中包含显示或隐藏它的命令。

鉴于 Michal 的上述回复,我重新构建了 MenuItems 的语言,现在我不再提供按钮来切换每个 DataGridTextColumn 的可见性,而是提供 "Show All" 和 "Hide Selected"。有了这个,用户可以 Control+Select 多个单元格来指示要隐藏的列。要返回基本状态,Show All 按钮将所有 Visibility 设置为 Visible。这个新设置还允许我使用单个单元格的选择来引用任何行以对其执行操作。就我而言,我需要能够删除行,这些行是我的 ObservableCollection 中的条目。

支持此行为的 .xaml 更改是:

<DataGrid x:Name="RollTestDataGrid" SelectionUnit="Cell" ItemsSource="{Binding Shots, IsAsync=True}" SelectedIndex="{Binding SelectedShot, Mode=TwoWay}"  AutoGenerateColumns="False" HorizontalScrollBarVisibility="Auto" IsReadOnly="True" AreRowDetailsFrozen="True" HeadersVisibility="All" >

和...

<DataGrid.ContextMenu>
    <ContextMenu>
        <MenuItem Header="Toggle Visibility">
            <MenuItem Header="Show All" Name="ShowAllToggle" Click="ShowAllToggle_Click" />
            <MenuItem Header="Hide Selected" Name="HideSelectedButton" Click="HideSelectedButton_Click"/>
        </MenuItem>
    </ContextMenu>
</DataGrid.ContextMenu>

为我的 SelectionUnit 选择 "Cell",使我能够访问一个元素,我可以从中派生相关的列。然后在后面的代码中,我只是遍历这些并将它们的可见性模式切换为折叠。

在我的 .xaml.cs 中,我有两个 "Click" 方法。

private void ShowAllToggle_Click(object sender, RoutedEventArgs e)
{
    foreach (DataGridTextColumn col in RollTestDataGrid.Columns)
    {
        col.Visibility = Visibility.Visible;
    }
}

private void HideSelectedButton_Click(object sender, RoutedEventArgs e)
{
    foreach (DataGridCellInfo cell in RollTestDataGrid.SelectedCells)
    {
        cell.Column.Visibility = Visibility.Collapsed;
    }
}

我在 ViewModel 中也有一个 "DeleteShot" 方法,这就是为什么我更新的 DataGrid .xaml 在 ItemsSource 中添加了一个 Name 和 IsAsync=True 属性 .

x:Name="RollTestDataGrid" SelectionUnit="Cell" ItemsSource="{Binding Shots, IsAsync=True}" 

IsAsync 允许我调用我的 DeleteShot 命令,从我的 ObservableCollection 中删除一个项目,更新我的 ObservableCollection 中每个项目的 "shotNumber" 属性,并让 DataGrid 更新以显示 "Shot #" 列正确,不需要 DataGrid.Items.Refresh() 在 .xaml.cs

.xaml

<MenuItem Header="Delete" Command="{Binding DataContext.DeleteShotCommand, Source={x:Reference dummyElement}}"

.ViewModel


public RelayCommand DeleteShotCommand { get; private set; }

DeleteShotCommand = new RelayCommand(DeleteShot);


public void DeleteShot(object obj)
{
    Shots.RemoveAt(SelectedIdx);
    foreach(nsbRollShot shot in shots)
    {
        shot.ShotNumber = shots.IndexOf(shot) + 1;
    }
    NotifyPropertyChanged("Shots");
}

我想我把所有 copy/pasted 都说对了,我会继续检查以回答出现的任何问题。