考虑以下情况:
MVVM 桌面 GUI 应用程序,用户可以在其中创建/编辑“项目”。
想象一个类似 Visual Studio 解决方案的项目:它是一个用户可以创建、打开、保存和关闭的文件(当前是 XML 文件)。
应用程序的一个实例一次只能打开一个项目,但用户可以在其磁盘上存储多个项目。
加载项目后,用户可以对项目列表执行 CRUD 操作(为简单起见,此示例将有一个“用户”列表)。
如果用户想要编辑另一个项目,可以关闭当前打开的项目或启动应用程序的另一个实例。
我很难理解在 MVVM WPF 应用程序中使用控制反转和依赖注入来实现此目的的正确方法是什么。
我目前的想法如下:
数据层
class User
{
public int Id { get; set; }
public string Name {get;set;}
}
// This exposes a "project" content without details of the file type.
// Internal: Not visibile to assembly with GUI (ViewModels)
// Registered in the IoC container as Singleton, so that repositories will always get the same "currently loaded" project.
internal interface IProjectDb
{
List<User> Users { get; }
/** other lists if required.. **/
void Init(string path);
void SaveChanges();
}
class XmlProjectDb : IProjectDb
{
/**
XML implementation:
Init will deserialize from file.
SaveChanges will serialize to file.
**/
}
// Allows operations to User list to ViewModels
// Registered as transient in the IoC container.
public interface IUserRepository
{
IEnumerable<User> GetAll();
User GetById(int id);
User GetByName(string name);
void Update(User user);
/** etc.**/
}
class UserRepository : IUserRepository
{
// This is injected from the IoC container.
// Is a singleton since once loaded I need to work on the same "project" file.
private readonly IProjectDb _db;
/** implementation that uses the injected "_db" instance **/
}
// Allows GUI ViewModels to load and save project
// Registered in the IoC container as Singleton, so that VMs will always get the same "currently loaded" project.
public interface IProject
{
void Init(string path);
void SaveChanges();
}
class Project : IProject
{
// This is injected from the IoC container.
// Is a singleton since once loaded I need to work on the same "project" file.
private readonly IProjectDb _db;
public void Init(string path) => _db.Init(path);
public void SaveChanges() => _db.SaveChanges();
}
UI 视图模型
// "Root" ViewModel that exposes commands to Create/Load/Save/Close a Project.
// Once a Project is loaded, it exposes a UserListViewModel that the View will bind as DataContext to a UserListView
class MainViewModel
{
private readonly IProject _project;
private readonly IUserListViewModelFactory _userListViewModelFactory;
public UserListViewModel UserListViewModel {get;set;}
// Called when user selects "Create new project" from menu
public void CreateNew()
{
string path = /* get path from dialog */
_project.Init(path);
UserListViewModel = _userListViewModelFactory.Create();
}
// Called when user selects "Create new project" from menu
public void Load()
{
string path = /* get path from dialog */
_project.Init(path);
UserListViewModel = _userListViewModelFactory.Create();
}
// Called when user selects "Save" from menu
public void Save()
{
_project.SaveChanges();
}
public void Close()
{
_project.SaveChanges();
UserListViewModel = null;
}
}
class UserListViewModel
{
private readonly IUserRepository _repo;
private readonly IUserViewModelFactory _userVmFactory;
public ObservableCollection<UserViewModel> Users {get;}
public void Load()
{
foreach(User user in _repo.GetAll())
{
UserViewModel vm = _userVmFactory();
vm.Load(user.Id);
Users.Add(vm);
}
}
public void AddNewUser()
{
UserViewModel vm = _userVmFactory();
vm.Load(0);
Users.Add(vm);
}
}
class UserViewModel
{
private readonly IUserRepository _repo;
/* omitted properties and other MVVM stuff */
// Called by who created the ViewModel to load it with data for a User
public void Load(int id)
{
User user = _repo.GetById(id);
// .. load vm data from model instance
}
// Called by the "save" command
public void Save()
{
User user = new User();
// .. Save vm data into model instance.
_repo.Update(user);
}
}
如您所见,我利用单例 IoC 容器注册来处理以下事实:我需要将“当前打开的项目”的同一实例共享给不同的 ViewModel 和服务。
这是处理这种情况的正确方法吗?
此外,在该示例中,只有一种对 XML 文件进行操作的“IProjectDb”接口的实现。
这种架构如何处理我需要根据项目文件扩展名选择正确实现的情况?
我可以使用一个通过查看扩展名来创建实例的工厂,但这意味着每个存储库都需要存储文件路径字符串,以便从工厂获取正确的 IProjectDb 实例。
感谢您的支持
您基本上正在实现一个先进且干净的 MVVM 应用程序。
虽然诸如“此实现是否符合特定设计模式”之类的问题不是基于意见的,但诸如“正确方法是什么”之类的问题却是。
一些想法:
处理对话框以获取文件路径必须发生在视图中(通常是代码隐藏,在对话框本身或对话框的所有者中)。控件不得由视图模型的任何类管理。对话框是专门设计用于与用户交互的 UI 控件。视图模型组件既不了解视图,也不了解用户,因此它自然不可能对对话框有任何兴趣。
您可以使用通常的方式将对话框结果传递给视图模型:数据绑定、作为方法调用的参数、作为命令参数。所有选项都是 100% 合法的 MVVM。
您不应将数据/数据模型管理分散在整个代码中。声明管理整个编辑过程的顶级类(参与视图模型类)。这样的类将保留编辑/创建的实体,并将其传递给其他视图模型类(如果确实需要)。这个类将创建一个新的并初始化的
User
并将其传递给特定上下文的视图模型。
通过这种方式,您可以在一个地方获得与该正在进行的流程相关的存储库(模型)的所有访问权限。这还将删除视图模型类的依赖关系(例如
IUserRepository
)。旨在将类依赖性(从而限制信息和责任)降至最低的目标始终会在各个方面产生更清晰的代码。
IEditableObject
界面。它很有用,因为它为您的应用程序引入了一个漂亮且干净的对象编辑模式,甚至可以实现撤消/取消和重做。通过事件向管理视图模型类(或一般而言此 CRUD 操作的所有者类)发出编辑已完成的信号,以允许它将更新的数据推送到存储库。
从应用程序视图模型中隐藏尽可能多的有关应用程序模型的信息。 MVVM 模型不应泄漏任何实现细节。例如,提供一个简单的 API,抽象出不同类型的存储库或数据接收器。这些是视图模型不必关心的细节。它只是请求数据并将数据传回。从这个意义上来说:
“这种架构如何处理我需要根据项目文件扩展名选择正确实现的情况?”
通常,你会有一种外观来隐藏这些细节。然后让视图模型传递数据,并让外观决定使用哪个存储库来保存(或读取)数据。无论是 XML 文件还是 JSON 文件还是数据库。视图模型一定不知道这种可能性。这不是它的责任。
考虑向视图模型公开单个(每个上下文)和very通用存储库API,例如
GetUser
或SaveUser
。在内部,该存储库将使用更专业的存储库和更专业的 API,例如 GetUserFromDatabase
或 SaveUserToJson
。