繁体   English   中英

带有 Prism MVVM 的实体框架:如何将自动递增的 id 获取到 CollectionViewSource?

[英]Entity Framework with Prism MVVM: How to get an auto incremented id to a CollectionViewSource?

我是 EF 新手,正在尝试使用 Prism MVVM 运行 EF Core Getting Started with WPF教程。

我目前遇到了一个丑陋的解决方案,以便在按下 Save 按钮后将插入项目的自动递增 id (sqlite) 反映回 DataGrid。 更新:我后来发现以这种方式完成所有排序和过滤都会丢失。

在非 mvvm 教程中,这是通过调用productsDataGrid.Items.Refresh()来完成的。 这很好用:

private void Button_Click(object sender, RoutedEventArgs e)
{
    _context.SaveChanges();
    productsDataGrid.Items.Refresh();
}

目前对我有用 的唯一解决方案 (更新:请参阅下文以获得更好的解决方案。)是将 ObservableCollection 设置为 null,然后在我的Save() function 中调用context.SaveChanges()后将其重新分配给数据库上下文。

这是工作代码(丢弃过滤和排序):

主窗口.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();
    }
}

类别.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; }
}

我尝试过但没有成功的事情:

ViewModel::Save() function:

  • RaisePropertyChanged(nameof(CategoriesCollection)
  • 刷新每个集合项和/或 id 属性:

.

foreach (var item in CategoriesCollection) {
    RaisePropertyChanged(nameof(item.CategoryId));
    RaisePropertyChanged(nameof(item));
}
  • 将 id 设置为零并返回原始值。 奇怪的事情发生在这里,就像除了新添加的项目外,数据网格中的所有 id 都为零:

.

foreach (var item in oc) {
    var temp = item.CategoryId;
    item.CategoryId = 0;
    item.CategoryId = temp;
}

主窗口.xaml:

  • 为 CategoryID 绑定尝试所有UpdateSourceTrigger

我可以看到集合发生了变化。 当我删除 DataGrids CategoryId 列上的IsReadonly="True"时,只要在保存后双击它,该值就会更新(我不知道 UI 是刚刚更新还是它实际上与数据库同步)。

类似于categoryDataGrid.Items.Refresh();更新 DataGrid 的正确 mvvm 方法是什么? 在教程的Button_Click function 中的_context.SaveChanges()之后调用?

更新:从 ViewModel 刷新回调到 View

以下工作并继续排序和过滤。 我不太介意背后的代码,因为它与视图严格相关,我认为这是可以接受的。

优点:无需手动执行将项目删除和添加回集合,即最少的工作代码(如果没有更好的解决方案)。

缺点:视图 model 必须调用委托。 所以它实际上必须预料到它使用的视图可能想要提供回调。

对上述代码的更改:

MainWindow.xaml:将x:Name添加到DataGrid以使其可从后面的代码访问:

[...]
<DataGrid Grid.Row="0"
          x:Name="CategoriesDataGrid"
          AutoGenerateColumns="False"
[...]

delegate添加到 MainWindowViewModel.cs 并在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();
    }
[...]

MainWindow.xaml.cs中实现并提供RefreshView方法:

namespace EfTestPrism;

public partial class MainWindow
{
    public MainWindow() {
        InitializeComponent();
        DataContext = new MainWindowViewModel(RefreshView);
    }

    private void RefreshView() {
        CategoriesDataGrid.Items.Refresh();
    }
}

除非 EF 为您设置 memory 中现有项目的属性,否则您需要从数据库中加载新的 id 并自行更新Category项目的CategoryId属性。

最简单的方法是在调用SaveChanges()后基本上执行与构造函数中相同的操作,例如将集合属性设置为包含数据库中新项目的新集合:

private void Save() {
    context.SaveChanges();
    CategoriesCollection = new ObservableCollection<Category>(context.Categories);
}

然后你应该得到一个包含新的更新Category项目的新集合。

或者,您可以Clear()现有集合中的项目,然后将数据库中的新项目添加到其中:

private void Save()
{
    context.SaveChanges();
    CategoriesCollection.Clear();
    foreach (var category in context.Categories.ToArray())
        CategoriesCollection.Add(category);
}

您想要的是 map 您的实体( Category )以查看模型(从BindableBase派生)并在实体更改时手动更新视图模型(包括在实体集合更改时更新视图 model 集合)。

这样,当您更新项目时,数据网格会保持其排序、过滤、滚动位置等。

RaisePropertyChanged(nameof(CategoryId))仅在从类别内部调用时起作用 - 它在其实例上引发PropertyChanged事件 - 即MainWindowViewModel并解释为什么数据网格不关心并且不更新其单元格。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM