I'm new to EF and trying to get the EF Core Getting Started with WPF tutorial running with Prism MVVM.
I'm currently stuck with an ugly solution to reflect the auto-incremented id (sqlite) for an inserted item back to the DataGrid after pressing the Save button. Update: I later found out that all sorting and filtering is lost when done this way.
In the non-mvvm tutorial this is done by calling productsDataGrid.Items.Refresh()
. That works nicely:
private void Button_Click(object sender, RoutedEventArgs e)
{
_context.SaveChanges();
productsDataGrid.Items.Refresh();
}
The only solution (Update: See below for a better solution.) that currently works for me is to set the ObservableCollection to null and then re-assign it to the database context after calling context.SaveChanges()
in my Save()
function.
This is the working code (which discards filtering and sorting):
MainWindow.xaml
<Window x:Class="EfTestPrism.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:EfTestPrism"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance local:MainWindowViewModel, IsDesignTimeCreatable=True}"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<CollectionViewSource x:Key="CategoryViewSource"
Source="{Binding CategoriesCollection}"/>
</Window.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closing">
<i:InvokeCommandAction Command="{Binding WindowCloseCommand, Mode=OneTime}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DataGrid Grid.Row="0"
AutoGenerateColumns="False"
RowDetailsVisibilityMode="VisibleWhenSelected"
ItemsSource="{Binding
Source={StaticResource CategoryViewSource}}">
<DataGrid.Columns>
<DataGridTextColumn Header="Category Id"
Width="SizeToHeader"
IsReadOnly="True"
Binding="{Binding CategoryId}"/>
<DataGridTextColumn Header="Name"
Width="*"
Binding="{Binding Name}"/>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="1"
Content="Save"
Command="{Binding SaveCommand}"/>
</Grid>
</Window>
MainWindow.xaml.cs:
namespace EfTestPrism;
public partial class MainWindow
{
public MainWindow() {
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
MainWindowViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
using Microsoft.EntityFrameworkCore;
using Prism.Commands;
using Prism.Mvvm;
namespace EfTestPrism;
public class MainWindowViewModel : BindableBase
{
public MainWindowViewModel() {
context.Database.EnsureCreated();
context.Categories!.Load();
CategoriesCollection = context.Categories!.Local.ToObservableCollection();
}
private readonly ProductContext context = new ();
private ObservableCollection<Category> ? categoriesCollection;
public ObservableCollection<Category> ? CategoriesCollection {
get => categoriesCollection!;
set => SetProperty(ref categoriesCollection, value);
}
public ICommand SaveCommand => new DelegateCommand(Save);
private void Save() {
context.SaveChanges();
/* I don't like the following but it works.
I tried different things here, see below. */
CategoriesCollection = null;
CategoriesCollection = context.Categories!.Local.ToObservableCollection();
}
public ICommand WindowCloseCommand => new DelegateCommand(WindowClose);
private void WindowClose() {
context.Dispose();
}
}
ProductContext.cs
using Microsoft.EntityFrameworkCore;
namespace EfTestPrism;
public class ProductContext : DbContext
{
public DbSet<Category> ? Categories { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) {
options.UseSqlite("Data Source=products.db");
options.UseLazyLoadingProxies();
}
}
Category.cs
namespace EfTestPrism;
public class Category // I tried making this a BindableBase and...
{
public int CategoryId { get; set; } // ...using SetProperty without success
public string Name { get; set; }
}
Things I've tried without success:
ViewModel::Save() function:
RaisePropertyChanged(nameof(CategoriesCollection)
- Refreshing each collection item and/or id property:
.
foreach (var item in CategoriesCollection) {
RaisePropertyChanged(nameof(item.CategoryId));
RaisePropertyChanged(nameof(item));
}
- Setting the id to zero and back to the original value. Strange things happen here like all ids being zero in the data grid except for the newly added items:
.
foreach (var item in oc) {
var temp = item.CategoryId;
item.CategoryId = 0;
item.CategoryId = temp;
}
MainWindow.xaml:
- Trying all
UpdateSourceTrigger
s for the CategoryID binding.
I can see that the collection changes. When I remove the IsReadonly="True"
on the DataGrids CategoryId column, the value gets updated as soon as I double click it after saving (I don't know if the UI is just updated or it actually syncs with the database).
What would be a proper mvvm way to update the DataGrid similarly to the categoryDataGrid.Items.Refresh();
call after _context.SaveChanges()
in the Button_Click
function of the tutorial?
Update: Refresh callback from ViewModel to View
The following works and keeps sorting and filtering. I don't mind too much about the code behind because it's strictly view related and I think that's acceptable.
Pro: No manual impementation of removing and adding back the items to the collection i.e. least code that works (if there's not a better solution).
Con: The view model has to call a delegate. So it actually has to anticipate that the view it's used in might want to provide a callback.
Changes to the above code:
MainWindow.xaml: Add an x:Name
to the DataGrid
to make it accessable from the code behind:
[...]
<DataGrid Grid.Row="0"
x:Name="CategoriesDataGrid"
AutoGenerateColumns="False"
[...]
Add a delegate
to MainWindowViewModel.cs and call it in Save()
:
[...]
public delegate void Callback();
public class MainWindowViewModel : BindableBase
{
public MainWindowViewModel(Callback ? refreshView = null) {
RefreshView = refreshView;
[...]
private readonly Callback ? RefreshView;
[...]
private void Save() {
context.SaveChanges();
RefreshView?.Invoke();
}
[...]
Implement and supply a RefreshView
method in MainWindow.xaml.cs
:
namespace EfTestPrism;
public partial class MainWindow
{
public MainWindow() {
InitializeComponent();
DataContext = new MainWindowViewModel(RefreshView);
}
private void RefreshView() {
CategoriesDataGrid.Items.Refresh();
}
}
CodePudding user response:
Unless EF sets the property of the existing item in memory for you, you need to load the new ids from the database and update the CategoryId
property of your Category
items yourself.
The easiest way to do this is to basically do the same thing that you do in the constructor after you have called SaveChanges()
, e.g. set the collection property to a new collection with fresh items from the database:
private void Save() {
context.SaveChanges();
CategoriesCollection = new ObservableCollection<Category>(context.Categories);
}
Then you should get a new collection with new updated Category
items.
Alternatively, you could Clear()
the items in the existing collection and then add the new items from the database to it:
private void Save()
{
context.SaveChanges();
CategoriesCollection.Clear();
foreach (var category in context.Categories.ToArray())
CategoriesCollection.Add(category);
}
CodePudding user response:
What you want is to map your entities (Category
) to view models (derived from BindableBase
) and manually update the view models when the entities change (that includes updating the view model collection when the entities collection changes).
This way the data grid keeps its sorting, filtering, scroll postion etc. when you update items.
RaisePropertyChanged(nameof(CategoryId))
works only when called from inside the category - it raises the PropertyChanged
event on its instance - that is, MainWindowViewModel
and explains why the data grid doesn't care and does not update its cells.