Search code examples
c#wpfmvvmdatagrid

How to add a row to DataGrid to display it immediately?


I am trying to add another row in C# WPF MVVM after clicking a button in DataGrid. The problem is that the row is added as soon as I click somewhere else (e.g. in a TextBox). This means that the DataGrid does not update immediately after I click the "Add Row" button.

NewDocumentView.xaml

<DataGrid x:Name="DataGrid"
          AutoGenerateColumns="False"
          HorizontalAlignment="Left"
          VerticalAlignment="Top"
          Grid.Row="1"
          Margin="20,350,0,0"
          ItemsSource="{Binding Items}">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Description" Binding="{Binding Description}" Width="500"/>
        <DataGridTextColumn Header="Unit" Binding="{Binding Unit}" Width="100"/>
        <DataGridTextColumn Header="Price" Binding="{Binding Price}" Width="100"/>
        <DataGridTextColumn Header="Total" Binding="{Binding Total}" Width="100"/>
        <DataGridTemplateColumn Header="Actions">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Button Content="Add Row" Command="{Binding AddItemCommand}" Width="100"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid> 

NewDocumentViewModel.cs

namespace App.MVVM.ViewModel
{
    internal class NewDocumentViewModel : ObservableObject
    {
        public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>();

        public RelayCommand AddItemCommand { get; set; }

        public NewDocumentViewModel()
        {
            Items = new ObservableCollection<Item>();

            AddItemCommand = new RelayCommand(o => { AddNewItem(); });
        }

        private void AddNewItem()
        {
            Items.Add(new Item());
        }
    }
}

NewDocumentView.xaml.cs

namespace App.MVVM.View
{
    public partial class NewDocumentView : UserControl
    {
        public NewDocumentView()
        {
            InitializeComponent();
            DataContext = new NewDocumentViewModel();
        }
    }
}

Item.cs

namespace App.MVVM.Model
{
    class Item : ObservableObject
    {
        private string _description;
        public string Description
        {
            get { return _description; }
            set
            {
                if (_description != value)
                {
                    _description = value;
                    OnPropertyChanged(nameof(Description));
                }
            }
        }

        private string _unit;
        public string Unit
        {
            get { return _unit; }
            set
            {
                if (_unit != value)
                {
                    _unit = value;
                    OnPropertyChanged(nameof(Unit));
                }
            }
        }

        private int _price;
        public int Price
        {
            get { return _price; }
            set
            {
                if (_price != value)
                {
                    _price = value;
                    OnPropertyChanged(nameof(Price));
                }
            }
        }

        private int _total;
        public int Total
        {
            get { return _total; }
            set
            {
                if (_total != value)
                {
                    _total = value;
                    OnPropertyChanged(nameof(Total));
                }
            }
        }
    }
}

ObservableObject.cs

namespace App.Core
{
    internal class ObservableObject : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;

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

RelayCommand.cs

namespace App.Core
{
    internal class RelayCommand : ICommand
    {
        private readonly Action<object> execute;
        private readonly Func<object, bool> canExecute;

        public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged
        {
            add => CommandManager.RequerySuggested += value;
            remove => CommandManager.RequerySuggested -= value;
        }

        public bool CanExecute(object parameter)
        {
            return canExecute == null || canExecute(parameter);
        }

        public void Execute(object parameter)
        {
            execute(parameter);
        }
    }
}

Where did I go wrong? How can I add a new row to the DataGrid using the button to update the DataGrid immediately?


Solution

  • When you bind ObservableCollection to Itemssource of your datagrid, each Item in that collection is templated out into a row. The datacontext of each row is an item.

    When you bind, it's looking for a property in the datacontext. Your binding to AddItemCommand fails completely because Item does not have an AddItemCommand property.

    You can use relativesource to search up the visual tree for a given type https://learn.microsoft.com/en-us/dotnet/desktop/wpf/advanced/relativesource-markupextension?view=netframeworkdesktop-4.8

    If you do that to find datagrid then it's datacontext is inherited from NewDocumentView and is NewDocumentViewModel. Which is of course where your command is.

    You need to tell it explicitly to use the property on the datacontext though, because otherwise it's looking at the Datagrid UI control.

    Here's a simplified example should hopefully make this clear. I am using mvvm community toolkit here and the code is quick and dirty just to illustrate this one point.

    Mainwindow

    <Window.DataContext>
        <local:MainWindowViewmodel/>
    </Window.DataContext>
    <Grid>
        <DataGrid ItemsSource="{Binding Items}">
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="Actions">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Button Content="Add Row" 
    Command="{Binding DataContext.AddItemCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}" Width="100"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
    

    Note that binding with relativesource and I'm using DataContext.AddItemCommand

    Viewmodel:

    public partial class MainWindowViewmodel : ObservableObject
    {
        [ObservableProperty]
        private ObservableCollection<string> items = new ObservableCollection<string>{"A", "B" };
        
        [RelayCommand]
        private async Task AddItem()
        {
            Items.Add("X");
        }
    }
    

    When I click the button, another row is added.

    In case you're interested, probably an idea to search and read a proper article on the toolkit but briefly:

    The toolkit uses code generators to generate the Items property and AddItemCommand in a partial class.