Search code examples
c#wpfmvvmdata-bindingcommandparameter

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


I have a DataGrid and would like to toggle Visibility of individual DataGridTextColumns with Commands sent from a ContextMenu. I need some way to associate a particular DataGridTextColumn, or its Visibility parameter with a ContextMenu MenuItem Command. I can set an individual Visibility variables in my ViewModel and toggle them with individual Commands, one per DataGridTextColumn, that works just fine, but I have many many DataGridTextColumns and that seems like a very repetitive, messy, and probably incorrect way to solve the problem.

Example .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 >

Currently, my View .xaml looks like the example above, but with way more Columns and a corresponding ContextMenu MenuItem for each. In my ViewModel, I can control the visibility by changing 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");
}

I do NOT want to have to set this up for each individual DataGridTextColumn. What is the correct way to pass any DataGridTextColumn to my ViewModel so that it's visibility can be toggled with a generic method?

From what I have seen, it sounds like I need to be able to use CommandParameter to send any DataGridTextColumn to my ToggleVisibility function. This is the part I can't figure out. I'm thinking something like the following in my .xaml but I haven't had it work yet.

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

In case it's still not clear, here's some pseudocode for the command I would like to have and how I would like to use it.

<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


}

Here's what my RelayCommand:ICommand class looks like...

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

Hopefully that's enough, this problem has been killing me for hours and I feel like I'm missing something basic. Any help is much appreciated.


Solution

  • Given Michal's response above, I have restructured the language of my MenuItems, and instead of providing a button to toggle visibility of each DataGridTextColumn, I now offer "Show All" and "Hide Selected". With this the user can Control+Select multiple cells to indicate which columns the would like to hide. To get back to a base state, the Show All button sets all Visibility to Visible. This new setup also lets me use the selection of an individual Cell to reference any Row to take actions on. In my case, I need to be able to delete rows, which are entries from my ObservableCollection.

    The .xaml changes to support this behavior are:

    <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" >
    

    and...

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

    Choosing "Cell" for my SelectionUnit, gives me access to an element from which I can derive the associated Column. Then in code behind, I just iterate through these and switch their Visibility modes to collapsed.

    In my .xaml.cs I have two "Click" methods.

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

    I also have a "DeleteShot" method in the ViewModel, which is why my updated DataGrid .xaml has the addition of a Name and the IsAsync=True property in the ItemsSource.

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

    The IsAsync allows me to call my DeleteShot command, Remove an item from my ObservableCollection, update the "shotNumber" property of each item in my ObservableCollection, and have the DataGrid update to present the "Shot #" column correctly, without needing to DataGrid.Items.Refresh() in the .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");
    }
    

    I think I got that all copy/pasted in there correctly, I'll keep checking back to answer any questions that come up.