Search code examples
c#wpfdatagridrowdetails

How do I set mouse focus to a WPF datagrid's row details?


So I am using WPF and the MVVM pattern. I have my datagrid's ItemSource property bound to a collection of domain objects in my ViewModel. The datagrid also has a RowDetailsTemplate defined which helps me modify all of my properties on my domain object. I have the datagrid set up so it only allows a single row to be selected at a time.

In case you are wondering here is what my datagrid declaration looks like:

<DataGrid Name="detailsDataGrid" Grid.Row="1" 
          AutoGenerateColumns="False"
          ItemsSource="{Binding Source={StaticResource Locator}, Path=Details.FileMoverDetails, Mode=OneWay, ValidatesOnDataErrors=True}" 
          SelectedItem="{Binding Source={StaticResource Locator}, Path=Details.SelectedDetail, Mode=TwoWay, ValidatesOnDataErrors=True}" 
          AreRowDetailsFrozen="True" CanUserAddRows="False" CanUserDeleteRows="False" IsReadOnly="True" SelectionMode="Single">

So you will also notice that the SelectedItem is also bound to a property in my ViewModel

Here is my problem: below my datagrid I have an 'add new record' button

<Button Name="newRecordBtn" DockPanel.Dock="Right" Margin="0,0,10,0" Click="newRecordBtn_Click" >New Record</Button>

which (inside the click event) executes my 'AddDetailCommand' in my ViewModel. The 'AddDetailCommand' simply creates a new domain object, adds it to the ViewModel's collection of domain objects and then sets the ViewModel's 'SelectedDetail' property to the newly created object. Then I call RaisePropertyChanged("FileMoverDetails") followed by RaisePropertyChanged("SelectedDetail").

This makes my datagrid select the newly created item and expand the datagrid's row details. My datagrid's row details has a lot of fields including ComboBoxes and TextBoxes. Which brings me to my real problem: when I left click once on one of my TextBoxes so that I can start typing it doesn't place focus in the text box and I have to click a second time before I can start typing. This happens with any control inside my row details though: I have to click once on any control and then click a second time before anything happens. This problem is only encountered if I click my 'add new record' button. Meaning if I select an already existing row in the datagrid then the focus is correct and I can click once on any row details control and it will work as expected.

For another example of the issue I am having, I have a 'remove record' button in the datagrid's row details which when activated removes the currently selected domain object from the datagrid's item collection. After clicking the 'Add new record' button I go and immediately click once on the 'remove record' button and nothing happens. But when I click a second time then the button works.

So what am I asking? After I click my 'add new record' button I want to be able to click once on my 'remove record' button and have its functionality activated without having to click another time.

.

.

Extra information:

I thought this was purely a focus issue so I ammended my 'add new record' button's Click event to set the focus to one of my datagrid's row details TextBox control after adding the new record:

    private void newRecordBtn_Click(object sender, RoutedEventArgs e)
    {
        if (e.Source == newRecordBtn)
        {
            ExecuteNewRecordButtonCommand();
            e.Handled = true;
        }
    }

    private void ExecuteNewRecordButtonCommand()
    {
        if (newRecordBtn.Command.CanExecute(null))
        {
            newRecordBtn.Command.Execute(null);

            if (detailsDataGrid.Items.Count > 0)
            {
                detailsDataGrid.UpdateLayout();

                DataGridRow selectedRow = (DataGridRow)(detailsDataGrid.ItemContainerGenerator.ContainerFromItem(detailsDataGrid.SelectedItem));


                //Get the ContentPresenter of the row details.
                DataGridDetailsPresenter presenter = FindVisualChild<DataGridDetailsPresenter>(selectedRow);

                // Find the localDirectoryText TextBox
                DataTemplate template = presenter.ContentTemplate;

                System.Windows.Controls.TextBox localDirTxt = (System.Windows.Controls.TextBox)template.FindName("localDirectoryText", presenter);

                localDirTxt.Focus();
            }

        }
    }

Guess what happened? It set the keyboard focus into the TextBox like I expected it would (I can start typing immediately and it will place my typed text into the TextBox). But when I click once on a different TextBox or my 'Remove record' button or a different control (all of which are in the datagrid's row details) nothing happens! I have to then click a second time for the control to do what I expected only a single click to do.

Please help, I have tried so hard to get this to work and it is very frustrating. Even if you can't help I want to thank you for getting to this point in this description, I know it is very long.

Here is my View's XAML (well at least the relevant parts, I remove stuff and placed "..." in the places where stuff was removed):

<Grid>
    <Grid.RowDefinitions>
    ...
    </Grid.RowDefinitions>
    <DataGrid Name="detailsDataGrid" Grid.Row="1" AutoGenerateColumns="False" ItemsSource="{Binding Source={StaticResource Locator}, Path=Details.FileMoverDetails, Mode=OneWay, ValidatesOnDataErrors=True}" SelectedItem="{Binding Source={StaticResource Locator}, Path=Details.SelectedDetail, Mode=TwoWay, ValidatesOnDataErrors=True}" AreRowDetailsFrozen="True" CanUserAddRows="False" CanUserDeleteRows="False" IsReadOnly="True" SelectionMode="Single">
        <DataGrid.Columns>
            ...
            <DataGridTextColumn Header="Local Directory" Binding="{Binding Path=LocalDirectory, ValidatesOnDataErrors=True, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" Width="175"/>
            <DataGridTextColumn Header="Remote Directory" Binding="{Binding Path=RemoteDirectory, ValidatesOnDataErrors=True, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" Width="175"/>
            ...
        </DataGrid.Columns>
        <DataGrid.RowDetailsTemplate>
            <DataTemplate>
                <Grid Name="rowDetailsGrid" Margin="5,10,0,10" Width="{Binding ElementName=detailsView, Path=ActualWidth}"  HorizontalAlignment="Left" VerticalAlignment="Top" DataContext="{Binding Source={StaticResource Locator}, Path=Details.SelectedDetail}" IsEnabled="{Binding Source={StaticResource Locator}, Path=Details.CanEditDetails, Mode=OneWay}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="1*"/>
                        <ColumnDefinition Width="20"/>
                        <ColumnDefinition Width="190"/>
                        <ColumnDefinition Width="20"/>
                        <ColumnDefinition Width="250"/>
                        <ColumnDefinition Width="40"/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="30"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <StackPanel Grid.Column="0" Grid.Row="8" Grid.ColumnSpan="3" Orientation="Vertical">
                        ...
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="1*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <TextBox Grid.Column="0" Name="localDirectoryText" Text="{Binding Path=LocalDirectory, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" PreviewKeyDown="localDirectoryText_PreviewKeyDown"/>
                            <Button Grid.Column="1" Width="20" Margin="3,3,0,3" Name="localDirectoryButton" Click="localDirectoryButton_Click">...</Button>
                        </Grid>
                    </StackPanel>
                    <StackPanel Grid.Column="0" Grid.Row="9" Grid.ColumnSpan="3" Orientation="Vertical">
                        ...
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="1*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <TextBox Grid.Column="0" Name="remoteDirectoryText" Text="{Binding Path=RemoteDirectory, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" PreviewKeyDown="remoteDirectoryText_PreviewKeyDown"/>
                            <Button Grid.Column="1" Width="20" Margin="3,3,0,3" Name="remoteDirectoryButton" Click="remoteDirectoryButton_Click">...</Button>
                        </Grid>
                    </StackPanel>
                    ...
                    <DockPanel Grid.Column="4" Grid.Row="10" LastChildFill="False">
                        <Button Name="removeRecordBtn" DockPanel.Dock="Right" Margin="0,15,0,0" Command="{Binding Path=RemoveSelectedDetailCommand}">Remove Record</Button>
                    </DockPanel>
                </Grid>
            </DataTemplate>
        </DataGrid.RowDetailsTemplate>
    </DataGrid>
    <Grid Grid.Row="2" DataContext="{Binding Source={StaticResource Locator}, Path=Details}" IsEnabled="{Binding Path=HasInstance, Mode=OneWay}" Width="{Binding ElementName=detailsView, Path=ActualWidth}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="Auto" MinWidth="125"/>
        </Grid.ColumnDefinitions>
        <DockPanel Grid.Column="1" LastChildFill="False" Margin="0,10,0,7">
            <Button Name="newRecordBtn" DockPanel.Dock="Right" Margin="0,0,10,0" Command="{Binding Path=AddDetailCommand}" Click="newRecordBtn_Click" >New Record</Button>
        </DockPanel>
    </Grid>
</Grid>

Solution

  • After many google chrome tabs and much patience I was finally able to fix my problem.

    I don't know why proper focus wasn't being achieved by my previous ExecuteNewRecord() method but I fixed it by: first giving focus to the first cell in the currently selected row, and then second by moving the focus to the second cell in the currently selected row. Doing that somehow corrected the focus issue.

    Here is my revised newRecordBtn_Click() and ExecuteNewRecordButtonCommand():

    private void newRecordBtn_Click(object sender, RoutedEventArgs e)
    {
        if (e.Source == newRecordBtn)
        {
            ExecuteNewRecordButtonCommand();
            e.Handled = true;
        }
    }
    
    private void ExecuteNewRecordButtonCommand()
    {
        if (newRecordBtn.Command.CanExecute(null))
        {
            newRecordBtn.Command.Execute(null);
    
            if (detailsDataGrid.Items.Count > 0)
            {
                detailsDataGrid.UpdateLayout();
    
                DataGridRow selectedRow = 
                    (DataGridRow)(detailsDataGrid.ItemContainerGenerator
                                                    .ContainerFromItem(detailsDataGrid.SelectedItem));
    
                DataGridCellsPresenter cellPresenter = FindVisualChild<DataGridCellsPresenter>(selectedRow);
    
                System.Windows.Controls.DataGridCell firstCell =
                    (System.Windows.Controls.DataGridCell)(cellPresenter.ItemContainerGenerator
                                                                        .ContainerFromIndex(0));
    
                firstCell.Focus();
                firstCell.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
            }
        }
    }
    

    Also, in case anyone was wondering what the FindVindVisualChild<>() method is then please note that I am not the original author of that method and I got it from http://msdn.microsoft.com/en-us/library/bb613579.aspx.