Как я могу передать любую DataGridTextColumn одной команде, которая будет переключать видимость DataGridTextColumn?

У меня есть DataGrid, и я хотел бы переключить видимость отдельных столбцов DataGridTextColumns с помощью команд, отправленных из ContextMenu. Мне нужен какой-то способ связать определенный DataGridTextColumn или его параметр Visibility с командой ContextMenu MenuItem. Я могу установить отдельные переменные видимости в моей ViewModel и переключать их с помощью отдельных команд, по одной на каждую 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 >

В настоящее время мой View .xaml выглядит так же, как в приведенном выше примере, но с большим количеством столбцов и соответствующим элементом меню ContextMenu для каждого. В моей 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...

 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);
        }
    }

Надеюсь, этого достаточно, эта проблема убивала меня часами, и я чувствую, что упускаю что-то основное. Любая помощь горячо приветствуется.


person nickEggs    schedule 07.02.2020    source источник


Ответы (2)


Учитывая приведенный выше ответ Михала, я реструктурировал язык своих элементов меню и вместо того, чтобы предоставлять кнопку для переключения видимости каждого DataGridTextColumn, теперь я предлагаю «Показать все» и «Скрыть выбранное». При этом пользователь может Control+Select несколько ячеек, чтобы указать, какие столбцы он хотел бы скрыть. Чтобы вернуться в исходное состояние, кнопка «Показать все» устанавливает для всей видимости значение «Видимый». Эта новая настройка также позволяет мне использовать выбор отдельной ячейки для ссылки на любую строку для выполнения действий. В моем случае мне нужно иметь возможность удалять строки, которые являются записями из моей 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>

Выбор «Cell» для моего SelectionUnit дает мне доступ к элементу, из которого я могу получить связанный столбец. Затем в коде позади я просто перебираю их и переключаю их режимы видимости на свернутый.

В моем .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;
    }
}

У меня также есть метод «DeleteShot» в ViewModel, поэтому в моем обновленном DataGrid .xaml добавлено имя и свойство IsAsync=True в ItemsSource.

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

IsAsync позволяет мне вызывать мою команду DeleteShot, удалять элемент из моей ObservableCollection, обновлять свойство «shotNumber» каждого элемента в моей ObservableCollection и обновлять DataGrid для правильного представления столбца «Shot #», без необходимости в DataGrid. Items.Refresh() в .xaml.cs

.xaml

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

.ВьюМодель


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");
}

Я думаю, что все правильно скопировал/вставил, я буду продолжать проверять, чтобы ответить на любые возникающие вопросы.

person nickEggs    schedule 07.02.2020

Вы можете сгенерировать ContextMenu и его функциональность в коде позади. Я знаю, что это не MVVM, но, честно говоря, действительно ли отображение и скрытие столбцов имеет какое-то отношение к бизнес-логике? Я думаю, что это просто материал пользовательского интерфейса, и поэтому он не обязательно должен быть в модели представления. Вот пример:

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;
        }
    }

}

Он генерирует DataGrid ContextMenu в методе OnContentRendered. Для каждого DataGridColumn создается MenuItem get с помощью команды, которая либо показывает, либо скрывает его.

person Michal Diviš    schedule 07.02.2020
comment
Ах, мне нравится рассуждение о том, что это свойство ТОЛЬКО для просмотра и, следовательно, не должно контролироваться моделью. В этом случае решение на самом деле намного проще, чем даже то, что вы опубликовали. Я добавлю свой код в другом комментарии ниже. - person nickEggs; 07.02.2020
comment
@nickeggs Рад, что смог помочь. - person Michal Diviš; 10.02.2020