Search code examples
c#wpfmvvmdatagrid

WPF DataGrid - How to automatically set EditMode on a newly added row?


I have a program which so far follows MVVM principles/rules with 0 code behind and I have a DataGrid where users can add, edit or delete rows which represent students. A user can add a row to the list by clicking on a "+" button but in order to edit the row the user has to first click on the row he just added which isn't so user-friendly.

I have been trying to set the newly added row in EditMode for a good while but all my attempts either failed or worked but with some disturbing side effects on the rest of the program. I looked it up online and the solutions I found either look like overkill or also have a bad side effect.

I created a similar program with less code to make it easier to show the structure of my program and DataGrid:

Model

public class Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string PhoneNumber { get; set; }
    public string Address { get; set; }
    public string Email { get; set; }
}

ViewModel

public class StudentsViewModel : INotifyPropertyChanged
{
    public StudentsViewModel()
    {
        Students = new ObservableCollection<Student>();
    }

    private ObservableCollection<Student> students;
    public ObservableCollection<Student> Students
    {
        get { return students; }
        set
        {
            students = value;
            NotifyPropertyChanged(nameof(Students));
        }
    }

    private Student selectedStudent;
    public Student SelectedStudent
    {
        get { return selectedStudent; }
        set
        {
            selectedStudent = value;
            NotifyPropertyChanged(nameof(SelectedStudent));
        }
    }

    private ICommand addRow;
    public ICommand AddRow
    {
        get
        {
            if(addRow == null)
            {
                addRow = new RelayCommand(
                    parameter => AddStudent(new Student()),
                    parameter => true
                );
            }
            return addRow;
        }
    }

    private ICommand removeCmd;
    public ICommand RemoveCmd
    {
        get
        {
            removeCmd = new RelayCommand(
                parameter => RemoveStudent(parameter as Student),
                parameter => parameter != null
            );
            return removeCmd;
        }
    }

    private void AddStudent(Student studentToAdd)
    {
        Students.Add(studentToAdd);
    }

    private void RemoveStudent(Student studentToRemove)
    {
        if (Students.Contains(studentToRemove))
        {
            Students.Remove(studentToRemove);
        }
    }

    #region INotify

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void NotifyPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion
}

View

<Window x:Class="DataGridExample.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:DataGridExample"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="600"
        Width="1000">
    <Window.Resources>
        <local:StudentsViewModel x:Key="StudentsVm"/>
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource StudentsVm}}">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="1*"/>
        </Grid.RowDefinitions>
        <DockPanel LastChildFill="False"
                   Background="#FF2C58EC">
            <Button Command="{Binding AddRow}"
                    Height="25"
                    Margin="5">
                <Button.Template>
                    <ControlTemplate>
                        <Image Source="/Images/AddItem.png"/>
                    </ControlTemplate>
                </Button.Template>
            </Button>
            <Button Command="{Binding RemoveCmd}"
                    CommandParameter="{Binding ElementName=StudentDataGrid, Path=SelectedItem}"
                    Height="25"
                    Margin="5">
                <Button.Template>
                    <ControlTemplate>
                        <Image Source="/Images/DeleteItem.png"/>
                    </ControlTemplate>
                </Button.Template>
            </Button>
        </DockPanel>
        <DataGrid ItemsSource="{Binding Students}"
                  SelectedItem="{Binding Source={StaticResource StudentsVm}, Path=SelectedStudent, Mode=TwoWay}"
                  x:Name="StudentDataGrid"
                  ColumnWidth="*"
                  CanUserAddRows="False"
                  CanUserResizeRows="False"
                  CanUserResizeColumns="False"
                  CanUserSortColumns="False"
                  CanUserDeleteRows="False"
                  AutoGenerateColumns="False"
                  Grid.Row="1">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                    Header="First Name">
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding LastName, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                    Header="Last Name">
                </DataGridTextColumn>
                <DataGridTemplateColumn Header="Date of Birth">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding DateOfBirth, StringFormat={}{0:dd.MM.yyyy}, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding DateOfBirth, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                        DisplayDate="{Binding DateOfBirth, Mode=OneWay, UpdateSourceTrigger=LostFocus}">
                            </DatePicker>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
                <DataGridTextColumn Binding="{Binding PhoneNumber, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                    Header="Phone Number">
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Address, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                    Header="Address">
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Email, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                    Header="Email">
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

I would prefer the solution to be MVVM compatible but honestly at this point I would be satisfied as long as it doesn't cause other problems and doesn't require tons of frameworks or multiples files full of code.

I would prefer the result to look like this example but this is also acceptable as long as no mouse movement is involved after the "+" button click.


Solution

  • I found a way to achieve what you want but it doesn't seem very robust so use with care. First, you should hookup to the LoadingRow event of your DataGrid.

    XAML

    <DataGrid ...
              LoadingRow="StudentDataGrid_LoadingRow"
              ...>
    

    Code-behind

    private async void StudentDataGrid_LoadingRow(object sender, DataGridRowEventArgs e) {
        // Force asynchrony to run callback after UI has finished loading.
        await Task.Yield();
        // Mark the new row as selected
        StudentDataGrid.SelectedItem = e.Row.Item;
        StudentDataGrid.CurrentCell = new DataGridCellInfo(e.Row.Item, StudentDataGrid.Columns[0]);
        // Enter edit mode
        StudentDataGrid.BeginEdit();
    }
    

    By making the method async and by forcing it to execute asynchronously through the await Task.Yield() call, you let the UI finish the loading of the row before asking it to start an edit through the BeginEdit() call.

    I'm writing this more as an experiment and I don't know if I would recommend this but I hope it helps while someone finds a better solution.