我正在按照 MVVM 模式编写 WPF 应用程序,我认为有 DataGrid。我还有 4 个文本框、1 个组合框和一个用于将新实体添加到 DataGrid 的按钮。我正在使用 Entity Framework Core 来处理数据。
这是组合框:
<ComboBox ItemsSource="{Binding NewWorker.JobTitles}"
SelectedItem="{Binding NewWorker.JobTitle, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
工人阶级:
public class Worker : ObservableObject
{
private int _id;
private string _name;
private string _secondName;
private int _age;
private string _email;
private List<JobTitle> _jobTitles;
private int _jobTitleId;
private JobTitle? _jobTitle;
public int Id
{
get => _id;
set => Set(ref _id, value);
}
public string Name
{
get => _name;
set => Set(ref _name, value);
}
[NotMapped]
public List<JobTitle> JobTitles
{
get => _jobTitles;
set => Set(ref _jobTitles, value);
}
public int JobTitleId
{
get => _jobTitleId;
set => Set(ref _jobTitleId, value);
}
public JobTitle? JobTitle
{
get => _jobTitle;
set => Set(ref _jobTitle, value);
}
}
JobTitle
班级:
public class JobTitle : ObservableObject
{
private int _id;
private string _title;
private ObservableCollection<Worker> _workers;
public int Id
{
get => _id;
set => Set(ref _id, value);
}
public string Title
{
get => _title;
set => Set(ref _title, value);
}
public ObservableCollection<Worker> Workers
{
get => _workers;
set => Set(ref _workers, value);
}
public override bool Equals(object? obj)
{
return (obj==null || obj is not JobTitle) ? false : Title == (obj as JobTitle).Title;
}
public override int GetHashCode()
{
return Title.GetHashCode();
}
}
使用组合框查看模型:
public class WorkersDataTableVM : ObservableObject
{
private WorkerRepository _workerRepository;
private JobTitleRepository _jobTitleRepository;
private ObservableCollection<Worker> _workers;
private Worker _newWorker;
public ObservableCollection<Worker> Workers
{
get => _workers;
set => Set(ref _workers, value);
}
public Worker NewWorker
{
get => _newWorker;
set => Set(ref _newWorker, value);
}
public RelayCommand AddWorkerCommand { get; set; }
public WorkersDataTableVM()
{
_workerRepository = new WorkerRepository();
_jobTitleRepository = new JobTitleRepository();
Workers = new ObservableCollection<Worker>(_workerRepository.GetAllWorkers());
NewWorker = new Worker();
LoadData();
AddWorkerCommand = new RelayCommand(AddWorker,CanAddWorker);
}
private bool CanAddWorker(object arg)
{
bool flag = false;
if (NewWorker.Name != default(string) && NewWorker.SecondName != default(string) && NewWorker.Age != default(int) && NewWorker.JobTitleId != default(int))
{
flag = true;
}
return flag;
}
private void AddWorker(object obj)
{
Workers.Add(NewWorker);
_workerRepository.AddWorker(NewWorker);
NewWorker = new();
}
private void LoadData()
{
var workers = _workerRepository.GetAllWorkers();
var jobTitle = _jobTitleRepository.GetAllJopTitles();
Workers = new ObservableCollection<Worker>(workers);
foreach (Worker worker in Workers)
{
worker.JobTitles = jobTitle;
}
NewWorker.JobTitles = jobTitle;
}
}
我不明白为什么我会遇到这个异常:
System.InvalidOperationException:无法跟踪实体类型“JobTitle”的实例,因为已跟踪具有相同键值 {'Id'} 的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging”来查看冲突的键值。
我编写了一个相同的简单控制台应用程序,它做同样的事情:
public class User
{
public int Id { get; set; }
public string? Name { get; set; }
[NotMapped]
public List<Company> Companies { get; set; }
public int CompanyId { get; set; }
public Company? Company { get; set; }
}
public class Company
{
public int Id { get; set; }
public string? Name { get; set; }
public List<User> Users { get; set; } = new();
public override bool Equals(object? obj)
{
return (obj == null || obj is not Company) ? false : Name == (obj as Company).Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
public class Program
{
static void Main(string[] args)
{
using (ApplicationContext db = new ApplicationContext())
{
User user = new User();
user.Companies = db.Companies.ToList();
user.Name = "Name";
user.Company = user.Companies[0];
db.Users.Add(user);
db.SaveChanges();
}
}
}
这个小程序运行良好,没有抛出异常。
PS:我尝试在VM中使用我的代码添加新的Worker,但异常仍然发生,所以我不认为问题出在XAML部分。
PS #2:如果我使用属性
JobTitleId
而不是 JobTitle
创建新的工作人员,我可以修复它,但随后我需要将转换器添加到我的组合框等。
更新:我知道我的问题是我在我的存储库中同时创建了多个
DbContext
,它们是
互相干扰,所以我的新问题我应该如何解决它:
我是否应该为所有存储库创建一个通用的
DbContext
,该存储库将存在于应用程序的整个生命周期中
或者我应该在每次需要执行CRUD操作时创建一个新的
DbContext
?
WPF 和 MVVM 上下文中哪种方式更合适?
首先彻底放弃存储库。 EF 已经以
DbSet
的形式提供了一个存储库。使用通用存储库(每个实体存储库)对此进行抽象是 EF 的反模式,只会让您头疼,尤其是在 WPF 中,您必须明确生命周期范围。
接下来,当涉及到实体时,请避免这种情况:
private List<JobTitle> _jobTitles;
[NotMapped]
public List<JobTitle> JobTitles
{
get => _jobTitles;
set => Set(ref _jobTitles, value);
}
切勿将 setter 暴露给集合导航属性。初始化列表并保护 setter,以便只有 EF 填充它:
private List<JobTitle> _jobTitles = new();
public List<JobTitle> JobTitles
{
get => _jobTitles;
protected set { _jobTitles = value; }
}
您永远不希望代码重新初始化集合。设置单一引用很好,这就是告诉 EF 更改实体的 FK 的方式,但永远不要更改集合的 FK。如果您公开被调用的公共 setter,EF 的更改跟踪将会变得混乱。像将集合设置为新集合以添加/删除项目之类的“秘籍”在 EF 中效果不佳。从集合中显式添加和删除项目。
接下来,当谈到使用 MVVM 在 WPF 中进行数据绑定时,我强烈建议避免直接绑定到实体。它可以工作,但您最终会混合 EF 问题。 (EF 代理行为)与绑定问题(可观察)ViewModel 不仅限于服务顶级视图(页面),还可以包含子视图模型来表示所涉及数据的片段视图。例如,如果您有一个 WorkersDataTableVM 来提供列出所有工作人员的页面,则可以使用 WorkerViewModel 集合来表示每个工作人员并投影 Worker 实体以构建 WorkerViewModel。与视图相关的关注点保留在视图模型中,数据关注点保留在实体中,并且可以根据视图模型中的需要对实体进行展平、转换、格式化等。通过这种方式,
DbContext
实例可以保持短暂的生命周期,仅存活足够长的时间来加载投影到视图绑定的 ViewModel 的数据,然后根据需要实例化以获取实体,以便在需要时根据 ViewModel 的当前状态进行更新。否则,您将保持 DbContext
实例处于打开状态,以便可以保留返回到实体的绑定(如果出现问题,则为中毒敞开大门)以及长期存在的 DbContext
的内存/性能/过时数据影响。或者您正在盯着与独立实体合作并处理并发问题以及潜在的参考跟踪问题(如您所看到的)的兔子洞。