在 EF Core/MVVM 上使用多个视图模型时,正确的请求流程是什么?
What is the correct flow of requests when working with multiple viewModels on EF Core/MVVM?
进一步研究 MVVM 我发现了一个我无法理解的问题,我找不到(更确切地说,我想我无法正确表达我的 Google 搜索)特定于此的信息情况:
我有以下实体:
public class Sale : EntityBase
{
public ICollection<SaleItem> SaleItems {get;set;}
}
public class SaleItem : EntityBase
{
public Sale Sale {get;set;}
public Stock Stock {get;set;}
public TaxType TaxType {get;set;}
}
public class Stock : EntityBase
{
public TaxType TaxType {get;set;}
public ICollection<Sale> SaleStocks {get;set;}
}
public class EntityBase
{
[Key]
public int ID {get;set;}
}
public TaxType
{
[Key]
public int TaxCode {get;set;}
public string Description {get;set;
}
TaxType
由数据库迁移播种。我正在使用 MySQL.
根据我在 上阅读的内容,要在我的数据库中记录一个新条目,我应该调用 _context.Update(Sale)
和 _context.SaveChangesAsync()
.
但是我仍然无法理解我在简单的 CRUD 上做错了什么:
用户被定向到视图模型:
SaleViewModel.cs
private SaleItemStore _saleItemStore;
public Sale Sale {get;set;} = new();
public ObservableCollection<SaleItem> SaleItems {get;set;} = new();
public SaleViewModel(IServiceProvider serviceProvider)
{
_saleItemStore = serviceProvider.GetRequiredService<SaleItemStore>();
SaveSale = new SaveSaleCommand(this, serviceProvider);
}
public class SaveSaleCommand()
{
public SaveSaleCommand(SaleViewModel saleViewModel, IServiceProvidere serviceProvider)
{
_parentViewModel = saleViewModel;
_saleDataService = serviceProvider.GetRequiredService<SalaDataService>();
}
public Execute()
{
foreach (SaleItem saleItem in SaleItems)
{
Sale.SaleItems.Add(saleItem);
}
_saleDataService.AddOrUpdate(Sale sale);
}
}
在不同的 viewModel 上,用户可以从下拉组合框中 select,以及其他属性 TaxType
。组合框的 ItemsSource 绑定到 TaxTypesList
,例如:
SelectStockToSaleItemViewModel
private TaxTypeDataService _taxTypeDataService;
public SaleItem SaleItem {get;set;} = new();
public TaxType SelectedTaxType {get;set;}
public ObservableCollection<TaxType> TaxTypesList {get;} = new();
public SelectStockToSaleItemViewModel(IServiceProvider serviceProvider)
{
_taxTypeDataService = serviceProvider.GetRequiredService<TaxTypeDataService>();
SendSaleItemBack = new SendSaleItemBackCommand(this, serviceProvider);
Task.Run(FillLists);
}
public async Task FillLists()
{
foreach (TaxType taxType in await _taxTypeDataService.GetAllAsNoTracking())
{
TaxTypesList.Add(taxType);
}
}
public SendSaleItemBackCommand()
{
private SaleItemStore _saleItemStore;
public SendSaleItemBackCommand(SelectStockToSaleItemViewModel selectStockToSaleItemViewModel, IServiceProvider serviceProvider)
{
_parentViewModel = selectStockToSaleItemViewModel;
_saleItemStore = serviceProvider.GetRequiredService<SaleItemStore>();
}
public Execute()
{
_saleItemStore.Message = new SaleItem()
{
TaxType = _parentViewModel.SelectedTaxType
}
//Takes user back to previous ViewModel;
}
}
所以,我的想法是,在 SelectStockToSaleItemViewModel
上,用户 select 从充满 _taxTypeDataService.GetAllAsNoTracking()
的组合框中输入 TaxType
,当他们执行 SendSaleItemBackCommand
, SaleItemStore
将其 属性 设置为 SaleItem
且 TaxType
不为空;
之后,如果用户将更多 SaleItems
添加到 Sale
,他们将不得不再次打开 SelectStockToSaleItemViewModel
,并对他们想要添加到 [的每个额外 SaleItem
执行此过程=29=]。
最后,他们将不得不执行 SaveSaleCommand
,我最终会在我的数据库中得到一个 Sale
及其关联的 SaleItems
。
我的数据服务是:
public class SaleDataService
{
private MyDbContext _context;
public SaleDataService(IServiceProvider serviceProvider)
{
_context = serviceProvider.GetRequiredService<MyDbContext>();
}
public async Task<Sale> AddOrUpdate(Sale sale)
{
_context.Update(sale);
await _context.SaveChangesAsync();
return sale;
}
}
public class TaxTypeDataService
{
private MyDbContext _contex;
public TaxTypeDataService(IServiceProvider serviceProvider0
{
_context = serviceProvider.GetRequiredService<MyDbContext>();
}
public async Task<List<TaxType>> GetAllAsNoTracking()
{
return _context.Set<TaxType>().AsNoTracking().ToListAsync();
}
}
public class Messaging<TObject> : IMessaging<TObject>
{
public TObject Message { get; set; }
}
SaleItemStore
是单例。 SelectStockToSaleItemViewModel
和 SelectStockToSaleItemViewModel
是短暂的; TaxTypeDataService
和 SaleDataService
是范围内的;至于语境,
public static IHostBuilder AddDbContext(this IHostBuilder host)
{
host.ConfigureServices((_, services) =>
{
string connString = $"MyConnString";
ServerVersion version = new MySqlServerVersion(new Version(8, 0, 23));
Action <DbContextOptionsBuilder> configureDbContext = c =>
{
c.UseMySql(connString, version, x =>
{
x.CommandTimeout(600);
x.EnableRetryOnFailure(3);
});
c.EnableSensitiveDataLogging();
};
services.AddSingleton(new AmbiStoreDbContextFactory(configureDbContext));
services.AddDbContext<AmbiStoreDbContext>(configureDbContext);
});
return host;
}
当用户尝试使用相同的 TaxType
保存具有两个或更多 SaleItems
的 Sale
时,会出现此问题,因为 EF Core 抛出“TaxType 的另一个实例已经被跟踪”。我知道 TaxType
在第一个 Sale.SaleItem
更新时开始被跟踪,但我该如何处理这个问题?
根据 and The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked,我确实使用 AsNoTracking
调用用 TaxType
填充了组合框,但我认为它不适用于我的情况。此外,他们说通过将上下文的跟踪条目的状态设置为 Detached
来在更新之前“刷新”跟踪的条目,但是,再次,TaxType
开始被跟踪 而 [=90= 】 被拯救。我检查了上下文的跟踪条目集合,在调用 Update(Sale)
之前根本没有提到 TaxType
被跟踪。
我能想到的唯一解决方法是使用 Fluent API 并设置外键,而不是 属性 本身(即 TaxTypeId = SelectedTaxType.ID
),这不会这看起来不是解决这个问题的最佳方法。
好的,我阅读了更多有关 EF Core 的内容,我想我理解了“推荐方式”。
每当使用数据服务和依赖项注入时,实体必须由将使用它们添加或更新上下文数据库的同一上下文加载。
我的错误是使用 AsNoTracking
加载界面使用的集合(组合框、下拉菜单和复选框)。发生的事情是,每当我尝试添加或更新两个使用相同 AsNoTracking
获得的实体的实体时,EF Core(它应该)尝试 将实体添加到上下文的更改跟踪器 。因此,当第二个实体尝试 add/update 时,它抛出了 The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked
异常。
所以“正确的流程”是使用将要使用它们的相同上下文来加载 collection/list。要么对整个应用程序使用单例 dbContext
而不是使用 AsNoTracking
,由于可能会导致并发操作,因此不推荐这样做;或对每个“工作单元”使用作用域dbContext
,即添加新记录的单个对话框,或使用从上下文的同一实例中提取的记录进行更新window保存它,并在处理完您想做的任何事情后处理上下文(及其更改跟踪器)。
我在使用范围上下文时犯的一个大错误是创建一个上下文来加载带有条目的数据网格,双击它会打开一个对话框(带有新的范围上下文)来编辑它而不用从先前的上下文中释放该记录,抛出 already being tracked
异常。
进一步研究 MVVM 我发现了一个我无法理解的问题,我找不到(更确切地说,我想我无法正确表达我的 Google 搜索)特定于此的信息情况:
我有以下实体:
public class Sale : EntityBase
{
public ICollection<SaleItem> SaleItems {get;set;}
}
public class SaleItem : EntityBase
{
public Sale Sale {get;set;}
public Stock Stock {get;set;}
public TaxType TaxType {get;set;}
}
public class Stock : EntityBase
{
public TaxType TaxType {get;set;}
public ICollection<Sale> SaleStocks {get;set;}
}
public class EntityBase
{
[Key]
public int ID {get;set;}
}
public TaxType
{
[Key]
public int TaxCode {get;set;}
public string Description {get;set;
}
TaxType
由数据库迁移播种。我正在使用 MySQL.
根据我在 _context.Update(Sale)
和 _context.SaveChangesAsync()
.
但是我仍然无法理解我在简单的 CRUD 上做错了什么:
用户被定向到视图模型:
SaleViewModel.cs
private SaleItemStore _saleItemStore;
public Sale Sale {get;set;} = new();
public ObservableCollection<SaleItem> SaleItems {get;set;} = new();
public SaleViewModel(IServiceProvider serviceProvider)
{
_saleItemStore = serviceProvider.GetRequiredService<SaleItemStore>();
SaveSale = new SaveSaleCommand(this, serviceProvider);
}
public class SaveSaleCommand()
{
public SaveSaleCommand(SaleViewModel saleViewModel, IServiceProvidere serviceProvider)
{
_parentViewModel = saleViewModel;
_saleDataService = serviceProvider.GetRequiredService<SalaDataService>();
}
public Execute()
{
foreach (SaleItem saleItem in SaleItems)
{
Sale.SaleItems.Add(saleItem);
}
_saleDataService.AddOrUpdate(Sale sale);
}
}
在不同的 viewModel 上,用户可以从下拉组合框中 select,以及其他属性 TaxType
。组合框的 ItemsSource 绑定到 TaxTypesList
,例如:
SelectStockToSaleItemViewModel
private TaxTypeDataService _taxTypeDataService;
public SaleItem SaleItem {get;set;} = new();
public TaxType SelectedTaxType {get;set;}
public ObservableCollection<TaxType> TaxTypesList {get;} = new();
public SelectStockToSaleItemViewModel(IServiceProvider serviceProvider)
{
_taxTypeDataService = serviceProvider.GetRequiredService<TaxTypeDataService>();
SendSaleItemBack = new SendSaleItemBackCommand(this, serviceProvider);
Task.Run(FillLists);
}
public async Task FillLists()
{
foreach (TaxType taxType in await _taxTypeDataService.GetAllAsNoTracking())
{
TaxTypesList.Add(taxType);
}
}
public SendSaleItemBackCommand()
{
private SaleItemStore _saleItemStore;
public SendSaleItemBackCommand(SelectStockToSaleItemViewModel selectStockToSaleItemViewModel, IServiceProvider serviceProvider)
{
_parentViewModel = selectStockToSaleItemViewModel;
_saleItemStore = serviceProvider.GetRequiredService<SaleItemStore>();
}
public Execute()
{
_saleItemStore.Message = new SaleItem()
{
TaxType = _parentViewModel.SelectedTaxType
}
//Takes user back to previous ViewModel;
}
}
所以,我的想法是,在 SelectStockToSaleItemViewModel
上,用户 select 从充满 _taxTypeDataService.GetAllAsNoTracking()
的组合框中输入 TaxType
,当他们执行 SendSaleItemBackCommand
, SaleItemStore
将其 属性 设置为 SaleItem
且 TaxType
不为空;
之后,如果用户将更多 SaleItems
添加到 Sale
,他们将不得不再次打开 SelectStockToSaleItemViewModel
,并对他们想要添加到 [的每个额外 SaleItem
执行此过程=29=]。
最后,他们将不得不执行 SaveSaleCommand
,我最终会在我的数据库中得到一个 Sale
及其关联的 SaleItems
。
我的数据服务是:
public class SaleDataService
{
private MyDbContext _context;
public SaleDataService(IServiceProvider serviceProvider)
{
_context = serviceProvider.GetRequiredService<MyDbContext>();
}
public async Task<Sale> AddOrUpdate(Sale sale)
{
_context.Update(sale);
await _context.SaveChangesAsync();
return sale;
}
}
public class TaxTypeDataService
{
private MyDbContext _contex;
public TaxTypeDataService(IServiceProvider serviceProvider0
{
_context = serviceProvider.GetRequiredService<MyDbContext>();
}
public async Task<List<TaxType>> GetAllAsNoTracking()
{
return _context.Set<TaxType>().AsNoTracking().ToListAsync();
}
}
public class Messaging<TObject> : IMessaging<TObject>
{
public TObject Message { get; set; }
}
SaleItemStore
是单例。 SelectStockToSaleItemViewModel
和 SelectStockToSaleItemViewModel
是短暂的; TaxTypeDataService
和 SaleDataService
是范围内的;至于语境,
public static IHostBuilder AddDbContext(this IHostBuilder host)
{
host.ConfigureServices((_, services) =>
{
string connString = $"MyConnString";
ServerVersion version = new MySqlServerVersion(new Version(8, 0, 23));
Action <DbContextOptionsBuilder> configureDbContext = c =>
{
c.UseMySql(connString, version, x =>
{
x.CommandTimeout(600);
x.EnableRetryOnFailure(3);
});
c.EnableSensitiveDataLogging();
};
services.AddSingleton(new AmbiStoreDbContextFactory(configureDbContext));
services.AddDbContext<AmbiStoreDbContext>(configureDbContext);
});
return host;
}
当用户尝试使用相同的 TaxType
保存具有两个或更多 SaleItems
的 Sale
时,会出现此问题,因为 EF Core 抛出“TaxType 的另一个实例已经被跟踪”。我知道 TaxType
在第一个 Sale.SaleItem
更新时开始被跟踪,但我该如何处理这个问题?
根据 AsNoTracking
调用用 TaxType
填充了组合框,但我认为它不适用于我的情况。此外,他们说通过将上下文的跟踪条目的状态设置为 Detached
来在更新之前“刷新”跟踪的条目,但是,再次,TaxType
开始被跟踪 而 [=90= 】 被拯救。我检查了上下文的跟踪条目集合,在调用 Update(Sale)
之前根本没有提到 TaxType
被跟踪。
我能想到的唯一解决方法是使用 Fluent API 并设置外键,而不是 属性 本身(即 TaxTypeId = SelectedTaxType.ID
),这不会这看起来不是解决这个问题的最佳方法。
好的,我阅读了更多有关 EF Core 的内容,我想我理解了“推荐方式”。
每当使用数据服务和依赖项注入时,实体必须由将使用它们添加或更新上下文数据库的同一上下文加载。
我的错误是使用 AsNoTracking
加载界面使用的集合(组合框、下拉菜单和复选框)。发生的事情是,每当我尝试添加或更新两个使用相同 AsNoTracking
获得的实体的实体时,EF Core(它应该)尝试 将实体添加到上下文的更改跟踪器 。因此,当第二个实体尝试 add/update 时,它抛出了 The instance of entity type cannot be tracked because another instance of this type with the same key is already being tracked
异常。
所以“正确的流程”是使用将要使用它们的相同上下文来加载 collection/list。要么对整个应用程序使用单例 dbContext
而不是使用 AsNoTracking
,由于可能会导致并发操作,因此不推荐这样做;或对每个“工作单元”使用作用域dbContext
,即添加新记录的单个对话框,或使用从上下文的同一实例中提取的记录进行更新window保存它,并在处理完您想做的任何事情后处理上下文(及其更改跟踪器)。
我在使用范围上下文时犯的一个大错误是创建一个上下文来加载带有条目的数据网格,双击它会打开一个对话框(带有新的范围上下文)来编辑它而不用从先前的上下文中释放该记录,抛出 already being tracked
异常。