Click here to Skip to main content
14,699,811 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I have a simple WPF application that has a data grid. What I want is to select and set focus data grid row once I click a button. When the row is selected, I need to change the selected (focused) row using keyboard up/down arrow keys. Most impotent thing is, I want to do this in MVVM design pattern. My application is as below.

My Item.cs class is as below:


public class Item
{
    public string ItemCode { get; set; }
    public string ItemName { get; set; }
    public double ItemPrice { get; set; }

    public Item(string itemCode, string itemName, double itemPrice)
    {
        this.ItemCode = itemCode;
        this.ItemName = itemName;
        this.ItemPrice = itemPrice;
    }
}


ItemViewModel.cs is as below:


public class ItemsViewModel : INotifyPropertyChanged
{
    private List<Item> _items;

    public List<Item> ItemsCollection
    {
        get { return this._items; }
        set
        {
            _items = value;
            OnPropertyChanged(nameof(ItemsCollection));
        }
    }

    public ItemsViewModel()
    {
        this.ItemsCollection = new List<Item>();
        //Add default items
        this.ItemsCollection.Add(new Item("I001", "Text Book", 10));
        this.ItemsCollection.Add(new Item("I002", "Pencil", 20));
        this.ItemsCollection.Add(new Item("I003", "Bag", 15));
    }

    public event PropertyChangedEventHandler PropertyChanged;

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


MainWindowViewModel.cs as below:


public class MainWindowViewModel : INotifyPropertyChanged
{
    public ICommand SelectRow { get; private set; }

    public MainWindowViewModel()
    {
        this.SelectRow = new RelayCommand(this.SelectGridRow, o => true);
    }

    private void SelectGridRow(object param)
    {
        //TODO: Code should goes here
    }

    public event PropertyChangedEventHandler PropertyChanged;

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


I have written following RelayCommand.cs to handle the command binding


public class RelayCommand : ICommand
{
    #region Fields

    /// <summary>
    /// Encapsulated the execute action
    /// </summary>
    private Action<object> execute;

    /// <summary>
    /// Encapsulated the representation for the validation of the execute method
    /// </summary>
    private Predicate<object> canExecute;

    #endregion // Fields

    #region Constructors

    /// <summary>
    /// Initializes a new instance of the RelayCommand class
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    public RelayCommand(Action<object> execute)
        : this(execute, DefaultCanExecute)
    {
    }

    /// <summary>
    /// Initializes a new instance of the RelayCommand class
    /// Creates a new command.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
        {
            throw new ArgumentNullException("execute");
        }

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

        this.execute = execute;
        this.canExecute = canExecute;
    }

    #endregion // Constructors

    #region ICommand Members

    /// <summary>
    /// An event to raise when the CanExecute value is changed
    /// </summary>
    /// <remarks>
    /// Any subscription to this event will automatically subscribe to both 
    /// the local OnCanExecuteChanged method AND
    /// the CommandManager RequerySuggested event
    /// </remarks>
    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            this.CanExecuteChangedInternal += value;
        }

        remove
        {
            CommandManager.RequerySuggested -= value;
            this.CanExecuteChangedInternal -= value;
        }
    }

    /// <summary>
    /// An event to allow the CanExecuteChanged event to be raised manually
    /// </summary>
    private event EventHandler CanExecuteChangedInternal;

    /// <summary>
    /// Defines if command can be executed
    /// </summary>
    /// <param name="parameter">the parameter that represents the validation method</param>
    /// <returns>true if the command can be executed</returns>
    public bool CanExecute(object parameter)
    {
        return this.canExecute != null && this.canExecute(parameter);
    }

    /// <summary>
    /// Execute the encapsulated command
    /// </summary>
    /// <param name="parameter">the parameter that represents the execution method</param>
    public void Execute(object parameter)
    {
        this.execute(parameter);
    }

    #endregion // ICommand Members

    /// <summary>
    /// Raises the can execute changed.
    /// </summary>
    public void OnCanExecuteChanged()
    {
        EventHandler handler = this.CanExecuteChangedInternal;
        if (handler != null)
        {
            //DispatcherHelper.BeginInvokeOnUIThread(() => handler.Invoke(this, EventArgs.Empty));
            handler.Invoke(this, EventArgs.Empty);
        }
    }

    /// <summary>
    /// Destroys this instance.
    /// </summary>
    public void Destroy()
    {
        this.canExecute = _ => false;
        this.execute = _ => { return; };
    }

    /// <summary>
    /// Defines if command can be executed (default behaviour)
    /// </summary>
    /// <param name="parameter">The parameter.</param>
    /// <returns>Always true</returns>
    private static bool DefaultCanExecute(object parameter)
    {
        return true;
    }
}


I have a ItemView.xaml User control as below:


<UserControl x:Class="DataGrid_FocusRow.ItemView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:DataGrid_FocusRow"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">
<Grid>
    <StackPanel Orientation="Vertical">
        <DataGrid x:Name="grdItems" ItemsSource="{Binding ItemsCollection}" AutoGenerateColumns="False" ColumnWidth="*">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Item Code" Binding="{Binding ItemCode}" />
                <DataGridTextColumn Header="Item Name" Binding="{Binding ItemName}" />
                <DataGridTextColumn Header="Item Price" Binding="{Binding ItemPrice}" />
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel>
</Grid></UserControl>


My MainWindow.xaml is as below:

<Window x:Class="DataGrid_FocusRow.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:local="clr-namespace:DataGrid_FocusRow"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<StackPanel>
    <local:ItemView/>

    <Button Command="{Binding SelectRow}">Select Row</Button>
</StackPanel></Window>


What I have tried:

I've already tried by setting "SelectedIndex" and "SelectedItem". It selects the row in the data grid. But it does not set focus to the particular row. Because of that I cannot change the selection by UP/DOWN keys in my keyboard.
Posted
Updated 6-May-20 16:45pm
Comments
Gerry Schmitz 24-Jan-20 13:30pm
   
"Most important" is that it works ... your "patterns" are secondary.

My understanding is that methods implementing the functionality of a user control should reside in the code behind the user control's xaml and not be defined in the ViewModel. The ViewModel should be decoupled from, and know nothing about, the View. My suggestion is to add the 'Select Row' button to the UserControl and subscribe to its button click event in the code behind. Set the DataContext in the xaml, binding the SelectedItem property of the DataGrid to a SelectedItem property defined in the ViewModel like this.

<UserControl x:Class="DataGrid_FocusRow.ItemView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:DataGrid_FocusRow"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">
  <UserControl.DataContext>
        <local:ItemsViewModel/>
    </UserControl.DataContext>
    <Grid>
        <StackPanel Orientation="Vertical">
            <DataGrid x:Name="grdItems" ItemsSource="{Binding ItemsCollection}" SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" AutoGenerateColumns="False" ColumnWidth="*">
                <DataGrid.Columns>
                    <DataGridTextColumn Header="Item Code" Binding="{Binding ItemCode}" />
                    <DataGridTextColumn Header="Item Name" Binding="{Binding ItemName}" />
                    <DataGridTextColumn Header="Item Price" Binding="{Binding ItemPrice}" />
                </DataGrid.Columns>
            </DataGrid>
            <Button Click="Button_Click">Select Row</Button>
        </StackPanel>
    </Grid>
</UserControl>

The code behind should contain the following partial class definition that adds the functionality you require when the button is clicked.
public partial class ItemView : UserControl
   {
       public ItemView()
       {
           InitializeComponent();
       }
       private void Button_Click(object sender, RoutedEventArgs e)
       {
           if (grdItems.Items.Count == 0) return;
           object item = grdItems.Items[0];
           grdItems.SelectedItem= item;
           grdItems.ScrollIntoView(item);
          var row = (DataGridRow)grdItems.ItemContainerGenerator.ContainerFromIndex(grdItems.SelectedIndex);
           row.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
        }
   }

If you add the SelectedItem property to the ViewModel it will be bound to the SelectedItem in the DataGrid.
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace DataGrid_FocusRow
{
    public class ItemsViewModel : INotifyPropertyChanged
    {
        private ObservableCollection<Item> _items;

        public ObservableCollection<Item> ItemsCollection
        {
            get { return _items; }
            set
            {
                _items = value;
                OnPropertyChanged();
            }
        }
        private Item selectedItem;
        public Item SelectedItem
        {
            get
            {
                return selectedItem;
            }
            set
            {
                selectedItem = value;
                OnPropertyChanged();
            }
        }

        public ItemsViewModel()
        {
            this.ItemsCollection = new ObservableCollection<Item>
            {
                //Add default items
                new Item("I001", "Text Book", 10),
                new Item("I002", "Pencil", 20),
                new Item("I003", "Bag", 15)
            };
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

}

I should add that the code for setting the focus on a DataGrid Row is not original, I've saved it as a snippet in the past but have failed to find the original source.

   
v2
Comments
Fehr Benjamin 6-May-20 21:54pm
   
Is a button click event in code behind not agains the MVVM pattern? Also, I am of the opinion that the user should only select the row, for getting the values inside the textboxes. An additional button click is unnecessary.
George Swan 7-May-20 2:41am
   
Thanks for the comment. It's a matter of personal choice, my feeling is that methods implementing the functionality of a user control should reside in the code behind the user control's xaml and not be defined in the ViewModel. The ViewModel shouldn't know anything about the view. I accept your point about the button click. Best wishes.
Actually I solve it without any code behind and button click event.

ViewModel:

 public ICommand SaveCommand { get; private set; }
        public ICommand UpdateCommand { get; private set; }
        public ICommand DeleteCommand { get; private set; }
        public ICommand PrintCommand { get; private set; }
        public ICommand ShowAdvCommand { get; private set; }
        public ICommand SelectionChangedCommand { get; set; }

        //observable collection for machine model
        private ObservableCollection<Machine> _dataContext;
        public ObservableCollection<Machine> DataContext
        {
            get { return _dataContext; }
            set { _dataContext = value; OnPropertyChanged(); }
        }

        private Machine machineSelectedItem;
        public Machine MachineSelectedItem
        {
            get { return machineSelectedItem; }
            set { machineSelectedItem = value; OnPropertyChanged(); }
        }
        public object MachineDataGrid { get; set; }

        //PRWContext for general use
        private PRWContext context = new PRWContext();

        public MachineViewModel()
        {
            //Commands for save, update, delete and print
            SaveCommand = new RelayCommand(() => ExecuteSaveCommand());
            UpdateCommand = new RelayCommand(() => ExecuteUpdateCommand());
            DeleteCommand = new RelayCommand(() => ExecuteDeleteCommand());
            PrintCommand = new RelayCommand(() => ExecutePrintCommand());
            SelectionChangedCommand = new RelayCommand(() => ExecuteSelectionChangedCommand());

            //Load the data from PRW Database to datagrid
            LoadData();
        }

        //execute save
        private void ExecuteSaveCommand()
        {
            Machine machine = new Machine
            {
                //Machine data
                MachineID = MachineID,
                CustomerID = CustomerID,
                CustomerName = CustomerName,
                City = City,
                Country = Country,

                //Serial data
                SpindleC1 = SpindleC1,
                SpindleC2 = SpindleC2,
                HoningHead = HoningHead,

                //Softwareversion data
                NCVersion = NCVersion,
                HMIVersion = HMIVersion,
                HRIVersion = HRIVersion,
                AHSVersion = AHSVersion
            };

            context.Machines.Add(machine);
            context.SaveChanges();

            ClearText();
        }

        //execute update
        private void ExecuteUpdateCommand()
        {
            Machine machine = context.Machines.FirstOrDefault(w => w.MachineID == MachineID);

            machine.CustomerID = CustomerID;
            machine.CustomerName = CustomerName;

            context.SaveChanges();
            ClearText();
        }

        //execute delete
        private void ExecuteDeleteCommand()
        {
            throw new NotImplementedException();
        }

        //execute print 
        private void ExecutePrintCommand()
        {
            throw new NotImplementedException();
        }

        // Execute selection changed
        private void ExecuteSelectionChangedCommand()
        {
            MachineID = machineSelectedItem.MachineID ? .ToString() ?? "";
            CustomerID = machineSelectedItem.CustomerID ? .ToString() ?? "";
        }
        //Load data from database to grid
        private void LoadData()
        {
            context.Machines.Load();
            this.DataContext = context.Machines.Local;
        }

        //Clear textboxes
        private void ClearText()
        {
            MachineID = string.Empty;
            CustomerID = string.Empty;
            CustomerName = string.Empty;
            City = string.Empty;
            Country = string.Empty;
            SpindleC1 = string.Empty;
            SpindleC2 = string.Empty;
            HoningHead = string.Empty;
            NCVersion = string.Empty;
            HMIVersion = string.Empty;
            HRIVersion = string.Empty;
            AHSVersion = string.Empty;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}


View:

<DataGrid x:Name="MachineDataGrid" AutoGenerateColumns="False" MaxHeight="750" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" ItemsSource="{Binding DataContext, Mode=TwoWay}" SelectedItem="{Binding Path=MachineSelectedItem, Mode=TwoWay}">
                    <ie:Interaction.Triggers>
                        <ie:EventTrigger EventName="SelectionChanged">
                            <ie:InvokeCommandAction Command="{Binding SelectionChangedCommand}"  CommandParameter="{Binding ElementName=MachineDataGrid, Path=SelectedItem}"/>
                        </ie:EventTrigger>
                    </ie:Interaction.Triggers>
                    <DataGrid.Columns>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgMachineID}" Binding="{Binding MachineID, Mode=TwoWay}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgCustomerId}" Binding="{Binding CustomerID}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgCustomerName}" Binding="{Binding CustomerName}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgCity}" Binding="{Binding City}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgCountry}" Binding="{Binding Country}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgSpindleC1}" Binding="{Binding SpindleC1}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgSpindleC2}" Binding="{Binding SpindleC2}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgHoningHead}" Binding="{Binding HoningHead}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgNCVersion}" Binding="{Binding NCVersion}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgHMIVersion}" Binding="{Binding HMIVersion}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgHRIVersion}" Binding="{Binding HRIVersion}"/>
                        <materialDesign:DataGridTextColumn Header="{x:Static language:Lang.DgAHSVersion}" Binding="{Binding AHSVersion}"/>
                    </DataGrid.Columns>
                </DataGrid>
   
v3

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)




CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900